fix(physics): preserve first-candidate scan semantics
Carry predicted position separately through exact record-ID chunks, apply magnetic rewrites only at records 6/153/154, and honor type-three/dynamic broadphase. Match raw 1000:c79c by resolving fixed candidate slot one rather than a later nearer record, and restore related capture-age, tilt, and stacked multiball scoring side effects. Test Plan: - bash original/tools/test_reconstructed_c.sh - python3 original/tools/audit_reconstruction.py --require-complete - cargo test --all-targets - cargo clippy --all-targets --all-features -- -D warnings - rumdl check original/C_RECONSTRUCTION_FINAL_AUDIT.md original/MECHANICS_PROGRESS.md tdkpin-rs/CHANGELOG.md tdkpin-rs/RECONSTRUCTION.md tdkpin-rs/README.md - git diff --check
This commit is contained in:
+85
-2
@@ -207,6 +207,13 @@ enum MultiballState {
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum ScoreMode {
|
||||
#[default]
|
||||
Normal,
|
||||
Multiball,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum CaptureStep {
|
||||
Outside,
|
||||
@@ -314,6 +321,7 @@ pub struct Game {
|
||||
target_effect: u8,
|
||||
claw_frame_seconds: f32,
|
||||
multiball_state: MultiballState,
|
||||
score_mode: ScoreMode,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
@@ -355,6 +363,7 @@ impl Game {
|
||||
target_effect: 0,
|
||||
claw_frame_seconds: CLAW_FRAME_SECONDS,
|
||||
multiball_state: MultiballState::Unavailable,
|
||||
score_mode: ScoreMode::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,6 +490,9 @@ impl Game {
|
||||
let panel_active = self.update_panel_completion(events);
|
||||
if !self.claw.ball_suspended && !panel_active {
|
||||
let had_secondary_ball = self.secondary_ball.is_some();
|
||||
if had_secondary_ball {
|
||||
self.score_mode = ScoreMode::Multiball;
|
||||
}
|
||||
for _ in 0..substeps {
|
||||
if self.fixed_update(STEP_SECONDS, events)
|
||||
|| self.finished
|
||||
@@ -489,6 +501,9 @@ impl Game {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if had_secondary_ball && self.secondary_ball.is_none() {
|
||||
self.score_mode = ScoreMode::Normal;
|
||||
}
|
||||
if had_secondary_ball && !self.finished && !self.claw.ball_suspended {
|
||||
if self.secondary_ball.is_some() {
|
||||
for _ in 0..substeps {
|
||||
@@ -728,6 +743,9 @@ impl Game {
|
||||
}
|
||||
}
|
||||
let collided = best_collision.is_some();
|
||||
if collided {
|
||||
ball.capture_age = 0;
|
||||
}
|
||||
let mut auxiliary_fired = false;
|
||||
let (hit_wall, hit_circle) = if let Some((object_id, is_wall, response)) = best_collision {
|
||||
velocity = response.velocity;
|
||||
@@ -1132,6 +1150,9 @@ impl Game {
|
||||
}
|
||||
let mut hit = None;
|
||||
let collided = best_collision.is_some();
|
||||
if collided {
|
||||
ball.capture_age = 0;
|
||||
}
|
||||
let mut auxiliary_fired = false;
|
||||
if let Some((object_id, is_wall, response)) = best_collision {
|
||||
velocity = response.velocity;
|
||||
@@ -1149,6 +1170,7 @@ impl Game {
|
||||
ball.position = old_position.add(velocity).to_position();
|
||||
ball.velocity = velocity.to_velocity_per_second();
|
||||
if hit == Some((2, true)) || ball.position.y > 470.0 {
|
||||
self.score_mode = ScoreMode::Normal;
|
||||
events.push(Event::Drain);
|
||||
return true;
|
||||
}
|
||||
@@ -1180,11 +1202,14 @@ impl Game {
|
||||
}
|
||||
|
||||
fn apply_wall_rule(&mut self, object_id: u8, events: &mut Vec<Event>) {
|
||||
if self.tilted {
|
||||
return;
|
||||
}
|
||||
let wall = WALLS
|
||||
.iter()
|
||||
.find(|wall| wall.id == object_id)
|
||||
.expect("a wall collision id must reference a recovered wall");
|
||||
if !self.tilted && wall.score != 0 {
|
||||
if wall.score != 0 {
|
||||
self.add_score(wall.score, events);
|
||||
events.push(if object_id == 121 {
|
||||
Event::Wheel
|
||||
@@ -1643,6 +1668,7 @@ impl Game {
|
||||
capture_age: 0,
|
||||
});
|
||||
self.multiball_state = MultiballState::Unavailable;
|
||||
self.score_mode = ScoreMode::Multiball;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1875,11 +1901,16 @@ impl Game {
|
||||
|
||||
fn add_score(&mut self, points: u32, events: &mut Vec<Event>) {
|
||||
const THRESHOLDS: [u32; 4] = [140_000, 650_000, 1_300_000, 4_000_000];
|
||||
let multiplier = if self.player().double_score {
|
||||
if self.tilted {
|
||||
return;
|
||||
}
|
||||
let multiball_multiplier = if self.score_mode == ScoreMode::Multiball {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let permanent_multiplier = if self.player().double_score { 2 } else { 1 };
|
||||
let multiplier = multiball_multiplier * permanent_multiplier;
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.score = player.score.wrapping_add(points.wrapping_mul(multiplier));
|
||||
let level = usize::from(player.media_level);
|
||||
@@ -1895,6 +1926,7 @@ impl Game {
|
||||
fn drain(&mut self, events: &mut Vec<Event>) {
|
||||
if let Some(remaining_ball) = self.secondary_ball.take() {
|
||||
self.ball = remaining_ball;
|
||||
self.score_mode = ScoreMode::Normal;
|
||||
events.push(Event::Drain);
|
||||
return;
|
||||
}
|
||||
@@ -1921,6 +1953,7 @@ impl Game {
|
||||
self.flippers = Flippers::default();
|
||||
self.pending_flipper_edges.fill(0);
|
||||
self.secondary_ball = None;
|
||||
self.score_mode = ScoreMode::Normal;
|
||||
self.nudge_shake = 0.0;
|
||||
self.launcher_charge = 0.0;
|
||||
self.launcher_was_down = false;
|
||||
@@ -2357,6 +2390,39 @@ mod tests {
|
||||
assert_eq!(game.player().secondary_score, 100_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_helper_stacks_multiball_and_permanent_double_and_honors_tilt() {
|
||||
let mut game = Game::new(1);
|
||||
let mut events = Vec::new();
|
||||
game.score_mode = ScoreMode::Multiball;
|
||||
game.add_score(100, &mut events);
|
||||
assert_eq!(game.player().score, 200);
|
||||
|
||||
game.players[0].double_score = true;
|
||||
game.add_score(100, &mut events);
|
||||
assert_eq!(game.player().score, 600);
|
||||
|
||||
game.tilted = true;
|
||||
game.add_score(100, &mut events);
|
||||
assert_eq!(game.player().score, 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tilted_collision_dispatch_leaves_rule_records_unchanged() {
|
||||
let mut game = Game::new(1);
|
||||
game.tilted = true;
|
||||
let mut events = Vec::new();
|
||||
|
||||
game.apply_wall_rule(90, &mut events);
|
||||
game.apply_wall_rule(84, &mut events);
|
||||
|
||||
assert!(game.object_active[90]);
|
||||
assert_eq!(game.target_effect, 0);
|
||||
assert!(!game.effect_target_active());
|
||||
assert_eq!(game.player().score, 0);
|
||||
assert!(events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_target_banks_deactivate_and_rearm_their_line_groups() {
|
||||
let mut game = Game::new(1);
|
||||
@@ -2687,6 +2753,23 @@ mod tests {
|
||||
assert!(game.ball.velocity.y > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_collision_candidate_clears_the_slot_capture_age() {
|
||||
let mut game = Game::new(1);
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = vec2(270.0, 30.0);
|
||||
game.ball.velocity = vec2(0.0, -380.0);
|
||||
game.ball.capture_age = 42;
|
||||
|
||||
for _ in 0..5 {
|
||||
if game.fixed_update(STEP_SECONDS, &mut Vec::new()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(game.ball.capture_age, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collision_scan_selects_the_layer_from_the_previous_y_position() {
|
||||
let mut game = Game::new(1);
|
||||
|
||||
Reference in New Issue
Block a user