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