1use crate::shared_world::SabSlot;
7use aetheris_protocol::error::WorldError;
8use aetheris_protocol::events::{ComponentUpdate, ReplicationEvent};
9use aetheris_protocol::traits::WorldState;
10use aetheris_protocol::types::{
11 AgentKind, AgentProperties, BEAM_MARKER_KIND, ClientId, ComponentKind, LocalId, NetworkId,
12 Transform,
13};
14use std::collections::{BTreeMap, VecDeque};
15
16#[derive(Clone, Copy, Debug)]
17pub struct InputRecord {
18 pub tick: u64,
19 pub move_x: f32,
20 pub move_y: f32,
21 pub actions_mask: u8,
22}
23
24#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
25pub struct Velocity {
26 pub dx: f32,
27 pub dy: f32,
28 pub dz: f32,
29}
30
31#[derive(Debug)]
33pub struct ClientWorld {
34 pub entities: BTreeMap<NetworkId, SabSlot>,
36 pub player_network_id: Option<NetworkId>,
38 pub latest_tick: u64,
40 pub workspace_manifest: BTreeMap<String, String>,
42 pub shared_world_ref: Option<usize>,
44 pub input_history: VecDeque<InputRecord>,
46 pub last_reconciled_tick: u64,
48 pub prediction_enabled: bool,
53 pub workspace_bounds: Option<aetheris_protocol::types::WorkspaceBounds>,
56}
57
58impl Default for ClientWorld {
59 fn default() -> Self {
60 Self::new()
61 }
62}
63
64impl ClientWorld {
65 #[must_use]
71 pub fn new() -> Self {
72 Self::with_prediction(false)
73 }
74
75 #[must_use]
77 pub fn with_prediction(prediction_enabled: bool) -> Self {
78 Self {
79 entities: BTreeMap::new(),
80 player_network_id: None,
81 latest_tick: 0,
82 workspace_manifest: BTreeMap::new(),
83 shared_world_ref: None,
84 input_history: VecDeque::with_capacity(120), last_reconciled_tick: 0,
86 prediction_enabled,
87 workspace_bounds: if prediction_enabled {
88 Some(aetheris_protocol::types::WorkspaceBounds {
89 min_x: -250.0,
90 min_y: -250.0,
91 max_x: 250.0,
92 max_y: 250.0,
93 })
94 } else {
95 None
96 },
97 }
98 }
99}
100
101impl WorldState for ClientWorld {
102 fn get_local_id(&self, network_id: NetworkId) -> Option<LocalId> {
103 Some(LocalId(network_id.0))
104 }
105
106 fn get_network_id(&self, local_id: LocalId) -> Option<NetworkId> {
107 Some(NetworkId(local_id.0))
108 }
109
110 fn extract_deltas(&mut self) -> Vec<ReplicationEvent> {
111 Vec::new()
112 }
113
114 fn apply_updates(&mut self, updates: &[(ClientId, ComponentUpdate)]) {
115 if !updates.is_empty() {
116 tracing::debug!(
117 count = updates.len(),
118 player_network_id = ?self.player_network_id,
119 total_entities = self.entities.len(),
120 "[apply_updates] Processing updates batch"
121 );
122 }
123 for (_, update) in updates {
124 if update.tick > self.latest_tick {
125 self.latest_tick = update.tick;
126 }
127
128 let is_new = !self.entities.contains_key(&update.network_id);
129
130 let entry = self.entities.entry(update.network_id).or_insert_with(|| {
132 tracing::trace!(
133 network_id = update.network_id.0,
134 kind = update.component_kind.0,
135 player_network_id = ?self.player_network_id,
136 "[apply_updates] NEW entity from server"
137 );
138 SabSlot {
139 network_id: update.network_id.0,
140 x: 0.0,
141 y: 0.0,
142 z: 0.0,
143 rotation: 0.0,
144 dx: 0.0,
145 dy: 0.0,
146 dz: 0.0,
147 integrity: 100,
148 priority: 0,
149 entity_type: 0,
150 flags: 1,
151 extraction_active: 0,
152 payload_count: 0,
153 payload_capacity: 0,
154 extraction_target_id: 0,
155 interaction_target_id: 0,
156 interaction_flash_ticks: 0,
157 padding: [0; 3],
158 }
159 });
160
161 let is_player = Some(update.network_id) == self.player_network_id;
164 if is_player {
165 tracing::trace!(
166 network_id = update.network_id.0,
167 is_new,
168 "[apply_updates] Setting 0x04 (LocalPlayer) flag on entity"
169 );
170 entry.flags |= 0x04;
171 } else if is_new {
172 tracing::trace!(
173 network_id = update.network_id.0,
174 player_network_id = ?self.player_network_id,
175 flags = entry.flags,
176 "[apply_updates] New NON-player entity - no possession flag"
177 );
178 }
179
180 self.apply_component_update(update);
181 }
182 }
183
184 fn simulate(&mut self) {
185 const DRAG: f32 = 1.0;
186 const DT: f32 = 1.0 / 60.0;
187
188 for slot in self.entities.values_mut() {
189 let current_drag = if slot.entity_type == 20 { 0.0 } else { DRAG };
192 let drag_factor = 1.0 / (1.0 + current_drag * DT);
193
194 slot.dx *= drag_factor;
195 slot.dy *= drag_factor;
196 slot.x += slot.dx * DT;
197 slot.y += slot.dy * DT;
198
199 if slot.interaction_flash_ticks > 0 {
201 slot.interaction_flash_ticks -= 1;
202 }
203
204 if let Some(bounds) = self.workspace_bounds {
206 let width = bounds.max_x - bounds.min_x;
207 let height = bounds.max_y - bounds.min_y;
208 if width > 0.0 {
209 slot.x = ((slot.x - bounds.min_x).rem_euclid(width)) + bounds.min_x;
210 }
211 if height > 0.0 {
212 slot.y = ((slot.y - bounds.min_y).rem_euclid(height)) + bounds.min_y;
213 }
214 }
215 }
216 }
217
218 fn spawn_networked(&mut self) -> NetworkId {
219 NetworkId(0)
220 }
221
222 fn spawn_networked_for(&mut self, _client_id: ClientId) -> NetworkId {
223 self.spawn_networked()
224 }
225
226 fn despawn_networked(&mut self, network_id: NetworkId) -> Result<(), WorldError> {
227 self.entities
228 .remove(&network_id)
229 .map(|_| ())
230 .ok_or(WorldError::EntityNotFound(network_id))
231 }
232
233 fn stress_test(&mut self, _count: u16, _rotate: bool) {}
234
235 fn spawn_kind(&mut self, _kind: u16, _x: f32, _y: f32, _rot: f32) -> NetworkId {
236 NetworkId(1)
237 }
238
239 fn clear_world(&mut self) {
240 self.entities.clear();
241 }
242
243 fn state_hash(&self) -> u64 {
244 use std::hash::{Hash, Hasher};
245 use twox_hash::XxHash64;
246
247 let mut hasher = XxHash64::with_seed(0);
249 self.latest_tick.hash(&mut hasher);
250
251 for (nid, slot) in &self.entities {
253 nid.hash(&mut hasher);
254
255 slot.x.to_bits().hash(&mut hasher);
257 slot.y.to_bits().hash(&mut hasher);
258 slot.z.to_bits().hash(&mut hasher);
259 slot.rotation.to_bits().hash(&mut hasher);
260
261 slot.dx.to_bits().hash(&mut hasher);
263 slot.dy.to_bits().hash(&mut hasher);
264 slot.dz.to_bits().hash(&mut hasher);
265
266 slot.integrity.hash(&mut hasher);
267 slot.priority.hash(&mut hasher);
268 slot.entity_type.hash(&mut hasher);
269 slot.flags.hash(&mut hasher);
270
271 slot.extraction_active.hash(&mut hasher);
272 slot.payload_count.hash(&mut hasher);
273 slot.extraction_target_id.hash(&mut hasher);
274 slot.interaction_target_id.hash(&mut hasher);
275 slot.interaction_flash_ticks.hash(&mut hasher);
276 }
277
278 hasher.finish()
279 }
280}
281
282impl ClientWorld {
283 pub fn handle_platform_event(&mut self, event: &aetheris_protocol::events::PlatformEvent) {
285 if let aetheris_protocol::events::PlatformEvent::Possession { network_id } = event {
286 let prev = self.player_network_id;
287 tracing::info!(
288 ?network_id,
289 ?prev,
290 entity_exists = self.entities.contains_key(network_id),
291 total_entities = self.entities.len(),
292 "[handle_platform_event] POSSESSION received โ updating player_network_id"
293 );
294 if let Some(slot) = prev
296 .filter(|&id| id != *network_id)
297 .and_then(|id| self.entities.get_mut(&id))
298 {
299 slot.flags &= !0x04;
300 tracing::info!(
301 network_id = ?prev,
302 flags = slot.flags,
303 "[handle_platform_event] 0x04 flag cleared from previous entity"
304 );
305 }
306 self.player_network_id = Some(*network_id);
307 if let Some(slot) = self.entities.get_mut(network_id) {
308 slot.flags |= 0x04;
309 tracing::info!(
310 ?network_id,
311 flags = slot.flags,
312 "[handle_platform_event] 0x04 flag applied to entity"
313 );
314 } else {
315 tracing::warn!(
316 ?network_id,
317 "[handle_platform_event] Possession entity not yet in world - will apply when it arrives"
318 );
319 }
320 } else if let aetheris_protocol::events::PlatformEvent::Interaction {
321 source,
322 target,
323 amount,
324 } = event
325 {
326 tracing::info!(
327 ?source,
328 ?target,
329 amount,
330 "[handle_platform_event] Interaction received"
331 );
332
333 if let Some(slot) = self.entities.get_mut(source) {
335 slot.interaction_target_id = (target.0 & 0xFFFF) as u16;
336 slot.interaction_flash_ticks = 10;
337 }
338
339 if let Some(slot) = self.entities.get_mut(target) {
341 slot.interaction_flash_ticks = 10;
342 }
343 } else if let aetheris_protocol::events::PlatformEvent::Termination { target } = event {
344 tracing::info!(?target, "[handle_platform_event] Termination received");
345 let _ = self.despawn_networked(*target);
348 } else if let aetheris_protocol::events::PlatformEvent::Reinitialization { target, x, y } =
349 event
350 {
351 tracing::info!(
352 ?target,
353 x,
354 y,
355 "[handle_platform_event] Reinitialization received"
356 );
357 } else if let aetheris_protocol::events::PlatformEvent::PayloadCollected {
358 network_id,
359 amount,
360 } = event
361 {
362 tracing::info!(
363 ?network_id,
364 amount,
365 "[handle_platform_event] PayloadCollected received"
366 );
367 let _ = self.despawn_networked(*network_id);
368 }
369 }
370
371 fn apply_component_update(&mut self, update: &ComponentUpdate) {
372 match update.component_kind {
373 ComponentKind(1) => self.handle_transform_update(update),
374 ComponentKind(2) => self.handle_velocity_update(update),
375 ComponentKind(5) => self.handle_agent_kind_update(update),
376 ComponentKind(3) => self.handle_agent_properties_update(update),
377 aetheris_protocol::types::EXTRACTION_BEAM_KIND => {
378 self.handle_extraction_beam_update(update);
379 }
380 aetheris_protocol::types::DATA_STORE_KIND => self.handle_data_store_update(update),
381 aetheris_protocol::types::RESOURCE_KIND => {
382 if let Some(entry) = self.entities.get_mut(&update.network_id)
383 && entry.entity_type == 0
384 {
385 entry.entity_type = 5;
386 }
387 }
388 aetheris_protocol::types::INTEGRITY_POOL_KIND => {
389 self.handle_integrity_pool_update(update);
390 }
391 aetheris_protocol::types::PRIORITY_POOL_KIND => {
392 self.handle_priority_pool_update(update);
393 }
394 aetheris_protocol::types::DATA_DROP_KIND => {
395 if let Some(entry) = self.entities.get_mut(&update.network_id) {
396 entry.entity_type = 6;
397 }
398 }
399 BEAM_MARKER_KIND => {
400 if let Some(entry) = self.entities.get_mut(&update.network_id) {
402 entry.entity_type = 20;
403 }
404 }
405 aetheris_protocol::types::WORKSPACE_BOUNDS_KIND => {
406 self.handle_workspace_bounds_update(update);
407 }
408 ComponentKind(0x2007) => self.handle_presence_update(update),
409 kind => {
410 tracing::debug!(
411 network_id = update.network_id.0,
412 kind = kind.0,
413 "Unhandled component kind"
414 );
415 }
416 }
417 }
418
419 fn handle_transform_update(&mut self, update: &ComponentUpdate) {
420 match rmp_serde::from_slice::<Transform>(&update.payload) {
421 Ok(transform) => {
422 if let Some(entry) = self.entities.get_mut(&update.network_id) {
423 if (entry.flags & 0x04) != 0 && self.prediction_enabled {
424 let mut authoritative_x = transform.x;
426 let mut authoritative_y = transform.y;
427
428 if let Some(bounds) = self.workspace_bounds {
430 let width = bounds.max_x - bounds.min_x;
431 let height = bounds.max_y - bounds.min_y;
432 if width > 0.0 {
433 let dx = authoritative_x - entry.x;
434 if dx.abs() > width * 0.5 {
435 if dx > 0.0 {
436 authoritative_x -= width;
437 } else {
438 authoritative_x += width;
439 }
440 }
441 }
442 if height > 0.0 {
443 let dy = authoritative_y - entry.y;
444 if dy.abs() > height * 0.5 {
445 if dy > 0.0 {
446 authoritative_y -= height;
447 } else {
448 authoritative_y += height;
449 }
450 }
451 }
452 }
453
454 entry.x = authoritative_x;
455 entry.y = authoritative_y;
456 entry.z = transform.z;
457 entry.rotation = transform.rotation;
458
459 let server_tick = update.tick;
460 for record in self.input_history.iter().filter(|r| r.tick > server_tick) {
461 Self::simulate_slot_wrapped(
462 entry,
463 record.move_x,
464 record.move_y,
465 self.workspace_bounds,
466 );
467 }
468 while self
469 .input_history
470 .front()
471 .is_some_and(|r| r.tick <= server_tick)
472 {
473 self.input_history.pop_front();
474 }
475 } else {
476 entry.x = transform.x;
478 entry.y = transform.y;
479 entry.z = transform.z;
480 entry.rotation = transform.rotation;
481 self.input_history.clear();
482 }
483
484 if transform.entity_type != 0 && entry.entity_type != 0x2007 {
485 entry.entity_type = transform.entity_type;
486 }
487 }
488 }
489 Err(e) => {
490 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode Transform");
491 }
492 }
493 }
494
495 fn handle_velocity_update(&mut self, update: &ComponentUpdate) {
496 match rmp_serde::from_slice::<Velocity>(&update.payload) {
497 Ok(velocity) => {
498 if let Some(entry) = self.entities.get_mut(&update.network_id) {
499 entry.dx = velocity.dx;
500 entry.dy = velocity.dy;
501 entry.dz = velocity.dz;
502 }
503 }
504 Err(e) => {
505 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode Velocity");
506 }
507 }
508 }
509
510 fn handle_agent_kind_update(&mut self, update: &ComponentUpdate) {
511 match rmp_serde::from_slice::<AgentKind>(&update.payload) {
512 Ok(agent_kind) => {
513 if let Some(entry) = self.entities.get_mut(&update.network_id) {
514 entry.entity_type = match agent_kind {
515 AgentKind::Standard => 1,
516 AgentKind::Heavy => 3,
517 AgentKind::Carrier => 4,
518 };
519 }
520 }
521 Err(e) => {
522 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode AgentKind");
523 }
524 }
525 }
526
527 fn handle_agent_properties_update(&mut self, update: &ComponentUpdate) {
528 match rmp_serde::from_slice::<AgentProperties>(&update.payload) {
529 Ok(properties) => {
530 if let Some(entry) = self.entities.get_mut(&update.network_id) {
531 entry.integrity = properties.integrity;
532 entry.priority = properties.priority;
533 }
534 }
535 Err(e) => {
536 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode AgentProperties");
537 }
538 }
539 }
540
541 fn handle_extraction_beam_update(&mut self, update: &ComponentUpdate) {
542 use aetheris_protocol::types::ExtractionBeam;
543 match rmp_serde::from_slice::<ExtractionBeam>(&update.payload) {
544 Ok(beam) => {
545 if let Some(entry) = self.entities.get_mut(&update.network_id) {
546 entry.extraction_active = u8::from(beam.active);
547 #[allow(clippy::cast_possible_truncation)]
548 {
549 entry.extraction_target_id = beam.target.map_or(0, |id| id.0 as u16);
550 }
551 }
552 }
553 Err(e) => {
554 tracing::warn!(
555 network_id = update.network_id.0,
556 error = ?e,
557 payload = %hex::encode(&update.payload),
558 "Failed to decode ExtractionBeam"
559 );
560 }
561 }
562 }
563
564 fn handle_data_store_update(&mut self, update: &ComponentUpdate) {
565 use aetheris_protocol::types::DataStore;
566 match rmp_serde::from_slice::<DataStore>(&update.payload) {
567 Ok(store) => {
568 if let Some(entry) = self.entities.get_mut(&update.network_id) {
569 entry.payload_count = store.payload_count;
570 entry.payload_capacity = store.capacity;
571 entry.flags |= 0x04;
572 }
573 }
574 Err(e) => {
575 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode DataStore");
576 }
577 }
578 }
579
580 fn handle_workspace_bounds_update(&mut self, update: &ComponentUpdate) {
581 use aetheris_protocol::types::WorkspaceBounds;
582 if let (Ok(bounds), Some(ptr_val)) = (
583 rmp_serde::from_slice::<WorkspaceBounds>(&update.payload),
584 self.shared_world_ref,
585 ) {
586 let sw = unsafe { crate::shared_world::SharedWorld::from_ptr(ptr_val as *mut u8) };
587 sw.set_workspace_bounds(bounds.min_x, bounds.min_y, bounds.max_x, bounds.max_y);
588 self.workspace_bounds = Some(bounds);
589 }
590 }
591
592 fn handle_priority_pool_update(&mut self, update: &ComponentUpdate) {
593 use aetheris_protocol::types::PriorityPool;
594 match rmp_serde::from_slice::<PriorityPool>(&update.payload) {
595 Ok(pool) => {
596 if let Some(entry) = self.entities.get_mut(&update.network_id) {
597 entry.priority = pool.current;
598 }
599 }
600 Err(e) => {
601 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode PriorityPool");
602 }
603 }
604 }
605
606 fn handle_integrity_pool_update(&mut self, update: &ComponentUpdate) {
607 use aetheris_protocol::types::IntegrityPool;
608 match rmp_serde::from_slice::<IntegrityPool>(&update.payload) {
609 Ok(pool) => {
610 if let Some(entry) = self.entities.get_mut(&update.network_id) {
611 entry.integrity = pool.current;
612 }
613 }
614 Err(e) => {
615 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode IntegrityPool");
616 }
617 }
618 }
619
620 fn handle_presence_update(&mut self, update: &ComponentUpdate) {
621 #[derive(serde::Deserialize)]
622 struct PresenceMinimal {
623 x: f32,
624 y: f32,
625 #[allow(dead_code)]
626 name: String,
627 #[allow(dead_code)]
628 client_id: ClientId,
629 }
630
631 match rmp_serde::from_slice::<PresenceMinimal>(&update.payload) {
632 Ok(presence) => {
633 if let Some(entry) = self.entities.get_mut(&update.network_id) {
634 entry.x = presence.x;
635 entry.y = presence.y;
636 entry.entity_type = 0x2007; }
638 }
639 Err(e) => {
640 tracing::warn!(
641 network_id = update.network_id.0,
642 error = ?e,
643 "Failed to decode PresenceMinimal"
644 );
645 }
646 }
647 }
648}
649
650impl ClientWorld {
651 fn simulate_slot(slot: &mut SabSlot, move_x: f32, move_y: f32) {
653 const THRUST_FORCE: f32 = 8000.0;
654 const BASE_MASS: f32 = 100.0;
655 const MASS_PER_PAYLOAD: f32 = 2.0;
656 const DRAG: f32 = 2.0;
657 const MAX_SPEED: f32 = 75.0;
658 const DT: f32 = 1.0 / 60.0;
659
660 let total_mass = BASE_MASS + (f32::from(slot.payload_count) * MASS_PER_PAYLOAD);
662
663 let mut mx = move_x;
665 let mut my = move_y;
666 let input_len_sq = mx * mx + my * my;
667 if input_len_sq > 1.0 {
668 let input_len = input_len_sq.sqrt();
669 mx /= input_len;
670 my /= input_len;
671 }
672
673 let accel_x = mx * (THRUST_FORCE / total_mass);
675 let accel_y = my * (THRUST_FORCE / total_mass);
676
677 slot.dx += accel_x * DT;
678 slot.dy += accel_y * DT;
679
680 let drag_factor = 1.0 / (1.0 + DRAG * DT);
682 slot.dx *= drag_factor;
683 slot.dy *= drag_factor;
684
685 let speed_sq = slot.dx * slot.dx + slot.dy * slot.dy;
687 if speed_sq > MAX_SPEED * MAX_SPEED {
688 let speed = speed_sq.sqrt();
689 slot.dx = (slot.dx / speed) * MAX_SPEED;
690 slot.dy = (slot.dy / speed) * MAX_SPEED;
691 }
692
693 if speed_sq > 0.01 {
695 const TURN_RATE: f32 = 5.0;
696 let target_rot = slot.dy.atan2(slot.dx);
697 let current_rot = slot.rotation;
698 let diff = (target_rot - current_rot + std::f32::consts::PI)
699 .rem_euclid(std::f32::consts::TAU)
700 - std::f32::consts::PI;
701
702 if diff.abs() > 0.001 {
703 slot.rotation += diff.clamp(-TURN_RATE * DT, TURN_RATE * DT);
704 } else {
705 slot.rotation = target_rot;
706 }
707 }
708
709 slot.x += slot.dx * DT;
711 slot.y += slot.dy * DT;
712 slot.z += slot.dz * DT;
713 }
714
715 fn simulate_slot_wrapped(
720 slot: &mut SabSlot,
721 move_x: f32,
722 move_y: f32,
723 bounds: Option<aetheris_protocol::types::WorkspaceBounds>,
724 ) {
725 Self::simulate_slot(slot, move_x, move_y);
726
727 if let Some(bounds) = bounds {
728 let width = bounds.max_x - bounds.min_x;
729 let height = bounds.max_y - bounds.min_y;
730 if width > 0.0 {
731 slot.x = ((slot.x - bounds.min_x).rem_euclid(width)) + bounds.min_x;
732 }
733 if height > 0.0 {
734 slot.y = ((slot.y - bounds.min_y).rem_euclid(height)) + bounds.min_y;
735 }
736 }
737 }
738
739 pub fn playground_apply_input(&mut self, move_x: f32, move_y: f32, actions_mask: u32) -> bool {
742 if self.prediction_enabled {
743 self.input_history.push_back(InputRecord {
745 tick: self.latest_tick,
746 move_x,
747 move_y,
748 #[allow(clippy::cast_possible_truncation)]
749 actions_mask: actions_mask as u8,
750 });
751
752 if self.input_history.len() > 300 {
754 self.input_history.pop_front();
755 }
756 }
757
758 let mut found = false;
759 for slot in self.entities.values_mut() {
761 if (slot.flags & 0x04) != 0 {
762 found = true;
763 if self.prediction_enabled {
764 Self::simulate_slot_wrapped(slot, move_x, move_y, self.workspace_bounds);
767 }
768 }
771 }
772 found
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use crate::shared_world::SabSlot;
780 use aetheris_protocol::types::NetworkId;
781 use bytemuck::Zeroable;
782
783 #[test]
784 fn test_playground_movement() {
785 let mut world = ClientWorld::with_prediction(true);
786
787 world.entities.insert(
789 NetworkId(1),
790 SabSlot {
791 network_id: 1,
792 flags: 0x04, ..SabSlot::zeroed()
794 },
795 );
796
797 world.playground_apply_input(1.0, 0.0, 0);
799
800 let player = world.entities.get(&NetworkId(1)).unwrap();
801 assert!(player.dx > 0.0);
802 assert!(player.x > 0.0);
803 assert!((player.dx - 1.2903225).abs() < 0.0001);
806 }
807
808 #[test]
809 fn test_playground_speed_clamp() {
810 let mut world = ClientWorld::with_prediction(true);
811 world.entities.insert(
812 NetworkId(1),
813 SabSlot {
814 network_id: 1,
815 flags: 0x04,
816 dx: 10.0,
817 dy: 10.0,
818 ..SabSlot::zeroed()
819 },
820 );
821
822 world.playground_apply_input(1.0, 1.0, 0);
824
825 let player = world.entities.get(&NetworkId(1)).unwrap();
826 let speed = (player.dx * player.dx + player.dy * player.dy).sqrt();
827 assert!(speed <= 30.0 + 0.0001);
828 }
829
830 #[test]
831 fn test_playground_drag() {
832 let mut world = ClientWorld::with_prediction(true);
833 world.entities.insert(
834 NetworkId(1),
835 SabSlot {
836 network_id: 1,
837 flags: 0x04,
838 ..SabSlot::zeroed()
839 },
840 );
841
842 world.playground_apply_input(1.0, 0.0, 0);
844 let v1 = world.entities.get(&NetworkId(1)).unwrap().dx;
845
846 world.playground_apply_input(0.0, 0.0, 0);
848 let v2 = world.entities.get(&NetworkId(1)).unwrap().dx;
849
850 assert!(v2 < v1);
851 assert!((v2 - v1 * (1.0 / (1.0 + 2.0 / 60.0))).abs() < 0.0001);
853 }
854}