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