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 ground_accel(&self) -> Option<f32> {
207 match self {
208 Self::FishSmall(_) | Self::FishMedium(_) => None,
209 _ => Some(self.base_accel()),
210 }
211 }
212
213 pub fn max_speed_approx(&self) -> f32 {
216 let v = match self {
217 Body::Ship(ship) => ship.get_speed(),
218 _ => -self.base_accel() / (60.0 * (1.0 - FRIC_GROUND).ln()),
225 };
226 debug_assert!(v >= 0.0, "Speed must be positive!");
227 v
228 }
229
230 pub fn ori_damping(&self) -> f32 {
237 match self {
238 Body::Humanoid(_) | Body::BipedLarge(_) | Body::Golem(_) => 1.0,
239 _ => 0.0,
240 }
241 }
242
243 pub fn base_ori_rate(&self) -> f32 {
245 match self {
246 Body::Humanoid(_) => 2.65,
247 Body::QuadrupedSmall(_) => 3.0,
248 Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
249 quadruped_medium::Species::Mammoth => 1.0,
250 _ => 2.8,
251 },
252 Body::BirdMedium(_) => 6.0,
253 Body::FishMedium(_) => 6.0,
254 Body::Dragon(_) => 1.0,
255 Body::BirdLarge(_) => 7.0,
256 Body::FishSmall(_) => 7.0,
257 Body::BipedLarge(biped_large) => match biped_large.species {
258 biped_large::Species::Harvester => 2.0,
259 _ => 2.7,
260 },
261 Body::BipedSmall(_) => 3.5,
262 Body::Object(_) => 2.0,
263 Body::Item(_) => 2.0,
264 Body::Golem(golem) => match golem.species {
265 golem::Species::WoodGolem => 1.2,
266 _ => 2.0,
267 },
268 Body::Theropod(theropod) => match theropod.species {
269 theropod::Species::Archaeos => 2.3,
270 theropod::Species::Odonto => 2.3,
271 theropod::Species::Ntouka => 2.3,
272 theropod::Species::Dodarock => 2.0,
273 _ => 2.5,
274 },
275 Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
276 quadruped_low::Species::Asp => 2.2,
277 quadruped_low::Species::Tortoise => 1.5,
278 quadruped_low::Species::Rocksnapper => 1.8,
279 quadruped_low::Species::Rootsnapper => 1.8,
280 quadruped_low::Species::Lavadrake => 1.7,
281 quadruped_low::Species::Icedrake => 1.7,
282 quadruped_low::Species::Mossdrake => 1.7,
283 _ => 2.0,
284 },
285 Body::Ship(ship::Body::Carriage) => 0.04,
286 Body::Ship(ship::Body::Train) => 0.0,
287 Body::Ship(ship) if ship.has_water_thrust() => 5.0 / self.dimensions().y,
288 Body::Ship(_) => 6.0 / self.dimensions().y,
289 Body::Arthropod(_) => 3.5,
290 Body::Crustacean(_) => 3.5,
291 Body::Plugin(body) => body.base_ori_rate(),
292 }
293 }
294
295 pub fn swim_thrust(&self) -> Option<f32> {
297 let front_profile = self.dimensions().x * self.dimensions().z;
301 Some(
302 match self {
303 Body::Object(_) => return None,
304 Body::Item(_) => return None,
305 Body::Ship(ship::Body::Submarine) => 1000.0 * self.mass().0,
306 Body::Ship(ship) if ship.has_water_thrust() => 500.0 * self.mass().0,
307 Body::Ship(_) => return None,
308 Body::BipedLarge(_) => 120.0 * self.mass().0,
309 Body::Golem(_) => 100.0 * self.mass().0,
310 Body::BipedSmall(_) => 1000.0 * self.mass().0,
311 Body::BirdMedium(_) => 400.0 * self.mass().0,
312 Body::BirdLarge(_) => 400.0 * self.mass().0,
313 Body::FishMedium(_) => 200.0 * self.mass().0,
314 Body::FishSmall(_) => 300.0 * self.mass().0,
315 Body::Dragon(_) => 50.0 * self.mass().0,
316 Body::Humanoid(body) => {
319 return Some(6_500_000.0 / self.mass().0 * body.scaler().powi(2));
320 },
321 Body::Theropod(body) => match body.species {
322 theropod::Species::Sandraptor
323 | theropod::Species::Snowraptor
324 | theropod::Species::Sunlizard
325 | theropod::Species::Woodraptor
326 | theropod::Species::Dodarock
327 | theropod::Species::Axebeak
328 | theropod::Species::Yale => 500.0 * self.mass().0,
329 _ => 150.0 * self.mass().0,
330 },
331 Body::QuadrupedLow(_) => 1200.0 * self.mass().0,
332 Body::QuadrupedMedium(body) => match body.species {
333 quadruped_medium::Species::Mammoth => 150.0 * self.mass().0,
334 quadruped_medium::Species::Kelpie => 3500.0 * self.mass().0,
335 _ => 1000.0 * self.mass().0,
336 },
337 Body::QuadrupedSmall(_) => 1500.0 * self.mass().0,
338 Body::Arthropod(_) => 500.0 * self.mass().0,
339 Body::Crustacean(_) => 400.0 * self.mass().0,
340 Body::Plugin(body) => body.swim_thrust()?,
341 } * front_profile,
342 )
343 }
344
345 pub fn fly_thrust(&self) -> Option<f32> {
347 match self {
348 Body::BirdMedium(body) => match body.species {
349 bird_medium::Species::Bat | bird_medium::Species::BloodmoonBat => {
350 Some(GRAVITY * self.mass().0 * 0.5)
351 },
352 _ => Some(GRAVITY * self.mass().0 * 2.0),
353 },
354 Body::BirdLarge(_) => Some(GRAVITY * self.mass().0 * 0.5),
355 Body::Dragon(_) => Some(200_000.0),
356 Body::Ship(ship) if ship.can_fly() => Some(390_000.0),
357 Body::Object(object::Body::Crux) => Some(1_000.0),
358 _ => None,
359 }
360 }
361
362 pub fn vectored_propulsion(&self) -> bool {
364 match self {
365 Body::Ship(ship) => ship.vectored_propulsion(),
366 _ => false,
367 }
368 }
369
370 pub fn jump_impulse(&self) -> Option<f32> {
372 match self {
373 Body::Object(_) | Body::Ship(_) | Body::Item(_) => None,
374 Body::BipedLarge(_) | Body::Dragon(_) => Some(0.6 * self.mass().0),
375 Body::Golem(_) | Body::QuadrupedLow(_) => Some(0.4 * self.mass().0),
376 Body::QuadrupedMedium(_) => Some(0.4 * self.mass().0),
377 Body::Theropod(body) => match body.species {
378 theropod::Species::Snowraptor
379 | theropod::Species::Sandraptor
380 | theropod::Species::Woodraptor => Some(0.4 * self.mass().0),
381 _ => None,
382 },
383 Body::Arthropod(_) => Some(1.0 * self.mass().0),
384 _ => Some(0.4 * self.mass().0),
385 }
386 .map(|f| f * GRAVITY)
387 }
388
389 pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) }
390
391 pub fn reverse_move_factor(&self) -> f32 { 0.45 }
394
395 pub fn projectile_offsets(&self, ori: Vec3<f32>, scale: f32) -> Vec3<f32> {
398 let body_offsets_z = match self {
399 Body::Golem(_) => self.height() * 0.4,
400 _ => self.eye_height(scale),
401 };
402
403 let dim = self.dimensions();
404 let (width, length) = (dim.x, dim.y);
406 let body_radius = if length > width {
407 self.max_radius()
409 } else {
410 self.min_radius()
412 };
413
414 Vec3::new(
415 body_radius * ori.x * 1.1,
416 body_radius * ori.y * 1.1,
417 body_offsets_z,
418 )
419 }
420}
421
422pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) {
424 if let &Idle(idle::Data {
425 ref is_sneaking,
426 ref time_entered,
427 mut footwear,
428 }) = data.character
429 {
430 if footwear.is_none() {
431 footwear = data.inventory.and_then(|inv| {
432 inv.equipped(EquipSlot::Armor(ArmorSlot::Feet))
433 .map(|armor| match armor.kind().as_ref() {
434 ItemKind::Armor(a) => {
435 a.stats(data.msm, armor.stats_durability_multiplier())
436 .ground_contact
437 },
438 _ => Friction::Normal,
439 })
440 });
441 update.character = Idle(idle::Data {
442 is_sneaking: *is_sneaking,
443 time_entered: *time_entered,
444 footwear,
445 });
446 }
447 if data.physics.skating_active {
448 update.character =
449 CharacterState::Skate(skate::Data::new(data, footwear.unwrap_or(Friction::Normal)));
450 }
451 }
452}
453
454pub fn handle_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
456 if data.volume_mount_data.is_some() {
457 return;
458 }
459 let submersion = data
460 .physics
461 .in_liquid()
462 .map(|depth| depth / data.body.height());
463
464 if input_is_pressed(data, InputKind::Fly)
465 && submersion.is_none_or(|sub| sub < 1.0)
466 && (data.physics.on_ground.is_none() || data.body.jump_impulse().is_none())
467 && data.body.fly_thrust().is_some()
468 {
469 fly_move(data, update, efficiency);
470 } else if let Some(submersion) = (data.physics.in_liquid().is_some()
471 && data.body.swim_thrust().is_some())
472 .then_some(submersion)
473 .flatten()
474 {
475 swim_move(data, update, efficiency, submersion);
476 } else {
477 ground_move(data, update, efficiency);
478 }
479}
480
481fn ground_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
483 let section_modifier = match data.character.stage_section() {
484 Some(StageSection::Buildup) => data.stats.buildup_move_speed_modifier,
485 Some(StageSection::Charge) => data.stats.charge_move_speed_modifier,
486 _ => 1.0,
487 };
488 let efficiency = efficiency
489 * data.stats.move_speed_modifier
490 * data.stats.friction_modifier
491 * section_modifier;
492
493 let accel = if let Some(block) = data.physics.on_ground {
494 data.body.ground_accel().unwrap_or(0.0)
496 * data.scale.map_or(1.0, |s| s.0.sqrt())
497 * block.get_traction()
498 * block.get_friction()
499 / FRIC_GROUND
500 } else {
501 data.body.air_accel()
502 } * efficiency;
503
504 update.vel.0 += Vec2::broadcast(data.dt.0)
506 * accel
507 * if data.body.can_strafe() {
508 data.inputs.move_dir
509 * if is_strafing(data, update) {
510 Lerp::lerp(
511 Vec2::from(update.ori)
512 .try_normalized()
513 .unwrap_or_else(Vec2::zero)
514 .dot(
515 data.inputs
516 .move_dir
517 .try_normalized()
518 .unwrap_or_else(Vec2::zero),
519 )
520 .add(1.0)
521 .div(2.0)
522 .max(0.0),
523 1.0,
524 data.body.reverse_move_factor(),
525 )
526 } else {
527 1.0
528 }
529 } else {
530 let fw = Vec2::from(update.ori);
531 fw * data.inputs.move_dir.dot(fw).max(0.0)
532 };
533}
534
535pub fn handle_forced_movement(
537 data: &JoinData<'_>,
538 update: &mut StateUpdate,
539 movement: ForcedMovement,
540) {
541 match movement {
542 ForcedMovement::Forward(strength) => {
543 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
544 if let Some(accel) = data.physics.on_ground.map(|block| {
545 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
547 }) {
548 update.vel.0 += Vec2::broadcast(data.dt.0)
549 * accel
550 * data.scale.map_or(1.0, |s| s.0.sqrt())
551 * Vec2::from(*data.ori)
552 * strength;
553 }
554 },
555 ForcedMovement::Reverse(strength) => {
556 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
557 if let Some(accel) = data.physics.on_ground.map(|block| {
558 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
560 }) {
561 update.vel.0 += Vec2::broadcast(data.dt.0)
562 * accel
563 * data.scale.map_or(1.0, |s| s.0.sqrt())
564 * -Vec2::from(*data.ori)
565 * strength;
566 }
567 },
568 ForcedMovement::Sideways(strength) => {
569 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
570 if let Some(accel) = data.physics.on_ground.map(|block| {
571 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
573 }) {
574 let direction = {
575 let side = Vec2::from(*data.ori)
577 .rotated_z(PI / 2.)
578 .dot(data.inputs.move_dir)
579 .signum();
580 if side > 0.0 {
581 Vec2::from(*data.ori).rotated_z(PI / 2.)
582 } else {
583 -Vec2::from(*data.ori).rotated_z(PI / 2.)
584 }
585 };
586
587 update.vel.0 += Vec2::broadcast(data.dt.0)
588 * accel
589 * data.scale.map_or(1.0, |s| s.0.sqrt())
590 * direction
591 * strength;
592 }
593 },
594 ForcedMovement::DirectedReverse(strength) => {
595 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
596 if let Some(accel) = data.physics.on_ground.map(|block| {
597 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
599 }) {
600 let direction = if Vec2::from(*data.ori).dot(data.inputs.move_dir).signum() > 0.0 {
601 data.inputs.move_dir.reflected(Vec2::from(*data.ori))
602 } else {
603 data.inputs.move_dir
604 }
605 .try_normalized()
606 .unwrap_or_else(|| -Vec2::from(*data.ori));
607 update.vel.0 += direction * strength * accel * data.dt.0;
608 }
609 },
610 ForcedMovement::AntiDirectedForward(strength) => {
611 let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
612 if let Some(accel) = data.physics.on_ground.map(|block| {
613 data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
615 }) {
616 let direction = if Vec2::from(*data.ori).dot(data.inputs.move_dir).signum() < 0.0 {
617 data.inputs.move_dir.reflected(Vec2::from(*data.ori))
618 } else {
619 data.inputs.move_dir
620 }
621 .try_normalized()
622 .unwrap_or_else(|| Vec2::from(*data.ori));
623 let direction = direction.reflected(Vec2::from(*data.ori).rotated_z(PI / 2.));
624 update.vel.0 += direction * strength * accel * data.dt.0;
625 }
626 },
627 ForcedMovement::Leap {
628 vertical,
629 forward,
630 progress,
631 direction,
632 } => {
633 let dir = direction.get_2d_dir(data);
634 update.vel.0 = Vec3::new(
636 dir.x,
637 dir.y,
638 vertical,
639 )
640 * data.scale.map_or(1.0, |s| s.0.sqrt())
641 * 2.0 * progress
643 + Vec3::from(dir)
645 * forward
647 * (1.0 - data.inputs.look_dir.z.abs());
651 },
652 }
653}
654
655pub fn handle_orientation(
656 data: &JoinData<'_>,
657 update: &mut StateUpdate,
658 efficiency: f32,
659 dir_override: Option<Dir>,
660) {
661 fn to_horizontal_fast(ori: &crate::comp::Ori) -> crate::comp::Ori {
663 if ori.to_quat().into_vec4().xy().is_approx_zero() {
664 *ori
665 } else {
666 ori.to_horizontal()
667 }
668 }
669 fn ori_absdiff(a: &crate::comp::Ori, b: &crate::comp::Ori) -> f32 {
671 (a.to_quat().into_vec4() - b.to_quat().into_vec4()).reduce(|a, b| a.abs() + b.abs())
672 }
673
674 update.character_activity.look_dir = Some(data.controller.inputs.look_dir);
676
677 let (tilt_ori, efficiency) = if let Body::Ship(ship) = data.body
678 && ship.has_wheels()
679 {
680 let height_at = |rpos| {
681 data.terrain
682 .ray(
683 data.pos.0 + rpos + Vec3::unit_z() * 4.0,
684 data.pos.0 + rpos - Vec3::unit_z() * 4.0,
685 )
686 .until(Block::is_solid)
687 .cast()
688 .0
689 };
690
691 let x_diff = (height_at(data.ori.to_horizontal().right().to_vec() * 3.0)
694 - height_at(data.ori.to_horizontal().right().to_vec() * -3.0))
695 / 10.0;
696 let y_diff = (height_at(data.ori.to_horizontal().look_dir().to_vec() * -4.5)
697 - height_at(data.ori.to_horizontal().look_dir().to_vec() * 4.5))
698 / 10.0;
699
700 (
701 Quaternion::rotation_y(x_diff.atan()) * Quaternion::rotation_x(y_diff.atan()),
702 (data.vel.0 - data.physics.ground_vel)
703 .xy()
704 .magnitude()
705 .max(3.0)
706 * efficiency,
707 )
708 } else {
709 (Quaternion::identity(), efficiency)
710 };
711
712 let target_ori = if let Some(dir_override) = dir_override {
717 dir_override.into()
718 } else if let CharacterState::Talk(t) = data.character
719 && let Some(tgt_uid) = t.tgt
720 && let Some(tgt) = data.id_maps.uid_entity(tgt_uid)
721 && let (tgt_body, Some(tgt_prev_phys)) =
722 (data.bodies.get(tgt), data.prev_phys_caches.get(tgt))
723 && let Some(tgt_pos) = tgt_prev_phys.pos.as_ref()
724 && let Some(dir) = Dir::look_toward(
725 data.pos,
726 Some(data.body),
727 data.scale,
728 tgt_pos,
729 tgt_body,
730 Some(&Scale(tgt_prev_phys.scale)),
731 )
732 {
733 update.character_activity.look_dir = Some(dir);
734 Dir::to_horizontal(dir).unwrap_or(dir).into()
735 } else if is_strafing(data, update) || update.character.should_follow_look() {
736 data.inputs
737 .look_dir
738 .to_horizontal()
739 .unwrap_or_default()
740 .into()
741 } else {
742 Dir::from_unnormalized(data.inputs.move_dir.into())
743 .map_or_else(|| to_horizontal_fast(data.ori), |dir| dir.into())
744 }
745 .rotated(tilt_ori);
746 let half_turns_per_tick = data.body.base_ori_rate() / data.scale.map_or(1.0, |s| s.0.sqrt())
748 * efficiency
749 * if data.physics.in_liquid().is_some() {
750 0.4
751 } else if data.physics.on_ground.is_some() || data.mount_data.is_some() {
752 1.0
753 } else {
754 0.2
755 }
756 * data.dt.0;
757 let ticks_from_target_guess = ori_absdiff(&update.ori, &target_ori) / half_turns_per_tick;
759 let instantaneous = ticks_from_target_guess < 1.0;
760 update.ori = if data.volume_mount_data.is_some() {
761 update.ori
762 } else if instantaneous {
763 target_ori
764 } else {
765 let target_fraction = {
766 let damping_multiplier = if update.character.is_wield() {
767 1.0
768 } else {
769 1.0 - data.body.ori_damping()
770 };
771 let angle_factor = 2.0 / (1.0 - update.ori.dot(target_ori) * damping_multiplier).sqrt();
774
775 half_turns_per_tick * angle_factor
776 };
777 update
778 .ori
779 .slerped_towards(target_ori, target_fraction.min(1.0))
780 };
781}
782
783fn swim_move(
785 data: &JoinData<'_>,
786 update: &mut StateUpdate,
787 efficiency: f32,
788 submersion: f32,
789) -> bool {
790 let efficiency = efficiency * data.stats.swim_speed_modifier * data.stats.friction_modifier;
791 if let Some(force) = data.body.swim_thrust() {
792 let force = efficiency * force * data.scale.map_or(1.0, |s| s.0);
793 let mut water_accel = force / data.mass.0;
794
795 if let Ok(level) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) {
796 let modifiers = SKILL_MODIFIERS.general_tree.swim;
797 water_accel *= modifiers.speed.powi(level.into());
798 }
799
800 let dir = if data.body.can_strafe() {
801 data.inputs.move_dir
802 } else {
803 let fw = Vec2::from(update.ori);
804 fw * data.inputs.move_dir.dot(fw).max(0.0)
805 };
806
807 let move_z = if submersion < 1.0
809 && data.inputs.move_z.abs() < f32::EPSILON
810 && data.physics.on_ground.is_none()
811 {
812 submersion.max(0.0) * 0.1
813 } else {
814 data.inputs.move_z
815 };
816
817 let move_z = move_z.min((submersion * 1.5 - 0.5).clamp(0.0, 1.0).powi(2));
819
820 update.vel.0 += Vec3::new(dir.x, dir.y, move_z)
821 * water_accel
825 * submersion.clamp(0.0, 1.0).sqrt()
827 * data.dt.0 * 0.04;
829
830 true
831 } else {
832 false
833 }
834}
835
836pub fn fly_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) -> bool {
838 let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
839
840 let glider = match data.character {
841 CharacterState::Glide(data) => Some(data),
842 _ => None,
843 };
844 if let Some(force) = data
845 .body
846 .fly_thrust()
847 .or_else(|| glider.is_some().then_some(0.0))
848 {
849 let thrust = efficiency * force;
850 let accel = thrust / data.mass.0;
851
852 match data.body {
853 Body::Ship(ship::Body::DefaultAirship) => {
854 handle_orientation(
858 data,
859 update,
860 efficiency * 2.0,
861 Some(data.controller.inputs.look_dir),
862 );
863 },
864 _ => {
865 handle_orientation(data, update, efficiency, None);
866 },
867 }
868
869 let mut update_fw_vel = true;
870 match data.body {
872 Body::Dragon(_) | Body::BirdLarge(_) | Body::BirdMedium(_) => {
874 let anti_grav = GRAVITY * (1.0 + data.inputs.move_z.min(0.0));
875 update.vel.0.z += data.dt.0 * (anti_grav + accel * data.inputs.move_z.max(0.0));
876 },
877 Body::Ship(ship::Body::DefaultAirship) => {
879 update_fw_vel = false;
880 let thrust_dir = data.inputs.move_dir.with_z(data.inputs.move_z);
887 update.vel.0 += thrust_dir * data.dt.0 * accel;
888 },
889 Body::Ship(ship) if ship.can_fly() => {
891 let regulate_density = |min: f32, max: f32, def: f32, rate: f32| -> Density {
896 let change = if data.inputs.move_z.abs() > f32::EPSILON {
898 -data.inputs.move_z
899 } else {
900 (def - data.density.0).clamp(-1.0, 1.0)
901 };
902 Density((update.density.0 + data.dt.0 * rate * change).clamp(min, max))
903 };
904 let def_density = ship.density().0;
905 if data.physics.in_liquid().is_some() {
906 let hull_density = ship.hull_density().0;
907 update.density.0 =
908 regulate_density(def_density * 0.6, hull_density, hull_density, 25.0).0;
909 } else {
910 update.density.0 =
911 regulate_density(def_density * 0.5, def_density * 1.5, def_density, 0.5).0;
912 };
913 },
914 _ => {},
917 };
918
919 if update_fw_vel {
920 update.vel.0 += Vec2::broadcast(data.dt.0)
921 * accel
922 * if data.body.can_strafe() {
923 data.inputs.move_dir
924 } else {
925 let fw = Vec2::from(update.ori);
926 fw * data.inputs.move_dir.dot(fw).max(0.0)
927 };
928 }
929 true
930 } else {
931 false
932 }
933}
934
935pub fn handle_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
938 if data.controller.queued_inputs.keys().any(|i| i.is_ability()) {
939 attempt_wield(data, update);
940 }
941}
942
943pub fn attempt_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
945 let equip_time = |equip_slot| {
948 data.inventory
949 .and_then(|inv| inv.equipped(equip_slot))
950 .and_then(|item| match &*item.kind() {
951 ItemKind::Tool(tool) => Some(Duration::from_secs_f32(
952 tool.stats(item.stats_durability_multiplier())
953 .equip_time_secs,
954 )),
955 _ => None,
956 })
957 };
958
959 let mainhand_equip_time = equip_time(EquipSlot::ActiveMainhand);
962 let offhand_equip_time = equip_time(EquipSlot::ActiveOffhand);
963 let equip_time = match (mainhand_equip_time, offhand_equip_time) {
964 (Some(a), Some(b)) => Some(a.max(b)),
965 (Some(a), None) | (None, Some(a)) => Some(a),
966 (None, None) => None,
967 };
968
969 if let Some(equip_time) = equip_time {
972 update.character = CharacterState::Equipping(equipping::Data {
973 static_data: equipping::StaticData {
974 buildup_duration: equip_time,
975 },
976 timer: Duration::default(),
977 is_sneaking: update.character.is_stealthy(),
978 });
979 } else {
980 update.character = CharacterState::Wielding(wielding::Data {
981 is_sneaking: update.character.is_stealthy(),
982 });
983 }
984}
985
986pub fn attempt_sit(data: &JoinData<'_>, update: &mut StateUpdate) {
988 if data.physics.on_ground.is_some() {
989 update.character = CharacterState::Sit;
990 }
991}
992
993pub fn attempt_crawl(data: &JoinData<'_>, update: &mut StateUpdate) {
995 if data.physics.on_ground.is_some() {
996 update.character = CharacterState::Crawl;
997 }
998}
999
1000pub fn attempt_dance(data: &JoinData<'_>, update: &mut StateUpdate) {
1001 if data.physics.on_ground.is_some() && data.body.is_humanoid() {
1002 update.character = CharacterState::Dance;
1003 }
1004}
1005
1006pub fn can_perform_pet(position: Pos, target_position: Pos, target_alignment: Alignment) -> bool {
1007 let within_distance = position.0.distance_squared(target_position.0) <= MAX_MOUNT_RANGE.powi(2);
1008 let valid_alignment = matches!(target_alignment, Alignment::Owned(_) | Alignment::Tame);
1009
1010 within_distance && valid_alignment
1011}
1012
1013pub fn attempt_talk(data: &JoinData<'_>, update: &mut StateUpdate, tgt: Option<Uid>) {
1014 if data.physics.on_ground.is_some() {
1015 update.character = CharacterState::Talk(match update.character {
1016 CharacterState::Talk(t) if t.tgt == tgt => t.refreshed(),
1017 _ => talk::Data::at(tgt),
1018 });
1019 }
1020}
1021
1022pub fn attempt_sneak(data: &JoinData<'_>, update: &mut StateUpdate) {
1023 if data.physics.on_ground.is_some() && data.body.is_humanoid() {
1024 update.character = Idle(idle::Data {
1025 is_sneaking: true,
1026 time_entered: *data.time,
1027 footwear: data.character.footwear(),
1028 });
1029 }
1030}
1031
1032pub fn handle_climb(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
1034 let Some(wall_dir) = data.physics.on_wall else {
1035 return false;
1036 };
1037
1038 let towards_wall = data.inputs.move_dir.dot(wall_dir.xy()) > 0.0;
1039 let underwater = data
1041 .physics
1042 .in_liquid()
1043 .map(|depth| depth > 2.0)
1044 .unwrap_or(false);
1045 let can_climb = data.body.can_climb() || data.physics.in_liquid().is_some();
1046 let in_air = data.physics.on_ground.is_none();
1047 if towards_wall && in_air && !underwater && can_climb && update.energy.current() > 1.0 {
1048 update.character = CharacterState::Climb(
1049 climb::Data::create_adjusted_by_skills(data)
1050 .with_wielded(data.character.is_wield() || data.character.was_wielded()),
1051 );
1052 true
1053 } else {
1054 false
1055 }
1056}
1057
1058pub fn handle_wallrun(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
1059 if data.physics.on_wall.is_some()
1060 && data.physics.on_ground.is_none()
1061 && data.physics.in_liquid().is_none()
1062 && data.body.can_climb()
1063 {
1064 update.character = CharacterState::Wallrun(wallrun::Data {
1065 was_wielded: data.character.is_wield() || data.character.was_wielded(),
1066 });
1067 true
1068 } else {
1069 false
1070 }
1071}
1072pub fn attempt_swap_equipped_weapons(
1074 data: &JoinData<'_>,
1075 update: &mut StateUpdate,
1076 output_events: &mut OutputEvents,
1077) {
1078 if data
1079 .inventory
1080 .and_then(|inv| inv.equipped(EquipSlot::InactiveMainhand))
1081 .is_some()
1082 || data
1083 .inventory
1084 .and_then(|inv| inv.equipped(EquipSlot::InactiveOffhand))
1085 .is_some()
1086 {
1087 update.swap_equipped_weapons = true;
1088 loadout_change_hook(data, output_events, false);
1089 }
1090}
1091
1092fn can_reach_block(
1094 player_pos: Vec3<f32>,
1095 block_pos: Vec3<i32>,
1096 range: f32,
1097 body: &Body,
1098 terrain: &TerrainGrid,
1099) -> bool {
1100 let block_pos_f32 = block_pos.map(|x| x as f32 + 0.5);
1101 let block_range_check = |pos: Vec3<f32>| {
1104 (block_pos_f32 - pos).magnitude_squared() < (range + body.max_radius()).powi(2)
1105 };
1106
1107 let close_to_block = block_range_check(player_pos)
1109 || block_range_check(player_pos + Vec3::new(0.0, 0.0, body.height()));
1110 if close_to_block {
1111 let iters = (3.0 * (block_pos_f32 - player_pos).map(|x| x.abs()).sum()) as usize;
1115 let heuristic = move |pos: &Vec3<i32>| (block_pos - pos).map(|x| x.abs()).sum() as f32;
1117
1118 let mut astar = Astar::new(
1119 iters,
1120 player_pos.map(|x| x.floor() as i32),
1121 BuildHasherDefault::<FxHasher64>::default(),
1122 );
1123
1124 let transition = |a: Vec3<i32>, b: Vec3<i32>| {
1127 let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
1128 ((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
1129 };
1130 let neighbors = |pos: &Vec3<i32>| {
1132 const DIRS: [Vec3<i32>; 6] = [
1133 Vec3::new(1, 0, 0),
1134 Vec3::new(-1, 0, 0),
1135 Vec3::new(0, 1, 0),
1136 Vec3::new(0, -1, 0),
1137 Vec3::new(0, 0, 1),
1138 Vec3::new(0, 0, -1),
1139 ];
1140 let pos = *pos;
1141 DIRS.iter()
1142 .map(move |dir| {
1143 let dest = dir + pos;
1144 (dest, transition(pos, dest))
1145 })
1146 .filter(|(pos, _)| {
1147 terrain
1148 .get(*pos)
1149 .ok()
1150 .is_some_and(|block| !block.is_filled())
1151 })
1152 };
1153 let satisfied = |pos: &Vec3<i32>| *pos == block_pos;
1155
1156 astar
1157 .poll(iters, heuristic, neighbors, satisfied)
1158 .into_path()
1159 .is_some()
1160 } else {
1161 false
1162 }
1163}
1164
1165pub fn handle_manipulate_loadout(
1167 data: &JoinData<'_>,
1168 output_events: &mut OutputEvents,
1169 update: &mut StateUpdate,
1170 inv_action: InventoryAction,
1171) {
1172 if !matches!(inv_action, InventoryAction::Collect(_)) {
1175 loadout_change_hook(data, output_events, true);
1176 }
1177 match inv_action {
1178 InventoryAction::Use(slot @ Slot::Inventory(inv_slot)) => {
1179 use use_item::ItemUseKind;
1183 if let Some((item_kind, item)) = data
1184 .inventory
1185 .and_then(|inv| inv.get(inv_slot))
1186 .and_then(|item| Option::<ItemUseKind>::from(&*item.kind()).zip(Some(item)))
1187 {
1188 let (buildup_duration, use_duration, recover_duration) = item_kind.durations();
1189 update.character = CharacterState::UseItem(use_item::Data {
1191 static_data: use_item::StaticData {
1192 buildup_duration,
1193 use_duration,
1194 recover_duration,
1195 inv_slot,
1196 item_kind,
1197 item_hash: item.item_hash(),
1198 was_wielded: data.character.is_wield(),
1199 was_sneak: data.character.is_stealthy(),
1200 },
1201 timer: Duration::default(),
1202 stage_section: StageSection::Buildup,
1203 });
1204 } else {
1205 let inv_manip = InventoryManip::Use(slot);
1207 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1208 }
1209 },
1210 InventoryAction::Collect(sprite_pos) => {
1211 let sprite_at_pos = data
1213 .terrain
1214 .get(sprite_pos)
1215 .ok()
1216 .copied()
1217 .and_then(|b| b.get_sprite());
1218 let sprite_interact =
1221 sprite_at_pos.and_then(Option::<interact::SpriteInteractKind>::from);
1222 if let Some(sprite_interact) = sprite_interact
1223 && can_reach_block(
1224 data.pos.0,
1225 sprite_pos,
1226 MAX_PICKUP_RANGE,
1227 data.body,
1228 data.terrain,
1229 )
1230 {
1231 let sprite_cfg = data.terrain.sprite_cfg_at(sprite_pos);
1232 let required_item = sprite_at_pos.and_then(|s| {
1233 s.unlock_condition(sprite_cfg)
1234 .and_then(|unlock| match unlock.into_owned() {
1235 UnlockKind::Free => None,
1236 UnlockKind::Requires(item) => Some((item, false)),
1237 UnlockKind::Consumes(item) => Some((item, true)),
1238 })
1239 });
1240 let has_required_items = match required_item {
1244 Some((item_id, consume)) => data
1246 .inventory
1247 .and_then(|inv| inv.get_slot_of_item_by_def_id(&item_id))
1248 .map(|slot| Some((item_id, slot, consume))),
1249 None => Some(None),
1250 };
1251 if let Some(required_item) = has_required_items {
1252 let (buildup_duration, use_duration, recover_duration) =
1257 sprite_interact.durations();
1258
1259 update.character = CharacterState::Interact(interact::Data {
1260 static_data: interact::StaticData {
1261 buildup_duration,
1262 use_duration: Some(use_duration),
1264 recover_duration,
1265 interact: interact::InteractKind::Sprite {
1266 pos: sprite_pos,
1267 kind: sprite_interact,
1268 },
1269 was_wielded: data.character.is_wield(),
1270 was_sneak: data.character.is_stealthy(),
1271 required_item,
1272 },
1273 timer: Duration::default(),
1274 stage_section: StageSection::Buildup,
1275 })
1276 } else {
1277 output_events.emit_local(LocalEvent::CreateOutcome(
1278 Outcome::FailedSpriteUnlock { pos: sprite_pos },
1279 ));
1280 }
1281 }
1282 },
1283 InventoryAction::Swap(equip, slot) => {
1285 let inv_manip = InventoryManip::Swap(Slot::Equip(equip), slot);
1286 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1287 },
1288 InventoryAction::Drop(equip) => {
1289 let inv_manip = InventoryManip::Drop(Slot::Equip(equip));
1290 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1291 },
1292 InventoryAction::Sort(sort_order) => {
1293 output_events.emit_server(InventoryManipEvent(
1294 data.entity,
1295 InventoryManip::Sort(sort_order),
1296 ));
1297 },
1298 InventoryAction::Use(slot @ Slot::Equip(_)) => {
1299 let inv_manip = InventoryManip::Use(slot);
1300 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1301 },
1302 InventoryAction::Use(Slot::Overflow(_)) => {
1303 },
1305 InventoryAction::ToggleSpriteLight(pos, enable) => {
1306 if matches!(pos.kind, Volume::Terrain) {
1307 let sprite_interact = interact::SpriteInteractKind::ToggleLight(enable);
1308
1309 let (buildup_duration, use_duration, recover_duration) =
1310 sprite_interact.durations();
1311
1312 update.character = CharacterState::Interact(interact::Data {
1313 static_data: interact::StaticData {
1314 buildup_duration,
1315 use_duration: Some(use_duration),
1316 recover_duration,
1317 interact: interact::InteractKind::Sprite {
1318 pos: pos.pos,
1319 kind: sprite_interact,
1320 },
1321 was_wielded: data.character.is_wield(),
1322 was_sneak: data.character.is_stealthy(),
1323 required_item: None,
1324 },
1325 timer: Duration::default(),
1326 stage_section: StageSection::Buildup,
1327 });
1328 }
1329 },
1330 }
1331}
1332
1333pub fn attempt_glide_wield(
1335 data: &JoinData<'_>,
1336 update: &mut StateUpdate,
1337 output_events: &mut OutputEvents,
1338) {
1339 if data
1340 .inventory
1341 .and_then(|inv| inv.equipped(EquipSlot::Glider))
1342 .is_some()
1343 && !data
1344 .physics
1345 .in_liquid()
1346 .map(|depth| depth > 1.0)
1347 .unwrap_or(false)
1348 && data.body.is_humanoid()
1349 && data.mount_data.is_none()
1350 && data.volume_mount_data.is_none()
1351 {
1352 output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Glider {
1353 pos: data.pos.0,
1354 wielded: true,
1355 }));
1356 update.character = CharacterState::GlideWield(glide_wield::Data::from(data));
1357 }
1358}
1359
1360pub fn handle_jump(
1362 data: &JoinData<'_>,
1363 output_events: &mut OutputEvents,
1364 _update: &mut StateUpdate,
1365 strength: f32,
1366) -> bool {
1367 input_is_pressed(data, InputKind::Jump)
1368 .then(|| data.body.jump_impulse())
1369 .flatten()
1370 .and_then(|impulse| {
1371 if data.physics.in_liquid().is_some() {
1372 if data.physics.on_wall.is_some() {
1373 Some(impulse * 0.75)
1376 } else {
1377 None
1378 }
1379 } else if data.physics.on_ground.is_some() {
1380 Some(impulse)
1381 } else {
1382 None
1383 }
1384 })
1385 .map(|impulse| {
1386 output_events.emit_local(LocalEvent::Jump(
1387 data.entity,
1388 strength * impulse / data.mass.0
1389 * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25))
1390 * data.stats.jump_modifier,
1391 ));
1392 })
1393 .is_some()
1394}
1395
1396pub fn handle_walljump(
1397 data: &JoinData<'_>,
1398 output_events: &mut OutputEvents,
1399 update: &mut StateUpdate,
1400 was_wielded: bool,
1401) -> bool {
1402 let Some(wall_dir) = data.physics.on_wall else {
1403 return false;
1404 };
1405 const WALL_JUMP_Z: f32 = 0.7;
1406 let look_dir = data.inputs.look_dir.vec();
1407
1408 let jump_dir = if look_dir.xy().dot(wall_dir.xy()) > 0.0 {
1410 look_dir.xy().reflected(-wall_dir.xy()).with_z(WALL_JUMP_Z)
1411 } else {
1412 *look_dir
1413 };
1414
1415 let jump_dir = if data.inputs.move_dir.dot(-wall_dir.xy()) > 0.0 {
1417 data.inputs.move_dir.with_z(WALL_JUMP_Z)
1418 } else {
1419 jump_dir
1420 };
1421
1422 let jump_dir = if jump_dir.xy().iter().all(|e| *e < 0.001) {
1424 jump_dir - wall_dir.xy() * 0.1
1425 } else {
1426 jump_dir
1427 }
1428 .try_normalized()
1429 .unwrap_or(Vec3::zero());
1430
1431 if let Some(jump_impulse) = data.body.jump_impulse() {
1432 update.ori = update
1434 .ori
1435 .slerped_towards(Ori::from(Dir::new(jump_dir)), 20.0);
1436 const WALL_JUMP_FACTOR: f32 = 1.1;
1438 output_events.emit_local(LocalEvent::ApplyImpulse {
1440 entity: data.entity,
1441 impulse: jump_dir * WALL_JUMP_FACTOR * jump_impulse / data.mass.0
1442 * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25)),
1443 });
1444 }
1445 if was_wielded {
1446 update.character = CharacterState::Wielding(wielding::Data { is_sneaking: false });
1447 } else {
1448 update.character = CharacterState::Idle(idle::Data::default());
1449 }
1450 true
1451}
1452
1453fn handle_ability(
1454 data: &JoinData<'_>,
1455 update: &mut StateUpdate,
1456 output_events: &mut OutputEvents,
1457 input: InputKind,
1458) -> bool {
1459 if let Some(ability_input) = input.into()
1460 && let Some((ability, from_offhand, spec_ability)) = data
1461 .active_abilities
1462 .and_then(|a| {
1463 a.activate_ability(
1464 ability_input,
1465 data.inventory,
1466 data.skill_set,
1467 Some(data.body),
1468 Some(data.character),
1469 data.stance,
1470 data.combo,
1471 Some(data.stats),
1472 data.buffs,
1473 )
1474 })
1475 .map(|(mut a, f, s)| {
1476 let mut contextual_stats =
1477 if let Some(contextual_stats) = a.ability_meta().contextual_stats {
1478 contextual_stats.equivalent_stats(data)
1479 } else {
1480 tool::Stats::one()
1481 };
1482 contextual_stats.energy_efficiency *= data.stats.energy_efficiency_modifier;
1483 a = a.adjusted_by_stats(contextual_stats);
1484 (a, f, s)
1485 })
1486 .filter(|(ability, _, _)| ability.requirements_paid(data, update))
1487 {
1488 let ability_meta = ability.ability_meta();
1491 {
1492 let AbilityRequirements { stance: _, item } = ability_meta.requirements;
1493 let inv_slot = item.and_then(|item| {
1494 data.inventory
1495 .and_then(|inv| inv.get_slot_of_item_by_def_id(&item.item_def_id()))
1496 });
1497 if let Some(inv_slot) = inv_slot {
1498 let inv_manip = InventoryManip::Delete(
1499 inv_slot,
1500 NonZeroU32::new(1).expect("1 is greater than 0"),
1501 );
1502 output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
1503 }
1504 }
1505 match CharacterState::try_from((
1506 &ability,
1507 AbilityInfo::new(data, from_offhand, input, Some(spec_ability), ability_meta),
1508 data,
1509 )) {
1510 Ok(character_state) => {
1511 let tool_kind = character_state.ability_info().and_then(|ai| ai.tool);
1512 let target_uid = character_state
1513 .ability_info()
1514 .and_then(|ai| ai.input_attr)
1515 .and_then(|ia| ia.target_entity);
1516 update.character = character_state;
1517
1518 for init_event in ability
1519 .ability_meta()
1520 .init_event
1521 .iter()
1522 .chain(ability.ability_meta().init_event2.iter())
1523 {
1524 match init_event {
1525 AbilityInitEvent::EnterStance(stance) => {
1526 output_events.emit_server(ChangeStanceEvent {
1527 entity: data.entity,
1528 stance: *stance,
1529 });
1530 },
1531 AbilityInitEvent::GainBuff {
1532 kind,
1533 strength,
1534 duration,
1535 } => {
1536 let dest_info = DestInfo {
1537 stats: Some(data.stats),
1538 mass: Some(data.mass),
1539 };
1540 output_events.emit_server(BuffEvent {
1541 entity: data.entity,
1542 buff_change: BuffChange::Add(Buff::new(
1543 *kind,
1544 BuffData::new(*strength, *duration),
1545 vec![BuffCategory::SelfBuff],
1546 BuffSource::Character {
1547 by: *data.uid,
1548 tool_kind,
1549 },
1550 *data.time,
1551 dest_info,
1552 Some(data.mass),
1553 target_uid,
1554 )),
1555 });
1556 },
1557 AbilityInitEvent::RemoveBuff(buff) => {
1558 output_events.emit_server(BuffEvent {
1559 entity: data.entity,
1560 buff_change: BuffChange::RemoveByKind(*buff),
1561 });
1562 },
1563 }
1564 }
1565 if let CharacterState::Roll(roll) = &mut update.character {
1566 if data.character.is_wield() || data.character.was_wielded() {
1567 roll.was_wielded = true;
1568 }
1569 if data.character.is_stealthy() {
1570 roll.is_sneaking = true;
1571 }
1572 if data.character.is_aimed() {
1573 roll.prev_aimed_dir = Some(data.controller.inputs.look_dir);
1574 }
1575 }
1576 return true;
1577 },
1578 Err(err) => {
1579 warn!("Failed to enter character state: {err:?}");
1580 },
1581 }
1582 }
1583 false
1584}
1585
1586pub fn handle_input(
1587 data: &JoinData<'_>,
1588 output_events: &mut OutputEvents,
1589 update: &mut StateUpdate,
1590 input: InputKind,
1591) {
1592 match input {
1593 InputKind::Primary
1594 | InputKind::Secondary
1595 | InputKind::Ability(_)
1596 | InputKind::Block
1597 | InputKind::Roll => {
1598 handle_ability(data, update, output_events, input);
1599 },
1600 InputKind::Jump => {
1601 handle_jump(data, output_events, update, 1.0);
1602 },
1603 InputKind::WallJump | InputKind::Fly => {},
1604 }
1605}
1606
1607pub fn handle_glider_input_or(
1611 data: &JoinData<'_>,
1612 update: &mut StateUpdate,
1613 output_events: &mut OutputEvents,
1614 fallback_fn: fn(&JoinData<'_>, &mut StateUpdate),
1615) {
1616 if data
1617 .inventory
1618 .and_then(|inv| inv.equipped(EquipSlot::Glider))
1619 .and_then(|glider| glider.item_config())
1620 .is_none()
1621 {
1622 fallback_fn(data, update);
1623 return;
1624 };
1625
1626 if let Some(input) = data.controller.queued_inputs.keys().next() {
1627 handle_ability(data, update, output_events, *input);
1628 };
1629}
1630
1631pub fn attempt_input(
1632 data: &JoinData<'_>,
1633 output_events: &mut OutputEvents,
1634 update: &mut StateUpdate,
1635) {
1636 if let Some(input) = data.controller.queued_inputs.keys().next() {
1638 handle_input(data, output_events, update, *input);
1639 }
1640}
1641
1642pub fn handle_interrupts(
1644 data: &JoinData,
1645 update: &mut StateUpdate,
1646 output_events: &mut OutputEvents,
1647) -> bool {
1648 let can_dodge = matches!(
1649 data.character.stage_section(),
1650 Some(StageSection::Buildup | StageSection::Recover)
1651 );
1652 let can_block = data
1653 .character
1654 .ability_info()
1655 .map(|info| info.ability_meta)
1656 .is_some_and(|meta| meta.capabilities.contains(Capability::BLOCK_INTERRUPT));
1657 if can_dodge && input_is_pressed(data, InputKind::Roll) {
1658 handle_ability(data, update, output_events, InputKind::Roll)
1659 } else if can_block && input_is_pressed(data, InputKind::Block) {
1660 handle_ability(data, update, output_events, InputKind::Block)
1661 } else {
1662 false
1663 }
1664}
1665
1666pub fn is_strafing(data: &JoinData<'_>, update: &StateUpdate) -> bool {
1667 (update.character.is_aimed() || update.should_strafe) && data.body.can_strafe()
1670 && !matches!(unwrap_tool_data(data, EquipSlot::ActiveMainhand),
1672 Some((ToolKind::Instrument, _)))
1673}
1674
1675pub fn unwrap_tool_data(data: &JoinData, equip_slot: EquipSlot) -> Option<(ToolKind, Hands)> {
1677 if let Some(ItemKind::Tool(tool)) = data
1678 .inventory
1679 .and_then(|inv| inv.equipped(equip_slot))
1680 .map(|i| i.kind())
1681 .as_deref()
1682 {
1683 Some((tool.kind, tool.hands))
1684 } else {
1685 None
1686 }
1687}
1688
1689pub fn get_hands(data: &JoinData<'_>) -> (Option<Hands>, Option<Hands>) {
1690 let hand = |slot| {
1691 if let Some(ItemKind::Tool(tool)) = data
1692 .inventory
1693 .and_then(|inv| inv.equipped(slot))
1694 .map(|i| i.kind())
1695 .as_deref()
1696 {
1697 Some(tool.hands)
1698 } else {
1699 None
1700 }
1701 };
1702 (
1703 hand(EquipSlot::ActiveMainhand),
1704 hand(EquipSlot::ActiveOffhand),
1705 )
1706}
1707
1708pub fn get_tool_stats(data: &JoinData<'_>, ai: AbilityInfo) -> tool::Stats {
1709 ai.hand
1710 .map(|hand| hand.to_equip_slot())
1711 .and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
1712 .and_then(|item| {
1713 if let ItemKind::Tool(tool) = &*item.kind() {
1714 Some(tool.stats(item.stats_durability_multiplier()))
1715 } else {
1716 None
1717 }
1718 })
1719 .unwrap_or(tool::Stats::one())
1720}
1721
1722pub fn input_is_pressed(data: &JoinData<'_>, input: InputKind) -> bool {
1723 data.controller.queued_inputs.contains_key(&input)
1724}
1725
1726fn checked_tick(data: &JoinData<'_>, timer: Duration, modifier: Option<f32>) -> Option<Duration> {
1730 timer.checked_add(Duration::from_secs_f32(data.dt.0 * modifier.unwrap_or(1.0)))
1731}
1732
1733pub fn tick_or_default(data: &JoinData<'_>, timer: Duration, modifier: Option<f32>) -> Duration {
1736 checked_tick(data, timer, modifier).unwrap_or_default()
1737}
1738
1739fn checked_tick_attack(
1743 data: &JoinData<'_>,
1744 timer: Duration,
1745 other_modifier: Option<f32>,
1746) -> Option<Duration> {
1747 let section_modifier = match data.character.stage_section() {
1748 Some(StageSection::Buildup) => data.stats.buildup_speed_modifier,
1749 Some(StageSection::Charge) => data.stats.charge_speed_modifier,
1750 Some(StageSection::Recover) => data.stats.recovery_speed_modifier,
1751 _ => 1.0,
1752 };
1753 checked_tick(
1754 data,
1755 timer,
1756 Some(data.stats.attack_speed_modifier * section_modifier * other_modifier.unwrap_or(1.0)),
1757 )
1758}
1759
1760pub fn tick_attack_or_default(
1763 data: &JoinData<'_>,
1764 timer: Duration,
1765 other_modifier: Option<f32>,
1766) -> Duration {
1767 checked_tick_attack(data, timer, other_modifier).unwrap_or_default()
1768}
1769
1770#[derive(Clone, Copy, Debug, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
1774pub enum StageSection {
1775 Buildup,
1776 Recover,
1777 Charge,
1778 Movement,
1779 Action,
1780}
1781
1782#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1783pub enum ForcedMovement {
1784 Forward(f32),
1785 Reverse(f32),
1786 Sideways(f32),
1787 DirectedReverse(f32),
1788 AntiDirectedForward(f32),
1789 Leap {
1790 vertical: f32,
1791 forward: f32,
1792 progress: f32,
1793 direction: MovementDirection,
1794 },
1795}
1796
1797impl Mul<f32> for ForcedMovement {
1798 type Output = Self;
1799
1800 fn mul(self, scalar: f32) -> Self {
1801 use ForcedMovement::*;
1802 match self {
1803 Forward(x) => Forward(x * scalar),
1804 Reverse(x) => Reverse(x * scalar),
1805 Sideways(x) => Sideways(x * scalar),
1806 DirectedReverse(x) => DirectedReverse(x * scalar),
1807 AntiDirectedForward(x) => AntiDirectedForward(x * scalar),
1808 Leap {
1809 vertical,
1810 forward,
1811 progress,
1812 direction,
1813 } => Leap {
1814 vertical: vertical * scalar,
1815 forward: forward * scalar,
1816 progress,
1817 direction,
1818 },
1819 }
1820 }
1821}
1822
1823#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1824pub enum MovementDirection {
1825 Look,
1826 AntiLook,
1827 Move,
1828}
1829
1830impl MovementDirection {
1831 pub fn get_2d_dir(self, data: &JoinData<'_>) -> Vec2<f32> {
1832 use MovementDirection::*;
1833 match self {
1834 Look => data
1835 .inputs
1836 .look_dir
1837 .to_horizontal()
1838 .unwrap_or_default()
1839 .xy(),
1840 AntiLook => -data
1841 .inputs
1842 .look_dir
1843 .to_horizontal()
1844 .unwrap_or_default()
1845 .xy(),
1846 Move => data.inputs.move_dir,
1847 }
1848 .try_normalized()
1849 .unwrap_or_default()
1850 }
1851}
1852
1853#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
1854pub struct AbilityInfo {
1855 pub tool: Option<ToolKind>,
1856 pub hand: Option<HandInfo>,
1857 pub input: InputKind,
1858 pub input_attr: Option<InputAttr>,
1859 pub ability_meta: AbilityMeta,
1860 pub ability: Option<SpecifiedAbility>,
1861}
1862
1863impl AbilityInfo {
1864 pub fn new(
1865 data: &JoinData<'_>,
1866 from_offhand: bool,
1867 input: InputKind,
1868 ability: Option<SpecifiedAbility>,
1869 ability_meta: AbilityMeta,
1870 ) -> Self {
1871 let tool_data = if from_offhand {
1872 unwrap_tool_data(data, EquipSlot::ActiveOffhand)
1873 } else {
1874 unwrap_tool_data(data, EquipSlot::ActiveMainhand)
1875 };
1876 let (tool, hand) = tool_data.map_or((None, None), |(kind, hands)| {
1877 (
1878 Some(kind),
1879 Some(HandInfo::from_main_tool(hands, from_offhand)),
1880 )
1881 });
1882
1883 Self {
1884 tool,
1885 hand,
1886 input,
1887 input_attr: data.controller.queued_inputs.get(&input).copied(),
1888 ability_meta,
1889 ability,
1890 }
1891 }
1892}
1893
1894pub fn end_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
1895 if data.character.is_wield() || data.character.was_wielded() {
1896 update.character = CharacterState::Wielding(wielding::Data {
1897 is_sneaking: data.character.is_stealthy(),
1898 });
1899 } else {
1900 update.character = CharacterState::Idle(idle::Data {
1901 is_sneaking: data.character.is_stealthy(),
1902 footwear: None,
1903 time_entered: *data.time,
1904 });
1905 }
1906 if let CharacterState::Roll(roll) = data.character
1907 && let Some(dir) = roll.prev_aimed_dir
1908 {
1909 update.ori = dir.into();
1910 }
1911}
1912
1913pub fn end_melee_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
1914 end_ability(data, update);
1915 data.updater.remove::<Melee>(data.entity);
1916}
1917
1918#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1919pub enum HandInfo {
1920 TwoHanded,
1921 MainHand,
1922 OffHand,
1923}
1924
1925impl HandInfo {
1926 pub fn from_main_tool(tool_hands: Hands, from_offhand: bool) -> Self {
1927 match tool_hands {
1928 Hands::Two => Self::TwoHanded,
1929 Hands::One => {
1930 if from_offhand {
1931 Self::OffHand
1932 } else {
1933 Self::MainHand
1934 }
1935 },
1936 }
1937 }
1938
1939 pub fn to_equip_slot(&self) -> EquipSlot {
1940 match self {
1941 HandInfo::TwoHanded | HandInfo::MainHand => EquipSlot::ActiveMainhand,
1942 HandInfo::OffHand => EquipSlot::ActiveOffhand,
1943 }
1944 }
1945}
1946
1947pub fn leave_stance(data: &JoinData<'_>, output_events: &mut OutputEvents) {
1948 if !matches!(data.stance, Some(Stance::None)) {
1949 output_events.emit_server(ChangeStanceEvent {
1950 entity: data.entity,
1951 stance: Stance::None,
1952 });
1953 }
1954}
1955
1956#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
1957pub enum ComboConsumption {
1958 #[default]
1959 All,
1960 Half,
1961 Cost,
1962}
1963
1964impl ComboConsumption {
1965 pub fn consume(&self, data: &JoinData, output_events: &mut OutputEvents, cost: u32) {
1966 let combo = data.combo.map_or(0, |c| c.counter());
1967 let to_consume = match self {
1968 Self::All => combo,
1969 Self::Half => combo.div_ceil(2),
1970 Self::Cost => cost,
1971 };
1972 output_events.emit_server(ComboChangeEvent {
1973 entity: data.entity,
1974 change: -(to_consume as i32),
1975 });
1976 }
1977}
1978
1979fn loadout_change_hook(data: &JoinData<'_>, output_events: &mut OutputEvents, clear_combo: bool) {
1980 if clear_combo {
1981 output_events.emit_server(ComboChangeEvent {
1983 entity: data.entity,
1984 change: -data.combo.map_or(0, |c| c.counter() as i32),
1985 });
1986 }
1987 output_events.emit_server(BuffEvent {
1989 entity: data.entity,
1990 buff_change: BuffChange::RemoveByCategory {
1991 all_required: vec![BuffCategory::RemoveOnLoadoutChange],
1992 any_required: vec![],
1993 none_required: vec![],
1994 },
1995 });
1996}
1997
1998#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
1999#[serde(deny_unknown_fields)]
2000pub struct MovementModifier {
2001 pub buildup: Option<f32>,
2002 pub action: Option<f32>,
2003 pub recover: Option<f32>,
2004}
2005
2006#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
2007#[serde(deny_unknown_fields)]
2008pub struct OrientationModifier {
2009 pub buildup: Option<f32>,
2010 pub action: Option<f32>,
2011 pub recover: Option<f32>,
2012}
2013
2014#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
2015pub enum ProjectileSpread {
2016 Increasing(f32),
2017 Horizontal(f32),
2018}
2019
2020impl ProjectileSpread {
2021 pub fn compute_directions(
2022 self,
2023 init_dir: Dir,
2024 init_ori: Ori,
2025 num: u32,
2026 rng: &mut impl RngExt,
2027 ) -> impl Iterator<Item = Dir> + '_ {
2028 match self {
2029 Self::Increasing(spread) => Either::Left(
2030 (0..num).map(move |i| {
2033 Dir::from_unnormalized(init_dir.map(|x| {
2034 let offset = (2.0 * rng.random::<f32>() - 1.0) * spread * i as f32;
2035 x + offset
2036 }))
2037 .unwrap_or(init_dir)
2038 }),
2039 ),
2040 Self::Horizontal(spread) => Either::Right(if num < 2 {
2041 Either::Left(std::iter::once(init_dir))
2042 } else {
2043 let left = -spread.to_radians();
2044 let increment = spread.to_radians() * 2.0 / (num as f32 - 1.0);
2045 let rot_quat_dir = Quaternion::<f32>::rotation_from_to_3d(
2046 Vec3::unit_y(),
2047 Vec3::new(0.0, init_dir.xy().magnitude(), init_dir.z),
2048 );
2049 Either::Right((0..num).map(move |i| {
2050 let angle = left + increment * i as f32;
2051 let rot_quat_spread = Quaternion::<f32>::rotation_from_to_3d(
2052 Vec3::unit_y(),
2053 Vec2::unit_y().rotated_z(angle).with_z(0.0),
2054 );
2055 Dir::from_unnormalized(
2056 Ori::new(init_ori.to_quat() * rot_quat_dir * rot_quat_spread).look_vec(),
2057 )
2058 .unwrap_or(init_dir)
2059 }))
2060 }),
2061 }
2062 }
2063
2064 pub fn estimated_spread(&self) -> f32 {
2067 match self {
2068 Self::Increasing(spread) | Self::Horizontal(spread) => *spread,
2070 }
2071 }
2072}