jasonlooked #1
@ -107,7 +107,7 @@ impl PhysicsWorld {
|
||||
weight: u8,
|
||||
curl: i8,
|
||||
friction: f32,
|
||||
) -> Result<Vec<(f32, f32, f32)>, String> {
|
||||
) -> Result<Vec<StoneTrajectory>, String> {
|
||||
let weight = weight.clamp(1, 10) as f32;
|
||||
let t = (weight - 1.0) / 9.0;
|
||||
let speed = MIN_SPEED + t * (MAX_SPEED - MIN_SPEED);
|
||||
@ -132,7 +132,7 @@ impl PhysicsWorld {
|
||||
vy: f32,
|
||||
curl_sign: i8,
|
||||
damping_mult: f32,
|
||||
) -> Result<Vec<(f32, f32, f32)>, String> {
|
||||
) -> Result<Vec<StoneTrajectory>, String> {
|
||||
let id = self.next_stone_id;
|
||||
self.next_stone_id += 1;
|
||||
|
||||
@ -161,15 +161,31 @@ impl PhysicsWorld {
|
||||
self.simulate_until_rest(id)
|
||||
}
|
||||
|
||||
fn simulate_until_rest(&mut self, thrown_id: u32) -> Result<Vec<(f32, f32, f32)>, String> {
|
||||
let mut path: Vec<(f32, f32, f32)> = Vec::new();
|
||||
fn simulate_until_rest(&mut self, thrown_id: u32) -> Result<Vec<StoneTrajectory>, String> {
|
||||
// All paths share the thrown stone's release instant as t=0. This keeps the
|
||||
// frontend's existing trajectory helpers (which expect the thrown stone to
|
||||
// start at x=0, y=HACK_Y with t=0) working unchanged while also giving every
|
||||
// other stone a consistent timeline.
|
||||
let sample_step = 1.0 / SAMPLE_RATE_HZ as f32;
|
||||
let mut sample_accum: f32 = 0.0;
|
||||
let mut time: f32 = 0.0;
|
||||
|
||||
if let Some((_, h, _, _)) = self.stone_handles.iter().find(|(id, _, _, _)| *id == thrown_id) {
|
||||
let body = &self.bodies[*h];
|
||||
path.push((body.translation().x, body.translation().y, time));
|
||||
// Pre-allocate a path buffer for every stone currently in the world.
|
||||
let mut paths: Vec<(u32, RigidBodyHandle, Vec<(f32, f32, f32)>)> = self
|
||||
.stone_handles
|
||||
.iter()
|
||||
.map(|(id, handle, _, _)| (*id, *handle, Vec::new()))
|
||||
.collect();
|
||||
|
||||
// Record the initial sample at t=0 for every stone.
|
||||
for (id, handle, path) in &mut paths {
|
||||
if let Some(body) = self.bodies.get(*handle) {
|
||||
let pos = body.translation();
|
||||
path.push((pos.x, pos.y, time));
|
||||
} else {
|
||||
// Body missing for an tracked stone; this should not happen.
|
||||
return Err(format!("stone {} has no rigid body", id));
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
@ -180,9 +196,11 @@ impl PhysicsWorld {
|
||||
|
||||
if sample_accum >= sample_step {
|
||||
sample_accum -= sample_step;
|
||||
if let Some((_, h, _, _)) = self.stone_handles.iter().find(|(id, _, _, _)| *id == thrown_id) {
|
||||
let body = &self.bodies[*h];
|
||||
path.push((body.translation().x, body.translation().y, time));
|
||||
for (_, handle, path) in &mut paths {
|
||||
if let Some(body) = self.bodies.get(*handle) {
|
||||
let pos = body.translation();
|
||||
path.push((pos.x, pos.y, time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,7 +211,27 @@ impl PhysicsWorld {
|
||||
|
||||
self.prune_out_of_play();
|
||||
|
||||
Ok(path)
|
||||
// The thrown stone is released at (0.0, HACK_Y). Shift every path in time so
|
||||
// that t=0 corresponds to that release instant. Because we already started
|
||||
// sampling at the release instant, the first sample time is 0.0 and no shift
|
||||
// is required; this comment documents the invariant.
|
||||
let thrown_first_t = paths
|
||||
.iter()
|
||||
.find(|(id, _, _)| *id == thrown_id)
|
||||
.and_then(|(_, _, path)| path.first().map(|(_, _, t)| *t))
|
||||
.unwrap_or(0.0);
|
||||
|
||||
Ok(paths
|
||||
.into_iter()
|
||||
.map(|(id, _, mut path)| {
|
||||
if thrown_first_t != 0.0 {
|
||||
for (_, _, t) in &mut path {
|
||||
*t -= thrown_first_t;
|
||||
}
|
||||
}
|
||||
StoneTrajectory { stone_id: id, path }
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
// Rotate each stone's velocity slightly based on its selected curl direction.
|
||||
@ -377,4 +415,70 @@ mod tests {
|
||||
let stones = world.current_stones();
|
||||
assert!(stones.is_empty(), "stones short of the hog line should be pruned");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collision_records_trajectories_for_both_stones() {
|
||||
// Place a stationary stone on the center line and throw a second stone
|
||||
// straight at it so they collide. Both stones must have sampled paths.
|
||||
let mut world = PhysicsWorld::new();
|
||||
|
||||
// First stone: place it far enough up-sheet to stay in play after impact.
|
||||
world
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, 7, 0, 1.0)
|
||||
.unwrap();
|
||||
let first_id = world.next_stone_id - 1;
|
||||
|
||||
// Second stone: aimed directly at the first stone's final position.
|
||||
let target_y = final_y(&world, first_id);
|
||||
let target_x = final_x(&world, first_id);
|
||||
world
|
||||
.throw(Team::Yellow, target_x, target_y, 10, 0, 1.0)
|
||||
.unwrap();
|
||||
let second_id = world.next_stone_id - 1;
|
||||
|
||||
// Re-run the collision throw and capture trajectories.
|
||||
let mut world = PhysicsWorld::new();
|
||||
world
|
||||
.throw(Team::Red, 0.0, HOUSE_CENTER.1, 7, 0, 1.0)
|
||||
.unwrap();
|
||||
let first_id = world.next_stone_id - 1;
|
||||
let target_y = final_y(&world, first_id);
|
||||
let target_x = final_x(&world, first_id);
|
||||
let trajectories = world
|
||||
.throw(Team::Yellow, target_x, target_y, 10, 0, 1.0)
|
||||
.unwrap();
|
||||
|
||||
let by_id: std::collections::HashMap<u32, Vec<(f32, f32, f32)>> = trajectories
|
||||
.into_iter()
|
||||
.map(|st| (st.stone_id, st.path))
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
by_id.contains_key(&first_id),
|
||||
"trajectories should contain the first stone (id={})",
|
||||
first_id
|
||||
);
|
||||
assert!(
|
||||
by_id.contains_key(&second_id),
|
||||
"trajectories should contain the thrown stone (id={})",
|
||||
second_id
|
||||
);
|
||||
|
||||
let first_path = by_id.get(&first_id).unwrap();
|
||||
let second_path = by_id.get(&second_id).unwrap();
|
||||
assert!(
|
||||
first_path.len() > 1,
|
||||
"first stone path should have multiple samples, got {}",
|
||||
first_path.len()
|
||||
);
|
||||
assert!(
|
||||
second_path.len() > 1,
|
||||
"thrown stone path should have multiple samples, got {}",
|
||||
second_path.len()
|
||||
);
|
||||
|
||||
// Both paths should share the same t=0 reference (the thrown stone's release).
|
||||
assert_eq!(first_path[0].2, 0.0, "first stone path should start at t=0");
|
||||
assert_eq!(second_path[0].2, 0.0, "thrown stone path should start at t=0");
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user