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