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:
@@ -183,3 +183,9 @@ Top type-4 targets activate persistent field records rather than a shared timer:
|
||||
Entering active record 153 at `(15,350)` changed `(0,2015)` to `(0,-3039)`
|
||||
millipixels per substep in the live original. The field continued pulling upward
|
||||
inside its registered bounds and deactivated after the ball exited above them.
|
||||
|
||||
Collision with flagged line 84 selected effect 3 in a live probe, set the
|
||||
per-player effect word, and activated type-4 record 149. `1000:c4e1` maps effects
|
||||
1-5 to 10,000, 20,000, 50,000, 100,000, and 200,000 bonus; effect 6 transfers
|
||||
the accumulated bonus to score. Entering record 149 also awards its static 500,
|
||||
clears the selection, and disables the record until line 84 is hit again.
|
||||
|
||||
@@ -25,7 +25,7 @@ implementation.
|
||||
| Playfield collision layout | Recovered | All 109 active type-2 line objects and 40 static active type-1 circles are transcribed from the original 175-object registration table. The registration routine converts its sideways inputs with `screen = (y, x - 20)` and accumulates explicitly relative objects. Type-2 records retain every recovered Real48 normal/tangent response pair and registered one-sided orientation. Type-1 records retain their swept-circle radius, radial rebound, tangent coupling, and bumper kick. Each flipper uses its exact two line records plus moving tip circle in both positions. The upward edge transfer matches three live probes and the downstroke matches its live position-preserving `(3519,6302)` transfer; both are mirrored on the right. Object 174 is omitted because the original overwrites it with the live ball every frame. |
|
||||
| Ball launcher | Recovered | The initial 32-bit fixed-point coordinates decode to `(325, 413)` in the right shooter lane. The port reproduces the initial `-375` millipixel Down event, 650 ms repeat delay, 40 ms repeats, release impulse, and randomized clamp below the original `-3800` maximum. This replaces the former guessed 330-430 px/s shot. |
|
||||
| Physics arithmetic | Partly recovered | Production movement uses the original 10 ms millipixel substep, `+15` vertical acceleration, `3800` speed bound, point-path type-2 intersection, one-sided line response, swept type-1 circle response, and swept non-physical sensor contacts. It evaluates all records and applies the earliest contact along the substep. Live probes cover ordinary rails, ordinary circles, a kicked bumper, lock holes, and both flipper directions. Persistent physical-contact bookkeeping remains pending. |
|
||||
| Rules | Partly recovered | Player count, controls, the five three-line bumper-value groups, four three-line TDK-diamond groups, five doubling-value lock holes, wheel-reset target, permanent double scoring, KByte media progression, and media extra balls follow original help/code paths and object flags. Claw contact and all initially active type-4 targets use recovered records. The top three targets score 500 each and independently enable the left, center, or right magnetic field record; each field pulls the ball upward until it exits and then deactivates. Remaining special-target effects still require full rule restoration. The claw state machine and release table are readable and all four random terminals have live differential trajectory coverage. |
|
||||
| Rules | Partly recovered | Player count, controls, the five three-line bumper-value groups, four three-line TDK-diamond groups, five doubling-value lock holes, wheel-reset target, six-way effect selector/consumer, permanent double scoring, KByte media progression, and media extra balls follow original help/code paths and object flags. Claw contact and all initially active type-4 targets use recovered records. The top three targets score 500 each and independently enable the left, center, or right magnetic field record; each field pulls the ball upward until it exits and then deactivates. The claw state machine and release table are readable and all four random terminals have live differential trajectory coverage. Remaining rule uncertainty is concentrated in exact media thresholds and timer batching, not table collision routing. |
|
||||
| Numeric scoring | Partly inferred | Visible 2000-6000 target values and recovered registration values are preserved. Some bumper, bank-completion, robot, wheel, lock, and media thresholds are best-evidence reconstructions because the decompiler did not recover meaningful names or a clean rule table. |
|
||||
| High scores | Compatible import | The original 276-byte table is decoded as ten `IWIK`-XOR-obfuscated little-endian scores plus ten 22-byte names, sorted, then migrated to portable JSON. |
|
||||
| Configuration | Behaviorally compatible | Sound, language, and five detail levels are retained. Storage moves from a local Win16 INI file to the platform user-data directory. |
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user