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