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