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