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