fix(game): restore effect-seven special respawn
Track required effect-seven state through active multiball and implement 1000:ae6e's final single-ball respawn at record 148 without consuming a player marker. Preserve velocity, clear required state, and hold the 0875-style record-148 gate until a broadphase-active type-four record reenables it. Test Plan: - cargo test --all-targets - cargo clippy --all-targets --all-features -- -D warnings - rumdl check CHANGELOG.md RECONSTRUCTION.md README.md - git diff --check
This commit is contained in:
+120
-6
@@ -205,6 +205,7 @@ enum MultiballState {
|
||||
#[default]
|
||||
Unavailable,
|
||||
Ready,
|
||||
Active,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
@@ -214,6 +215,13 @@ enum ScoreMode {
|
||||
Multiball,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum WheelResetGate {
|
||||
#[default]
|
||||
Enabled,
|
||||
Suppressed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum CaptureStep {
|
||||
Outside,
|
||||
@@ -247,6 +255,15 @@ const fn predicted_outside_board(position: MilliVec) -> bool {
|
||||
position.x <= 0 || position.x > 340_000 || position.y <= 0 || position.y > 460_000
|
||||
}
|
||||
|
||||
fn trigger_broadphase_contains(predicted: MilliVec, center: Vec2, radius: f32) -> bool {
|
||||
let center = MilliVec::from_position(center);
|
||||
let margin = MilliVec::from_position(vec2(radius + 5.0, 0.0)).x;
|
||||
predicted.x >= center.x.wrapping_sub(margin)
|
||||
&& predicted.x <= center.x.wrapping_add(margin)
|
||||
&& predicted.y >= center.y.wrapping_sub(margin)
|
||||
&& predicted.y <= center.y.wrapping_add(margin)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct RuleState {
|
||||
wheel_holes: [bool; 5],
|
||||
@@ -324,6 +341,7 @@ pub struct Game {
|
||||
target_effect: u8,
|
||||
claw_frame_seconds: f32,
|
||||
multiball_state: MultiballState,
|
||||
wheel_reset_gate: WheelResetGate,
|
||||
score_mode: ScoreMode,
|
||||
}
|
||||
|
||||
@@ -365,6 +383,7 @@ impl Game {
|
||||
target_effect: 0,
|
||||
claw_frame_seconds: CLAW_FRAME_SECONDS,
|
||||
multiball_state: MultiballState::Unavailable,
|
||||
wheel_reset_gate: WheelResetGate::Enabled,
|
||||
score_mode: ScoreMode::Normal,
|
||||
}
|
||||
}
|
||||
@@ -927,6 +946,19 @@ impl Game {
|
||||
x: predicted.x.wrapping_sub(old_position.x),
|
||||
y: predicted.y.wrapping_sub(old_position.y),
|
||||
};
|
||||
if !self.tilted
|
||||
&& (TARGET_SENSORS.into_iter().any(|sensor| {
|
||||
self.object_active[usize::from(sensor.id)]
|
||||
&& trigger_broadphase_contains(predicted, sensor.center, sensor.radius)
|
||||
}) || self.object_active[usize::from(EFFECT_SENSOR.id)]
|
||||
&& trigger_broadphase_contains(
|
||||
predicted,
|
||||
EFFECT_SENSOR.center,
|
||||
EFFECT_SENSOR.radius,
|
||||
))
|
||||
{
|
||||
self.wheel_reset_gate = WheelResetGate::Enabled;
|
||||
}
|
||||
self.check_target_sensors(
|
||||
ball,
|
||||
old_position,
|
||||
@@ -1294,7 +1326,10 @@ impl Game {
|
||||
}
|
||||
if wall.flags & 0x0004 != 0 {
|
||||
events.push(Event::Sound(2012));
|
||||
if self.multiball_state == MultiballState::Ready {
|
||||
if matches!(
|
||||
self.multiball_state,
|
||||
MultiballState::Ready | MultiballState::Active
|
||||
) {
|
||||
self.target_effect = 7;
|
||||
} else {
|
||||
let previous = self.target_effect;
|
||||
@@ -1352,6 +1387,23 @@ impl Game {
|
||||
let mut capture_candidate = None;
|
||||
let mut action = BallAction::Keep;
|
||||
let predicted_position = old_position.add(movement_velocity);
|
||||
if !self.tilted
|
||||
&& (TARGET_SENSORS.into_iter().any(|sensor| {
|
||||
self.object_active[usize::from(sensor.id)]
|
||||
&& trigger_broadphase_contains(
|
||||
predicted_position,
|
||||
sensor.center,
|
||||
sensor.radius,
|
||||
)
|
||||
}) || self.object_active[usize::from(EFFECT_SENSOR.id)]
|
||||
&& trigger_broadphase_contains(
|
||||
predicted_position,
|
||||
EFFECT_SENSOR.center,
|
||||
EFFECT_SENSOR.radius,
|
||||
))
|
||||
{
|
||||
self.wheel_reset_gate = WheelResetGate::Enabled;
|
||||
}
|
||||
if old_position.y >= 250_000 {
|
||||
return SensorScanResult {
|
||||
action: BallAction::Keep,
|
||||
@@ -1473,7 +1525,10 @@ impl Game {
|
||||
let contact_allowed =
|
||||
contact == 0 || (contact != 99 && ball_count == 1) || contact == ball_number;
|
||||
|
||||
if surface_distance < -11_000 && contact_allowed {
|
||||
if surface_distance < -11_000
|
||||
&& contact_allowed
|
||||
&& !(record_id == 148 && self.wheel_reset_gate == WheelResetGate::Suppressed)
|
||||
{
|
||||
if ball.capture_age < 300 {
|
||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
let stationary = center.x.wrapping_sub(previous_position.x).wrapping_abs() < 500
|
||||
@@ -1667,7 +1722,12 @@ impl Game {
|
||||
self.add_score(transfer, events);
|
||||
self.players[self.current_player].secondary_score = 0;
|
||||
}
|
||||
7 if ball_number != 2 && self.multiball_state == MultiballState::Ready => {
|
||||
7 if ball_number != 2
|
||||
&& matches!(
|
||||
self.multiball_state,
|
||||
MultiballState::Ready | MultiballState::Active
|
||||
) =>
|
||||
{
|
||||
self.secondary_ball = Some(Ball {
|
||||
position: vec2(17.0, 23.0),
|
||||
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
||||
@@ -1675,7 +1735,7 @@ impl Game {
|
||||
spin: Real48::ZERO,
|
||||
capture_age: 0,
|
||||
});
|
||||
self.multiball_state = MultiballState::Unavailable;
|
||||
self.multiball_state = MultiballState::Active;
|
||||
self.score_mode = ScoreMode::Multiball;
|
||||
}
|
||||
_ => {}
|
||||
@@ -1778,6 +1838,7 @@ impl Game {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.wheel_reset_gate = WheelResetGate::Enabled;
|
||||
let damping = Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]);
|
||||
velocity.x = Real48::from_i32(velocity.x).multiply(damping).round_i32();
|
||||
let vertical_factor = Real48::from_i32(1).subtract(
|
||||
@@ -1942,6 +2003,21 @@ impl Game {
|
||||
events.push(Event::Drain);
|
||||
return;
|
||||
}
|
||||
if self.score_mode == ScoreMode::Normal
|
||||
&& matches!(self.multiball_state, MultiballState::Ready | MultiballState::Active)
|
||||
{
|
||||
self.ball = Ball {
|
||||
position: vec2(17.0, 23.0),
|
||||
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
||||
in_launcher: false,
|
||||
spin: Real48::ZERO,
|
||||
capture_age: 0,
|
||||
};
|
||||
self.multiball_state = MultiballState::Unavailable;
|
||||
self.wheel_reset_gate = WheelResetGate::Suppressed;
|
||||
return;
|
||||
}
|
||||
self.wheel_reset_gate = WheelResetGate::Enabled;
|
||||
let player = &mut self.players[self.current_player];
|
||||
if player.extra_balls > 0 {
|
||||
player.extra_balls -= 1;
|
||||
@@ -2528,6 +2604,7 @@ mod tests {
|
||||
let spawned = game
|
||||
.secondary_ball
|
||||
.expect("effect seven should spawn a second ball");
|
||||
assert_eq!(game.multiball_state, MultiballState::Active);
|
||||
assert_eq!(spawned.position, vec2(17.0, 23.0));
|
||||
assert_eq!(
|
||||
MilliVec::from_velocity_per_second(spawned.velocity),
|
||||
@@ -2545,6 +2622,44 @@ mod tests {
|
||||
assert!(game.secondary_ball.is_none());
|
||||
assert_eq!(game.player().balls, balls_before);
|
||||
assert_eq!(game.ball.position, moving_ball.position);
|
||||
|
||||
events.clear();
|
||||
game.drain(&mut events);
|
||||
assert!(events.is_empty());
|
||||
assert_eq!(game.player().balls, balls_before);
|
||||
assert_eq!(game.ball.position, vec2(17.0, 23.0));
|
||||
assert_eq!(
|
||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
||||
MilliVec { x: 0, y: 3_040 }
|
||||
);
|
||||
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
||||
assert_eq!(game.wheel_reset_gate, WheelResetGate::Suppressed);
|
||||
|
||||
game.fixed_update(&mut events);
|
||||
assert!(!game.ball.in_launcher);
|
||||
assert!(game.ball.position.y > 23.0);
|
||||
assert_eq!(game.wheel_reset_gate, WheelResetGate::Suppressed);
|
||||
|
||||
game.ball.position = vec2(17.0, 31.0);
|
||||
game.ball.velocity = Vec2::ZERO;
|
||||
game.fixed_update(&mut events);
|
||||
assert_eq!(game.wheel_reset_gate, WheelResetGate::Enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn armed_effect_seven_also_special_respawns_before_multiball_starts() {
|
||||
let mut game = Game::new(1);
|
||||
game.multiball_state = MultiballState::Ready;
|
||||
let balls = game.player().balls;
|
||||
let mut events = Vec::new();
|
||||
|
||||
game.drain(&mut events);
|
||||
|
||||
assert!(events.is_empty());
|
||||
assert_eq!(game.player().balls, balls);
|
||||
assert_eq!(game.ball.position, vec2(17.0, 23.0));
|
||||
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
||||
assert_eq!(game.wheel_reset_gate, WheelResetGate::Suppressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2753,7 +2868,6 @@ mod tests {
|
||||
game.trigger_flags[149] = true;
|
||||
game.object_active[90] = false;
|
||||
game.target_effect = 3;
|
||||
game.multiball_state = MultiballState::Ready;
|
||||
|
||||
game.drain(&mut Vec::new());
|
||||
assert_eq!(game.current_player, 1);
|
||||
@@ -2775,7 +2889,7 @@ mod tests {
|
||||
assert!(!game.trigger_flags[149]);
|
||||
assert!(!game.object_active[90]);
|
||||
assert_eq!(game.target_effect, 3);
|
||||
assert_eq!(game.multiball_state, MultiballState::Ready);
|
||||
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user