1mod peer;
3use ferravex_core::FixedClock;
4use ferravex_engine::play::particles::{ParticleDesc, ParticlePool};
5use ferravex_engine::{
6 audio::{Clip, Mixer},
7 dynamics::{BodyDesc, BodyHandle, PhysicsWorld, Shape},
8 replay::{Frame, Replay, checksum},
9};
10use ferravex_fps::{Arena, GameEvent, InputFrame, Match, forward};
11use ferravex_graphics::{
12 Camera, ClipDepth, Color, Mat4, Mesh, MeshCache, MeshId, RenderItem, SoftwareRenderer, Vec3,
13 vec3,
14};
15use std::sync::Mutex;
16#[derive(Clone)]
17struct Object {
18 mesh: MeshId,
19 position: Vec3,
20 scale: Vec3,
21 solid: Option<usize>,
22}
23pub struct Lab {
24 pub game: Match,
25 pub cache: MeshCache,
26 objects: Vec<Object>,
27 physics: PhysicsWorld<3>,
28 balls: Vec<(BodyHandle, MeshId)>,
29 terrain: Vec<BodyHandle>,
30 clock: FixedClock,
31 time: f32,
32 pub camera: Camera,
33 pub items: Vec<RenderItem>,
34 pub instances: Vec<f32>,
35 pub shadow_instances: Vec<f32>,
36 pub matrix: [f32; 16],
37 pub light: [f32; 16],
38 pub status: [f32; 11],
39 meshes: Vec<MeshId>,
40 mesh_data: Vec<Vec<f32>>,
41 mixer: Mixer,
42 pub audio: Vec<[f32; 2]>,
43 shot: Clip,
44 replay: Replay,
45 player_mesh: MeshId,
46 head_mesh: MeshId,
47 pub kills: u32,
48 recording: bool,
49 export: Vec<u8>,
50 particles: ParticlePool,
51 peer: peer::Peer,
52 network_out: Vec<u8>,
53 network_in: Vec<u8>,
54}
55impl Default for Lab {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60impl Lab {
61 pub fn new() -> Self {
62 let mut cache = MeshCache::new();
63 let colors = [
64 0xcbbfa6, 0xa29071, 0x586b67, 0x8c613c, 0x3d4547, 0xc58658, 0xe4d7bd, 0x6b7650,
65 0x4d6875, 0xd3b56b,
66 ];
67 let mut meshes: Vec<_> = colors
68 .into_iter()
69 .map(|c| {
70 cache
71 .insert(Mesh::cuboid(Vec3::ONE, Color::from_hex(c)))
72 .unwrap()
73 })
74 .collect();
75 let player_mesh = meshes[8];
76 let head_mesh = cache
77 .insert(Mesh::sphere(0.5, 4, 6, Color::from_hex(0xc5ad8b)))
78 .unwrap();
79 meshes.push(head_mesh);
80 let round = cache
81 .insert(Mesh::sphere(1., 5, 8, Color::from_hex(0xd3b56b)))
82 .unwrap();
83 meshes.push(round);
84 let mut objects = Vec::new();
85 let mut add = |material: usize, p: [f32; 3], s: [f32; 3], solid| {
86 objects.push(Object {
87 mesh: meshes[material],
88 position: p.into(),
89 scale: s.into(),
90 solid,
91 })
92 };
93 add(0, [0., -0.25, 0.], [48., 0.5, 56.], None);
94 let arena = Arena::default();
95 for (i, wall) in arena.solids.iter().enumerate() {
96 add(
97 if i < 4 { 0 } else { 2 },
98 wall.center.to_array(),
99 (wall.half_extents * 2.).to_array(),
100 Some(i),
101 );
102 }
103 for side in [-1., 1.] {
105 for z in [-21., -12., -3., 6., 15., 24.] {
106 add(1, [side * 21.95, 2.8, z], [0.18, 0.2, 7.5], None);
107 add(6, [side * 21.85, 5.8, z], [0.3, 0.35, 8.5], None);
108 add(4, [side * 21.84, 3.9, z], [0.18, 1.55, 1.3], None);
109 add(2, [side * 21.7, 3.9, z], [0.18, 1.35, 1.1], None);
110 add(1, [side * 21.7, 3.0, z], [0.5, 0.15, 1.7], None);
111 }
112 }
113 for x in [-18., -12., -6., 0., 6., 12., 18.] {
114 add(6, [x, 5.9, -25.9], [5.8, 0.4, 0.4], None);
115 add(2, [x, 4.4, -25.85], [1.45, 1.4, 0.2], None);
116 }
117 for (x, z) in [
119 (-16., 8.),
120 (-14., 8.),
121 (-16., 10.),
122 (13., -5.),
123 (14., -7.),
124 (-5., -18.),
125 ] {
126 add(3, [x, 0.65, z], [1.35, 1.3, 1.35], None);
127 for y in [0.16, 1.12] {
128 add(1, [x, y, z + 0.69], [1.4, 0.09, 0.1], None);
129 }
130 }
131 for x in [-12., 12.] {
132 add(4, [x, 1.5, -25.8], [2.5, 3., 0.25], None);
133 add(1, [x, 3.1, -25.6], [3., 0.35, 0.45], None);
134 }
135 for i in 0..8 {
136 add(
137 if i % 2 == 0 { 0 } else { 5 },
138 [-18. + i as f32 * 0.55, 4.2, -8.],
139 [0.55, 0.09, 5.],
140 None,
141 );
142 }
143 for z in [-10., -6.] {
144 add(3, [-18., 2., z], [0.13, 4., 0.13], None);
145 }
146 add(9, [7., 0.012, 18.], [6., 0.025, 0.13], None);
147 add(9, [10., 0.012, 15.], [0.13, 0.025, 6.], None);
148 let mut physics = PhysicsWorld::<3>::new([0., -9.81, 0.]);
149 physics
150 .insert(BodyDesc::fixed(
151 Shape::cuboid([24., 0.5, 28.]),
152 [0., -0.5, 0.],
153 ))
154 .unwrap();
155 let terrain = arena
156 .solids
157 .iter()
158 .map(|wall| {
159 physics
160 .insert(BodyDesc::fixed(
161 Shape::cuboid(wall.half_extents.to_array()),
162 wall.center.to_array(),
163 ))
164 .unwrap()
165 })
166 .collect();
167 let mut balls = Vec::new();
168 for i in 0..6 {
169 let mut desc = BodyDesc::dynamic(
170 Shape::ball(0.32),
171 [5. + i as f32 * 0.74, 3. + i as f32 * 0.5, 15.],
172 1.,
173 );
174 desc.restitution = 0.65;
175 desc.ccd = true;
176 let id = physics.insert(desc).unwrap();
177 balls.push((id, round));
178 }
179 let mesh_data = meshes
180 .iter()
181 .map(|id| {
182 let mesh = cache.get(*id).unwrap();
183 mesh.indices
184 .iter()
185 .flat_map(|&i| {
186 let v = mesh.vertices[i as usize];
187 v.position
188 .to_array()
189 .into_iter()
190 .chain(v.normal.to_array())
191 .chain(v.color.to_array())
192 })
193 .collect()
194 })
195 .collect();
196 let mut game = Match::new(arena);
197 for id in 0..5 {
198 game.spawn_player(id);
199 }
200 let camera = Camera::perspective(
201 vec3(-12., 10., 24.),
202 vec3(0., 1., 0.),
203 Vec3::Y,
204 1.05,
205 16. / 9.,
206 0.08,
207 120.,
208 );
209 let mut app = Self {
210 game,
211 cache,
212 objects,
213 physics,
214 balls,
215 terrain,
216 clock: FixedClock::new(64., 8),
217 time: 0.,
218 camera,
219 items: Vec::with_capacity(256),
220 instances: Vec::with_capacity(4096),
221 shadow_instances: Vec::with_capacity(4096),
222 matrix: [0.; 16],
223 light: (Mat4::orthographic_rh_gl(-40., 40., -40., 40., 1., 140.)
224 * Mat4::look_at_rh(vec3(-30., 51., 24.), Vec3::ZERO, Vec3::Y))
225 .to_cols_array(),
226 status: [0.; 11],
227 meshes,
228 mesh_data,
229 mixer: Mixer::new(48000, 32).unwrap(),
230 audio: Vec::new(),
231 shot: Clip::tone(48000, 95., 0.09).unwrap(),
232 replay: Replay::default(),
233 player_mesh,
234 head_mesh,
235 kills: 0,
236 recording: false,
237 export: Vec::new(),
238 particles: ParticlePool::new(96),
239 peer: peer::Peer::default(),
240 network_out: Vec::new(),
241 network_in: vec![0; 1200],
242 };
243 app.rebuild(16. / 9., 0);
244 app
245 }
246 pub fn update(&mut self, seconds: f64, input: InputFrame, view: u32, aspect: f32) {
247 let step = self.clock.advance(seconds);
248 for _ in 0..step.steps {
249 self.time += step.dt;
250 let command = self.peer.command(input.sanitized());
251 let mut inputs = vec![(0, command)];
252 self.peer.elapsed += step.dt;
253 if self.peer.mode == 1 {
254 if let Some(remote) = self.peer.queue.pop_latest() {
255 self.peer.last = remote;
256 self.peer.acknowledged = true;
257 }
258 let remote = if self.peer.elapsed < 2. {
259 self.peer.last
260 } else {
261 InputFrame::default()
262 };
263 inputs.push((1, remote));
264 }
265 for bot in self
266 .game
267 .players
268 .iter()
269 .filter(|p| p.id > 0 && !(self.peer.mode == 1 && p.id == 1))
270 {
271 let phase = self.time * 0.3 + bot.id as f32 * 1.4;
272 inputs.push((
273 bot.id,
274 InputFrame {
275 move_x: phase.sin() * 0.7,
276 move_z: 0.25,
277 yaw: phase.sin() * 2.,
278 ..Default::default()
279 },
280 ));
281 }
282 let events = if self.peer.mode == 2 {
283 let arena = &self.game.arena;
284 if ferravex_fps::step_player(
285 self.game.players.iter_mut().find(|p| p.id == 0).unwrap(),
286 command,
287 arena,
288 ) {
289 self.mixer.play(
290 self.shot.clone(),
291 self.game.player(0).unwrap().eye_position().to_array(),
292 0.8,
293 false,
294 );
295 }
296 self.game.tick = self.game.tick.wrapping_add(1);
297 Vec::new()
298 } else {
299 self.game.step(&inputs)
300 };
301 for event in events {
302 match event {
303 GameEvent::Shot { origin, end, .. } => {
304 self.mixer
305 .play(self.shot.clone(), origin.to_array(), 0.8, false);
306 for i in 0..6 {
307 let phase = i as f32 * 2.4 + self.time;
308 let _ = self.particles.spawn(ParticleDesc {
309 position: end.to_array(),
310 velocity: [phase.sin() * 2., 1.8, phase.cos() * 2.],
311 acceleration: [0., -8., 0.],
312 lifetime: 0.4,
313 start_size: 0.07,
314 end_size: 0.01,
315 ..Default::default()
316 });
317 }
318 }
319 GameEvent::Killed { attacker: 0, .. } => self.kills += 1,
320 _ => {}
321 }
322 }
323 self.physics.step(step.dt).unwrap();
324 self.particles.update(step.dt).unwrap();
325 if self.recording {
326 let player = self.game.player(0).unwrap();
327 let bytes: Vec<_> = player
328 .position
329 .to_array()
330 .iter()
331 .flat_map(|v| v.to_le_bytes())
332 .collect();
333 self.replay.record(Frame {
334 tick: u64::from(self.game.tick),
335 input: inputs[0].1,
336 state_hash: checksum(&bytes),
337 });
338 }
339 }
340 self.rebuild(aspect, view);
341 let count = if seconds.is_finite() {
342 (seconds.clamp(0., 0.1) * 48000.) as usize
343 } else {
344 0
345 };
346 self.audio.resize(count, [0.; 2]);
347 self.mixer.listener = self.camera.position.to_array();
348 self.mixer.right = vec3(input.yaw.cos(), 0., input.yaw.sin()).to_array();
349 self.mixer.render(&mut self.audio);
350 }
351 fn rebuild(&mut self, aspect: f32, view: u32) {
352 let aspect = if aspect.is_finite() {
353 aspect.clamp(0.2, 5.)
354 } else {
355 16. / 9.
356 };
357 let player = self.game.player(0).unwrap();
358 self.camera = match view {
359 1 => {
360 let eye = player.eye_position();
361 Camera::perspective(
362 eye,
363 eye + forward(player.yaw, player.pitch),
364 Vec3::Y,
365 1.15,
366 aspect,
367 0.06,
368 120.,
369 )
370 }
371 2 => Camera {
372 view_projection: Mat4::orthographic_rh(
373 -31. * aspect,
374 31. * aspect,
375 -31.,
376 31.,
377 0.1,
378 100.,
379 ) * Mat4::look_at_rh(vec3(0., 60., 0.), Vec3::ZERO, -Vec3::Z),
380 position: vec3(0., 60., 0.),
381 clip_depth: ClipDepth::ZeroToOne,
382 },
383 _ => {
384 let angle = self.time * 0.025;
385 let eye = vec3(-13. + angle.sin() * 8., 11., 24.);
386 Camera::perspective(eye, vec3(0., 1., 0.), Vec3::Y, 1., aspect, 0.08, 120.)
387 }
388 };
389 self.items.clear();
390 for o in &self.objects {
391 self.items.push(RenderItem::new(
392 o.mesh,
393 Mat4::from_scale_rotation_translation(
394 o.scale,
395 ferravex_graphics::Quat::IDENTITY,
396 o.position,
397 ),
398 ));
399 }
400 for (handle, mesh) in &self.balls {
401 let p = self.physics.body(*handle).unwrap().position();
402 self.items.push(RenderItem::new(
403 *mesh,
404 Mat4::from_scale_rotation_translation(
405 Vec3::splat(0.32),
406 ferravex_graphics::Quat::IDENTITY,
407 p.into(),
408 ),
409 ));
410 }
411 for p in self
412 .game
413 .players
414 .iter()
415 .filter(|p| p.is_alive() && (view != 1 || p.id != 0))
416 {
417 let rot = ferravex_graphics::Quat::from_rotation_y(-p.yaw);
418 let sway = (self.time * 8. + p.id as f32).sin() * 0.06;
419 for (offset, size, mesh) in [
420 (vec3(0., 1., 0.), vec3(0.56, 0.72, 0.32), self.player_mesh),
421 (vec3(0., 1.65, 0.), Vec3::splat(0.35), self.head_mesh),
422 (
423 vec3(-0.17, 0.34 + sway, 0.),
424 vec3(0.19, 0.68, 0.22),
425 self.meshes[4],
426 ),
427 (
428 vec3(0.17, 0.34 - sway, 0.),
429 vec3(0.19, 0.68, 0.22),
430 self.meshes[4],
431 ),
432 (
433 vec3(0.34, 1.12, -0.2),
434 vec3(0.12, 0.16, 0.65),
435 self.meshes[4],
436 ),
437 ] {
438 self.items.push(RenderItem::new(
439 mesh,
440 Mat4::from_scale_rotation_translation(size, rot, p.position + rot * offset),
441 ));
442 }
443 }
444 for p in self.particles.iter() {
445 self.items.push(RenderItem::new(
446 self.meshes[9],
447 Mat4::from_scale_rotation_translation(
448 Vec3::splat(p.size),
449 ferravex_graphics::Quat::IDENTITY,
450 p.position.into(),
451 ),
452 ));
453 }
454 if view == 1 {
455 let rot = ferravex_graphics::Quat::from_rotation_y(-player.yaw)
456 * ferravex_graphics::Quat::from_rotation_x(player.pitch);
457 let bob = (self.time * 10.).sin() * player.velocity.length().min(7.) * 0.0008;
458 let eye = player.eye_position();
459 for (offset, size, material) in [
460 (vec3(0.22, -0.22 + bob, -0.43), vec3(0.12, 0.15, 0.35), 4),
461 (vec3(0.22, -0.18 + bob, -0.68), vec3(0.045, 0.045, 0.32), 4),
462 (vec3(0.22, -0.32 + bob, -0.37), vec3(0.08, 0.16, 0.09), 3),
463 ] {
464 self.items.push(RenderItem::new(
465 self.meshes[material],
466 Mat4::from_scale_rotation_translation(size, rot, eye + rot * offset),
467 ));
468 }
469 }
470 self.shadow_instances.clear();
471 for item in &self.items {
472 self.shadow_instances.push(item.mesh.index as f32);
473 self.shadow_instances.extend(item.transform.to_cols_array());
474 }
475 self.instances.clear();
476 let frustum = self.camera.frustum().unwrap();
477 for item in &self.items {
478 if frustum.intersects_bounds(self.cache.bounds(item.mesh).unwrap(), item.transform) {
479 self.instances.push(item.mesh.index as f32);
480 self.instances.extend(item.transform.to_cols_array());
481 }
482 }
483 self.matrix = self.camera.view_projection.to_cols_array();
484 self.status = [
485 player.health as f32,
486 player.ammo as f32,
487 self.kills as f32,
488 self.game.tick as f32,
489 (self.instances.len() / 17) as f32,
490 self.items.len() as f32,
491 player.reload_ticks as f32,
492 self.time,
493 self.camera.position.x,
494 self.camera.position.y,
495 self.camera.position.z,
496 ];
497 }
498 pub fn render(&self, width: u32, height: u32) -> SoftwareRenderer {
499 let mut renderer = SoftwareRenderer::new(width, height).unwrap();
500 renderer.settings.clear_color = Color::from_hex(0xa9b8ba);
501 renderer.settings.ambient = 0.5;
502 renderer.settings.diffuse = 0.5;
503 renderer.settings.fog = Some(ferravex_graphics::Fog {
504 color: Color::from_hex(0xa9b8ba),
505 start: 40.,
506 end: 120.,
507 });
508 renderer
509 .render(&self.cache, &self.items, self.camera)
510 .unwrap();
511 renderer
512 }
513 pub fn edit(&mut self, index: usize, x: f32, y: f32, z: f32) -> bool {
514 if index == 0
515 || index >= self.objects.len()
516 || ![x, y, z].iter().all(|v| v.is_finite() && v.abs() < 100.)
517 {
518 return false;
519 }
520 let o = &mut self.objects[index];
521 o.position = vec3(x, y, z);
522 if let Some(i) = o.solid {
523 self.game.arena.solids[i].center = o.position;
524 self.physics
525 .body_mut(self.terrain[i])
526 .unwrap()
527 .set_position(o.position.to_array())
528 .unwrap();
529 }
530 true
531 }
532 pub fn object_count(&self) -> usize {
533 self.objects.len()
534 }
535 pub fn object(&self, index: usize) -> Option<[f32; 6]> {
536 self.objects.get(index).map(|o| {
537 [
538 o.position.x,
539 o.position.y,
540 o.position.z,
541 o.scale.x,
542 o.scale.y,
543 o.scale.z,
544 ]
545 })
546 }
547 pub fn replay_bytes(&self) -> Vec<u8> {
548 self.replay.encode()
549 }
550}
551static LAB: Mutex<Option<Lab>> = Mutex::new(None);
552#[unsafe(no_mangle)]
553pub extern "C" fn ferravex_button_mask(kind: u32) -> u32 {
554 match kind {
555 0 => u32::from(ferravex_fps::BUTTON_FIRE),
556 1 => u32::from(ferravex_fps::BUTTON_JUMP),
557 2 => u32::from(ferravex_fps::BUTTON_RELOAD),
558 _ => 0,
559 }
560}
561#[unsafe(no_mangle)]
562pub extern "C" fn ferravex_init() {
563 *LAB.lock().unwrap() = Some(Lab::new());
564}
565#[unsafe(no_mangle)]
566pub extern "C" fn ferravex_step(
567 dt: f64,
568 x: f32,
569 z: f32,
570 yaw: f32,
571 pitch: f32,
572 buttons: u32,
573 view: u32,
574 aspect: f32,
575) {
576 if let Some(lab) = LAB.lock().unwrap().as_mut() {
577 lab.update(
578 dt,
579 InputFrame {
580 move_x: x,
581 move_z: z,
582 yaw,
583 pitch,
584 buttons: buttons as u8,
585 ..Default::default()
586 },
587 view,
588 aspect,
589 );
590 }
591}
592macro_rules! buffer {
593 ($ptr:ident,$len:ident,$field:ident,$ty:ty) => {
594 #[unsafe(no_mangle)]
595 pub extern "C" fn $ptr() -> *const $ty {
596 LAB.lock().unwrap().as_ref().unwrap().$field.as_ptr()
597 }
598 #[unsafe(no_mangle)]
599 pub extern "C" fn $len() -> usize {
600 LAB.lock().unwrap().as_ref().unwrap().$field.len()
601 }
602 };
603}
604buffer!(
605 ferravex_shadow_instances,
606 ferravex_shadow_instances_len,
607 shadow_instances,
608 f32
609);
610buffer!(ferravex_instances, ferravex_instances_len, instances, f32);
611buffer!(ferravex_matrix, ferravex_matrix_len, matrix, f32);
612buffer!(ferravex_light, ferravex_light_len, light, f32);
613buffer!(ferravex_status, ferravex_status_len, status, f32);
614buffer!(ferravex_audio, ferravex_audio_len, audio, [f32; 2]);
615#[unsafe(no_mangle)]
616pub extern "C" fn ferravex_mesh_count() -> usize {
617 LAB.lock().unwrap().as_ref().unwrap().mesh_data.len()
618}
619#[unsafe(no_mangle)]
620pub extern "C" fn ferravex_mesh(index: usize) -> *const f32 {
621 LAB.lock().unwrap().as_ref().unwrap().mesh_data[index].as_ptr()
622}
623#[unsafe(no_mangle)]
624pub extern "C" fn ferravex_mesh_len(index: usize) -> usize {
625 LAB.lock().unwrap().as_ref().unwrap().mesh_data[index].len()
626}
627#[unsafe(no_mangle)]
628pub extern "C" fn ferravex_object_count() -> usize {
629 LAB.lock().unwrap().as_ref().unwrap().object_count()
630}
631#[unsafe(no_mangle)]
632pub extern "C" fn ferravex_object(index: usize, axis: usize) -> f32 {
633 LAB.lock()
634 .unwrap()
635 .as_ref()
636 .unwrap()
637 .object(index)
638 .and_then(|v| v.get(axis).copied())
639 .unwrap_or(0.)
640}
641#[unsafe(no_mangle)]
642pub extern "C" fn ferravex_edit(index: usize, x: f32, y: f32, z: f32) -> u32 {
643 u32::from(LAB.lock().unwrap().as_mut().unwrap().edit(index, x, y, z))
644}
645#[unsafe(no_mangle)]
646pub extern "C" fn ferravex_record(enabled: u32) {
647 LAB.lock().unwrap().as_mut().unwrap().recording = enabled != 0;
648}
649#[unsafe(no_mangle)]
650pub extern "C" fn ferravex_export_replay() -> usize {
651 let mut lock = LAB.lock().unwrap();
652 let lab = lock.as_mut().unwrap();
653 lab.export = lab.replay_bytes();
654 lab.export.len()
655}
656#[unsafe(no_mangle)]
657pub extern "C" fn ferravex_replay_bytes() -> *const u8 {
658 LAB.lock().unwrap().as_ref().unwrap().export.as_ptr()
659}
660#[unsafe(no_mangle)]
661pub extern "C" fn ferravex_peer_mode(mode: u32) {
662 LAB.lock().unwrap().as_mut().unwrap().peer.set_mode(mode);
663}
664#[unsafe(no_mangle)]
665pub extern "C" fn ferravex_peer_out() -> *const u8 {
666 let mut lock = LAB.lock().unwrap();
667 let lab = lock.as_mut().unwrap();
668 lab.network_out = lab.peer.packet(&lab.game);
669 lab.network_out.as_ptr()
670}
671#[unsafe(no_mangle)]
672pub extern "C" fn ferravex_peer_out_len() -> usize {
673 LAB.lock().unwrap().as_ref().unwrap().network_out.len()
674}
675#[unsafe(no_mangle)]
676pub extern "C" fn ferravex_peer_in() -> *mut u8 {
677 LAB.lock()
678 .unwrap()
679 .as_mut()
680 .unwrap()
681 .network_in
682 .as_mut_ptr()
683}
684#[unsafe(no_mangle)]
685pub extern "C" fn ferravex_peer_receive(len: usize) -> u32 {
686 let mut lock = LAB.lock().unwrap();
687 let lab = lock.as_mut().unwrap();
688 if len > 1200 {
689 return 0;
690 }
691 u32::from(lab.peer.receive(&lab.network_in[..len], &mut lab.game))
692}
693#[cfg(test)]
694mod tests {
695 use super::*;
696 #[test]
697 fn scene_runs_with_owned_physics_and_renders_nonempty() {
698 let mut lab = Lab::new();
699 for _ in 0..640 {
700 lab.update(1. / 64., InputFrame::default(), 0, 16. / 9.);
701 }
702 assert_eq!(lab.game.tick, 640);
703 assert_eq!(lab.status[0], 100.);
704 let renderer = lab.render(160, 90);
705 assert!(
706 renderer
707 .framebuffer
708 .depths()
709 .iter()
710 .filter(|d| d.is_finite())
711 .count()
712 > 2000
713 );
714 assert!(!lab.edit(usize::MAX, 0., 0., 0.));
715 }
716 #[test]
717 fn movement_recording_and_editor_apply_real_state() {
718 let mut lab = Lab::new();
719 lab.recording = true;
720 let start = lab.game.player(0).unwrap().position;
721 for _ in 0..64 {
722 lab.update(
723 1. / 64.,
724 InputFrame {
725 move_z: 1.,
726 ..Default::default()
727 },
728 1,
729 1.,
730 );
731 }
732 assert!((lab.game.player(0).unwrap().position - start).length() > 5.);
733 assert_eq!(
734 Replay::decode(&lab.replay_bytes()).unwrap().frames().len(),
735 64
736 );
737 let wall = lab.objects.iter().position(|o| o.solid == Some(0)).unwrap();
738 assert!(lab.edit(wall, -20., 3., 0.));
739 assert_eq!(lab.game.arena.solids[0].center.x, -20.);
740 }
741}