1mod animate;
26mod scaled;
27
28use std::sync::Arc;
29
30#[cfg(test)]
31use crate::anim::Params;
32use crate::anim::{AnimEvent, Animator, Driver, GraphSpec, SpecError};
33#[cfg(feature = "toolkit")]
34use crate::controls::{Controls, Scheme};
35use crate::hero::{HeroDriver, spec};
36use crate::sim::{
37 Ability, Contacts, DT, Event, Intent, Level, Mode, Preset, Rect, Spot, Terrain, Tuning, World,
38};
39
40use animate::AnimHook;
41pub use animate::Animate;
42pub(crate) use scaled::Scaled;
43
44const MAX_STEPS: u32 = 8;
46
47pub(crate) fn steps(acc: &mut f32, dt: f32) -> u32 {
50 *acc += dt;
51 let n = ((*acc / DT) as u32).min(MAX_STEPS);
52 *acc = (*acc - n as f32 * DT).clamp(0.0, DT);
53 n
54}
55
56pub struct Platformer {
69 pub(crate) world: World,
71 pub(crate) animator: Animator,
73 driver: Option<HeroDriver>,
75 hero_driver: bool,
77 animated: Vec<AnimHook>,
79 drivers: Vec<Box<dyn Driver<World>>>,
81 preset: Option<Preset>,
83 scale: i32,
85 acc: f32,
87 latch: Intent,
89 prev: (f32, f32),
91 hitboxes: Vec<Rect>,
93 events: Vec<Event>,
95 anim_events: Vec<AnimEvent>,
97 #[cfg(feature = "toolkit")]
99 pub controls: Controls,
100}
101
102pub struct Builder {
104 tuning: Tuning,
106 preset: Option<Preset>,
108 graph: Option<GraphSpec>,
110 partial: bool,
112 hero_driver: bool,
114 drivers: Vec<Box<dyn Driver<World>>>,
116 abilities: Vec<Box<dyn Ability>>,
118 animated: Vec<AnimHook>,
120 spawn: Spot,
122 respawn: bool,
124 scale: i32,
126 #[cfg(feature = "toolkit")]
128 scheme: Option<Scheme>,
129}
130
131fn animation(
134 graph: Option<GraphSpec>,
135 hero_driver: bool,
136 partial: bool,
137) -> Result<(Animator, Option<HeroDriver>), SpecError> {
138 let graph = graph.unwrap_or_else(spec).compile()?;
139 let mut animator = Animator::new(Arc::new(graph));
140 let driver = match (hero_driver, partial) {
141 (false, _) => None,
142 (true, false) => Some(HeroDriver::try_new(&animator.params)?),
143 (true, true) => Some(HeroDriver::declaring(&mut animator.params)),
144 };
145 Ok((animator, driver))
146}
147
148impl Builder {
149 pub fn preset(mut self, preset: Preset) -> Self {
151 self.preset = Some(preset);
152 self.tuning = preset.tuning();
153 self
154 }
155
156 pub fn tuning(mut self, tuning: Tuning) -> Self {
158 self.tuning = tuning;
159 self
160 }
161
162 pub fn tune(mut self, f: impl FnOnce(&mut Tuning)) -> Self {
164 f(&mut self.tuning);
165 self
166 }
167
168 pub fn graph(mut self, graph: GraphSpec) -> Self {
173 self.graph = Some(graph);
174 self.partial = false;
175 self
176 }
177
178 pub fn partial_graph(mut self, graph: GraphSpec) -> Self {
182 self.graph = Some(graph);
183 self.partial = true;
184 self
185 }
186
187 pub fn driver(mut self, driver: impl Driver<World> + 'static) -> Self {
193 self.drivers.push(Box::new(driver));
194 self
195 }
196
197 pub fn ability(mut self, ability: impl Ability + Clone + 'static) -> Self {
201 self.abilities.push(Box::new(ability));
202 self
203 }
204
205 pub fn animated<T: Ability + Animate + Clone + 'static>(mut self, ability: T) -> Self {
211 AnimHook::add(&mut self.animated, AnimHook::of::<T>());
212 self.abilities.push(Box::new(ability));
213 self
214 }
215
216 pub fn without_hero_driver(mut self) -> Self {
219 self.hero_driver = false;
220 self
221 }
222
223 pub fn scale(mut self, k: i32) -> Self {
235 assert!(k >= 1, "a scale of {k} pixels per unit");
236 self.scale = k;
237 self
238 }
239
240 pub fn spawn(mut self, spot: Spot) -> Self {
244 self.spawn = spot;
245 self
246 }
247
248 pub fn respawn(mut self, on: bool) -> Self {
251 self.respawn = on;
252 self
253 }
254
255 #[cfg(feature = "toolkit")]
257 pub fn controls(mut self, scheme: Scheme) -> Self {
258 self.scheme = Some(scheme);
259 self
260 }
261
262 pub fn build(self) -> Result<Platformer, SpecError> {
264 let (animator, driver) = animation(self.graph, self.hero_driver, self.partial)?;
265 let mut animated = self.animated;
266 for h in &mut animated {
267 h.resolve(&animator.params)?;
268 }
269 let k = self.scale;
270 let mut world = World::new(self.tuning, self.spawn.in_units(k));
271 world.respawn = self.respawn;
272 world.abilities = self.abilities;
273 Ok(Platformer {
274 prev: world.player.exact_pos(),
275 world,
276 animator,
277 driver,
278 hero_driver: self.hero_driver,
279 animated,
280 drivers: self.drivers,
281 preset: self.preset,
282 scale: k,
283 acc: 0.0,
284 latch: Intent::default(),
285 hitboxes: Vec::new(),
286 events: Vec::new(),
287 anim_events: Vec::new(),
288 #[cfg(feature = "toolkit")]
289 controls: Controls::new(
290 self.scheme
291 .unwrap_or_else(|| crate::controls::Scheme::of(self.preset)),
292 ),
293 })
294 }
295}
296
297impl Platformer {
298 pub fn on(level: &Level, preset: Preset) -> Self {
301 Self::at(level.start(), preset)
302 }
303
304 pub fn at(spot: Spot, preset: Preset) -> Self {
307 Self::builder()
308 .preset(preset)
309 .spawn(spot)
310 .build()
311 .expect("the built-in animation graph fits the built-in driver")
312 }
313
314 pub fn builder() -> Builder {
316 Builder {
317 tuning: Preset::Hybrid.tuning(),
318 preset: Some(Preset::Hybrid),
319 graph: None,
320 partial: false,
321 hero_driver: true,
322 drivers: Vec::new(),
323 abilities: Vec::new(),
324 animated: Vec::new(),
325 spawn: Spot::TopLeft(0, 0),
326 respawn: true,
327 scale: 1,
328 #[cfg(feature = "toolkit")]
329 scheme: None,
330 }
331 }
332
333 #[cfg(feature = "toolkit")]
336 pub fn update(
337 &mut self,
338 input: &game_toolkit::prelude::Input,
339 terrain: &mut dyn Terrain,
340 dt: f32,
341 ) -> u32 {
342 let intent = self.controls.read(input);
343 self.advance(intent, terrain, dt)
344 }
345
346 pub fn advance(&mut self, intent: Intent, terrain: &mut dyn Terrain, dt: f32) -> u32 {
352 self.begin_frame();
353 self.hold_presses(&intent);
354 let n = steps(&mut self.acc, dt);
355 for _ in 0..n {
356 let i = self.with_presses(intent);
357 self.step_once(&i, terrain);
358 }
359 n
360 }
361
362 pub(crate) fn hold_presses(&mut self, intent: &Intent) {
364 self.latch.jump_pressed |= intent.jump_pressed;
365 self.latch.dash_pressed |= intent.dash_pressed;
366 self.latch.dive_pressed |= intent.dive_pressed;
367 self.latch.custom_pressed |= intent.custom_pressed;
368 }
369
370 pub(crate) fn with_presses(&mut self, intent: Intent) -> Intent {
372 let i = Intent {
373 jump_pressed: self.latch.jump_pressed,
374 dash_pressed: self.latch.dash_pressed,
375 dive_pressed: self.latch.dive_pressed,
376 custom_pressed: self.latch.custom_pressed,
377 ..intent
378 };
379 self.latch = Intent::default();
380 i
381 }
382
383 pub fn step(&mut self, intent: &Intent, terrain: &mut dyn Terrain) {
386 self.begin_frame();
387 self.step_once(intent, terrain);
388 }
389
390 fn step_once(&mut self, intent: &Intent, terrain: &mut dyn Terrain) {
392 self.begin_step();
393 if self.scale == 1 {
394 self.world.step(terrain, intent);
395 } else {
396 let mut scaled = Scaled {
397 inner: terrain,
398 k: self.scale,
399 };
400 self.world.step(&mut scaled, intent);
401 }
402 self.end_step();
403 }
404
405 pub fn scale(&self) -> i32 {
407 self.scale
408 }
409
410 pub(crate) fn set_clock(&mut self, acc: f32) {
413 self.acc = acc;
414 }
415
416 pub(crate) fn begin_frame(&mut self) {
418 self.hitboxes.clear();
419 }
420
421 pub(crate) fn begin_step(&mut self) {
423 self.prev = self.world.player.exact_pos();
424 self.world.set_scale(self.scale);
426 }
427
428 pub(crate) fn end_step(&mut self) {
432 let k = self.scale;
433 self.events
434 .extend(self.world.events.iter().map(|e| e.in_pixels(k)));
435 self.hitboxes.push(self.hitbox());
436 if self.world.events.contains(&Event::Respawned) {
437 self.prev = self.world.player.exact_pos();
438 self.animator.reset();
439 return;
440 }
441 if let Some(d) = &mut self.driver {
442 d.drive(&self.world, &mut self.animator.params);
443 }
444 for h in &self.animated {
445 h.run(&self.world, &mut self.animator.params);
446 }
447 for d in &mut self.drivers {
448 d.drive(&self.world, &mut self.animator.params);
449 }
450 self.animator.update(DT);
451 self.anim_events.extend(self.animator.drain_events());
452 }
453
454 pub fn drain_events(&mut self) -> std::vec::Drain<'_, Event> {
458 self.events.drain(..)
459 }
460
461 pub fn drain_anim_events(&mut self) -> std::vec::Drain<'_, AnimEvent> {
464 self.anim_events.drain(..)
465 }
466
467 pub fn sprite(&self) -> u16 {
469 self.animator.sprite()
470 }
471
472 pub fn state(&self) -> &str {
474 self.animator.state_name()
475 }
476
477 pub fn facing(&self) -> i32 {
479 self.world.player.facing
480 }
481
482 pub fn grounded(&self) -> bool {
484 self.world.player.grounded
485 }
486
487 pub fn velocity(&self) -> (f32, f32) {
489 let k = self.scale as f32;
490 (self.world.player.vx * k, self.world.player.vy * k)
491 }
492
493 pub fn mode(&self) -> Mode {
496 self.world.player.mode
497 }
498
499 pub fn dashes(&self) -> u8 {
501 self.world.player.dashes
502 }
503
504 pub fn air_jumps(&self) -> u8 {
506 self.world.player.air_jumps
507 }
508
509 pub fn stamina(&self) -> f32 {
511 self.world.player.stamina
512 }
513
514 pub fn on_wall(&self) -> Option<i32> {
516 let p = &self.world.player;
517 match (p.mode, p.wall_slide) {
518 (Mode::Climb, _) => Some(p.facing),
519 (_, 0) => None,
520 (_, side) => Some(side),
521 }
522 }
523
524 pub fn position(&self) -> (f32, f32) {
527 self.position_at(self.acc)
528 }
529
530 pub(crate) fn position_at(&self, acc: f32) -> (f32, f32) {
532 let (x, y) = self.world.player.exact_pos();
533 let (px, py) = self.prev;
534 let k = self.scale as f32;
535 if (x - px).abs() > 16.0 || (y - py).abs() > 16.0 {
536 return (x * k, y * k);
537 }
538 let a = (acc / DT).clamp(0.0, 1.0);
539 ((px + (x - px) * a) * k, (py + (y - py) * a) * k)
540 }
541
542 pub fn hitbox(&self) -> Rect {
544 self.world.player.rect().scaled(self.scale)
545 }
546
547 pub fn hitboxes(&self) -> &[Rect] {
553 &self.hitboxes
554 }
555
556 pub fn touched(&self, thing: Rect) -> Option<Rect> {
561 self.hitboxes.iter().copied().find(|r| r.intersects(thing))
562 }
563
564 pub fn launch(&mut self, vx: f32, vy: f32) {
568 let k = self.scale as f32;
569 self.world.player.launch(vx / k, vy / k);
570 }
571
572 pub fn set_velocity(&mut self, vx: f32, vy: f32) {
576 let k = self.scale as f32;
577 (self.world.player.vx, self.world.player.vy) = (vx / k, vy / k);
578 }
579
580 pub fn set_facing(&mut self, dir: i32) {
583 if dir != 0 {
584 self.world.player.facing = dir.signum();
585 }
586 }
587
588 fn top_left(&self, spot: Spot) -> (i32, i32) {
590 spot.in_units(self.scale).top_left(self.world.tuning.size)
591 }
592
593 pub fn teleport(&mut self, spot: Spot) {
596 let (x, y) = self.top_left(spot);
597 self.world.player.teleport(x, y);
598 self.prev = self.world.player.exact_pos();
599 }
600
601 pub fn respawn(&mut self) {
604 self.respawn_in_units(self.world.spawn);
605 }
606
607 pub fn respawn_at(&mut self, spot: Spot) {
609 self.respawn_in_units(spot.in_units(self.scale));
610 }
611
612 fn respawn_in_units(&mut self, at: Spot) {
614 self.world.respawn_at(at);
615 self.events.push(Event::Respawned);
616 self.hitboxes.push(self.hitbox());
617 self.prev = self.world.player.exact_pos();
618 self.animator.reset();
619 }
620
621 pub fn set_spawn(&mut self, spot: Spot) {
624 self.world.spawn = spot.in_units(self.scale);
625 }
626
627 pub fn spawn(&self) -> Spot {
629 self.world.spawn.in_pixels(self.scale)
630 }
631
632 pub fn refill(&mut self) {
634 self.world.player.refill(&self.world.tuning);
635 }
636
637 pub fn preset(&self) -> Option<Preset> {
639 self.preset
640 }
641
642 pub fn set_preset(&mut self, preset: Preset) {
645 self.preset = Some(preset);
646 self.world.tuning = Tuning {
647 size: self.world.tuning.size,
648 ..preset.tuning()
649 };
650 #[cfg(feature = "toolkit")]
651 {
652 let custom = std::mem::take(&mut self.controls.scheme.custom);
654 self.controls = Controls::for_preset(preset);
655 self.controls.scheme.custom = custom;
656 }
657 }
658
659 pub fn tune(&mut self, f: impl FnOnce(&mut Tuning)) {
663 f(&mut self.world.tuning);
664 self.preset = None;
665 }
666
667 pub fn add_ability(&mut self, ability: impl Ability + Clone + 'static) {
670 self.world.abilities.push(Box::new(ability));
671 }
672
673 pub fn add_animated<T: Ability + Animate + Clone + 'static>(
677 &mut self,
678 ability: T,
679 ) -> Result<(), SpecError> {
680 let mut hook = AnimHook::of::<T>();
681 hook.resolve(&self.animator.params)?;
682 AnimHook::add(&mut self.animated, hook);
683 self.world.abilities.push(Box::new(ability));
684 Ok(())
685 }
686
687 pub fn remove_ability<T: Ability + Clone + 'static>(&mut self) -> Option<T> {
691 let i = self
692 .world
693 .abilities
694 .iter()
695 .position(|a| a.as_any().is::<T>())?;
696 let removed = self.world.abilities.remove(i);
697 if self.world.ability::<T>().is_none() {
698 let ty = std::any::TypeId::of::<T>();
699 self.animated.retain(|h| h.ty != ty);
700 }
701 removed.as_any().downcast_ref::<T>().cloned()
702 }
703
704 pub fn ability<T: Ability + 'static>(&self) -> Option<&T> {
706 self.world.ability()
707 }
708
709 pub fn ability_mut<T: Ability + 'static>(&mut self) -> Option<&mut T> {
711 self.world.ability_mut()
712 }
713
714 pub fn contacts(&self) -> Contacts {
717 self.world.player.contacts
718 }
719
720 pub fn tuning(&self) -> &Tuning {
722 &self.world.tuning
723 }
724
725 pub fn world(&self) -> &World {
728 &self.world
729 }
730
731 pub fn world_mut(&mut self) -> &mut World {
739 &mut self.world
740 }
741
742 pub fn animator(&self) -> &Animator {
744 &self.animator
745 }
746
747 pub fn animator_mut(&mut self) -> &mut Animator {
751 &mut self.animator
752 }
753
754 pub fn set_graph(&mut self, graph: GraphSpec) -> Result<(), SpecError> {
759 self.swap_graph(graph, false)
760 }
761
762 pub fn set_partial_graph(&mut self, graph: GraphSpec) -> Result<(), SpecError> {
765 self.swap_graph(graph, true)
766 }
767
768 fn swap_graph(&mut self, graph: GraphSpec, partial: bool) -> Result<(), SpecError> {
770 let (animator, driver) = animation(Some(graph), self.hero_driver, partial)?;
771 let ids = self
772 .animated
773 .iter()
774 .map(|h| h.lookup(&animator.params))
775 .collect::<Result<Vec<_>, _>>()?;
776 for (h, ids) in self.animated.iter_mut().zip(ids) {
777 h.set_ids(ids);
778 }
779 self.animator = animator;
780 self.driver = driver;
781 Ok(())
782 }
783}
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788 use crate::anim::{Clip, ParamId, ParamKind, Playback, StateSpec};
789 use crate::sim::Step;
790
791 #[test]
792 fn a_frame_without_a_step_keeps_the_press() {
793 let mut level = Level::demo();
795 let mut hero = Platformer::on(&level, Preset::Precise);
796 let press = Intent {
797 jump_pressed: true,
798 jump_held: true,
799 ..Intent::default()
800 };
801 assert_eq!(
802 hero.advance(press, &mut level, 0.0),
803 0,
804 "a frame without a step"
805 );
806 let held = Intent {
807 jump_held: true,
808 ..Intent::default()
809 };
810 hero.advance(held, &mut level, 1.0 / 60.0);
811 assert!(
812 hero.drain_events()
813 .any(|e| matches!(e, Event::Jumped { .. }))
814 );
815 assert_eq!(hero.state(), "takeoff");
816 }
817
818 #[test]
819 fn custom_presses_are_latched_like_the_built_in_ones() {
820 const ATTACK: u8 = 3;
822 let mut level = Level::demo();
823 let mut hero = Platformer::on(&level, Preset::Precise);
824 let press = Intent {
825 custom: 1 << ATTACK,
826 custom_pressed: 1 << ATTACK,
827 ..Intent::default()
828 };
829 assert_eq!(hero.advance(press, &mut level, 0.0), 0);
830 assert!(hero.latch.pressed(ATTACK), "kept for the next step");
831 hero.advance(Intent::default(), &mut level, 1.0 / 60.0);
832 assert!(!hero.latch.pressed(ATTACK), "used up by the step");
833 assert!(press.held(ATTACK) && !press.held(ATTACK + 1));
834 }
835
836 #[cfg(feature = "toolkit")]
837 #[test]
838 fn switching_presets_keeps_the_custom_buttons() {
839 use crate::controls::Custom;
840 use game_toolkit::prelude::Key;
841 let attack: std::borrow::Cow<'static, [Custom]> =
842 vec![Custom::new(0, &[Key::KeyX], &[])].into();
843 let mut hero = Platformer::builder()
844 .controls(Scheme {
845 custom: attack,
846 ..Scheme::SNAPPY
847 })
848 .build()
849 .unwrap();
850 hero.set_preset(Preset::Precise);
851 assert_eq!(hero.controls.scheme.jump, Scheme::PRECISE.jump);
852 assert_eq!(hero.controls.scheme.custom.len(), 1);
853 }
854
855 #[test]
856 fn tune_and_presets_set_the_feel() {
857 let hero = Platformer::builder()
858 .preset(Preset::Momentum)
859 .tune(|t| t.air_jumps = 2)
860 .build()
861 .unwrap();
862 assert_eq!(hero.world.tuning.air_jumps, 2);
863 assert_eq!(
864 hero.world.tuning.sprint_speed,
865 Preset::Momentum.tuning().sprint_speed
866 );
867 assert_eq!(hero.preset(), Some(Preset::Momentum));
868 }
869
870 #[test]
871 fn a_custom_graph_missing_a_driven_parameter_is_an_error() {
872 let tiny = GraphSpec::new().state(StateSpec::new(
873 "idle",
874 Clip::uniform(Playback::Loop, &[0], 0.1),
875 ));
876 let err = Platformer::builder().graph(tiny).build().err();
877 assert!(matches!(err, Some(SpecError::UnknownParam(_))), "{err:?}");
878 }
879
880 fn two_state_graph() -> GraphSpec {
882 use crate::anim::{Cond, TransitionSpec};
883 let c = |s: u16| Clip::uniform(Playback::Loop, &[s], 0.1);
884 GraphSpec::new()
885 .bool("grounded")
886 .state(StateSpec::new("idle", c(0)))
887 .state(StateSpec::new("air", c(1)))
888 .transition(TransitionSpec::new(
889 ["idle"],
890 "air",
891 Cond::not(Cond::Is("grounded".into())),
892 ))
893 .transition(TransitionSpec::new(
894 ["air"],
895 "idle",
896 Cond::Is("grounded".into()),
897 ))
898 }
899
900 #[test]
901 fn a_partial_graph_gets_only_what_it_declares() {
902 assert!(
903 Platformer::builder()
904 .graph(two_state_graph())
905 .build()
906 .is_err(),
907 "strict by default"
908 );
909 let mut hero = Platformer::builder()
910 .partial_graph(two_state_graph())
911 .spawn(Spot::TopLeft(20, 40))
912 .build()
913 .unwrap();
914 idle(&mut hero);
915 assert_eq!(hero.state(), "air");
916 for _ in 0..60 {
917 idle(&mut hero);
918 }
919 assert_eq!(hero.state(), "idle");
920 hero.respawn();
922 idle(&mut hero);
923 }
924
925 #[test]
926 fn own_drivers_run_after_the_built_in_one() {
927 let hero = |b: Builder| {
928 let mut h = b
929 .graph(spec().bool("fast"))
930 .driver(|w: &World, p: &mut Params| {
931 let id = p.id("fast").unwrap();
932 p.set_bool(id, w.player.vx.abs() > 50.0);
933 })
934 .spawn(Spot::TopLeft(20, 88))
935 .build()
936 .unwrap();
937 for _ in 0..30 {
938 h.advance(
939 Intent {
940 move_x: 1.0,
941 ..Intent::default()
942 },
943 &mut floor,
944 1.0 / 60.0,
945 );
946 }
947 h
948 };
949 let h = hero(Platformer::builder());
950 let fast = h.animator.params.id("fast").unwrap();
951 assert!(h.animator.params.get_bool(fast), "set by the game's driver");
952
953 let h = hero(Platformer::builder().without_hero_driver());
955 let grounded = h.animator.params.id("grounded").unwrap();
956 assert!(!h.animator.params.get_bool(grounded));
957 assert!(h.animator.params.get_bool(fast));
958
959 let mut h = Platformer::builder()
961 .driver(|_: &World, p: &mut Params| {
962 let id = p.id("grounded").unwrap();
963 p.set_bool(id, false);
964 })
965 .spawn(Spot::TopLeft(20, 88))
966 .build()
967 .unwrap();
968 idle(&mut h);
969 assert!(h.world.player.grounded);
970 assert!(!h.animator.params.get_bool(grounded));
971 }
972
973 fn floor(r: Rect) -> bool {
975 r.bottom() > 100
976 }
977
978 fn idle(hero: &mut Platformer) -> Vec<Event> {
980 hero.advance(Intent::default(), &mut floor, 1.0 / 60.0);
981 hero.drain_events().collect()
982 }
983
984 #[test]
985 fn a_launch_is_not_cut_and_leaves_the_ground() {
986 let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
987 idle(&mut hero);
988 assert!(hero.world.player.grounded);
989 hero.launch(0.0, -300.0);
990 let events = idle(&mut hero);
992 assert!(!events.contains(&Event::JumpCut), "{events:?}");
993 assert!(!hero.world.player.grounded);
994 assert!(hero.world.player.vy < -250.0, "{}", hero.world.player.vy);
995 assert!(hero.hitbox().y < 88);
996 }
997
998 #[test]
999 fn a_launch_ends_a_dash() {
1000 let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
1001 idle(&mut hero);
1002 let dash = Intent {
1003 move_x: 1.0,
1004 dash_pressed: true,
1005 ..Intent::default()
1006 };
1007 hero.advance(dash, &mut floor, 1.0 / 60.0);
1008 assert!(matches!(
1009 hero.world.player.mode,
1010 crate::sim::Mode::Dash { .. }
1011 ));
1012 hero.launch(-100.0, -200.0);
1013 assert_eq!(hero.world.player.mode, crate::sim::Mode::Normal);
1014 idle(&mut hero);
1015 assert!(hero.world.player.vx < 0.0, "knocked back");
1016 }
1017
1018 #[test]
1019 fn a_teleport_snaps_without_smearing() {
1020 let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
1021 hero.advance(Intent::default(), &mut floor, 1.5 * DT);
1023 hero.teleport(Spot::TopLeft(150, 40));
1024 assert_eq!(hero.position(), (150.0, 40.0));
1025 assert_eq!(hero.hitbox().x, 150);
1026 let mut events = Vec::new();
1028 for _ in 0..60 {
1029 events.extend(idle(&mut hero));
1030 }
1031 assert!(hero.world.player.grounded);
1032 assert_eq!(hero.hitbox().bottom(), 100);
1033 assert!(
1034 events
1035 .iter()
1036 .any(|e| matches!(e, Event::Landed { drop, .. } if *drop >= 40)),
1037 "{events:?}"
1038 );
1039 }
1040
1041 #[test]
1042 fn respawning_resets_body_and_animation_and_says_so() {
1043 let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
1044 let run = Intent {
1045 move_x: 1.0,
1046 ..Intent::default()
1047 };
1048 for _ in 0..20 {
1049 hero.advance(run, &mut floor, 1.0 / 60.0);
1050 }
1051 hero.world.player.dashes = 0;
1052 hero.drain_events();
1053 assert_ne!(hero.state(), "idle");
1054
1055 hero.set_spawn(Spot::TopLeft(60, 88));
1056 hero.respawn();
1057 assert_eq!(hero.drain_events().collect::<Vec<_>>(), [Event::Respawned]);
1058 assert_eq!(hero.hitbox().x, 60);
1059 assert_eq!(hero.position(), (60.0, 88.0));
1060 assert_eq!(hero.world.player.vx, 0.0);
1061 assert_eq!(hero.world.player.dashes, hero.world.tuning.dashes);
1062 assert_eq!(hero.state(), "idle");
1063
1064 hero.respawn_at(Spot::TopLeft(100, 88));
1065 assert_eq!(hero.hitbox().x, 100);
1066 assert_eq!(
1067 hero.spawn(),
1068 Spot::TopLeft(60, 88),
1069 "respawn_at doesn't move the spawn"
1070 );
1071 }
1072
1073 #[test]
1074 fn refill_restores_dashes_air_jumps_and_stamina() {
1075 let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Snappy);
1076 let p = &mut hero.world.player;
1077 (p.dashes, p.air_jumps, p.stamina) = (0, 0, 0.0);
1078 hero.refill();
1079 let (p, t) = (&hero.world.player, &hero.world.tuning);
1080 assert_eq!(
1081 (p.dashes, p.air_jumps, p.stamina),
1082 (t.dashes, t.air_jumps, t.stamina)
1083 );
1084 }
1085
1086 #[test]
1087 fn spawning_standing_on_a_custom_floor_or_a_level_tile() {
1088 let mut hero = Platformer::builder()
1089 .spawn(Spot::Feet(50, 100))
1090 .build()
1091 .unwrap();
1092 let r = hero.hitbox();
1093 assert_eq!((r.bottom(), r.x + r.w / 2), (100, 50));
1094 let events = idle(&mut hero);
1095 assert!(hero.world.player.grounded, "stands at once");
1096 assert!(events.is_empty(), "no fall, no landing: {events:?}");
1097 assert_eq!(Spot::Feet(50, 100).top_left((r.w, r.h)), (r.x, r.y));
1098
1099 let level = Level::demo();
1101 let hero = Platformer::on(&level, Preset::Hybrid);
1102 let (sx, sy) = level.spawn_tile();
1103 let r = hero.hitbox();
1104 assert_eq!((r.x + r.w / 2, r.bottom()), (sx * 8 + 4, (sy + 1) * 8));
1105 assert_eq!(level.start(), Spot::Feet(sx * 8 + 4, (sy + 1) * 8));
1106 }
1107
1108 #[test]
1109 fn the_hitbox_size_comes_from_the_tuning() {
1110 let mut level = Level::demo();
1111 let mut hero = Platformer::builder()
1112 .tune(|t| t.size = (10, 20))
1113 .spawn(level.start())
1114 .build()
1115 .unwrap();
1116 let r = hero.hitbox();
1117 assert_eq!((r.w, r.h), (10, 20));
1118 let (sx, sy) = level.spawn_tile();
1119 let t = level.tile_size();
1120 assert_eq!(r.bottom(), (sy + 1) * t, "stands on the spawn tile");
1121 assert_eq!(r.x + r.w / 2, sx * t + t / 2, "centered on it");
1122 for _ in 0..30 {
1123 hero.advance(Intent::default(), &mut level, 1.0 / 60.0);
1124 }
1125 assert!(hero.world.player.grounded);
1126 assert_eq!(hero.hitbox().bottom(), (sy + 1) * t);
1127
1128 hero.set_preset(Preset::Momentum);
1129 hero.respawn();
1130 assert_eq!(hero.hitbox().h, 20, "a preset is a feel, not a body");
1131 }
1132
1133 #[test]
1134 fn abilities_from_the_builder_run_with_custom_buttons() {
1135 use crate::sim::{CustomId, Mode, Step};
1136 const BRAKE: CustomId = CustomId::new("tests::brake");
1138 #[derive(Clone, Debug)]
1140 struct Brake;
1141 impl Ability for Brake {
1142 fn before(&mut self, s: &mut Step<'_>) {
1143 match s.player.mode {
1144 Mode::Normal if s.intent.pressed(0) => {
1145 s.player.mode = Mode::Custom { id: BRAKE, t: 0.0 };
1146 s.player.vx = 0.0;
1147 s.events.push(Event::custom(BRAKE));
1148 }
1149 Mode::Custom { id: BRAKE, t } if t >= 0.1 => s.player.mode = Mode::Normal,
1150 _ => {}
1151 }
1152 }
1153 }
1154 let mut hero = Platformer::builder()
1155 .ability(Brake)
1156 .spawn(Spot::TopLeft(20, 88))
1157 .build()
1158 .unwrap();
1159 let run = Intent {
1160 move_x: 1.0,
1161 ..Intent::default()
1162 };
1163 for _ in 0..20 {
1164 hero.advance(run, &mut floor, 1.0 / 60.0);
1165 }
1166 hero.drain_events();
1167 let brake = Intent {
1169 custom_pressed: 1,
1170 ..run
1171 };
1172 hero.advance(brake, &mut floor, 0.0);
1173 hero.advance(run, &mut floor, DT);
1174 assert!(hero.drain_events().any(|e| e == Event::custom(BRAKE)));
1175 assert_eq!(hero.world.player.vx, 0.0, "the run doesn't accelerate it");
1176 for _ in 0..10 {
1177 hero.advance(run, &mut floor, 1.0 / 60.0);
1178 }
1179 assert_eq!(hero.world.player.mode, Mode::Normal, "handed back");
1180 assert!(hero.world.player.vx > 0.0, "running again");
1181 }
1182
1183 #[test]
1184 fn a_closure_is_enough_terrain_and_falling_out_respawns_or_not() {
1185 let mut floor = |r: Rect| r.bottom() > 100 && r.x < 200;
1187 let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Hybrid);
1188 for _ in 0..30 {
1189 hero.advance(Intent::default(), &mut floor, 1.0 / 60.0);
1190 }
1191 assert!(hero.world.player.grounded, "stands on the closure's floor");
1192 assert_eq!(hero.state(), "idle");
1193
1194 struct Pit;
1196 impl Terrain for Pit {
1197 fn solid(&self, r: Rect) -> bool {
1198 r.bottom() > 100 && r.x < 200
1199 }
1200 fn fell_out(&self, r: Rect) -> bool {
1201 r.y > 300
1202 }
1203 crate::terrain_layer!();
1204 }
1205 let mut hero = Platformer::builder()
1206 .spawn(Spot::TopLeft(220, 50))
1207 .respawn(false)
1208 .build()
1209 .unwrap();
1210 let mut events = Vec::new();
1211 for _ in 0..180 {
1212 hero.advance(Intent::default(), &mut Pit, 1.0 / 60.0);
1213 events.extend(hero.drain_events());
1214 }
1215 assert!(events.contains(&Event::FellOut));
1216 assert!(!events.contains(&Event::Respawned), "respawning was off");
1217 }
1218 #[test]
1219 fn standing_spawns_like_the_builder_route() {
1220 let mut hero = Platformer::at(Spot::Feet(50, 100), Preset::Snappy);
1221 let built = Platformer::builder()
1222 .preset(Preset::Snappy)
1223 .spawn(Spot::Feet(50, 100))
1224 .build()
1225 .unwrap();
1226 assert_eq!(hero.hitbox(), built.hitbox());
1227 assert_eq!(hero.preset(), Some(Preset::Snappy));
1228 let events = idle(&mut hero);
1229 assert!(hero.grounded(), "stands on the closure's floor at once");
1230 assert!(events.is_empty(), "{events:?}");
1231 }
1232
1233 #[test]
1234 fn accessors_match_the_body() {
1235 let mut hero = Platformer::at(Spot::TopLeft(20, 88), Preset::Precise);
1236 let run = Intent {
1237 move_x: 1.0,
1238 ..Intent::default()
1239 };
1240 for _ in 0..10 {
1241 hero.advance(run, &mut floor, 1.0 / 60.0);
1242 }
1243 let p = &hero.world.player;
1244 assert_eq!(hero.grounded(), p.grounded);
1245 assert_eq!(hero.velocity(), (p.vx, p.vy));
1246 assert!(hero.velocity().0 > 0.0);
1247 assert_eq!(hero.mode(), p.mode);
1248 assert_eq!(hero.dashes(), p.dashes);
1249 assert_eq!(hero.air_jumps(), p.air_jumps);
1250 assert_eq!(hero.stamina(), p.stamina);
1251 assert_eq!(hero.on_wall(), None);
1252
1253 let mut wall = |r: Rect| r.bottom() > 100 || r.right() > 60;
1255 let grab = Intent {
1256 move_x: 1.0,
1257 grab: true,
1258 ..Intent::default()
1259 };
1260 for _ in 0..30 {
1261 hero.advance(grab, &mut wall, 1.0 / 60.0);
1262 }
1263 assert_eq!(hero.mode(), Mode::Climb, "{:?}", hero.hitbox());
1264 assert_eq!(hero.on_wall(), Some(1));
1265 assert!(hero.stamina() < hero.world.tuning.stamina);
1266 }
1267
1268 #[test]
1269 fn falling_out_starts_the_animation_over() {
1270 struct Pit;
1272 impl Terrain for Pit {
1273 fn solid(&self, r: Rect) -> bool {
1274 r.bottom() > 100 && r.x < 200
1275 }
1276 fn fell_out(&self, r: Rect) -> bool {
1277 r.y > 300
1278 }
1279 crate::terrain_layer!();
1280 }
1281 let mut hero = Platformer::at(Spot::TopLeft(180, 88), Preset::Hybrid);
1282 let run = Intent {
1283 move_x: 1.0,
1284 ..Intent::default()
1285 };
1286 let entry = {
1287 let mut h = Platformer::at(Spot::TopLeft(20, 88), Preset::Hybrid);
1288 h.respawn();
1289 h.state().to_owned()
1290 };
1291 let mut respawned = false;
1292 for _ in 0..600 {
1293 hero.step(&run, &mut Pit);
1294 if hero.drain_events().any(|e| e == Event::Respawned) {
1295 respawned = true;
1296 break;
1297 }
1298 assert!(hero.world.tick < 590, "never fell out");
1299 }
1300 assert!(respawned);
1301 assert_eq!(
1302 hero.state(),
1303 entry,
1304 "from the entry state, as after respawn()"
1305 );
1306 assert_eq!(
1307 hero.position(),
1308 (180.0, 88.0),
1309 "drawn at the spawn, not sliding there"
1310 );
1311 }
1312 #[test]
1313 fn a_scaled_terrain_plays_out_like_one_in_units() {
1314 let units = |r: Rect| r.bottom() > 100 || r.right() > 200 || (r.x < 60 && r.bottom() > 76);
1316 let pixels =
1317 |r: Rect| r.bottom() > 200 || r.right() > 400 || (r.x < 120 && r.bottom() > 152);
1318 let mut a = Platformer::at(Spot::Feet(100, 100), Preset::Snappy);
1319 let mut b = Platformer::builder()
1320 .preset(Preset::Snappy)
1321 .scale(2)
1322 .spawn(Spot::Feet(200, 200))
1323 .build()
1324 .unwrap();
1325 assert_eq!(b.hitbox(), a.hitbox().scaled(2));
1326 let script = |k: u32| Intent {
1327 move_x: if k % 120 < 70 { 1.0 } else { -1.0 },
1328 jump_pressed: k.is_multiple_of(50),
1329 jump_held: k % 50 < 25,
1330 dash_pressed: k % 80 == 30,
1331 grab: k % 200 > 150,
1332 ..Intent::default()
1333 };
1334 for k in 0..600 {
1335 a.advance(script(k), &mut units.clone(), 1.0 / 60.0);
1336 b.advance(script(k), &mut pixels.clone(), 1.0 / 60.0);
1337 assert_eq!(b.hitbox(), a.hitbox().scaled(2), "frame {k}");
1338 assert!(
1339 a.drain_events()
1340 .map(|e| e.in_pixels(2))
1341 .eq(b.drain_events()),
1342 "frame {k}"
1343 );
1344 let ((ax, ay), (bx, by)) = (a.position(), b.position());
1345 assert!((bx - 2.0 * ax).abs() < 1e-3 && (by - 2.0 * ay).abs() < 1e-3);
1346 let ((avx, avy), (bvx, bvy)) = (a.velocity(), b.velocity());
1347 assert!((bvx - 2.0 * avx).abs() < 1e-3 && (bvy - 2.0 * avy).abs() < 1e-3);
1348 }
1349
1350 b.teleport(Spot::TopLeft(240, 100));
1352 assert_eq!((b.hitbox().x, b.hitbox().y), (240, 100));
1353 assert_eq!((b.world.player.x, b.world.player.y), (120, 50));
1354 b.launch(-200.0, 0.0);
1355 assert_eq!(b.velocity(), (-200.0, 0.0));
1356 assert_eq!(b.world.player.vx, -100.0);
1357 b.set_spawn(Spot::Feet(200, 200));
1358 assert_eq!(b.spawn(), Spot::Feet(200, 200));
1359 b.respawn();
1360 a.respawn_at(Spot::Feet(100, 100));
1361 assert_eq!(b.hitbox(), a.hitbox().scaled(2));
1362 assert_eq!(b.hitbox().bottom(), 200);
1363 }
1364
1365 #[test]
1366 fn an_ability_asks_a_found_terrain_in_its_pixels() {
1367 struct Map {
1369 marked: std::ops::Range<i32>,
1371 }
1372 impl Terrain for Map {
1373 fn solid(&self, r: Rect) -> bool {
1374 r.bottom() > 200
1375 }
1376 crate::terrain_layer!();
1377 }
1378 #[derive(Clone, Debug, Default)]
1380 struct Probe {
1381 hits: u32,
1383 unscaled: u32,
1385 }
1386 impl Ability for Probe {
1387 fn before(&mut self, s: &mut Step<'_>) {
1388 let body = s.player.rect();
1389 let k = s.scale;
1390 let map = s.terrain.find_mut::<Map>().expect("the game's map");
1391 let on = |r: Rect| map.marked.start < r.right() && r.x < map.marked.end;
1392 self.hits += u32::from(on(body.scaled(k)));
1393 self.unscaled += u32::from(on(body));
1394 }
1395 }
1396 let mut map = Map { marked: 200..216 };
1397 let mut hero = Platformer::builder()
1398 .scale(2)
1399 .spawn(Spot::Feet(206, 200))
1400 .ability(Probe::default())
1401 .build()
1402 .unwrap();
1403 hero.advance(Intent::default(), &mut map, 1.0 / 60.0);
1404 assert_eq!(hero.world().scale(), 2);
1405 let probe = hero.ability::<Probe>().unwrap();
1406 assert_eq!(probe.hits, 2, "both steps on the mark, in pixels");
1407 assert_eq!(probe.unscaled, 0, "units against pixels miss it");
1408 }
1409
1410 #[test]
1411 fn a_scaled_landing_reports_pixels() {
1412 let drop_of = |k: i32| {
1413 let mut floor = move |r: Rect| r.bottom() > 100 * k;
1414 let mut hero = Platformer::builder()
1415 .scale(k)
1416 .spawn(Spot::Feet(20 * k, 50 * k))
1417 .build()
1418 .unwrap();
1419 (0..120)
1420 .flat_map(|_| {
1421 hero.advance(Intent::default(), &mut floor, 1.0 / 60.0);
1422 hero.drain_events().collect::<Vec<_>>()
1423 })
1424 .find_map(|e| match e {
1425 Event::Landed { drop, impact } => Some((drop, impact)),
1426 _ => None,
1427 })
1428 .expect("landed")
1429 };
1430 let ((d1, i1), (d2, i2)) = (drop_of(1), drop_of(2));
1431 assert!(d1 >= 40, "{d1}");
1432 assert_eq!(d2, 2 * d1, "the drop in the terrain's pixels");
1433 assert!((i2 - 2.0 * i1).abs() < 1e-3, "{i1} {i2}");
1434 }
1435
1436 #[test]
1437 fn set_velocity_and_facing_change_only_that() {
1438 let mut hero = Platformer::builder()
1439 .scale(2)
1440 .spawn(Spot::Feet(40, 200))
1441 .build()
1442 .unwrap();
1443 let mut floor = |r: Rect| r.bottom() > 200;
1444 hero.advance(Intent::default(), &mut floor, 1.0 / 60.0);
1445 hero.set_velocity(-60.0, 0.0);
1446 assert_eq!(hero.velocity(), (-60.0, 0.0), "in pixels/s");
1447 assert_eq!(hero.world.player.vx, -30.0, "units inside");
1448 assert!(hero.grounded(), "still standing: not a launch");
1449 hero.set_facing(-5);
1450 assert_eq!(hero.facing(), -1);
1451 hero.set_facing(0);
1452 assert_eq!(hero.facing(), -1, "0 keeps it");
1453 hero.set_facing(1);
1454 assert_eq!(hero.facing(), 1);
1455 }
1456
1457 #[test]
1458 fn hitboxes_hold_every_step_of_the_frame() {
1459 let mut hero = Platformer::at(Spot::Feet(20, 100), Preset::Hybrid);
1460 let run = Intent {
1461 move_x: 1.0,
1462 ..Intent::default()
1463 };
1464 assert_eq!(hero.advance(run, &mut floor, 1.0 / 15.0), 8);
1465 let boxes = hero.hitboxes().to_vec();
1466 assert_eq!(boxes.len(), 8);
1467 assert_eq!(*boxes.last().unwrap(), hero.hitbox());
1468 assert!(boxes.windows(2).all(|p| p[0].x <= p[1].x), "{boxes:?}");
1469 assert!(boxes[0].x < boxes[7].x, "moved along");
1470 hero.advance(run, &mut floor, 0.0);
1472 assert!(hero.hitboxes().is_empty());
1473 hero.step(&run, &mut floor);
1474 assert_eq!(hero.hitboxes(), [hero.hitbox()]);
1475 }
1476
1477 #[test]
1478 fn touched_and_touches_find_a_steps_hitbox_by_value() {
1479 let mut level = Level::parse_with(&["......", ".P.^..", "######"], |c| {
1480 (c == '^').then_some(crate::sim::Tile::Custom(0))
1481 });
1482 let mut hero = Platformer::on(&level, Preset::Hybrid);
1483 let run = Intent {
1484 move_x: 1.0,
1485 ..Intent::default()
1486 };
1487 let spikes = crate::sim::Tile::Custom(0);
1488 let mut hit = None;
1489 for _ in 0..60 {
1490 hero.advance(run, &mut level, 1.0 / 15.0);
1491 if let Some(r) = level.touches(hero.hitboxes(), spikes) {
1492 hit = Some(r);
1493 hero.respawn(); break;
1495 }
1496 }
1497 let r = hit.expect("ran into the spikes");
1498 assert!(r.right() > 24 && r.x < 32, "{r:?}");
1499 assert!(
1500 hero.touched(r).is_some(),
1501 "r is one of the frame's hitboxes"
1502 );
1503 assert_eq!(level.touches(&[], spikes), None);
1504 assert_eq!(
1505 hero.touched(Rect {
1506 x: 500,
1507 y: 0,
1508 w: 1,
1509 h: 1
1510 }),
1511 None
1512 );
1513 }
1514
1515 #[test]
1516 fn runtime_changes_keep_the_facade_consistent() {
1517 let mut hero = Platformer::at(Spot::Feet(20, 100), Preset::Snappy);
1518 hero.tune(|t| t.air_jumps = 3);
1519 assert_eq!(hero.world.tuning.air_jumps, 3);
1520 assert_eq!(hero.preset(), None, "no longer the preset's feel");
1521 hero.set_preset(Preset::Precise);
1522 assert_eq!(hero.preset(), Some(Preset::Precise));
1523
1524 let tiny = GraphSpec::new().state(StateSpec::new(
1526 "idle",
1527 Clip::uniform(Playback::Loop, &[0], 0.1),
1528 ));
1529 let before = hero.state().to_owned();
1530 assert!(matches!(
1531 hero.set_graph(tiny.clone()),
1532 Err(SpecError::UnknownParam(_))
1533 ));
1534 assert_eq!(hero.state(), before);
1535 hero.set_partial_graph(two_state_graph()).unwrap();
1536 idle(&mut hero);
1537 assert_eq!(hero.state(), "idle", "the new graph runs, driven");
1538 hero.set_graph(spec()).unwrap();
1539 idle(&mut hero);
1540 assert_eq!(hero.state(), "idle");
1541 }
1542
1543 #[derive(Clone, Debug, Default)]
1545 struct Charge {
1546 level: f32,
1548 }
1549
1550 impl Ability for Charge {
1551 fn before(&mut self, s: &mut crate::sim::Step<'_>) {
1552 if s.intent.held(0) {
1553 self.level += s.dt;
1554 s.moves.max_run = 0.0;
1555 } else {
1556 self.level = 0.0;
1557 }
1558 }
1559 }
1560
1561 impl Animate for Charge {
1562 const PARAMS: &'static [(&'static str, ParamKind)] =
1563 &[("power", ParamKind::Float), ("powering", ParamKind::Bool)];
1564
1565 fn drive(&self, _world: &World, p: &mut Params, ids: &[ParamId]) {
1566 p.set_float(ids[0], self.level);
1567 p.set_bool(ids[1], self.level > 0.0);
1568 }
1569 }
1570
1571 #[test]
1572 fn an_animated_ability_drives_its_own_parameters() {
1573 use crate::anim::{Cond, TransitionSpec};
1574 let graph = spec()
1575 .float("power")
1576 .bool("powering")
1577 .state(StateSpec::new(
1578 "power",
1579 Clip::uniform(Playback::Loop, &[0], 0.1),
1580 ))
1581 .transition(TransitionSpec::new(
1582 ["idle"],
1583 "power",
1584 Cond::Is("powering".into()),
1585 ));
1586 let mut hero = Platformer::builder()
1587 .graph(graph.clone())
1588 .animated(Charge::default())
1589 .spawn(Spot::Feet(20, 100))
1590 .build()
1591 .unwrap();
1592 idle(&mut hero);
1593 let hold = Intent {
1594 custom: 1,
1595 ..Intent::default()
1596 };
1597 for _ in 0..30 {
1598 hero.advance(hold, &mut floor, 1.0 / 60.0);
1599 }
1600 let charge = hero.animator.params.id("power").unwrap();
1601 assert!((hero.animator.params.get_float(charge) - 0.5).abs() < 0.02);
1602 assert_eq!(hero.state(), "power");
1603 assert_eq!(
1604 hero.world.ability::<Charge>().map(|c| c.level > 0.4),
1605 Some(true)
1606 );
1607
1608 let mut hero = Platformer::builder()
1610 .graph(graph)
1611 .spawn(Spot::Feet(20, 100))
1612 .build()
1613 .unwrap();
1614 hero.add_ability(Brakes);
1615 hero.add_animated(Charge::default()).unwrap();
1616 for _ in 0..10 {
1617 hero.advance(hold, &mut floor, 1.0 / 60.0);
1618 }
1619 assert_eq!(hero.state(), "power");
1620 assert_eq!(hero.world.abilities.len(), 2);
1621 }
1622
1623 #[test]
1624 fn removing_or_reordering_abilities_keeps_each_driving_its_own_animation() {
1625 let graph = spec().float("power").bool("powering");
1626 let mut hero = Platformer::builder()
1627 .partial_graph(graph)
1628 .ability(Brakes)
1629 .animated(Charge::default())
1630 .spawn(Spot::Feet(20, 100))
1631 .build()
1632 .unwrap();
1633 idle(&mut hero);
1634 let hold = Intent {
1635 custom: 1,
1636 ..Intent::default()
1637 };
1638 let power = hero.animator().params.id("power").unwrap();
1639 let charged = |hero: &Platformer| hero.animator().params.get_float(power);
1640
1641 assert!(hero.remove_ability::<Brakes>().is_some());
1643 assert!(
1644 hero.remove_ability::<Brakes>().is_none(),
1645 "only one was there"
1646 );
1647 for _ in 0..15 {
1648 hero.advance(hold, &mut floor, 1.0 / 60.0);
1649 }
1650 assert!(charged(&hero) > 0.2, "{}", charged(&hero));
1651
1652 hero.add_ability(Brakes);
1654 hero.world_mut().abilities.reverse();
1655 for _ in 0..15 {
1656 hero.advance(hold, &mut floor, 1.0 / 60.0);
1657 }
1658 assert!(charged(&hero) > 0.45, "{}", charged(&hero));
1659 assert_eq!(
1660 hero.ability::<Charge>().map(|c| c.level),
1661 Some(charged(&hero))
1662 );
1663 hero.ability_mut::<Charge>().unwrap().level = 2.0;
1664 assert_eq!(hero.ability::<Charge>().unwrap().level, 2.0);
1665
1666 let removed = hero.remove_ability::<Charge>().unwrap();
1668 assert_eq!(removed.level, 2.0);
1669 let before = charged(&hero);
1670 for _ in 0..15 {
1671 hero.advance(hold, &mut floor, 1.0 / 60.0);
1672 }
1673 assert_eq!(charged(&hero), before);
1674 assert!(hero.animated.is_empty());
1675
1676 hero.add_animated(Charge::default()).unwrap();
1678 hero.add_animated(Charge::default()).unwrap();
1679 assert_eq!(hero.animated.len(), 1);
1680 }
1681
1682 #[test]
1683 fn an_animated_abilitys_parameters_are_checked_when_the_graph_is_built() {
1684 let only = |power: ParamKind| {
1685 let g = spec().bool("powering");
1686 match power {
1687 ParamKind::Float => g.float("power"),
1688 _ => g.bool("power"),
1689 }
1690 };
1691 let build = |g: GraphSpec| {
1692 Platformer::builder()
1693 .partial_graph(g)
1694 .animated(Charge::default())
1695 .build()
1696 };
1697 assert!(matches!(
1698 build(spec().bool("powering")).err(),
1699 Some(SpecError::UnknownParam(n)) if n == "power"
1700 ));
1701 assert!(matches!(
1702 build(only(ParamKind::Bool)).err(),
1703 Some(SpecError::WrongKind { param, declared: ParamKind::Bool }) if param == "power"
1704 ));
1705 let mut hero = build(only(ParamKind::Float)).unwrap();
1706
1707 let before = hero.animator().graph().find("idle");
1709 assert!(hero.set_partial_graph(spec()).is_err());
1710 assert_eq!(hero.animator().graph().find("idle"), before);
1711 assert!(hero.animator().params.id("power").is_some());
1712
1713 let mut plain = Platformer::builder().build().unwrap();
1715 assert!(plain.add_animated(Charge::default()).is_err());
1716 assert!(plain.ability::<Charge>().is_none());
1717 }
1718
1719 #[test]
1720 fn accessors_read_what_games_need_without_the_world() {
1721 let mut hero = Platformer::builder()
1722 .tune(|t| t.air_jumps = 2)
1723 .spawn(Spot::Feet(20, 100))
1724 .build()
1725 .unwrap();
1726 assert_eq!(hero.tuning().air_jumps, 2);
1727 let mut walled = |r: Rect| r.bottom() > 100 || r.right() > 60;
1729 let run = Intent {
1730 move_x: 1.0,
1731 ..Intent::default()
1732 };
1733 let mut hit = false;
1734 for _ in 0..30 {
1735 hero.advance(run, &mut walled, 1.0 / 60.0);
1736 hit |= hero.contacts().wall == 1;
1737 }
1738 assert!(hit, "ran into the wall");
1739 assert_eq!(hero.contacts(), hero.world().player.contacts);
1740 assert_eq!(hero.animator().state_name(), hero.state());
1741 }
1742
1743 #[derive(Clone, Debug)]
1745 struct Brakes;
1746
1747 impl Ability for Brakes {}
1748
1749 #[test]
1750 fn draining_keeps_the_buffers() {
1751 let mut hero = Platformer::at(Spot::Feet(20, 100), Preset::Hybrid);
1752 let jump = Intent {
1753 jump_pressed: true,
1754 jump_held: true,
1755 ..Intent::default()
1756 };
1757 hero.advance(jump, &mut floor, 1.0 / 60.0);
1758 let cap = hero.events.capacity();
1759 assert!(hero.drain_events().count() > 0);
1760 assert_eq!(hero.events.capacity(), cap, "no new allocation next frame");
1761 assert!(hero.drain_events().next().is_none());
1762 hero.drain_anim_events().for_each(drop);
1763 assert!(hero.anim_events.capacity() > 0);
1764 }
1765}