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(body) => 2.65 / body.scaler(),
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 angle_factor =
762 2.0 / (1.0 - update.ori.dot(target_ori) * (1.0 - data.body.ori_damping())).sqrt();
763
764 half_turns_per_tick * angle_factor
765 };
766 update
767 .ori
768 .slerped_towards(target_ori, target_fraction.min(1.0))
769 };
770}
771
772fn swim_move(
774 data: &JoinData<'_>,
775 update: &mut StateUpdate,
776 efficiency: f32,
777 submersion: f32,
778) -> bool {
779 let efficiency = efficiency * data.stats.swim_speed_modifier * data.stats.friction_modifier;
780 if let Some(force) = data.body.swim_thrust() {
781 let force = efficiency * force * data.scale.map_or(1.0, |s| s.0);
782 let mut water_accel = force / data.mass.0;
783
784 if let Ok(level) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) {
785 let modifiers = SKILL_MODIFIERS.general_tree.swim;
786 water_accel *= modifiers.speed.powi(level.into());
787 }
788
789 let dir = if data.body.can_strafe() {
790 data.inputs.move_dir
791 } else {
792 let fw = Vec2::from(update.ori);
793 fw * data.inputs.move_dir.dot(fw).max(0.0)
794 };
795
796 let move_z = if submersion < 1.0
798 && data.inputs.move_z.abs() < f32::EPSILON
799 && data.physics.on_ground.is_none()
800 {
801 submersion.max(0.0) * 0.1
802 } else {
803 data.inputs.move_z
804 };
805
806 let move_z = move_z.min((submersion * 1.5 - 0.5).clamp(0.0, 1.0).powi(2));
808
809 update.vel.0 += Vec3::new(dir.x, dir.y, move_z)
810 * water_accel
814 * submersion.clamp(0.0, 1.0).sqrt()
816 * data.dt.0 * 0.04;
818
819 true
820 } else {
821 false
822 }
823}
824
825pub fn fly_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) -> bool {
827 let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
828
829 let glider = match data.character {
830 CharacterState::Glide(data) => Some(data),
831 _ => None,
832 };
833 if let Some(force) = data
834 .body
835 .fly_thrust()
836 .or_else(|| glider.is_some().then_some(0.0))
837 {
838 let thrust = efficiency * force;
839 let accel = thrust / data.mass.0;
840
841 match data.body {
842 Body::Ship(ship::Body::DefaultAirship) => {
843 handle_orientation(
847 data,
848 update,
849 efficiency * 2.0,
850 Some(data.controller.inputs.look_dir),
851 );
852 },
853 _ => {
854 handle_orientation(data, update, efficiency, None);
855 },
856 }
857
858 let mut update_fw_vel = true;
859 match data.body {
861 Body::Dragon(_) | Body::BirdLarge(_) | Body::BirdMedium(_) => {
863 let anti_grav = GRAVITY * (1.0 + data.inputs.move_z.min(0.0));
864 update.vel.0.z += data.dt.0 * (anti_grav + accel * data.inputs.move_z.max(0.0));
865 },
866 Body::Ship(ship::Body::DefaultAirship) => {
868 update_fw_vel = false;
869 let thrust_dir = data.inputs.move_dir.with_z(data.inputs.move_z);
876 update.vel.0 += thrust_dir * data.dt.0 * accel;
877 },
878 Body::Ship(ship) if ship.can_fly() => {
880 let regulate_density = |min: f32, max: f32, def: f32, rate: f32| -> Density {
885 let change = if data.inputs.move_z.abs() > f32::EPSILON {
887 -data.inputs.move_z
888 } else {
889 (def - data.density.0).clamp(-1.0, 1.0)
890 };
891 Density((update.density.0 + data.dt.0 * rate * change).clamp(min, max))
892 };
893 let def_density = ship.density().0;
894 if data.physics.in_liquid().is_some() {
895 let hull_density = ship.hull_density().0;
896 update.density.0 =
897 regulate_density(def_density * 0.6, hull_density, hull_density, 25.0).0;
898 } else {
899 update.density.0 =
900 regulate_density(def_density * 0.5, def_density * 1.5, def_density, 0.5).0;
901 };
902 },
903 _ => {},
906 };
907
908 if update_fw_vel {
909 update.vel.0 += Vec2::broadcast(data.dt.0)
910 * accel
911 * if data.body.can_strafe() {
912 data.inputs.move_dir
913 } else {
914 let fw = Vec2::from(update.ori);
915 fw * data.inputs.move_dir.dot(fw).max(0.0)
916 };
917 }
918 true
919 } else {
920 false
921 }
922}
923
924pub fn handle_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
927 if data.controller.queued_inputs.keys().any(|i| i.is_ability()) {
928 attempt_wield(data, update);
929 }
930}
931
932pub fn attempt_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
934 let equip_time = |equip_slot| {
937 data.inventory
938 .and_then(|inv| inv.equipped(equip_slot))
939 .and_then(|item| match &*item.kind() {
940 ItemKind::Tool(tool) => Some(Duration::from_secs_f32(
941 tool.stats(item.stats_durability_multiplier())
942 .equip_time_secs,
943 )),
944 _ => None,
945 })
946 };
947
948 let mainhand_equip_time = equip_time(EquipSlot::ActiveMainhand);
951 let offhand_equip_time = equip_time(EquipSlot::ActiveOffhand);
952 let equip_time = match (mainhand_equip_time, offhand_equip_time) {
953 (Some(a), Some(b)) => Some(a.max(b)),
954 (Some(a), None) | (None, Some(a)) => Some(a),
955 (None, None) => None,
956 };
957
958 if let Some(equip_time) = equip_time {
961 update.character = CharacterState::Equipping(equipping::Data {
962 static_data: equipping::StaticData {
963 buildup_duration: equip_time,
964 },
965 timer: Duration::default(),
966 is_sneaking: update.character.is_stealthy(),
967 });
968 } else {
969 update.character = CharacterState::Wielding(wielding::Data {
970 is_sneaking: update.character.is_stealthy(),
971 });
972 }
973}
974
975pub fn attempt_sit(data: &JoinData<'_>, update: &mut StateUpdate) {
977 if data.physics.on_ground.is_some() {
978 update.character = CharacterState::Sit;
979 }
980}
981
982pub fn attempt_crawl(data: &JoinData<'_>, update: &mut StateUpdate) {
984 if data.physics.on_ground.is_some() {
985 update.character = CharacterState::Crawl;
986 }
987}
988
989pub fn attempt_dance(data: &JoinData<'_>, update: &mut StateUpdate) {
990 if data.physics.on_ground.is_some() && data.body.is_humanoid() {
991 update.character = CharacterState::Dance;
992 }
993}
994
995pub fn can_perform_pet(position: Pos, target_position: Pos, target_alignment: Alignment) -> bool {
996 let within_distance = position.0.distance_squared(target_position.0) <= MAX_MOUNT_RANGE.powi(2);
997 let valid_alignment = matches!(target_alignment, Alignment::Owned(_) | Alignment::Tame);
998
999 within_distance && valid_alignment
1000}
1001
1002pub fn attempt_talk(data: &JoinData<'_>, update: &mut StateUpdate, tgt: Option<Uid>) {
1003 if data.physics.on_ground.is_some() {
1004 update.character = CharacterState::Talk(match update.character {
1005 CharacterState::Talk(t) if t.tgt == tgt => t.refreshed(),
1006 _ => talk::Data::at(tgt),
1007 });
1008 }
1009}
1010
1011pub fn attempt_sneak(data: &JoinData<'_>, update: &mut StateUpdate) {
1012 if data.physics.on_ground.is_some() && data.body.is_humanoid() {
1013 update.character = Idle(idle::Data {
1014 is_sneaking: true,
1015 time_entered: *data.time,
1016 footwear: data.character.footwear(),
1017 });
1018 }
1019}
1020
1021pub fn handle_climb(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
1023 let Some(wall_dir) = data.physics.on_wall else {
1024 return false;
1025 };
1026
1027 let towards_wall = data.inputs.move_dir.dot(wall_dir.xy()) > 0.0;
1028 let underwater = data
1030 .physics
1031 .in_liquid()
1032 .map(|depth| depth > 2.0)
1033 .unwrap_or(false);
1034 let can_climb = data.body.can_climb() || data.physics.in_liquid().is_some();
1035 let in_air = data.physics.on_ground.is_none();
1036 if towards_wall && in_air && !underwater && can_climb && update.energy.current() > 1.0 {
1037 update.character = CharacterState::Climb(
1038 climb::Data::create_adjusted_by_skills(data)
1039 .with_wielded(data.character.is_wield() || data.character.was_wielded()),
1040 );
1041 true
1042 } else {
1043 false
1044 }
1045}
1046
1047pub fn handle_wallrun(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
1048 if data.physics.on_wall.is_some()
1049 && data.physics.on_ground.is_none()
1050 && data.physics.in_liquid().is_none()
1051 && data.body.can_climb()
1052 {
1053 update.character = CharacterState::Wallrun(wallrun::Data {
1054 was_wielded: data.character.is_wield() || data.character.was_wielded(),
1055 });
1056 true
1057 } else {
1058 false
1059 }
1060}
1061pub fn attempt_swap_equipped_weapons(
1063 data: &JoinData<'_>,
1064 update: &mut StateUpdate,
1065 output_events: &mut OutputEvents,
1066) {
1067 if data
1068 .inventory
1069 .and_then(|inv| inv.equipped(EquipSlot::InactiveMainhand))
1070 .is_some()
1071 || data
1072 .inventory
1073 .and_then(|inv| inv.equipped(EquipSlot::InactiveOffhand))
1074 .is_some()
1075 {
1076 update.swap_equipped_weapons = true;
1077 loadout_change_hook(data, output_events, false);
1078 }
1079}
1080
1081fn can_reach_block(
1083 player_pos: Vec3<f32>,
1084 block_pos: Vec3<i32>,
1085 range: f32,
1086 body: &Body,
1087 terrain: &TerrainGrid,
1088) -> bool {
1089 let block_pos_f32 = block_pos.map(|x| x as f32 + 0.5);
1090 let block_range_check = |pos: Vec3<f32>| {
1093 (block_pos_f32 - pos).magnitude_squared() < (range + body.max_radius()).powi(2)
1094 };
1095
1096 let close_to_block = block_range_check(player_pos)
1098 || block_range_check(player_pos + Vec3::new(0.0, 0.0, body.height()));
1099 if close_to_block {
1100 let iters = (3.0 * (block_pos_f32 - player_pos).map(|x| x.abs()).sum()) as usize;
1104 let heuristic = move |pos: &Vec3<i32>| (block_pos - pos).map(|x| x.abs()).sum() as f32;
1106
1107 let mut astar = Astar::new(
1108 iters,
1109 player_pos.map(|x| x.floor() as i32),
1110 BuildHasherDefault::<FxHasher64>::default(),
1111 );
1112
1113 let transition = |a: Vec3<i32>, b: Vec3<i32>| {
1116 let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
1117 ((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
1118 };
1119 let neighbors = |pos: &Vec3<i32>| {
1121 const DIRS: [Vec3<i32>; 6] = [
1122 Vec3::new(1, 0, 0),
1123 Vec3::new(-1, 0, 0),
1124 Vec3::new(0, 1, 0),
1125 Vec3::new(0, -1, 0),
1126 Vec3::new(0, 0, 1),
1127 Vec3::new(0, 0, -1),
1128 ];
1129 let pos = *pos;
1130 DIRS.iter()
1131 .map(move |dir| {
1132 let dest = dir + pos;
1133 (dest, transition(pos, dest))
1134 })
1135 .filter(|(pos, _)| {
1136 terrain
1137 .get(*pos)
1138 .ok()
1139 .is_some_and(|block| !block.is_filled())
1140 })
1141 };
1142 let satisfied = |pos: &Vec3<i32>| *pos == block_pos;
1144
1145 astar
1146 .poll(iters, heuristic, neighbors, satisfied)
1147 .into_path()
1148 .is_some()
1149 } else {
1150 false
1151 }
1152}
1153
1154pub fn handle_manipulate_loadout(
1156 data: &JoinData<'_>,
1157 output_events: &mut OutputEvents,
1158 update: &mut StateUpdate,
1159 inv_action: InventoryAction,
1160) {
1161 if !matches!(inv_action, InventoryAction::Collect(_)) {
1164 loadout_change_hook(data, output_events, true);
1165 }
1166 match inv_action {
1167 InventoryAction::Use(slot @ Slot::Inventory(inv_slot)) => {
1168 use use_item::ItemUseKind;
1172 if let Some((item_kind, item)) = data
1173 .inventory
1174 .and_then(|inv| inv.get(inv_slot))
1175 .and_then(|item| Option::<ItemUseKind>::from(&*item.kind()).zip(Some(item)))
1176 {
1177 let (buildup_duration, use_duration, recover_duration) = item_kind.durations();
1178 update.character = CharacterState::UseItem(use_item::Data {
1180 static_data: use_item::StaticData {
1181 buildup_duration,
1182 use_duration,
1183 recover_duration,
1184 inv_slot,
1185 item_kind,
1186 item_hash: item.item_hash(),
1187 was_wielded: data.character.is_wield(),
1188 was_sneak: data.character.is_stealthy(),
1189 },
1190 timer: Duration::default(),
1191 stage_section: StageSection::Buildup,
1192 });
1193 } else {
1194 let inv_manip = InventoryManip::Use(slot);
1196 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1197 }
1198 },
1199 InventoryAction::Collect(sprite_pos) => {
1200 let sprite_at_pos = data
1202 .terrain
1203 .get(sprite_pos)
1204 .ok()
1205 .copied()
1206 .and_then(|b| b.get_sprite());
1207 let sprite_interact =
1210 sprite_at_pos.and_then(Option::<interact::SpriteInteractKind>::from);
1211 if let Some(sprite_interact) = sprite_interact
1212 && can_reach_block(
1213 data.pos.0,
1214 sprite_pos,
1215 MAX_PICKUP_RANGE,
1216 data.body,
1217 data.terrain,
1218 )
1219 {
1220 let sprite_cfg = data.terrain.sprite_cfg_at(sprite_pos);
1221 let required_item = sprite_at_pos.and_then(|s| {
1222 s.unlock_condition(sprite_cfg)
1223 .and_then(|unlock| match unlock.into_owned() {
1224 UnlockKind::Free => None,
1225 UnlockKind::Requires(item) => Some((item, false)),
1226 UnlockKind::Consumes(item) => Some((item, true)),
1227 })
1228 });
1229 let has_required_items = match required_item {
1233 Some((item_id, consume)) => data
1235 .inventory
1236 .and_then(|inv| inv.get_slot_of_item_by_def_id(&item_id))
1237 .map(|slot| Some((item_id, slot, consume))),
1238 None => Some(None),
1239 };
1240 if let Some(required_item) = has_required_items {
1241 let (buildup_duration, use_duration, recover_duration) =
1246 sprite_interact.durations();
1247
1248 update.character = CharacterState::Interact(interact::Data {
1249 static_data: interact::StaticData {
1250 buildup_duration,
1251 use_duration: Some(use_duration),
1253 recover_duration,
1254 interact: interact::InteractKind::Sprite {
1255 pos: sprite_pos,
1256 kind: sprite_interact,
1257 },
1258 was_wielded: data.character.is_wield(),
1259 was_sneak: data.character.is_stealthy(),
1260 required_item,
1261 },
1262 timer: Duration::default(),
1263 stage_section: StageSection::Buildup,
1264 })
1265 } else {
1266 output_events.emit_local(LocalEvent::CreateOutcome(
1267 Outcome::FailedSpriteUnlock { pos: sprite_pos },
1268 ));
1269 }
1270 }
1271 },
1272 InventoryAction::Swap(equip, slot) => {
1274 let inv_manip = InventoryManip::Swap(Slot::Equip(equip), slot);
1275 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1276 },
1277 InventoryAction::Drop(equip) => {
1278 let inv_manip = InventoryManip::Drop(Slot::Equip(equip));
1279 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1280 },
1281 InventoryAction::Sort(sort_order) => {
1282 output_events.emit_server(InventoryManipEvent(
1283 data.entity,
1284 InventoryManip::Sort(sort_order),
1285 ));
1286 },
1287 InventoryAction::Use(slot @ Slot::Equip(_)) => {
1288 let inv_manip = InventoryManip::Use(slot);
1289 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1290 },
1291 InventoryAction::Use(Slot::Overflow(_)) => {
1292 },
1294 InventoryAction::ToggleSpriteLight(pos, enable) => {
1295 if matches!(pos.kind, Volume::Terrain) {
1296 let sprite_interact = interact::SpriteInteractKind::ToggleLight(enable);
1297
1298 let (buildup_duration, use_duration, recover_duration) =
1299 sprite_interact.durations();
1300
1301 update.character = CharacterState::Interact(interact::Data {
1302 static_data: interact::StaticData {
1303 buildup_duration,
1304 use_duration: Some(use_duration),
1305 recover_duration,
1306 interact: interact::InteractKind::Sprite {
1307 pos: pos.pos,
1308 kind: sprite_interact,
1309 },
1310 was_wielded: data.character.is_wield(),
1311 was_sneak: data.character.is_stealthy(),
1312 required_item: None,
1313 },
1314 timer: Duration::default(),
1315 stage_section: StageSection::Buildup,
1316 });
1317 }
1318 },
1319 }
1320}
1321
1322pub fn attempt_glide_wield(
1324 data: &JoinData<'_>,
1325 update: &mut StateUpdate,
1326 output_events: &mut OutputEvents,
1327) {
1328 if data
1329 .inventory
1330 .and_then(|inv| inv.equipped(EquipSlot::Glider))
1331 .is_some()
1332 && !data
1333 .physics
1334 .in_liquid()
1335 .map(|depth| depth > 1.0)
1336 .unwrap_or(false)
1337 && data.body.is_humanoid()
1338 && data.mount_data.is_none()
1339 && data.volume_mount_data.is_none()
1340 {
1341 output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Glider {
1342 pos: data.pos.0,
1343 wielded: true,
1344 }));
1345 update.character = CharacterState::GlideWield(glide_wield::Data::from(data));
1346 }
1347}
1348
1349pub fn handle_jump(
1351 data: &JoinData<'_>,
1352 output_events: &mut OutputEvents,
1353 _update: &mut StateUpdate,
1354 strength: f32,
1355) -> bool {
1356 input_is_pressed(data, InputKind::Jump)
1357 .then(|| data.body.jump_impulse())
1358 .flatten()
1359 .and_then(|impulse| {
1360 if data.physics.in_liquid().is_some() {
1361 if data.physics.on_wall.is_some() {
1362 Some(impulse * 0.75)
1365 } else {
1366 None
1367 }
1368 } else if data.physics.on_ground.is_some() {
1369 Some(impulse)
1370 } else {
1371 None
1372 }
1373 })
1374 .map(|impulse| {
1375 output_events.emit_local(LocalEvent::Jump(
1376 data.entity,
1377 strength * impulse / data.mass.0
1378 * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25))
1379 * data.stats.jump_modifier,
1380 ));
1381 })
1382 .is_some()
1383}
1384
1385pub fn handle_walljump(
1386 data: &JoinData<'_>,
1387 output_events: &mut OutputEvents,
1388 update: &mut StateUpdate,
1389 was_wielded: bool,
1390) -> bool {
1391 let Some(wall_dir) = data.physics.on_wall else {
1392 return false;
1393 };
1394 const WALL_JUMP_Z: f32 = 0.7;
1395 let look_dir = data.inputs.look_dir.vec();
1396
1397 let jump_dir = if look_dir.xy().dot(wall_dir.xy()) > 0.0 {
1399 look_dir.xy().reflected(-wall_dir.xy()).with_z(WALL_JUMP_Z)
1400 } else {
1401 *look_dir
1402 };
1403
1404 let jump_dir = if data.inputs.move_dir.dot(-wall_dir.xy()) > 0.0 {
1406 data.inputs.move_dir.with_z(WALL_JUMP_Z)
1407 } else {
1408 jump_dir
1409 };
1410
1411 let jump_dir = if jump_dir.xy().iter().all(|e| *e < 0.001) {
1413 jump_dir - wall_dir.xy() * 0.1
1414 } else {
1415 jump_dir
1416 }
1417 .try_normalized()
1418 .unwrap_or(Vec3::zero());
1419
1420 if let Some(jump_impulse) = data.body.jump_impulse() {
1421 update.ori = update
1423 .ori
1424 .slerped_towards(Ori::from(Dir::new(jump_dir)), 20.0);
1425 const WALL_JUMP_FACTOR: f32 = 1.1;
1427 output_events.emit_local(LocalEvent::ApplyImpulse {
1429 entity: data.entity,
1430 impulse: jump_dir * WALL_JUMP_FACTOR * jump_impulse / data.mass.0
1431 * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25)),
1432 });
1433 }
1434 if was_wielded {
1435 update.character = CharacterState::Wielding(wielding::Data { is_sneaking: false });
1436 } else {
1437 update.character = CharacterState::Idle(idle::Data::default());
1438 }
1439 true
1440}
1441
1442fn handle_ability(
1443 data: &JoinData<'_>,
1444 update: &mut StateUpdate,
1445 output_events: &mut OutputEvents,
1446 input: InputKind,
1447) -> bool {
1448 if let Some(ability_input) = input.into()
1449 && let Some((ability, from_offhand, spec_ability)) = data
1450 .active_abilities
1451 .and_then(|a| {
1452 a.activate_ability(
1453 ability_input,
1454 data.inventory,
1455 data.skill_set,
1456 Some(data.body),
1457 Some(data.character),
1458 data.stance,
1459 data.combo,
1460 Some(data.stats),
1461 data.buffs,
1462 )
1463 })
1464 .map(|(mut a, f, s)| {
1465 let mut contextual_stats =
1466 if let Some(contextual_stats) = a.ability_meta().contextual_stats {
1467 contextual_stats.equivalent_stats(data)
1468 } else {
1469 tool::Stats::one()
1470 };
1471 contextual_stats.energy_efficiency *= data.stats.energy_efficiency_modifier;
1472 a = a.adjusted_by_stats(contextual_stats);
1473 (a, f, s)
1474 })
1475 .filter(|(ability, _, _)| ability.requirements_paid(data, update))
1476 {
1477 let ability_meta = ability.ability_meta();
1480 {
1481 let AbilityRequirements { stance: _, item } = ability_meta.requirements;
1482 let inv_slot = item.and_then(|item| {
1483 data.inventory
1484 .and_then(|inv| inv.get_slot_of_item_by_def_id(&item.item_def_id()))
1485 });
1486 if let Some(inv_slot) = inv_slot {
1487 let inv_manip = InventoryManip::Delete(
1488 inv_slot,
1489 NonZeroU32::new(1).expect("1 is greater than 0"),
1490 );
1491 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1492 }
1493 }
1494 match CharacterState::try_from((
1495 &ability,
1496 AbilityInfo::new(data, from_offhand, input, Some(spec_ability), ability_meta),
1497 data,
1498 )) {
1499 Ok(character_state) => {
1500 let tool_kind = character_state.ability_info().and_then(|ai| ai.tool);
1501 let target_uid = character_state
1502 .ability_info()
1503 .and_then(|ai| ai.input_attr)
1504 .and_then(|ia| ia.target_entity);
1505 update.character = character_state;
1506
1507 for init_event in ability
1508 .ability_meta()
1509 .init_event
1510 .iter()
1511 .chain(ability.ability_meta().init_event2.iter())
1512 {
1513 match init_event {
1514 AbilityInitEvent::EnterStance(stance) => {
1515 output_events.emit_server(ChangeStanceEvent {
1516 entity: data.entity,
1517 stance: *stance,
1518 });
1519 },
1520 AbilityInitEvent::GainBuff {
1521 kind,
1522 strength,
1523 duration,
1524 } => {
1525 let dest_info = DestInfo {
1526 stats: Some(data.stats),
1527 mass: Some(data.mass),
1528 };
1529 output_events.emit_server(BuffEvent {
1530 entity: data.entity,
1531 buff_change: BuffChange::Add(Buff::new(
1532 *kind,
1533 BuffData::new(*strength, *duration),
1534 vec![BuffCategory::SelfBuff],
1535 BuffSource::Character {
1536 by: *data.uid,
1537 tool_kind,
1538 },
1539 *data.time,
1540 dest_info,
1541 Some(data.mass),
1542 target_uid,
1543 )),
1544 });
1545 },
1546 AbilityInitEvent::RemoveBuff(buff) => {
1547 output_events.emit_server(BuffEvent {
1548 entity: data.entity,
1549 buff_change: BuffChange::RemoveByKind(*buff),
1550 });
1551 },
1552 }
1553 }
1554 if let CharacterState::Roll(roll) = &mut update.character {
1555 if data.character.is_wield() || data.character.was_wielded() {
1556 roll.was_wielded = true;
1557 }
1558 if data.character.is_stealthy() {
1559 roll.is_sneaking = true;
1560 }
1561 if data.character.is_aimed() {
1562 roll.prev_aimed_dir = Some(data.controller.inputs.look_dir);
1563 }
1564 }
1565 return true;
1566 },
1567 Err(err) => {
1568 warn!("Failed to enter character state: {err:?}");
1569 },
1570 }
1571 }
1572 false
1573}
1574
1575pub fn handle_input(
1576 data: &JoinData<'_>,
1577 output_events: &mut OutputEvents,
1578 update: &mut StateUpdate,
1579 input: InputKind,
1580) {
1581 match input {
1582 InputKind::Primary
1583 | InputKind::Secondary
1584 | InputKind::Ability(_)
1585 | InputKind::Block
1586 | InputKind::Roll => {
1587 handle_ability(data, update, output_events, input);
1588 },
1589 InputKind::Jump => {
1590 handle_jump(data, output_events, update, 1.0);
1591 },
1592 InputKind::WallJump | InputKind::Fly => {},
1593 }
1594}
1595
1596pub fn handle_glider_input_or(
1600 data: &JoinData<'_>,
1601 update: &mut StateUpdate,
1602 output_events: &mut OutputEvents,
1603 fallback_fn: fn(&JoinData<'_>, &mut StateUpdate),
1604) {
1605 if data
1606 .inventory
1607 .and_then(|inv| inv.equipped(EquipSlot::Glider))
1608 .and_then(|glider| glider.item_config())
1609 .is_none()
1610 {
1611 fallback_fn(data, update);
1612 return;
1613 };
1614
1615 if let Some(input) = data.controller.queued_inputs.keys().next() {
1616 handle_ability(data, update, output_events, *input);
1617 };
1618}
1619
1620pub fn attempt_input(
1621 data: &JoinData<'_>,
1622 output_events: &mut OutputEvents,
1623 update: &mut StateUpdate,
1624) {
1625 if let Some(input) = data.controller.queued_inputs.keys().next() {
1627 handle_input(data, output_events, update, *input);
1628 }
1629}
1630
1631pub fn handle_interrupts(
1633 data: &JoinData,
1634 update: &mut StateUpdate,
1635 output_events: &mut OutputEvents,
1636) -> bool {
1637 let can_dodge = matches!(
1638 data.character.stage_section(),
1639 Some(StageSection::Buildup | StageSection::Recover)
1640 );
1641 let can_block = data
1642 .character
1643 .ability_info()
1644 .map(|info| info.ability_meta)
1645 .is_some_and(|meta| meta.capabilities.contains(Capability::BLOCK_INTERRUPT));
1646 if can_dodge && input_is_pressed(data, InputKind::Roll) {
1647 handle_ability(data, update, output_events, InputKind::Roll)
1648 } else if can_block && input_is_pressed(data, InputKind::Block) {
1649 handle_ability(data, update, output_events, InputKind::Block)
1650 } else {
1651 false
1652 }
1653}
1654
1655pub fn is_strafing(data: &JoinData<'_>, update: &StateUpdate) -> bool {
1656 (update.character.is_aimed() || update.should_strafe) && data.body.can_strafe()
1659 && !matches!(unwrap_tool_data(data, EquipSlot::ActiveMainhand),
1661 Some((ToolKind::Instrument, _)))
1662}
1663
1664pub fn unwrap_tool_data(data: &JoinData, equip_slot: EquipSlot) -> Option<(ToolKind, Hands)> {
1666 if let Some(ItemKind::Tool(tool)) = data
1667 .inventory
1668 .and_then(|inv| inv.equipped(equip_slot))
1669 .map(|i| i.kind())
1670 .as_deref()
1671 {
1672 Some((tool.kind, tool.hands))
1673 } else {
1674 None
1675 }
1676}
1677
1678pub fn get_hands(data: &JoinData<'_>) -> (Option<Hands>, Option<Hands>) {
1679 let hand = |slot| {
1680 if let Some(ItemKind::Tool(tool)) = data
1681 .inventory
1682 .and_then(|inv| inv.equipped(slot))
1683 .map(|i| i.kind())
1684 .as_deref()
1685 {
1686 Some(tool.hands)
1687 } else {
1688 None
1689 }
1690 };
1691 (
1692 hand(EquipSlot::ActiveMainhand),
1693 hand(EquipSlot::ActiveOffhand),
1694 )
1695}
1696
1697pub fn get_tool_stats(data: &JoinData<'_>, ai: AbilityInfo) -> tool::Stats {
1698 ai.hand
1699 .map(|hand| hand.to_equip_slot())
1700 .and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
1701 .and_then(|item| {
1702 if let ItemKind::Tool(tool) = &*item.kind() {
1703 Some(tool.stats(item.stats_durability_multiplier()))
1704 } else {
1705 None
1706 }
1707 })
1708 .unwrap_or(tool::Stats::one())
1709}
1710
1711pub fn input_is_pressed(data: &JoinData<'_>, input: InputKind) -> bool {
1712 data.controller.queued_inputs.contains_key(&input)
1713}
1714
1715fn checked_tick(data: &JoinData<'_>, timer: Duration, modifier: Option<f32>) -> Option<Duration> {
1719 timer.checked_add(Duration::from_secs_f32(data.dt.0 * modifier.unwrap_or(1.0)))
1720}
1721
1722pub fn tick_or_default(data: &JoinData<'_>, timer: Duration, modifier: Option<f32>) -> Duration {
1725 checked_tick(data, timer, modifier).unwrap_or_default()
1726}
1727
1728fn checked_tick_attack(
1732 data: &JoinData<'_>,
1733 timer: Duration,
1734 other_modifier: Option<f32>,
1735) -> Option<Duration> {
1736 let section_modifier = match data.character.stage_section() {
1737 Some(StageSection::Buildup) => data.stats.buildup_speed_modifier,
1738 Some(StageSection::Charge) => data.stats.charge_speed_modifier,
1739 Some(StageSection::Recover) => data.stats.recovery_speed_modifier,
1740 _ => 1.0,
1741 };
1742 checked_tick(
1743 data,
1744 timer,
1745 Some(data.stats.attack_speed_modifier * section_modifier * other_modifier.unwrap_or(1.0)),
1746 )
1747}
1748
1749pub fn tick_attack_or_default(
1752 data: &JoinData<'_>,
1753 timer: Duration,
1754 other_modifier: Option<f32>,
1755) -> Duration {
1756 checked_tick_attack(data, timer, other_modifier).unwrap_or_default()
1757}
1758
1759#[derive(Clone, Copy, Debug, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
1763pub enum StageSection {
1764 Buildup,
1765 Recover,
1766 Charge,
1767 Movement,
1768 Action,
1769}
1770
1771#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1772pub enum ForcedMovement {
1773 Forward(f32),
1774 Reverse(f32),
1775 Sideways(f32),
1776 DirectedReverse(f32),
1777 AntiDirectedForward(f32),
1778 Leap {
1779 vertical: f32,
1780 forward: f32,
1781 progress: f32,
1782 direction: MovementDirection,
1783 },
1784}
1785
1786impl Mul<f32> for ForcedMovement {
1787 type Output = Self;
1788
1789 fn mul(self, scalar: f32) -> Self {
1790 use ForcedMovement::*;
1791 match self {
1792 Forward(x) => Forward(x * scalar),
1793 Reverse(x) => Reverse(x * scalar),
1794 Sideways(x) => Sideways(x * scalar),
1795 DirectedReverse(x) => DirectedReverse(x * scalar),
1796 AntiDirectedForward(x) => AntiDirectedForward(x * scalar),
1797 Leap {
1798 vertical,
1799 forward,
1800 progress,
1801 direction,
1802 } => Leap {
1803 vertical: vertical * scalar,
1804 forward: forward * scalar,
1805 progress,
1806 direction,
1807 },
1808 }
1809 }
1810}
1811
1812#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1813pub enum MovementDirection {
1814 Look,
1815 AntiLook,
1816 Move,
1817}
1818
1819impl MovementDirection {
1820 pub fn get_2d_dir(self, data: &JoinData<'_>) -> Vec2<f32> {
1821 use MovementDirection::*;
1822 match self {
1823 Look => data
1824 .inputs
1825 .look_dir
1826 .to_horizontal()
1827 .unwrap_or_default()
1828 .xy(),
1829 AntiLook => -data
1830 .inputs
1831 .look_dir
1832 .to_horizontal()
1833 .unwrap_or_default()
1834 .xy(),
1835 Move => data.inputs.move_dir,
1836 }
1837 .try_normalized()
1838 .unwrap_or_default()
1839 }
1840}
1841
1842#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1843pub struct AbilityInfo {
1844 pub tool: Option<ToolKind>,
1845 pub hand: Option<HandInfo>,
1846 pub input: InputKind,
1847 pub input_attr: Option<InputAttr>,
1848 pub ability_meta: AbilityMeta,
1849 pub ability: Option<SpecifiedAbility>,
1850}
1851
1852impl AbilityInfo {
1853 pub fn new(
1854 data: &JoinData<'_>,
1855 from_offhand: bool,
1856 input: InputKind,
1857 ability: Option<SpecifiedAbility>,
1858 ability_meta: AbilityMeta,
1859 ) -> Self {
1860 let tool_data = if from_offhand {
1861 unwrap_tool_data(data, EquipSlot::ActiveOffhand)
1862 } else {
1863 unwrap_tool_data(data, EquipSlot::ActiveMainhand)
1864 };
1865 let (tool, hand) = tool_data.map_or((None, None), |(kind, hands)| {
1866 (
1867 Some(kind),
1868 Some(HandInfo::from_main_tool(hands, from_offhand)),
1869 )
1870 });
1871
1872 Self {
1873 tool,
1874 hand,
1875 input,
1876 input_attr: data.controller.queued_inputs.get(&input).copied(),
1877 ability_meta,
1878 ability,
1879 }
1880 }
1881}
1882
1883pub fn end_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
1884 if data.character.is_wield() || data.character.was_wielded() {
1885 update.character = CharacterState::Wielding(wielding::Data {
1886 is_sneaking: data.character.is_stealthy(),
1887 });
1888 } else {
1889 update.character = CharacterState::Idle(idle::Data {
1890 is_sneaking: data.character.is_stealthy(),
1891 footwear: None,
1892 time_entered: *data.time,
1893 });
1894 }
1895 if let CharacterState::Roll(roll) = data.character
1896 && let Some(dir) = roll.prev_aimed_dir
1897 {
1898 update.ori = dir.into();
1899 }
1900}
1901
1902pub fn end_melee_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
1903 end_ability(data, update);
1904 data.updater.remove::<Melee>(data.entity);
1905}
1906
1907#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1908pub enum HandInfo {
1909 TwoHanded,
1910 MainHand,
1911 OffHand,
1912}
1913
1914impl HandInfo {
1915 pub fn from_main_tool(tool_hands: Hands, from_offhand: bool) -> Self {
1916 match tool_hands {
1917 Hands::Two => Self::TwoHanded,
1918 Hands::One => {
1919 if from_offhand {
1920 Self::OffHand
1921 } else {
1922 Self::MainHand
1923 }
1924 },
1925 }
1926 }
1927
1928 pub fn to_equip_slot(&self) -> EquipSlot {
1929 match self {
1930 HandInfo::TwoHanded | HandInfo::MainHand => EquipSlot::ActiveMainhand,
1931 HandInfo::OffHand => EquipSlot::ActiveOffhand,
1932 }
1933 }
1934}
1935
1936pub fn leave_stance(data: &JoinData<'_>, output_events: &mut OutputEvents) {
1937 if !matches!(data.stance, Some(Stance::None)) {
1938 output_events.emit_server(ChangeStanceEvent {
1939 entity: data.entity,
1940 stance: Stance::None,
1941 });
1942 }
1943}
1944
1945#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
1946pub enum ComboConsumption {
1947 #[default]
1948 All,
1949 Half,
1950 Cost,
1951}
1952
1953impl ComboConsumption {
1954 pub fn consume(&self, data: &JoinData, output_events: &mut OutputEvents, cost: u32) {
1955 let combo = data.combo.map_or(0, |c| c.counter());
1956 let to_consume = match self {
1957 Self::All => combo,
1958 Self::Half => combo.div_ceil(2),
1959 Self::Cost => cost,
1960 };
1961 output_events.emit_server(ComboChangeEvent {
1962 entity: data.entity,
1963 change: -(to_consume as i32),
1964 });
1965 }
1966}
1967
1968fn loadout_change_hook(data: &JoinData<'_>, output_events: &mut OutputEvents, clear_combo: bool) {
1969 if clear_combo {
1970 output_events.emit_server(ComboChangeEvent {
1972 entity: data.entity,
1973 change: -data.combo.map_or(0, |c| c.counter() as i32),
1974 });
1975 }
1976 output_events.emit_server(BuffEvent {
1978 entity: data.entity,
1979 buff_change: BuffChange::RemoveByCategory {
1980 all_required: vec![BuffCategory::RemoveOnLoadoutChange],
1981 any_required: vec![],
1982 none_required: vec![],
1983 },
1984 });
1985}
1986
1987#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1988#[serde(deny_unknown_fields)]
1989pub struct MovementModifier {
1990 pub buildup: Option<f32>,
1991 pub action: Option<f32>,
1992 pub recover: Option<f32>,
1993}
1994
1995#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1996#[serde(deny_unknown_fields)]
1997pub struct OrientationModifier {
1998 pub buildup: Option<f32>,
1999 pub action: Option<f32>,
2000 pub recover: Option<f32>,
2001}
2002
2003#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
2004pub enum ProjectileSpread {
2005 Increasing(f32),
2006 Horizontal(f32),
2007}
2008
2009impl ProjectileSpread {
2010 pub fn compute_directions(
2011 self,
2012 init_dir: Dir,
2013 init_ori: Ori,
2014 num: u32,
2015 rng: &mut impl RngExt,
2016 ) -> impl Iterator<Item = Dir> + '_ {
2017 match self {
2018 Self::Increasing(spread) => Either::Left(
2019 (0..num).map(move |i| {
2022 Dir::from_unnormalized(init_dir.map(|x| {
2023 let offset = (2.0 * rng.random::<f32>() - 1.0) * spread * i as f32;
2024 x + offset
2025 }))
2026 .unwrap_or(init_dir)
2027 }),
2028 ),
2029 Self::Horizontal(spread) => Either::Right(if num < 2 {
2030 Either::Left(std::iter::once(init_dir))
2031 } else {
2032 let left = -spread.to_radians();
2033 let increment = spread.to_radians() * 2.0 / (num as f32 - 1.0);
2034 let rot_quat_dir = Quaternion::<f32>::rotation_from_to_3d(
2035 Vec3::unit_y(),
2036 Vec3::new(0.0, init_dir.xy().magnitude(), init_dir.z),
2037 );
2038 Either::Right((0..num).map(move |i| {
2039 let angle = left + increment * i as f32;
2040 let rot_quat_spread = Quaternion::<f32>::rotation_from_to_3d(
2041 Vec3::unit_y(),
2042 Vec2::unit_y().rotated_z(angle).with_z(0.0),
2043 );
2044 Dir::from_unnormalized(
2045 Ori::new(init_ori.to_quat() * rot_quat_dir * rot_quat_spread).look_vec(),
2046 )
2047 .unwrap_or(init_dir)
2048 }))
2049 }),
2050 }
2051 }
2052
2053 pub fn estimated_spread(&self) -> f32 {
2056 match self {
2057 Self::Increasing(spread) | Self::Horizontal(spread) => *spread,
2059 }
2060 }
2061}