use crate::Platformer;
use crate::platformer::{Scaled, steps};
use crate::sim::{DT, Intent, Rect, Terrain};
pub struct Party {
pub members: Vec<Platformer>,
pub solid: bool,
acc: f32,
}
impl Party {
pub fn new(members: Vec<Platformer>) -> Self {
Self {
members,
solid: true,
acc: 0.0,
}
}
pub fn advance(&mut self, intents: &[Intent], terrain: &mut dyn Terrain, dt: f32) -> u32 {
let intent = |k: usize| intents.get(k).copied().unwrap_or_default();
for (k, m) in self.members.iter_mut().enumerate() {
m.begin_frame();
m.hold_presses(&intent(k));
}
let n = steps(&mut self.acc, dt);
for _ in 0..n {
let step: Vec<Intent> = (self.members.iter_mut().enumerate())
.map(|(k, m)| m.with_presses(intent(k)))
.collect();
self.step_once(&step, terrain);
}
for m in &mut self.members {
m.set_clock(self.acc);
}
n
}
pub fn step(&mut self, intents: &[Intent], terrain: &mut dyn Terrain) {
for m in &mut self.members {
m.begin_frame();
}
self.step_once(intents, terrain);
}
fn step_once(&mut self, intents: &[Intent], terrain: &mut dyn Terrain) {
let k = self.members.first().map_or(1, Platformer::scale);
debug_assert!(
self.members.iter().all(|m| m.scale() == k),
"party members on one terrain share a scale"
);
if k == 1 {
self.step_in(intents, terrain);
} else {
self.step_in(intents, &mut Scaled { inner: terrain, k });
}
}
fn step_in(&mut self, intents: &[Intent], terrain: &mut dyn Terrain) {
let n = self.members.len();
let before: Vec<Rect> = self.members.iter().map(|m| m.world.player.rect()).collect();
let carrier: Vec<Option<usize>> = (0..n)
.map(|i| {
let feet = self.members[i].world.player.riding()?;
let grounded = self.members[i].world.player.grounded;
(self.solid && grounded)
.then(|| (0..n).find(|&j| j != i && feet.intersects(before[j])))
.flatten()
})
.collect();
let others = |i: usize, now: &[Rect]| -> Vec<Rect> {
if !self.solid {
return Vec::new();
}
(0..n)
.filter(|&j| j != i && carrier[j] != Some(i))
.map(|j| now[j])
.collect()
};
let mut order: Vec<usize> = (0..n).collect();
order.sort_by_key(|&i| before[i].y);
let mut now = before.clone();
for &i in &order {
let mut among = Among {
terrain: &mut *terrain,
others: others(i, &now),
};
let intent = intents.get(i).copied().unwrap_or_default();
let everyone_else: Vec<Rect> = (0..n).filter(|&j| j != i).map(|j| now[j]).collect();
self.members[i].begin_step();
self.members[i]
.world
.act_among(&mut among, &intent, &everyone_else);
now[i] = self.members[i].world.player.rect();
}
terrain.update(DT, &now);
for &i in order.iter().rev() {
let extra = carrier[i].map(|j| {
let (b, a) = (before[j], now[j]);
((a.x - b.x) as f32, (a.y - b.y) as f32)
});
let mut among = Among {
terrain: &mut *terrain,
others: others(i, &now),
};
self.members[i].world.settle(&mut among, extra);
now[i] = self.members[i].world.player.rect();
}
for m in &mut self.members {
m.end_step();
}
}
}
struct Among<'a> {
terrain: &'a mut dyn Terrain,
others: Vec<Rect>,
}
impl Among<'_> {
fn hits_other(&self, r: Rect) -> bool {
self.others.iter().any(|o| o.intersects(r))
}
}
impl Terrain for Among<'_> {
fn solid(&self, r: Rect) -> bool {
self.terrain.solid(r) || self.hits_other(r)
}
fn base(&self) -> Option<&dyn Terrain> {
Some(&*self.terrain)
}
fn base_mut(&mut self) -> Option<&mut dyn Terrain> {
Some(&mut *self.terrain)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::{Event, Level, Preset, Spot};
#[derive(Default)]
struct Floor {
updates: u32,
bodies: usize,
}
impl Terrain for Floor {
fn solid(&self, r: Rect) -> bool {
r.bottom() > 100
}
fn tick(&mut self, _dt: f32, bodies: &[Rect]) {
self.updates += 1;
self.bodies = bodies.len();
}
crate::terrain_layer!();
}
fn at(x: i32, y: i32) -> Platformer {
Platformer::at(Spot::TopLeft(x, y), Preset::Hybrid)
}
fn frames(p: &mut Party, intents: &[Intent], floor: &mut Floor, n: usize) {
for _ in 0..n {
p.advance(intents, floor, 1.0 / 60.0);
}
}
#[test]
fn the_terrain_updates_once_per_step_with_every_body() {
let mut floor = Floor::default();
let mut p = Party::new(vec![at(20, 88), at(60, 88), at(100, 88)]);
let steps = p.advance(&[], &mut floor, 1.0 / 60.0);
assert_eq!(steps, 2);
assert_eq!(floor.updates, 2);
assert_eq!(floor.bodies, 3);
}
#[test]
fn members_stand_on_and_ride_each_other() {
let mut floor = Floor::default();
let mut p = Party::new(vec![at(20, 88), at(20, 40)]);
frames(&mut p, &[], &mut floor, 60);
let (a, b) = (p.members[0].hitbox(), p.members[1].hitbox());
assert_eq!(b.bottom(), a.y, "B stands on A");
assert!(p.members[1].world.player.grounded);
let walk = [Intent {
move_x: 1.0,
..Intent::default()
}];
frames(&mut p, &walk, &mut floor, 30);
let (a2, b2) = (p.members[0].hitbox(), p.members[1].hitbox());
assert!(a2.x > a.x + 20, "A walked: {} -> {}", a.x, a2.x);
assert_eq!(b2.x - a2.x, b.x - a.x, "B kept its place on A's head");
assert_eq!(b2.bottom(), a2.y);
let jump = [Intent {
jump_pressed: true,
jump_held: true,
..Intent::default()
}];
let mut events = Vec::new();
p.advance(&jump, &mut floor, 1.0 / 60.0);
for _ in 0..10 {
p.advance(
&[Intent {
jump_held: true,
..Intent::default()
}],
&mut floor,
1.0 / 60.0,
);
events.extend(p.members[1].drain_events());
}
assert!(p.members[1].hitbox().y < b2.y - 10, "carried up");
assert!(!events.contains(&Event::Squished), "{events:?}");
}
#[test]
fn members_block_each_other_unless_not_solid() {
let mut floor = Floor::default();
let right = [Intent {
move_x: 1.0,
..Intent::default()
}];
let mut p = Party::new(vec![at(20, 88), at(60, 88)]);
frames(&mut p, &right, &mut floor, 60);
assert_eq!(p.members[0].hitbox().right(), 60, "stopped by B");
let mut p = Party::new(vec![at(20, 88), at(60, 88)]);
p.solid = false;
frames(&mut p, &right, &mut floor, 60);
assert!(p.members[0].hitbox().x > 60, "walked through B");
}
#[test]
fn abilities_reach_the_shared_terrain_through_the_others() {
use crate::sim::{Ability, Step, Tile};
#[derive(Clone, Debug)]
struct Dig;
impl Ability for Dig {
fn before(&mut self, s: &mut Step<'_>) {
if s.intent.pressed(0) {
let r = s.player.rect();
let level = s.terrain.find_mut::<Level>().expect("behind the party");
level.set((r.x + r.w / 2) / 8, r.bottom() / 8, Tile::Empty);
}
}
}
let mut level = Level::parse(&["........", "........", "........", "########"]);
let a = Platformer::builder()
.ability(Dig)
.spawn(Spot::Feet(12, 24))
.build()
.unwrap();
let b = Platformer::builder()
.spawn(Spot::Feet(44, 24))
.build()
.unwrap();
let mut p = Party::new(vec![a, b]);
let dig = Intent {
custom_pressed: 1,
..Intent::default()
};
p.step(&[dig], &mut level);
assert_eq!(level.tile(1, 3), Tile::Empty, "dug out");
assert_eq!(level.tile(5, 3), Tile::Solid);
}
#[test]
fn abilities_see_the_other_members() {
use crate::sim::{Ability, CustomId, Step};
#[derive(Clone, Debug)]
struct Look;
const SAW: CustomId = CustomId::new("tests::saw");
impl Ability for Look {
fn after(&mut self, s: &mut Step<'_>) {
let me = s.player.rect();
let near = s.others.iter().map(|o| (o.x - me.x).abs()).min();
let n = s.others.len() as f32;
let d = near.map_or(-1.0, |d| d as f32);
s.events.push(Event::custom(SAW).with([n, d, 0.0, 0.0]));
}
}
let a = Platformer::builder()
.ability(Look)
.spawn(Spot::TopLeft(20, 88))
.build()
.unwrap();
let mut p = Party::new(vec![a, at(60, 88), at(100, 88)]);
let mut floor = Floor::default();
p.step(&[], &mut floor);
let saw: Vec<_> = p.members[0]
.drain_events()
.filter_map(|e| e.custom_data(SAW))
.collect();
assert_eq!(saw, [[2.0, 40.0, 0.0, 0.0]]);
assert_eq!(p.members[0].hitboxes().len(), 1);
let mut solo = Platformer::builder().ability(Look).build().unwrap();
solo.step(&Intent::default(), &mut floor);
let saw: Vec<_> = solo
.drain_events()
.filter_map(|e| e.custom_data(SAW))
.collect();
assert_eq!(saw, [[0.0, -1.0, 0.0, 0.0]]);
}
}