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