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