fix(game): restore the six-way effect target

Implement the flagged object-84 selector and initially inactive type-4 object
149. Choose a different effect from 1-6, render the enabled target from its
original active-table rectangle, award its static 500 points, then apply the
recovered bonus or cashout action and disable it until reselected.

This replaces the final unimplemented help-screen target effect with its
original dispatcher values.

Test Plan:
- `cargo test --all-targets` -- 49 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- live object-84 probe activated record 149 and selected effect 3
- effect selection/consumption test -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:50:50 +02:00
parent 27749cb5f8
commit b786a75544
5 changed files with 146 additions and 4 deletions
+14
View File
@@ -429,6 +429,20 @@ impl App {
);
}
}
if game.effect_target_active() {
let region = Rect::new(60.0, 57.0, 18.0, 17.0);
draw_texture_ex(
&self.assets.active_table,
region.x,
region.y,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(region.w, region.h)),
source: Some(region),
..Default::default()
},
);
}
if game.ball.in_launcher {
draw_texture_ex(
&self.assets.plunger,
+123 -3
View File
@@ -4,7 +4,10 @@ use crate::{
CollisionResponse, GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec,
STEP_SECONDS, circle_collision_response, line_collision_response, path_intersects_circle,
},
table::{BUMPERS, LOCK_HOLES, PASSIVE_CIRCLES, TARGET_SENSORS, WALLS, WHEEL_RESET_SENSOR},
table::{
BUMPERS, EFFECT_SENSOR, LOCK_HOLES, PASSIVE_CIRCLES, TARGET_SENSORS, WALLS,
WHEEL_RESET_SENSOR,
},
};
use macroquad::prelude::{Rect, Vec2, vec2};
@@ -221,6 +224,7 @@ pub struct Game {
pending_flipper_edges: [i8; 2],
trigger_contacts: [bool; 176],
object_active: [bool; 176],
target_effect: u8,
}
impl Game {
@@ -258,6 +262,7 @@ impl Game {
pending_flipper_edges: [0; 2],
trigger_contacts: [false; 176],
object_active: initial_object_activity(),
target_effect: 0,
}
}
@@ -568,6 +573,16 @@ impl Game {
if wall.flags & 0x0400 != 0 {
self.wheel_animation = 0.65;
}
if wall.flags & 0x0004 != 0 {
let previous = self.target_effect;
loop {
self.target_effect = u8::try_from(self.next_random_value() % 6 + 1).unwrap_or(1);
if self.target_effect != previous {
break;
}
}
self.object_active[usize::from(EFFECT_SENSOR.id)] = true;
}
}
fn check_media(&mut self, events: &mut Vec<Event>) {
@@ -604,6 +619,22 @@ impl Game {
}
let current_position = MilliVec::from_position(self.ball.position);
if self.check_lock_holes(old_position, current_position, movement_velocity, events)
|| self.check_wheel_reset(old_position, current_position, movement_velocity, events)
{
return;
}
self.check_effect_sensor(old_position, current_position, movement_velocity, events);
self.check_target_sensors(old_position, current_position, movement_velocity, events);
}
fn check_lock_holes(
&mut self,
old_position: MilliVec,
current_position: MilliVec,
movement_velocity: MilliVec,
events: &mut Vec<Event>,
) -> bool {
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
let touched = path_intersects_circle(
old_position,
@@ -634,9 +665,18 @@ impl Game {
}
self.reset_ball_to_launcher();
events.push(Event::Lock);
return;
return true;
}
false
}
fn check_wheel_reset(
&mut self,
old_position: MilliVec,
current_position: MilliVec,
movement_velocity: MilliVec,
events: &mut Vec<Event>,
) -> bool {
let reset_touched = path_intersects_circle(
old_position,
movement_velocity,
@@ -656,9 +696,61 @@ impl Game {
self.wheel_animation = 0.65;
self.reset_ball_to_launcher();
events.push(Event::Wheel);
return;
return true;
}
false
}
fn check_effect_sensor(
&mut self,
old_position: MilliVec,
current_position: MilliVec,
movement_velocity: MilliVec,
events: &mut Vec<Event>,
) {
let effect_index = usize::from(EFFECT_SENSOR.id);
if self.object_active[effect_index] {
let touched = path_intersects_circle(
old_position,
movement_velocity,
EFFECT_SENSOR.center,
EFFECT_SENSOR.radius,
);
let entered = touched && !self.trigger_contacts[effect_index];
self.trigger_contacts[effect_index] = path_intersects_circle(
current_position,
MilliVec::default(),
EFFECT_SENSOR.center,
EFFECT_SENSOR.radius,
);
if entered && !self.tilted {
self.add_score(EFFECT_SENSOR.score);
match self.target_effect {
1 => self.bonus = self.bonus.saturating_add(10_000),
2 => self.bonus = self.bonus.saturating_add(20_000),
3 => self.bonus = self.bonus.saturating_add(50_000),
4 => self.bonus = self.bonus.saturating_add(100_000),
5 => self.bonus = self.bonus.saturating_add(200_000),
6 => {
self.add_score(self.bonus);
self.bonus = 0;
}
_ => {}
}
self.target_effect = 0;
self.object_active[effect_index] = false;
events.push(Event::Target);
}
}
}
fn check_target_sensors(
&mut self,
old_position: MilliVec,
current_position: MilliVec,
movement_velocity: MilliVec,
events: &mut Vec<Event>,
) {
for sensor in TARGET_SENSORS {
let touched = path_intersects_circle(
old_position,
@@ -729,6 +821,10 @@ impl Game {
self.object_active[object_id]
}
pub fn effect_target_active(&self) -> bool {
self.object_active[usize::from(EFFECT_SENSOR.id)]
}
fn next_claw_terminal_frame(&mut self) -> u8 {
let value = self.next_random_value();
CLAW_TERMINAL_FRAMES[value as usize % CLAW_TERMINAL_FRAMES.len()]
@@ -875,6 +971,7 @@ impl Game {
self.claw = Claw::default();
self.trigger_contacts.fill(false);
self.object_active = initial_object_activity();
self.target_effect = 0;
self.nudge_shake = 0.0;
self.launcher_charge = 0.0;
self.launcher_was_down = false;
@@ -1133,6 +1230,29 @@ mod tests {
assert!(game.object_active[109..=120].iter().all(|active| *active));
}
#[test]
fn effect_selector_activates_and_consumes_record_149() {
let mut game = Game::new_with_seed(1, 7);
let mut events = Vec::new();
game.apply_wall_rule(84, &mut events);
assert!((1..=6).contains(&game.target_effect));
assert!(game.effect_target_active());
game.target_effect = 3;
game.ball.in_launcher = false;
game.ball.position = EFFECT_SENSOR.center;
game.check_sensor_objects(
MilliVec::from_position(EFFECT_SENSOR.center),
MilliVec::default(),
&mut events,
);
assert_eq!(game.bonus, 50_000);
assert_eq!(game.player().score, 2_000);
assert_eq!(game.target_effect, 0);
assert!(!game.effect_target_active());
}
#[test]
fn center_drain_advances_to_a_fresh_ball() {
let mut game = Game::new(1);
+2
View File
@@ -333,6 +333,8 @@ pub const LOCK_HOLES: [MechanismSensor; 5] = [
pub const WHEEL_RESET_SENSOR: MechanismSensor =
MechanismSensor::new(148, Vec2::new(17.0, 23.0), 19.0);
pub const EFFECT_SENSOR: TargetSensor = TargetSensor::new(149, Vec2::new(68.0, 65.0), 16.0, 500);
#[cfg(test)]
mod tests {
use super::*;