1use crate::{
2 astar::Astar,
3 comp::{
4 Alignment, Body, CharacterState, Density, InputAttr, InputKind, InventoryAction, Melee,
5 Ori, Pos, StateUpdate,
6 ability::{AbilityInitEvent, AbilityMeta, Capability, SpecifiedAbility, Stance},
7 arthropod, biped_large, biped_small, bird_medium,
8 buff::{Buff, BuffCategory, BuffChange, BuffData, BuffSource, DestInfo},
9 character_state::OutputEvents,
10 controller::InventoryManip,
11 crustacean, golem,
12 inventory::slot::{ArmorSlot, EquipSlot, Slot},
13 item::{
14 Hands, ItemKind, ToolKind,
15 armor::Friction,
16 tool::{self, AbilityContext},
17 },
18 object, quadruped_low, quadruped_medium, quadruped_small, ship,
19 skills::{SKILL_MODIFIERS, Skill, SwimSkill},
20 theropod,
21 },
22 consts::{FRIC_GROUND, GRAVITY, MAX_MOUNT_RANGE, MAX_PICKUP_RANGE},
23 event::{BuffEvent, ChangeStanceEvent, ComboChangeEvent, InventoryManipEvent, LocalEvent},
24 mounting::Volume,
25 outcome::Outcome,
26 states::{behavior::JoinData, utils::CharacterState::Idle, *},
27 terrain::{Block, TerrainGrid, UnlockKind},
28 util::Dir,
29 vol::ReadVol,
30};
31use core::hash::BuildHasherDefault;
32use fxhash::FxHasher64;
33use serde::{Deserialize, Serialize};
34use std::{
35 f32::consts::PI,
36 ops::{Add, Div, Mul},
37 time::Duration,
38};
39use strum::Display;
40use tracing::warn;
41use vek::*;
42
43pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0;
44
45impl Body {
46 pub fn base_accel(&self) -> f32 {
47 match self {
48 Body::Humanoid(_) => 100.0,
51 Body::QuadrupedSmall(body) => match body.species {
52 quadruped_small::Species::Turtle => 30.0,
53 quadruped_small::Species::Axolotl => 70.0,
54 quadruped_small::Species::Pig => 70.0,
55 quadruped_small::Species::Sheep => 70.0,
56 quadruped_small::Species::Truffler => 70.0,
57 quadruped_small::Species::Fungome => 70.0,
58 quadruped_small::Species::Goat => 80.0,
59 quadruped_small::Species::Raccoon => 100.0,
60 quadruped_small::Species::Frog => 150.0,
61 quadruped_small::Species::Porcupine => 100.0,
62 quadruped_small::Species::Beaver => 100.0,
63 quadruped_small::Species::Rabbit => 110.0,
64 quadruped_small::Species::Cat => 150.0,
65 quadruped_small::Species::Quokka => 100.0,
66 quadruped_small::Species::MossySnail => 20.0,
67 _ => 125.0,
68 },
69 Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
70 quadruped_medium::Species::Grolgar => 100.0,
71 quadruped_medium::Species::Saber => 110.0,
72 quadruped_medium::Species::Tiger => 110.0,
73 quadruped_medium::Species::Tuskram => 85.0,
74 quadruped_medium::Species::Lion => 105.0,
75 quadruped_medium::Species::Tarasque => 100.0,
76 quadruped_medium::Species::Wolf => 130.0,
77 quadruped_medium::Species::Frostfang => 115.0,
78 quadruped_medium::Species::Mouflon => 75.0,
79 quadruped_medium::Species::Catoblepas => 60.0,
80 quadruped_medium::Species::Bonerattler => 115.0,
81 quadruped_medium::Species::Deer => 120.0,
82 quadruped_medium::Species::Hirdrasil => 110.0,
83 quadruped_medium::Species::Roshwalr => 70.0,
84 quadruped_medium::Species::Donkey => 90.0,
85 quadruped_medium::Species::Camel => 75.0,
86 quadruped_medium::Species::Zebra => 150.0,
87 quadruped_medium::Species::Antelope => 155.0,
88 quadruped_medium::Species::Kelpie => 140.0,
89 quadruped_medium::Species::Horse => 140.0,
90 quadruped_medium::Species::Barghest => 80.0,
91 quadruped_medium::Species::Cattle => 80.0,
92 quadruped_medium::Species::Darkhound => 115.0,
93 quadruped_medium::Species::Highland => 80.0,
94 quadruped_medium::Species::Yak => 80.0,
95 quadruped_medium::Species::Panda => 90.0,
96 quadruped_medium::Species::Bear => 90.0,
97 quadruped_medium::Species::Dreadhorn => 95.0,
98 quadruped_medium::Species::Moose => 105.0,
99 quadruped_medium::Species::Snowleopard => 115.0,
100 quadruped_medium::Species::Mammoth => 75.0,
101 quadruped_medium::Species::Ngoubou => 95.0,
102 quadruped_medium::Species::Llama => 100.0,
103 quadruped_medium::Species::Alpaca => 100.0,
104 quadruped_medium::Species::Akhlut => 90.0,
105 quadruped_medium::Species::Bristleback => 105.0,
106 quadruped_medium::Species::ClaySteed => 85.0,
107 },
108 Body::BipedLarge(body) => match body.species {
109 biped_large::Species::Slysaurok => 100.0,
110 biped_large::Species::Occultsaurok => 100.0,
111 biped_large::Species::Mightysaurok => 100.0,
112 biped_large::Species::Mindflayer => 90.0,
113 biped_large::Species::Minotaur => 60.0,
114 biped_large::Species::Huskbrute => 130.0,
115 biped_large::Species::Cultistwarlord => 110.0,
116 biped_large::Species::Cultistwarlock => 90.0,
117 biped_large::Species::Gigasfrost => 45.0,
118 biped_large::Species::Gigasfire => 50.0,
119 biped_large::Species::Forgemaster => 100.0,
120 _ => 80.0,
121 },
122 Body::BirdMedium(_) => 80.0,
123 Body::FishMedium(_) => 80.0,
124 Body::Dragon(_) => 250.0,
125 Body::BirdLarge(_) => 110.0,
126 Body::FishSmall(_) => 60.0,
127 Body::BipedSmall(biped_small) => match biped_small.species {
128 biped_small::Species::Haniwa => 65.0,
129 biped_small::Species::Boreal => 100.0,
130 biped_small::Species::Gnarling => 70.0,
131 _ => 80.0,
132 },
133 Body::Object(_) => 0.0,
134 Body::Item(_) => 0.0,
135 Body::Golem(body) => match body.species {
136 golem::Species::ClayGolem => 120.0,
137 golem::Species::IronGolem => 100.0,
138 _ => 60.0,
139 },
140 Body::Theropod(theropod) => match theropod.species {
141 theropod::Species::Archaeos
142 | theropod::Species::Odonto
143 | theropod::Species::Ntouka => 110.0,
144 theropod::Species::Dodarock => 75.0,
145 theropod::Species::Yale => 115.0,
146 _ => 125.0,
147 },
148 Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
149 quadruped_low::Species::Crocodile => 60.0,
150 quadruped_low::Species::SeaCrocodile => 60.0,
151 quadruped_low::Species::Alligator => 65.0,
152 quadruped_low::Species::Salamander => 85.0,
153 quadruped_low::Species::Elbst => 85.0,
154 quadruped_low::Species::Monitor => 130.0,
155 quadruped_low::Species::Asp => 100.0,
156 quadruped_low::Species::Tortoise => 60.0,
157 quadruped_low::Species::Rocksnapper => 70.0,
158 quadruped_low::Species::Rootsnapper => 70.0,
159 quadruped_low::Species::Reefsnapper => 70.0,
160 quadruped_low::Species::Pangolin => 90.0,
161 quadruped_low::Species::Maneater => 80.0,
162 quadruped_low::Species::Sandshark => 125.0,
163 quadruped_low::Species::Hakulaq => 125.0,
164 quadruped_low::Species::Dagon => 140.0,
165 quadruped_low::Species::Lavadrake => 100.0,
166 quadruped_low::Species::Icedrake => 100.0,
167 quadruped_low::Species::Basilisk => 85.0,
168 quadruped_low::Species::Deadwood => 110.0,
169 quadruped_low::Species::Mossdrake => 100.0,
170 quadruped_low::Species::Driggle => 120.0,
171 quadruped_low::Species::Snaretongue => 120.0,
172 quadruped_low::Species::Hydra => 100.0,
173 },
174 Body::Ship(ship::Body::Carriage) => 40.0,
175 Body::Ship(_) => 0.0,
176 Body::Arthropod(arthropod) => match arthropod.species {
177 arthropod::Species::Tarantula => 85.0,
178 arthropod::Species::Blackwidow => 95.0,
179 arthropod::Species::Antlion => 115.0,
180 arthropod::Species::Hornbeetle => 80.0,
181 arthropod::Species::Leafbeetle => 65.0,
182 arthropod::Species::Stagbeetle => 80.0,
183 arthropod::Species::Weevil => 70.0,
184 arthropod::Species::Cavespider => 90.0,
185 arthropod::Species::Moltencrawler => 70.0,
186 arthropod::Species::Mosscrawler => 70.0,
187 arthropod::Species::Sandcrawler => 70.0,
188 arthropod::Species::Dagonite => 70.0,
189 arthropod::Species::Emberfly => 75.0,
190 },
191 Body::Crustacean(body) => match body.species {
192 crustacean::Species::Crab | crustacean::Species::SoldierCrab => 80.0,
193 crustacean::Species::Karkatha => 120.0,
194 },
195 Body::Plugin(body) => body.base_accel(),
196 }
197 }
198
199 pub fn air_accel(&self) -> f32 { self.base_accel() * 0.025 }
200
201 pub fn max_speed_approx(&self) -> f32 {
204 let v = match self {
215 Body::Ship(ship) => ship.get_speed(),
216 _ => (-self.base_accel() / 30.0) / ((1.0 - FRIC_GROUND).powi(2) - 1.0),
217 };
218 debug_assert!(v >= 0.0, "Speed must be positive!");
219 v
220 }
221
222 pub fn base_ori_rate(&self) -> f32 {
224 match self {
225 Body::Humanoid(_) => 3.5,
226 Body::QuadrupedSmall(_) => 3.0,
227 Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
228 quadruped_medium::Species::Mammoth => 1.0,
229 _ => 2.8,
230 },
231 Body::BirdMedium(_) => 6.0,
232 Body::FishMedium(_) => 6.0,
233 Body::Dragon(_) => 1.0,
234 Body::BirdLarge(_) => 7.0,
235 Body::FishSmall(_) => 7.0,
236 Body::BipedLarge(biped_large) => match biped_large.species {
237 biped_large::Species::Harvester => 2.0,
238 _ => 2.7,
239 },
240 Body::BipedSmall(_) => 3.5,
241 Body::Object(_) => 2.0,
242 Body::Item(_) => 2.0,
243 Body::Golem(golem) => match golem.species {
244 golem::Species::WoodGolem => 1.2,
245 _ => 2.0,
246 },
247 Body::Theropod(theropod) => match theropod.species {
248 theropod::Species::Archaeos => 2.3,
249 theropod::Species::Odonto => 2.3,
250 theropod::Species::Ntouka => 2.3,
251 theropod::Species::Dodarock => 2.0,
252 _ => 2.5,
253 },
254 Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
255 quadruped_low::Species::Asp => 2.2,
256 quadruped_low::Species::Tortoise => 1.5,
257 quadruped_low::Species::Rocksnapper => 1.8,
258 quadruped_low::Species::Rootsnapper => 1.8,
259 quadruped_low::Species::Lavadrake => 1.7,
260 quadruped_low::Species::Icedrake => 1.7,
261 quadruped_low::Species::Mossdrake => 1.7,
262 _ => 2.0,
263 },
264 Body::Ship(ship::Body::Carriage) => 0.04,
265 Body::Ship(ship) if ship.has_water_thrust() => 5.0 / self.dimensions().y,
266 Body::Ship(_) => 6.0 / self.dimensions().y,
267 Body::Arthropod(_) => 3.5,
268 Body::Crustacean(_) => 3.5,
269 Body::Plugin(body) => body.base_ori_rate(),
270 }
271 }
272
273 pub fn swim_thrust(&self) -> Option<f32> {
275 let front_profile = self.dimensions().x * self.dimensions().z;
279 Some(
280 match self {
281 Body::Object(_) => return None,
282 Body::Item(_) => return None,
283 Body::Ship(ship::Body::Submarine) => 1000.0 * self.mass().0,
284 Body::Ship(ship) if ship.has_water_thrust() => 500.0 * self.mass().0,
285 Body::Ship(_) => return None,
286 Body::BipedLarge(_) => 120.0 * self.mass().0,
287 Body::Golem(_) => 100.0 * self.mass().0,
288 Body::BipedSmall(_) => 1000.0 * self.mass().0,
289 Body::BirdMedium(_) => 400.0 * self.mass().0,
290 Body::BirdLarge(_) => 400.0 * self.mass().0,
291 Body::FishMedium(_) => 200.0 * self.mass().0,
292 Body::FishSmall(_) => 300.0 * self.mass().0,
293 Body::Dragon(_) => 50.0 * self.mass().0,
294 Body::Humanoid(_) => 4_000_000.0 / self.mass().0,
297 Body::Theropod(body) => match body.species {
298 theropod::Species::Sandraptor
299 | theropod::Species::Snowraptor
300 | theropod::Species::Sunlizard
301 | theropod::Species::Woodraptor
302 | theropod::Species::Dodarock
303 | theropod::Species::Axebeak
304 | theropod::Species::Yale => 500.0 * self.mass().0,
305 _ => 150.0 * self.mass().0,
306 },
307 Body::QuadrupedLow(_) => 1200.0 * self.mass().0,
308 Body::QuadrupedMedium(body) => match body.species {
309 quadruped_medium::Species::Mammoth => 150.0 * self.mass().0,
310 quadruped_medium::Species::Kelpie => 3500.0 * self.mass().0,
311 _ => 1000.0 * self.mass().0,
312 },
313 Body::QuadrupedSmall(_) => 1500.0 * self.mass().0,
314 Body::Arthropod(_) => 500.0 * self.mass().0,
315 Body::Crustacean(_) => 400.0 * self.mass().0,
316 Body::Plugin(body) => body.swim_thrust()?,
317 } * front_profile,
318 )
319 }
320
321 pub fn fly_thrust(&self) -> Option<f32> {
323 match self {
324 Body::BirdMedium(body) => match body.species {
325 bird_medium::Species::Bat | bird_medium::Species::BloodmoonBat => {
326 Some(GRAVITY * self.mass().0 * 0.5)
327 },
328 _ => Some(GRAVITY * self.mass().0 * 2.0),
329 },
330 Body::BirdLarge(_) => Some(GRAVITY * self.mass().0 * 0.5),
331 Body::Dragon(_) => Some(200_000.0),
332 Body::Ship(ship) if ship.can_fly() => Some(390_000.0),
333 Body::Object(object::Body::Crux) => Some(1_000.0),
334 _ => None,
335 }
336 }
337
338 pub fn vectored_propulsion(&self) -> bool {
340 match self {
341 Body::Ship(ship) => ship.vectored_propulsion(),
342 _ => false,
343 }
344 }
345
346 pub fn jump_impulse(&self) -> Option<f32> {
348 match self {
349 Body::Object(_) | Body::Ship(_) | Body::Item(_) => None,
350 Body::BipedLarge(_) | Body::Dragon(_) => Some(0.6 * self.mass().0),
351 Body::Golem(_) | Body::QuadrupedLow(_) => Some(0.4 * self.mass().0),
352 Body::QuadrupedMedium(_) => Some(0.4 * self.mass().0),
353 Body::Theropod(body) => match body.species {
354 theropod::Species::Snowraptor
355 | theropod::Species::Sandraptor
356 | theropod::Species::Woodraptor => Some(0.4 * self.mass().0),
357 _ => None,
358 },
359 Body::Arthropod(_) => Some(1.0 * self.mass().0),
360 _ => Some(0.4 * self.mass().0),
361 }
362 .map(|f| f * GRAVITY)
363 }
364
365 pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) }
366
367 pub fn reverse_move_factor(&self) -> f32 { 0.45 }
370
371 pub fn projectile_offsets(&self, ori: Vec3<f32>, scale: f32) -> Vec3<f32> {
374 let body_offsets_z = match self {
375 Body::Golem(_) => self.height() * 0.4,
376 _ => self.eye_height(scale),
377 };
378
379 let dim = self.dimensions();
380 let (width, length) = (dim.x, dim.y);
382 let body_radius = if length > width {
383 self.max_radius()
385 } else {
386 self.min_radius()
388 };
389
390 Vec3::new(
391 body_radius * ori.x * 1.1,
392 body_radius * ori.y * 1.1,
393 body_offsets_z,
394 )
395 }
396}
397
398pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) {
400 if let &Idle(idle::Data {
401 ref is_sneaking,
402 ref time_entered,
403 mut footwear,
404 }) = data.character
405 {
406 if footwear.is_none() {
407 footwear = data.inventory.and_then(|inv| {
408 inv.equipped(EquipSlot::Armor(ArmorSlot::Feet))
409 .map(|armor| match armor.kind().as_ref() {
410 ItemKind::Armor(a) => {
411 a.stats(data.msm, armor.stats_durability_multiplier())
412 .ground_contact
413 },
414 _ => Friction::Normal,
415 })
416 });
417 update.character = Idle(idle::Data {
418 is_sneaking: *is_sneaking,
419 time_entered: *time_entered,
420 footwear,
421 });
422 }
423 if data.physics.skating_active {
424 update.character =
425 CharacterState::Skate(skate::Data::new(data, footwear.unwrap_or(Friction::Normal)));
426 }
427 }
428}
429
430pub fn handle_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
432 if data.volume_mount_data.is_some() {
433 return;
434 }
435 let submersion = data
436 .physics
437 .in_liquid()
438 .map(|depth| depth / data.body.height());
439
440 if input_is_pressed(data, InputKind::Fly)
441 && submersion.is_none_or(|sub| sub < 1.0)
442 && (data.physics.on_ground.is_none() || data.body.jump_impulse().is_none())
443 && data.body.fly_thrust().is_some()
444 {
445 fly_move(data, update, efficiency);
446 } else if let Some(submersion) = (data.physics.in_liquid().is_some()
447 && data.body.swim_thrust().is_some())
448 .then_some(submersion)
449 .flatten()
450 {
451 swim_move(data, update, efficiency, submersion);
452 } else {
453 basic_move(data, update, efficiency);
454 }
455}
456
457fn basic_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
459 let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
460
461 let accel = if let Some(block) = data.physics.on_ground {
462 data.body.base_accel()
464 * data.scale.map_or(1.0, |s| s.0.sqrt())
465 * block.get_traction()
466 * block.get_friction()
467 / FRIC_GROUND
468 } else {
469 data.body.air_accel()
470 } * efficiency;
471
472 update.vel.0 += Vec2::broadcast(data.dt.0)
474 * accel
475 * if data.body.can_strafe() {
476 data.inputs.move_dir
477 * if is_strafing(data, update) {
478 Lerp::lerp(
479 Vec2::from(update.ori)
480 .try_normalized()
481 .unwrap_or_else(Vec2::zero)
482 .dot(
483 data.inputs
484 .move_dir
485 .try_normalized()
486 .unwrap_or_else(Vec2::zero),
487 )
488 .add(1.0)
489 .div(2.0)
490 .max(0.0),
491 1.0,
492 data.body.reverse_move_factor(),
493 )
494 } else {
495 1.0
496 }
497 } else {
498 let fw = Vec2::from(update.ori);
499 fw * data.inputs.move_dir.dot(fw).max(0.0)
500 };
501}
502
503pub fn handle_forced_movement(
505 data: &JoinData<'_>,
506 update: &mut StateUpdate,
507 movement: ForcedMovement,
508) {
509 match movement {
510 ForcedMovement::Forward(strength) => {
511 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
512 if let Some(accel) = data.physics.on_ground.map(|block| {
513 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
515 }) {
516 update.vel.0 += Vec2::broadcast(data.dt.0)
517 * accel
518 * data.scale.map_or(1.0, |s| s.0.sqrt())
519 * Vec2::from(*data.ori)
520 * strength;
521 }
522 },
523 ForcedMovement::Reverse(strength) => {
524 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
525 if let Some(accel) = data.physics.on_ground.map(|block| {
526 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
528 }) {
529 update.vel.0 += Vec2::broadcast(data.dt.0)
530 * accel
531 * data.scale.map_or(1.0, |s| s.0.sqrt())
532 * -Vec2::from(*data.ori)
533 * strength;
534 }
535 },
536 ForcedMovement::Sideways(strength) => {
537 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
538 if let Some(accel) = data.physics.on_ground.map(|block| {
539 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
541 }) {
542 let direction = {
543 let side = Vec2::from(*data.ori)
545 .rotated_z(PI / 2.)
546 .dot(data.inputs.move_dir)
547 .signum();
548 if side > 0.0 {
549 Vec2::from(*data.ori).rotated_z(PI / 2.)
550 } else {
551 -Vec2::from(*data.ori).rotated_z(PI / 2.)
552 }
553 };
554
555 update.vel.0 += Vec2::broadcast(data.dt.0)
556 * accel
557 * data.scale.map_or(1.0, |s| s.0.sqrt())
558 * direction
559 * strength;
560 }
561 },
562 ForcedMovement::DirectedReverse(strength) => {
563 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
564 if let Some(accel) = data.physics.on_ground.map(|block| {
565 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
567 }) {
568 let direction = if Vec2::from(*data.ori).dot(data.inputs.move_dir).signum() > 0.0 {
569 data.inputs.move_dir.reflected(Vec2::from(*data.ori))
570 } else {
571 data.inputs.move_dir
572 }
573 .try_normalized()
574 .unwrap_or_else(|| -Vec2::from(*data.ori));
575 update.vel.0 += direction * strength * accel * data.dt.0;
576 }
577 },
578 ForcedMovement::AntiDirectedForward(strength) => {
579 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
580 if let Some(accel) = data.physics.on_ground.map(|block| {
581 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
583 }) {
584 let direction = if Vec2::from(*data.ori).dot(data.inputs.move_dir).signum() < 0.0 {
585 data.inputs.move_dir.reflected(Vec2::from(*data.ori))
586 } else {
587 data.inputs.move_dir
588 }
589 .try_normalized()
590 .unwrap_or_else(|| Vec2::from(*data.ori));
591 let direction = direction.reflected(Vec2::from(*data.ori).rotated_z(PI / 2.));
592 update.vel.0 += direction * strength * accel * data.dt.0;
593 }
594 },
595 ForcedMovement::Leap {
596 vertical,
597 forward,
598 progress,
599 direction,
600 } => {
601 let dir = direction.get_2d_dir(data);
602 update.vel.0 = Vec3::new(
604 dir.x,
605 dir.y,
606 vertical,
607 )
608 * data.scale.map_or(1.0, |s| s.0.sqrt())
609 * 2.0 * progress
611 + Vec3::from(dir)
613 * forward
615 * (1.0 - data.inputs.look_dir.z.abs());
619 },
620 ForcedMovement::Hover { move_input } => {
621 update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0)
622 + move_input
623 * data.scale.map_or(1.0, |s| s.0.sqrt())
624 * data.inputs.move_dir.try_normalized().unwrap_or_default();
625 },
626 }
627}
628
629pub fn handle_orientation(
630 data: &JoinData<'_>,
631 update: &mut StateUpdate,
632 efficiency: f32,
633 dir_override: Option<Dir>,
634) {
635 fn to_horizontal_fast(ori: &crate::comp::Ori) -> crate::comp::Ori {
637 if ori.to_quat().into_vec4().xy().is_approx_zero() {
638 *ori
639 } else {
640 ori.to_horizontal()
641 }
642 }
643 fn ori_absdiff(a: &crate::comp::Ori, b: &crate::comp::Ori) -> f32 {
645 (a.to_quat().into_vec4() - b.to_quat().into_vec4()).reduce(|a, b| a.abs() + b.abs())
646 }
647
648 let (tilt_ori, efficiency) = if let Body::Ship(ship) = data.body
649 && ship.has_wheels()
650 {
651 let height_at = |rpos| {
652 data.terrain
653 .ray(
654 data.pos.0 + rpos + Vec3::unit_z() * 4.0,
655 data.pos.0 + rpos - Vec3::unit_z() * 4.0,
656 )
657 .until(Block::is_solid)
658 .cast()
659 .0
660 };
661
662 let x_diff = (height_at(data.ori.to_horizontal().right().to_vec() * 3.0)
665 - height_at(data.ori.to_horizontal().right().to_vec() * -3.0))
666 / 10.0;
667 let y_diff = (height_at(data.ori.to_horizontal().look_dir().to_vec() * -4.5)
668 - height_at(data.ori.to_horizontal().look_dir().to_vec() * 4.5))
669 / 10.0;
670
671 (
672 Quaternion::rotation_y(x_diff.atan()) * Quaternion::rotation_x(y_diff.atan()),
673 (data.vel.0 - data.physics.ground_vel)
674 .xy()
675 .magnitude()
676 .max(3.0)
677 * efficiency,
678 )
679 } else {
680 (Quaternion::identity(), efficiency)
681 };
682
683 let target_ori = if let Some(dir_override) = dir_override {
687 dir_override.into()
688 } else if is_strafing(data, update) || update.character.should_follow_look() {
689 data.inputs
690 .look_dir
691 .to_horizontal()
692 .unwrap_or_default()
693 .into()
694 } else {
695 Dir::from_unnormalized(data.inputs.move_dir.into())
696 .map_or_else(|| to_horizontal_fast(data.ori), |dir| dir.into())
697 }
698 .rotated(tilt_ori);
699 let half_turns_per_tick = data.body.base_ori_rate() / data.scale.map_or(1.0, |s| s.0.sqrt())
701 * efficiency
702 * if data.physics.on_ground.is_some() {
703 1.0
704 } else if data.physics.in_liquid().is_some() {
705 0.4
706 } else {
707 0.2
708 }
709 * data.dt.0;
710 let ticks_from_target_guess = ori_absdiff(&update.ori, &target_ori) / half_turns_per_tick;
712 let instantaneous = ticks_from_target_guess < 1.0;
713 update.ori = if data.volume_mount_data.is_some() {
714 update.ori
715 } else if instantaneous {
716 target_ori
717 } else {
718 let target_fraction = {
719 let angle_factor = 2.0 / (1.0 - update.ori.dot(target_ori)).sqrt();
722
723 half_turns_per_tick * angle_factor
724 };
725 update
726 .ori
727 .slerped_towards(target_ori, target_fraction.min(1.0))
728 };
729
730 update.character_activity.look_dir = Some(data.controller.inputs.look_dir);
732}
733
734fn swim_move(
736 data: &JoinData<'_>,
737 update: &mut StateUpdate,
738 efficiency: f32,
739 submersion: f32,
740) -> bool {
741 let efficiency = efficiency * data.stats.swim_speed_modifier * data.stats.friction_modifier;
742 if let Some(force) = data.body.swim_thrust() {
743 let force = efficiency * force * data.scale.map_or(1.0, |s| s.0);
744 let mut water_accel = force / data.mass.0;
745
746 if let Ok(level) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) {
747 let modifiers = SKILL_MODIFIERS.general_tree.swim;
748 water_accel *= modifiers.speed.powi(level.into());
749 }
750
751 let dir = if data.body.can_strafe() {
752 data.inputs.move_dir
753 } else {
754 let fw = Vec2::from(update.ori);
755 fw * data.inputs.move_dir.dot(fw).max(0.0)
756 };
757
758 let move_z = if submersion < 1.0
760 && data.inputs.move_z.abs() < f32::EPSILON
761 && data.physics.on_ground.is_none()
762 {
763 submersion.max(0.0) * 0.1
764 } else {
765 data.inputs.move_z
766 };
767
768 let move_z = move_z.min((submersion * 1.5 - 0.5).clamp(0.0, 1.0).powi(2));
770
771 update.vel.0 += Vec3::new(dir.x, dir.y, move_z)
772 * water_accel
776 * submersion.clamp(0.0, 1.0).sqrt()
778 * data.dt.0 * 0.04;
780
781 true
782 } else {
783 false
784 }
785}
786
787pub fn fly_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) -> bool {
789 let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
790
791 let glider = match data.character {
792 CharacterState::Glide(data) => Some(data),
793 _ => None,
794 };
795 if let Some(force) = data
796 .body
797 .fly_thrust()
798 .or_else(|| glider.is_some().then_some(0.0))
799 {
800 let thrust = efficiency * force;
801 let accel = thrust / data.mass.0;
802
803 match data.body {
804 Body::Ship(ship::Body::DefaultAirship) => {
805 handle_orientation(
809 data,
810 update,
811 efficiency * 2.0,
812 Some(data.controller.inputs.look_dir),
813 );
814 },
815 _ => {
816 handle_orientation(data, update, efficiency, None);
817 },
818 }
819
820 let mut update_fw_vel = true;
821 match data.body {
823 Body::Dragon(_) | Body::BirdLarge(_) | Body::BirdMedium(_) => {
825 let anti_grav = GRAVITY * (1.0 + data.inputs.move_z.min(0.0));
826 update.vel.0.z += data.dt.0 * (anti_grav + accel * data.inputs.move_z.max(0.0));
827 },
828 Body::Ship(ship::Body::DefaultAirship) => {
830 update_fw_vel = false;
831 let thrust_dir = data.inputs.move_dir.with_z(data.inputs.move_z);
838 update.vel.0 += thrust_dir * data.dt.0 * accel;
839 },
840 Body::Ship(ship) if ship.can_fly() => {
842 let regulate_density = |min: f32, max: f32, def: f32, rate: f32| -> Density {
847 let change = if data.inputs.move_z.abs() > f32::EPSILON {
849 -data.inputs.move_z
850 } else {
851 (def - data.density.0).clamp(-1.0, 1.0)
852 };
853 Density((update.density.0 + data.dt.0 * rate * change).clamp(min, max))
854 };
855 let def_density = ship.density().0;
856 if data.physics.in_liquid().is_some() {
857 let hull_density = ship.hull_density().0;
858 update.density.0 =
859 regulate_density(def_density * 0.6, hull_density, hull_density, 25.0).0;
860 } else {
861 update.density.0 =
862 regulate_density(def_density * 0.5, def_density * 1.5, def_density, 0.5).0;
863 };
864 },
865 _ => {},
868 };
869
870 if update_fw_vel {
871 update.vel.0 += Vec2::broadcast(data.dt.0)
872 * accel
873 * if data.body.can_strafe() {
874 data.inputs.move_dir
875 } else {
876 let fw = Vec2::from(update.ori);
877 fw * data.inputs.move_dir.dot(fw).max(0.0)
878 };
879 }
880 true
881 } else {
882 false
883 }
884}
885
886pub fn handle_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
889 if data.controller.queued_inputs.keys().any(|i| i.is_ability()) {
890 attempt_wield(data, update);
891 }
892}
893
894pub fn attempt_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
896 let equip_time = |equip_slot| {
899 data.inventory
900 .and_then(|inv| inv.equipped(equip_slot))
901 .and_then(|item| match &*item.kind() {
902 ItemKind::Tool(tool) => Some(Duration::from_secs_f32(
903 tool.stats(item.stats_durability_multiplier())
904 .equip_time_secs,
905 )),
906 _ => None,
907 })
908 };
909
910 let mainhand_equip_time = equip_time(EquipSlot::ActiveMainhand);
913 let offhand_equip_time = equip_time(EquipSlot::ActiveOffhand);
914 let equip_time = match (mainhand_equip_time, offhand_equip_time) {
915 (Some(a), Some(b)) => Some(a.max(b)),
916 (Some(a), None) | (None, Some(a)) => Some(a),
917 (None, None) => None,
918 };
919
920 if let Some(equip_time) = equip_time {
923 update.character = CharacterState::Equipping(equipping::Data {
924 static_data: equipping::StaticData {
925 buildup_duration: equip_time,
926 },
927 timer: Duration::default(),
928 is_sneaking: update.character.is_stealthy(),
929 });
930 } else {
931 update.character = CharacterState::Wielding(wielding::Data {
932 is_sneaking: update.character.is_stealthy(),
933 });
934 }
935}
936
937pub fn attempt_sit(data: &JoinData<'_>, update: &mut StateUpdate) {
939 if data.physics.on_ground.is_some() {
940 update.character = CharacterState::Sit;
941 }
942}
943
944pub fn attempt_crawl(data: &JoinData<'_>, update: &mut StateUpdate) {
946 if data.physics.on_ground.is_some() {
947 update.character = CharacterState::Crawl;
948 }
949}
950
951pub fn attempt_dance(data: &JoinData<'_>, update: &mut StateUpdate) {
952 if data.physics.on_ground.is_some() && data.body.is_humanoid() {
953 update.character = CharacterState::Dance;
954 }
955}
956
957pub fn can_perform_pet(position: Pos, target_position: Pos, target_alignment: Alignment) -> bool {
958 let within_distance = position.0.distance_squared(target_position.0) <= MAX_MOUNT_RANGE.powi(2);
959 let valid_alignment = matches!(target_alignment, Alignment::Owned(_) | Alignment::Tame);
960
961 within_distance && valid_alignment
962}
963
964pub fn attempt_talk(data: &JoinData<'_>, update: &mut StateUpdate) {
965 if data.physics.on_ground.is_some() {
966 update.character = CharacterState::Talk(Default::default());
967 }
968}
969
970pub fn attempt_sneak(data: &JoinData<'_>, update: &mut StateUpdate) {
971 if data.physics.on_ground.is_some() && data.body.is_humanoid() {
972 update.character = Idle(idle::Data {
973 is_sneaking: true,
974 time_entered: *data.time,
975 footwear: data.character.footwear(),
976 });
977 }
978}
979
980pub fn handle_climb(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
982 let Some(wall_dir) = data.physics.on_wall else {
983 return false;
984 };
985
986 let towards_wall = data.inputs.move_dir.dot(wall_dir.xy()) > 0.0;
987 let underwater = data
989 .physics
990 .in_liquid()
991 .map(|depth| depth > 2.0)
992 .unwrap_or(false);
993 let can_climb = data.body.can_climb() || data.physics.in_liquid().is_some();
994 let in_air = data.physics.on_ground.is_none();
995 if towards_wall && in_air && !underwater && can_climb && update.energy.current() > 1.0 {
996 update.character = CharacterState::Climb(
997 climb::Data::create_adjusted_by_skills(data)
998 .with_wielded(data.character.is_wield() || data.character.was_wielded()),
999 );
1000 true
1001 } else {
1002 false
1003 }
1004}
1005
1006pub fn handle_wallrun(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
1007 if data.physics.on_wall.is_some()
1008 && data.physics.on_ground.is_none()
1009 && data.physics.in_liquid().is_none()
1010 && data.body.can_climb()
1011 {
1012 update.character = CharacterState::Wallrun(wallrun::Data {
1013 was_wielded: data.character.is_wield() || data.character.was_wielded(),
1014 });
1015 true
1016 } else {
1017 false
1018 }
1019}
1020pub fn attempt_swap_equipped_weapons(
1022 data: &JoinData<'_>,
1023 update: &mut StateUpdate,
1024 output_events: &mut OutputEvents,
1025) {
1026 if data
1027 .inventory
1028 .and_then(|inv| inv.equipped(EquipSlot::InactiveMainhand))
1029 .is_some()
1030 || data
1031 .inventory
1032 .and_then(|inv| inv.equipped(EquipSlot::InactiveOffhand))
1033 .is_some()
1034 {
1035 update.swap_equipped_weapons = true;
1036 loadout_change_hook(data, output_events, false);
1037 }
1038}
1039
1040fn can_reach_block(
1042 player_pos: Vec3<f32>,
1043 block_pos: Vec3<i32>,
1044 range: f32,
1045 body: &Body,
1046 terrain: &TerrainGrid,
1047) -> bool {
1048 let block_pos_f32 = block_pos.map(|x| x as f32 + 0.5);
1049 let block_range_check = |pos: Vec3<f32>| {
1052 (block_pos_f32 - pos).magnitude_squared() < (range + body.max_radius()).powi(2)
1053 };
1054
1055 let close_to_block = block_range_check(player_pos)
1057 || block_range_check(player_pos + Vec3::new(0.0, 0.0, body.height()));
1058 if close_to_block {
1059 let iters = (3.0 * (block_pos_f32 - player_pos).map(|x| x.abs()).sum()) as usize;
1063 let heuristic = move |pos: &Vec3<i32>| (block_pos - pos).map(|x| x.abs()).sum() as f32;
1065
1066 let mut astar = Astar::new(
1067 iters,
1068 player_pos.map(|x| x.floor() as i32),
1069 BuildHasherDefault::<FxHasher64>::default(),
1070 );
1071
1072 let transition = |a: Vec3<i32>, b: Vec3<i32>| {
1075 let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
1076 ((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
1077 };
1078 let neighbors = |pos: &Vec3<i32>| {
1080 const DIRS: [Vec3<i32>; 6] = [
1081 Vec3::new(1, 0, 0),
1082 Vec3::new(-1, 0, 0),
1083 Vec3::new(0, 1, 0),
1084 Vec3::new(0, -1, 0),
1085 Vec3::new(0, 0, 1),
1086 Vec3::new(0, 0, -1),
1087 ];
1088 let pos = *pos;
1089 DIRS.iter()
1090 .map(move |dir| {
1091 let dest = dir + pos;
1092 (dest, transition(pos, dest))
1093 })
1094 .filter(|(pos, _)| {
1095 terrain
1096 .get(*pos)
1097 .ok()
1098 .is_some_and(|block| !block.is_filled())
1099 })
1100 };
1101 let satisfied = |pos: &Vec3<i32>| *pos == block_pos;
1103
1104 astar
1105 .poll(iters, heuristic, neighbors, satisfied)
1106 .into_path()
1107 .is_some()
1108 } else {
1109 false
1110 }
1111}
1112
1113pub fn handle_manipulate_loadout(
1115 data: &JoinData<'_>,
1116 output_events: &mut OutputEvents,
1117 update: &mut StateUpdate,
1118 inv_action: InventoryAction,
1119) {
1120 loadout_change_hook(data, output_events, true);
1121 match inv_action {
1122 InventoryAction::Use(slot @ Slot::Inventory(inv_slot)) => {
1123 use use_item::ItemUseKind;
1127 if let Some((item_kind, item)) = data
1128 .inventory
1129 .and_then(|inv| inv.get(inv_slot))
1130 .and_then(|item| Option::<ItemUseKind>::from(&*item.kind()).zip(Some(item)))
1131 {
1132 let (buildup_duration, use_duration, recover_duration) = item_kind.durations();
1133 update.character = CharacterState::UseItem(use_item::Data {
1135 static_data: use_item::StaticData {
1136 buildup_duration,
1137 use_duration,
1138 recover_duration,
1139 inv_slot,
1140 item_kind,
1141 item_hash: item.item_hash(),
1142 was_wielded: data.character.is_wield(),
1143 was_sneak: data.character.is_stealthy(),
1144 },
1145 timer: Duration::default(),
1146 stage_section: StageSection::Buildup,
1147 });
1148 } else {
1149 let inv_manip = InventoryManip::Use(slot);
1151 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1152 }
1153 },
1154 InventoryAction::Collect(sprite_pos) => {
1155 let sprite_at_pos = data
1157 .terrain
1158 .get(sprite_pos)
1159 .ok()
1160 .copied()
1161 .and_then(|b| b.get_sprite());
1162 let sprite_interact =
1165 sprite_at_pos.and_then(Option::<interact::SpriteInteractKind>::from);
1166 if let Some(sprite_interact) = sprite_interact {
1167 let sprite_chunk_pos = TerrainGrid::chunk_offs(sprite_pos);
1168 let sprite_cfg = data
1169 .terrain
1170 .pos_chunk(sprite_pos)
1171 .and_then(|chunk| chunk.meta().sprite_cfg_at(sprite_chunk_pos));
1172 if can_reach_block(
1173 data.pos.0,
1174 sprite_pos,
1175 MAX_PICKUP_RANGE,
1176 data.body,
1177 data.terrain,
1178 ) {
1179 let required_item =
1180 sprite_at_pos.and_then(|s| match s.unlock_condition(sprite_cfg.cloned()) {
1181 UnlockKind::Free => None,
1182 UnlockKind::Requires(item) => Some((item, false)),
1183 UnlockKind::Consumes(item) => Some((item, true)),
1184 });
1185 let has_required_items = match required_item {
1189 Some((item_id, consume)) => data
1191 .inventory
1192 .and_then(|inv| inv.get_slot_of_item_by_def_id(&item_id))
1193 .map(|slot| Some((item_id, slot, consume))),
1194 None => Some(None),
1195 };
1196 if let Some(required_item) = has_required_items {
1197 let (buildup_duration, use_duration, recover_duration) =
1202 sprite_interact.durations();
1203
1204 update.character = CharacterState::Interact(interact::Data {
1205 static_data: interact::StaticData {
1206 buildup_duration,
1207 use_duration: Some(use_duration),
1209 recover_duration,
1210 interact: interact::InteractKind::Sprite {
1211 pos: sprite_pos,
1212 kind: sprite_interact,
1213 },
1214 was_wielded: data.character.is_wield(),
1215 was_sneak: data.character.is_stealthy(),
1216 required_item,
1217 },
1218 timer: Duration::default(),
1219 stage_section: StageSection::Buildup,
1220 })
1221 } else {
1222 output_events.emit_local(LocalEvent::CreateOutcome(
1223 Outcome::FailedSpriteUnlock { pos: sprite_pos },
1224 ));
1225 }
1226 }
1227 }
1228 },
1229 InventoryAction::Swap(equip, slot) => {
1231 let inv_manip = InventoryManip::Swap(Slot::Equip(equip), slot);
1232 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1233 },
1234 InventoryAction::Drop(equip) => {
1235 let inv_manip = InventoryManip::Drop(Slot::Equip(equip));
1236 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1237 },
1238 InventoryAction::Sort => {
1239 output_events.emit_server(InventoryManipEvent(data.entity, InventoryManip::Sort));
1240 },
1241 InventoryAction::Use(slot @ Slot::Equip(_)) => {
1242 let inv_manip = InventoryManip::Use(slot);
1243 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1244 },
1245 InventoryAction::Use(Slot::Overflow(_)) => {
1246 },
1248 InventoryAction::ToggleSpriteLight(pos, enable) => {
1249 if matches!(pos.kind, Volume::Terrain) {
1250 let sprite_interact = interact::SpriteInteractKind::ToggleLight(enable);
1251
1252 let (buildup_duration, use_duration, recover_duration) =
1253 sprite_interact.durations();
1254
1255 update.character = CharacterState::Interact(interact::Data {
1256 static_data: interact::StaticData {
1257 buildup_duration,
1258 use_duration: Some(use_duration),
1259 recover_duration,
1260 interact: interact::InteractKind::Sprite {
1261 pos: pos.pos,
1262 kind: sprite_interact,
1263 },
1264 was_wielded: data.character.is_wield(),
1265 was_sneak: data.character.is_stealthy(),
1266 required_item: None,
1267 },
1268 timer: Duration::default(),
1269 stage_section: StageSection::Buildup,
1270 });
1271 }
1272 },
1273 }
1274}
1275
1276pub fn attempt_glide_wield(
1278 data: &JoinData<'_>,
1279 update: &mut StateUpdate,
1280 output_events: &mut OutputEvents,
1281) {
1282 if data
1283 .inventory
1284 .and_then(|inv| inv.equipped(EquipSlot::Glider))
1285 .is_some()
1286 && !data
1287 .physics
1288 .in_liquid()
1289 .map(|depth| depth > 1.0)
1290 .unwrap_or(false)
1291 && data.body.is_humanoid()
1292 && data.mount_data.is_none()
1293 && data.volume_mount_data.is_none()
1294 {
1295 output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Glider {
1296 pos: data.pos.0,
1297 wielded: true,
1298 }));
1299 update.character = CharacterState::GlideWield(glide_wield::Data::from(data));
1300 }
1301}
1302
1303pub fn handle_jump(
1305 data: &JoinData<'_>,
1306 output_events: &mut OutputEvents,
1307 _update: &mut StateUpdate,
1308 strength: f32,
1309) -> bool {
1310 input_is_pressed(data, InputKind::Jump)
1311 .then(|| data.body.jump_impulse())
1312 .flatten()
1313 .and_then(|impulse| {
1314 if data.physics.in_liquid().is_some() {
1315 if data.physics.on_wall.is_some() {
1316 Some(impulse * 0.75)
1319 } else {
1320 None
1321 }
1322 } else if data.physics.on_ground.is_some() {
1323 Some(impulse)
1324 } else {
1325 None
1326 }
1327 })
1328 .map(|impulse| {
1329 output_events.emit_local(LocalEvent::Jump(
1330 data.entity,
1331 strength * impulse / data.mass.0
1332 * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25))
1333 * data.stats.jump_modifier,
1334 ));
1335 })
1336 .is_some()
1337}
1338
1339pub fn handle_walljump(
1340 data: &JoinData<'_>,
1341 output_events: &mut OutputEvents,
1342 update: &mut StateUpdate,
1343 was_wielded: bool,
1344) -> bool {
1345 let Some(wall_dir) = data.physics.on_wall else {
1346 return false;
1347 };
1348 const WALL_JUMP_Z: f32 = 0.7;
1349 let look_dir = data.inputs.look_dir.vec();
1350
1351 let jump_dir = if look_dir.xy().dot(wall_dir.xy()) > 0.0 {
1353 look_dir.xy().reflected(-wall_dir.xy()).with_z(WALL_JUMP_Z)
1354 } else {
1355 *look_dir
1356 };
1357
1358 let jump_dir = if data.inputs.move_dir.dot(-wall_dir.xy()) > 0.0 {
1360 data.inputs.move_dir.with_z(WALL_JUMP_Z)
1361 } else {
1362 jump_dir
1363 };
1364
1365 let jump_dir = if jump_dir.xy().iter().all(|e| *e < 0.001) {
1367 jump_dir - wall_dir.xy() * 0.1
1368 } else {
1369 jump_dir
1370 }
1371 .try_normalized()
1372 .unwrap_or(Vec3::zero());
1373
1374 if let Some(jump_impulse) = data.body.jump_impulse() {
1375 update.ori = update
1377 .ori
1378 .slerped_towards(Ori::from(Dir::new(jump_dir)), 20.0);
1379 const WALL_JUMP_FACTOR: f32 = 1.1;
1381 output_events.emit_local(LocalEvent::ApplyImpulse {
1383 entity: data.entity,
1384 impulse: jump_dir * WALL_JUMP_FACTOR * jump_impulse / data.mass.0
1385 * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25)),
1386 });
1387 }
1388 if was_wielded {
1389 update.character = CharacterState::Wielding(wielding::Data { is_sneaking: false });
1390 } else {
1391 update.character = CharacterState::Idle(idle::Data::default());
1392 }
1393 true
1394}
1395
1396fn handle_ability(
1397 data: &JoinData<'_>,
1398 update: &mut StateUpdate,
1399 output_events: &mut OutputEvents,
1400 input: InputKind,
1401) -> bool {
1402 let context = AbilityContext::from(data.stance, data.inventory, data.combo);
1403 if let Some(ability_input) = input.into() {
1404 if let Some((ability, from_offhand, spec_ability)) = data
1405 .active_abilities
1406 .and_then(|a| {
1407 a.activate_ability(
1408 ability_input,
1409 data.inventory,
1410 data.skill_set,
1411 Some(data.body),
1412 Some(data.character),
1413 &context,
1414 Some(data.stats),
1415 )
1416 })
1417 .map(|(mut a, f, s)| {
1418 if let Some(contextual_stats) = a.ability_meta().contextual_stats {
1419 a = a.adjusted_by_stats(contextual_stats.equivalent_stats(data))
1420 }
1421 (a, f, s)
1422 })
1423 .filter(|(ability, _, _)| ability.requirements_paid(data, update))
1424 {
1425 match CharacterState::try_from((
1426 &ability,
1427 AbilityInfo::new(
1428 data,
1429 from_offhand,
1430 input,
1431 Some(spec_ability),
1432 ability.ability_meta(),
1433 ),
1434 data,
1435 )) {
1436 Ok(character_state) => {
1437 update.character = character_state;
1438
1439 if let Some(init_event) = ability.ability_meta().init_event {
1440 match init_event {
1441 AbilityInitEvent::EnterStance(stance) => {
1442 output_events.emit_server(ChangeStanceEvent {
1443 entity: data.entity,
1444 stance,
1445 });
1446 },
1447 AbilityInitEvent::GainBuff {
1448 kind,
1449 strength,
1450 duration,
1451 } => {
1452 let dest_info = DestInfo {
1453 stats: Some(data.stats),
1454 mass: Some(data.mass),
1455 };
1456 output_events.emit_server(BuffEvent {
1457 entity: data.entity,
1458 buff_change: BuffChange::Add(Buff::new(
1459 kind,
1460 BuffData::new(strength, duration),
1461 vec![BuffCategory::SelfBuff],
1462 BuffSource::Character { by: *data.uid },
1463 *data.time,
1464 dest_info,
1465 Some(data.mass),
1466 )),
1467 });
1468 },
1469 }
1470 }
1471 if let CharacterState::Roll(roll) = &mut update.character {
1472 if data.character.is_wield() || data.character.was_wielded() {
1473 roll.was_wielded = true;
1474 }
1475 if data.character.is_stealthy() {
1476 roll.is_sneaking = true;
1477 }
1478 if data.character.is_aimed() {
1479 roll.prev_aimed_dir = Some(data.controller.inputs.look_dir);
1480 }
1481 }
1482 return true;
1483 },
1484 Err(err) => {
1485 warn!("Failed to enter character state: {err:?}");
1486 },
1487 }
1488 }
1489 }
1490 false
1491}
1492
1493pub fn handle_input(
1494 data: &JoinData<'_>,
1495 output_events: &mut OutputEvents,
1496 update: &mut StateUpdate,
1497 input: InputKind,
1498) {
1499 match input {
1500 InputKind::Primary
1501 | InputKind::Secondary
1502 | InputKind::Ability(_)
1503 | InputKind::Block
1504 | InputKind::Roll => {
1505 handle_ability(data, update, output_events, input);
1506 },
1507 InputKind::Jump => {
1508 handle_jump(data, output_events, update, 1.0);
1509 },
1510 InputKind::Fly => {},
1511 }
1512}
1513
1514pub fn handle_glider_input_or(
1518 data: &JoinData<'_>,
1519 update: &mut StateUpdate,
1520 output_events: &mut OutputEvents,
1521 fallback_fn: fn(&JoinData<'_>, &mut StateUpdate),
1522) {
1523 if data
1524 .inventory
1525 .and_then(|inv| inv.equipped(EquipSlot::Glider))
1526 .and_then(|glider| glider.item_config())
1527 .is_none()
1528 {
1529 fallback_fn(data, update);
1530 return;
1531 };
1532
1533 if let Some(input) = data.controller.queued_inputs.keys().next() {
1534 handle_ability(data, update, output_events, *input);
1535 };
1536}
1537
1538pub fn attempt_input(
1539 data: &JoinData<'_>,
1540 output_events: &mut OutputEvents,
1541 update: &mut StateUpdate,
1542) {
1543 if let Some(input) = data.controller.queued_inputs.keys().next() {
1545 handle_input(data, output_events, update, *input);
1546 }
1547}
1548
1549pub fn handle_interrupts(
1551 data: &JoinData,
1552 update: &mut StateUpdate,
1553 output_events: &mut OutputEvents,
1554) -> bool {
1555 let can_dodge = matches!(
1556 data.character.stage_section(),
1557 Some(StageSection::Buildup | StageSection::Recover)
1558 );
1559 let can_block = data
1560 .character
1561 .ability_info()
1562 .map(|info| info.ability_meta)
1563 .is_some_and(|meta| meta.capabilities.contains(Capability::BLOCK_INTERRUPT));
1564 if can_dodge && input_is_pressed(data, InputKind::Roll) {
1565 handle_ability(data, update, output_events, InputKind::Roll)
1566 } else if can_block && input_is_pressed(data, InputKind::Block) {
1567 handle_ability(data, update, output_events, InputKind::Block)
1568 } else {
1569 false
1570 }
1571}
1572
1573pub fn is_strafing(data: &JoinData<'_>, update: &StateUpdate) -> bool {
1574 (update.character.is_aimed() || update.should_strafe) && data.body.can_strafe()
1577 && !matches!(unwrap_tool_data(data, EquipSlot::ActiveMainhand),
1579 Some((ToolKind::Instrument, _)))
1580}
1581
1582pub fn unwrap_tool_data(data: &JoinData, equip_slot: EquipSlot) -> Option<(ToolKind, Hands)> {
1584 if let Some(ItemKind::Tool(tool)) = data
1585 .inventory
1586 .and_then(|inv| inv.equipped(equip_slot))
1587 .map(|i| i.kind())
1588 .as_deref()
1589 {
1590 Some((tool.kind, tool.hands))
1591 } else {
1592 None
1593 }
1594}
1595
1596pub fn get_hands(data: &JoinData<'_>) -> (Option<Hands>, Option<Hands>) {
1597 let hand = |slot| {
1598 if let Some(ItemKind::Tool(tool)) = data
1599 .inventory
1600 .and_then(|inv| inv.equipped(slot))
1601 .map(|i| i.kind())
1602 .as_deref()
1603 {
1604 Some(tool.hands)
1605 } else {
1606 None
1607 }
1608 };
1609 (
1610 hand(EquipSlot::ActiveMainhand),
1611 hand(EquipSlot::ActiveOffhand),
1612 )
1613}
1614
1615pub fn get_tool_stats(data: &JoinData<'_>, ai: AbilityInfo) -> tool::Stats {
1616 ai.hand
1617 .map(|hand| hand.to_equip_slot())
1618 .and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
1619 .and_then(|item| {
1620 if let ItemKind::Tool(tool) = &*item.kind() {
1621 Some(tool.stats(item.stats_durability_multiplier()))
1622 } else {
1623 None
1624 }
1625 })
1626 .unwrap_or(tool::Stats::one())
1627}
1628
1629pub fn input_is_pressed(data: &JoinData<'_>, input: InputKind) -> bool {
1630 data.controller.queued_inputs.contains_key(&input)
1631}
1632
1633fn checked_tick(data: &JoinData<'_>, timer: Duration, modifier: Option<f32>) -> Option<Duration> {
1637 timer.checked_add(Duration::from_secs_f32(data.dt.0 * modifier.unwrap_or(1.0)))
1638}
1639
1640pub fn tick_or_default(data: &JoinData<'_>, timer: Duration, modifier: Option<f32>) -> Duration {
1643 checked_tick(data, timer, modifier).unwrap_or_default()
1644}
1645
1646fn checked_tick_attack(
1650 data: &JoinData<'_>,
1651 timer: Duration,
1652 other_modifier: Option<f32>,
1653) -> Option<Duration> {
1654 checked_tick(
1655 data,
1656 timer,
1657 Some(data.stats.attack_speed_modifier * other_modifier.unwrap_or(1.0)),
1658 )
1659}
1660
1661pub fn tick_attack_or_default(
1664 data: &JoinData<'_>,
1665 timer: Duration,
1666 other_modifier: Option<f32>,
1667) -> Duration {
1668 checked_tick_attack(data, timer, other_modifier).unwrap_or_default()
1669}
1670
1671#[derive(Clone, Copy, Debug, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
1675pub enum StageSection {
1676 Buildup,
1677 Recover,
1678 Charge,
1679 Movement,
1680 Action,
1681}
1682
1683#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1684pub enum ForcedMovement {
1685 Forward(f32),
1686 Reverse(f32),
1687 Sideways(f32),
1688 DirectedReverse(f32),
1689 AntiDirectedForward(f32),
1690 Leap {
1691 vertical: f32,
1692 forward: f32,
1693 progress: f32,
1694 direction: MovementDirection,
1695 },
1696 Hover {
1697 move_input: f32,
1698 },
1699}
1700
1701impl Mul<f32> for ForcedMovement {
1702 type Output = Self;
1703
1704 fn mul(self, scalar: f32) -> Self {
1705 use ForcedMovement::*;
1706 match self {
1707 Forward(x) => Forward(x * scalar),
1708 Reverse(x) => Reverse(x * scalar),
1709 Sideways(x) => Sideways(x * scalar),
1710 DirectedReverse(x) => DirectedReverse(x * scalar),
1711 AntiDirectedForward(x) => AntiDirectedForward(x * scalar),
1712 Leap {
1713 vertical,
1714 forward,
1715 progress,
1716 direction,
1717 } => Leap {
1718 vertical: vertical * scalar,
1719 forward: forward * scalar,
1720 progress,
1721 direction,
1722 },
1723 Hover { move_input } => Hover { move_input },
1724 }
1725 }
1726}
1727
1728#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1729pub enum MovementDirection {
1730 Look,
1731 Move,
1732}
1733
1734impl MovementDirection {
1735 pub fn get_2d_dir(self, data: &JoinData<'_>) -> Vec2<f32> {
1736 use MovementDirection::*;
1737 match self {
1738 Look => data
1739 .inputs
1740 .look_dir
1741 .to_horizontal()
1742 .unwrap_or_default()
1743 .xy(),
1744 Move => data.inputs.move_dir,
1745 }
1746 .try_normalized()
1747 .unwrap_or_default()
1748 }
1749}
1750
1751#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1752pub struct AbilityInfo {
1753 pub tool: Option<ToolKind>,
1754 pub hand: Option<HandInfo>,
1755 pub input: InputKind,
1756 pub input_attr: Option<InputAttr>,
1757 pub ability_meta: AbilityMeta,
1758 pub ability: Option<SpecifiedAbility>,
1759}
1760
1761impl AbilityInfo {
1762 pub fn new(
1763 data: &JoinData<'_>,
1764 from_offhand: bool,
1765 input: InputKind,
1766 ability: Option<SpecifiedAbility>,
1767 ability_meta: AbilityMeta,
1768 ) -> Self {
1769 let tool_data = if from_offhand {
1770 unwrap_tool_data(data, EquipSlot::ActiveOffhand)
1771 } else {
1772 unwrap_tool_data(data, EquipSlot::ActiveMainhand)
1773 };
1774 let (tool, hand) = tool_data.map_or((None, None), |(kind, hands)| {
1775 (
1776 Some(kind),
1777 Some(HandInfo::from_main_tool(hands, from_offhand)),
1778 )
1779 });
1780
1781 Self {
1782 tool,
1783 hand,
1784 input,
1785 input_attr: data.controller.queued_inputs.get(&input).copied(),
1786 ability_meta,
1787 ability,
1788 }
1789 }
1790}
1791
1792pub fn end_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
1793 if data.character.is_wield() || data.character.was_wielded() {
1794 update.character = CharacterState::Wielding(wielding::Data {
1795 is_sneaking: data.character.is_stealthy(),
1796 });
1797 } else {
1798 update.character = CharacterState::Idle(idle::Data {
1799 is_sneaking: data.character.is_stealthy(),
1800 footwear: None,
1801 time_entered: *data.time,
1802 });
1803 }
1804 if let CharacterState::Roll(roll) = data.character {
1805 if let Some(dir) = roll.prev_aimed_dir {
1806 update.ori = dir.into();
1807 }
1808 }
1809}
1810
1811pub fn end_melee_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
1812 end_ability(data, update);
1813 data.updater.remove::<Melee>(data.entity);
1814}
1815
1816#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1817pub enum HandInfo {
1818 TwoHanded,
1819 MainHand,
1820 OffHand,
1821}
1822
1823impl HandInfo {
1824 pub fn from_main_tool(tool_hands: Hands, from_offhand: bool) -> Self {
1825 match tool_hands {
1826 Hands::Two => Self::TwoHanded,
1827 Hands::One => {
1828 if from_offhand {
1829 Self::OffHand
1830 } else {
1831 Self::MainHand
1832 }
1833 },
1834 }
1835 }
1836
1837 pub fn to_equip_slot(&self) -> EquipSlot {
1838 match self {
1839 HandInfo::TwoHanded | HandInfo::MainHand => EquipSlot::ActiveMainhand,
1840 HandInfo::OffHand => EquipSlot::ActiveOffhand,
1841 }
1842 }
1843}
1844
1845pub fn leave_stance(data: &JoinData<'_>, output_events: &mut OutputEvents) {
1846 if !matches!(data.stance, Some(Stance::None)) {
1847 output_events.emit_server(ChangeStanceEvent {
1848 entity: data.entity,
1849 stance: Stance::None,
1850 });
1851 }
1852}
1853
1854#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1855pub enum ScalingKind {
1856 Linear,
1859 Sqrt,
1861}
1862
1863impl ScalingKind {
1864 pub fn factor(&self, val: f32, norm: f32) -> f32 {
1865 match self {
1866 Self::Linear => val / norm,
1867 Self::Sqrt => (val / norm).sqrt(),
1868 }
1869 }
1870}
1871
1872#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
1873pub enum ComboConsumption {
1874 #[default]
1875 All,
1876 Half,
1877 Cost,
1878}
1879
1880impl ComboConsumption {
1881 pub fn consume(&self, data: &JoinData, output_events: &mut OutputEvents, cost: u32) {
1882 let combo = data.combo.map_or(0, |c| c.counter());
1883 let to_consume = match self {
1884 Self::All => combo,
1885 Self::Half => combo.div_ceil(2),
1886 Self::Cost => cost,
1887 };
1888 output_events.emit_server(ComboChangeEvent {
1889 entity: data.entity,
1890 change: -(to_consume as i32),
1891 });
1892 }
1893}
1894
1895fn loadout_change_hook(data: &JoinData<'_>, output_events: &mut OutputEvents, clear_combo: bool) {
1896 if clear_combo {
1897 output_events.emit_server(ComboChangeEvent {
1899 entity: data.entity,
1900 change: -data.combo.map_or(0, |c| c.counter() as i32),
1901 });
1902 }
1903 output_events.emit_server(BuffEvent {
1905 entity: data.entity,
1906 buff_change: BuffChange::RemoveByCategory {
1907 all_required: vec![BuffCategory::RemoveOnLoadoutChange],
1908 any_required: vec![],
1909 none_required: vec![],
1910 },
1911 });
1912}
1913
1914#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1915#[serde(deny_unknown_fields)]
1916pub struct MovementModifier {
1917 pub buildup: Option<f32>,
1918 pub swing: Option<f32>,
1919 pub recover: Option<f32>,
1920}
1921
1922#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1923#[serde(deny_unknown_fields)]
1924pub struct OrientationModifier {
1925 pub buildup: Option<f32>,
1926 pub swing: Option<f32>,
1927 pub recover: Option<f32>,
1928}