fix(game): restore six-state target rotation
Replace the free-running 68x60 wheel animation with the original six-callback 91x90 DAT600 mechanism. Start WAVE 2011 at state one, advance once per selected detail timer, and rotate the five per-player lock contact/item values exactly at state six. Decode the 17x17 DAT600 target image/mask pair and render all six recovered position sets over the correct atlas or overlay-B base. Preserve filled targets in their resting state after rotation and add a deterministic `targets` scenario for trace and framebuffer validation. Test Plan: - `cargo test --all-targets` -- passed, 74 tests - `cargo clippy --all-targets -- -D warnings` -- passed - `rumdl check CHANGELOG.md README.md RECONSTRUCTION.md` -- passed - rendered all six target-rotation states -- visually verified - `git diff --cached --check` -- passed
This commit is contained in:
@@ -10,6 +10,9 @@ and this project adheres to
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Replace the free-running wheel animation with the original six-callback
|
||||||
|
91x90 target rotation, WAVE 2011 start, exact DAT600 frames/target positions,
|
||||||
|
and state-6 rotation of the five per-player contact/item values.
|
||||||
- Restore the complete 281-callback DAT600 panel sequence after the fifth lock
|
- Restore the complete 281-callback DAT600 panel sequence after the fifth lock
|
||||||
hole, including physics suspension, contact/item cleanup, all nine image
|
hole, including physics suspension, contact/item cleanup, all nine image
|
||||||
phases, and the original WAVE 2013/2012/stop boundaries.
|
phases, and the original WAVE 2013/2012/stop boundaries.
|
||||||
|
|||||||
+4
-4
@@ -44,10 +44,10 @@ cargo run -- --simulate claw-6 --at 0.15 \
|
|||||||
```
|
```
|
||||||
|
|
||||||
Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The
|
Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The
|
||||||
available scenarios are `autoplay`, `launcher`, `flippers`, `panel`, `claw-1`,
|
available scenarios are `autoplay`, `launcher`, `flippers`, `panel`, `targets`,
|
||||||
`claw-6`, `claw-7`, and `claw-18`; `--seed N` fixes random choices. `autoplay` charges
|
`claw-1`, `claw-6`, `claw-7`, and `claw-18`; `--seed N` fixes random choices.
|
||||||
each ball and operates the flippers from live ball position for long end-to-end
|
`autoplay` charges each ball and operates the flippers from live ball position
|
||||||
validation runs.
|
for long end-to-end validation runs.
|
||||||
|
|
||||||
## Original and modern controls
|
## Original and modern controls
|
||||||
|
|
||||||
|
|||||||
@@ -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. Moving-flipper contact ports `1000:7ed9` rather than fitting live samples: delta-specific pivots/edges, integer cross gates, radial/penetration calculations, response-record gain, and position/velocity publication are tested against all four C harness directions and the raised release geometry. Object 174 is overwritten with the live first ball and Rust handles its ball-to-ball role directly. |
|
| 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. Moving-flipper contact ports `1000:7ed9` rather than fitting live samples: delta-specific pivots/edges, integer cross gates, radial/penetration calculations, response-record gain, and position/velocity publication are tested against all four C harness directions and the raised release geometry. Object 174 is overwritten with the live first ball and Rust handles its ball-to-ball role directly. |
|
||||||
| Ball launcher and nudge input | Recovered | The initial 32-bit fixed-point coordinates decode to `(325, 413)` in the right shooter lane. Each Down keydown subtracts `15*50 = 750` millipixels, release subtracts another `15*100 = 1500`, and the result follows the recovered randomized `-3800` lower and `-2280` weak upper clamp branches. The ten decoration frames use the same strict 750-millipixel thresholds. Left Shift and keypad 3 apply their directional `(50-Random(20))*15` impulses; Space uses the recovered Real48 horizontal factor and `(60-Random(20))*15` vertical impulse. Each nudge adds 25 to the wrapping 16-bit tilt counter, compares it with `30+Random(10)`, and the detail timer decrements a nonzero counter once per callback. |
|
| Ball launcher and nudge input | Recovered | The initial 32-bit fixed-point coordinates decode to `(325, 413)` in the right shooter lane. Each Down keydown subtracts `15*50 = 750` millipixels, release subtracts another `15*100 = 1500`, and the result follows the recovered randomized `-3800` lower and `-2280` weak upper clamp branches. The ten decoration frames use the same strict 750-millipixel thresholds. Left Shift and keypad 3 apply their directional `(50-Random(20))*15` impulses; Space uses the recovered Real48 horizontal factor and `(60-Random(20))*15` vertical impulse. Each nudge adds 25 to the wrapping 16-bit tilt counter, compares it with `30+Random(10)`, and the detail timer decrements a nonzero counter once per callback. |
|
||||||
| Physics arithmetic | Recovered Real48 core | Production movement uses the original 10 ms millipixel substep, `+15` vertical acceleration, Real48 `3800/speed` clamp, type-2 distance/cross gates, type-1 midpoint normal, surface-distance candidate ordering, persistent Real48 spin, and the common impulse response. Dynamic records 174/175 transfer normal impulse to the other ball before applying `normal_velocity-1000` to the moving ball. The original Borland seed update, high-word `Random(n)`, and normalized Real48 random register result drive launcher variation, effects, claw terminals, magnetic fields, and trigger response. Type-4 triggers retain a separate transient entry flag; type-3 captures retain per-player 16-bit contact words, deep-inside pull/hold progression to age 300, first-contact sound, and `99`/`2` completion sentinels. Zero-spin C harnesses and retained-spin Wine probes are tested separately rather than conflated. |
|
| Physics arithmetic | Recovered Real48 core | Production movement uses the original 10 ms millipixel substep, `+15` vertical acceleration, Real48 `3800/speed` clamp, type-2 distance/cross gates, type-1 midpoint normal, surface-distance candidate ordering, persistent Real48 spin, and the common impulse response. Dynamic records 174/175 transfer normal impulse to the other ball before applying `normal_velocity-1000` to the moving ball. The original Borland seed update, high-word `Random(n)`, and normalized Real48 random register result drive launcher variation, effects, claw terminals, magnetic fields, and trigger response. Type-4 triggers retain a separate transient entry flag; type-3 captures retain per-player 16-bit contact words, deep-inside pull/hold progression to age 300, first-contact sound, and `99`/`2` completion sentinels. Zero-spin C harnesses and retained-spin Wine probes are tested separately rather than conflated. |
|
||||||
| Rules | Recovered gameplay paths | Player count, controls, the five three-line bumper-value groups, four three-line TDK-diamond groups, five doubling-value lock holes, wheel-reset target, seven-way effect selector/consumer including multiball, permanent double scoring, and four exact media/extra-ball thresholds follow original help/code paths, globals, and object flags. The ninth diamond pays the original 24,464 completion value; the following completed bank enables double scoring, and later completions add 100,000 to the per-player secondary score. Completing all five lock holes suspends physics for the full 281-callback DAT600 panel animation, clears contact/item state at the recovered boundaries, and uses the exact WAVE 2013/2012/stop sequence. Turn changes mirror the original save/load of all 175 collision record states: wheel/top targets, active/contact slots, selected effect, and multiball readiness remain attached to their player. 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 have live differential coverage for all four random terminals. |
|
| Rules | Recovered gameplay paths | Player count, controls, the five three-line bumper-value groups, four three-line TDK-diamond groups, five doubling-value lock holes, wheel-reset target, seven-way effect selector/consumer including multiball, permanent double scoring, and four exact media/extra-ball thresholds follow original help/code paths, globals, and object flags. The ninth diamond pays the original 24,464 completion value; the following completed bank enables double scoring, and later completions add 100,000 to the per-player secondary score. Record 121 runs the six-callback DAT600 target rotation with exact 91x90 frames/target points and rotates the five contact/item values at state 6. Completing all five lock holes suspends physics for the full 281-callback panel animation, clears contact/item state at the recovered boundaries, and uses the exact WAVE 2013/2012/stop sequence. Turn changes mirror the original save/load of all 175 collision record states: wheel/top targets, active/contact slots, selected effect, and multiball readiness remain attached to their player. 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 have live differential coverage for all four random terminals. |
|
||||||
| Numeric scoring | Recovered gameplay values | Static scores come from the initialized 175-object ledger. Dynamic bumper progression, target-bank completion, diamond awards, 10k-160k lock bonuses, 310k transfer, six effect values, multiball mode, and all four media thresholds are transcribed from `1000:b476`, `1000:c4e1`, `1000:bc36`, and live state probes. Lock and effect awards share the original per-player secondary score and display multiplier; the fifth hole transfers and clears it, increments the multiplier, and grants the recovered ball award. Score mutation uses the original 32-bit wrapping behavior, and each add operation can advance at most one media threshold. |
|
| Numeric scoring | Recovered gameplay values | Static scores come from the initialized 175-object ledger. Dynamic bumper progression, target-bank completion, diamond awards, 10k-160k lock bonuses, 310k transfer, six effect values, multiball mode, and all four media thresholds are transcribed from `1000:b476`, `1000:c4e1`, `1000:bc36`, and live state probes. Lock and effect awards share the original per-player secondary score and display multiplier; the fifth hole transfers and clears it, increments the multiplier, and grants the recovered ball award. Score mutation uses the original 32-bit wrapping behavior, and each add operation can advance at most one media threshold. |
|
||||||
| High scores | Recovered visible flow; portable storage | The original 276-byte table is decoded as ten `IWIK`-XOR-obfuscated little-endian scores plus ten 22-byte names. Each player is checked immediately when their own last ball is lost; qualifying scores use the original signed-high/unsigned-low comparison and a `TDK Pinball Player`-prefilled name screen before the table is shown and play resumes. Persisted updates use portable JSON rather than rewriting the Win16 file. |
|
| High scores | Recovered visible flow; portable storage | The original 276-byte table is decoded as ten `IWIK`-XOR-obfuscated little-endian scores plus ten 22-byte names. Each player is checked immediately when their own last ball is lost; qualifying scores use the original signed-high/unsigned-low comparison and a `TDK Pinball Player`-prefilled name screen before the table is shown and play resumes. Persisted updates use portable JSON rather than rewriting the Win16 file. |
|
||||||
| 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. |
|
| 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. |
|
||||||
|
|||||||
+50
-20
@@ -11,6 +11,14 @@ use std::path::Path;
|
|||||||
|
|
||||||
const WIDTH: f32 = 640.0;
|
const WIDTH: f32 = 640.0;
|
||||||
const HEIGHT: f32 = 460.0;
|
const HEIGHT: f32 = 460.0;
|
||||||
|
const TARGET_POSITIONS: [[(f32, f32); 5]; 6] = [
|
||||||
|
[(9.0, 41.0), (23.0, 11.0), (56.0, 14.0), (63.0, 47.0), (35.0, 63.0)],
|
||||||
|
[(11.0, 45.0), (18.0, 13.0), (52.0, 11.0), (63.0, 42.0), (39.0, 64.0)],
|
||||||
|
[(13.0, 50.0), (15.0, 17.0), (46.0, 9.0), (64.0, 37.0), (44.0, 62.0)],
|
||||||
|
[(17.0, 55.0), (11.0, 23.0), (39.0, 7.0), (64.0, 30.0), (50.0, 59.0)],
|
||||||
|
[(21.0, 60.0), (9.0, 28.0), (35.0, 7.0), (62.0, 25.0), (54.0, 56.0)],
|
||||||
|
[(8.0, 35.0), (26.0, 8.0), (59.0, 18.0), (59.0, 50.0), (27.0, 61.0)],
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum Screen {
|
enum Screen {
|
||||||
@@ -369,21 +377,11 @@ impl App {
|
|||||||
|
|
||||||
if let Some(frame) = game.panel_frame {
|
if let Some(frame) = game.panel_frame {
|
||||||
self.draw_panel_animation(frame);
|
self.draw_panel_animation(frame);
|
||||||
}
|
} else if let Some(state) = game
|
||||||
|
.target_rotation_state
|
||||||
if game.wheel_animation > 0.0 {
|
.or_else(|| game.wheel_holes.iter().any(|filled| *filled).then_some(6))
|
||||||
let frame = animation_frame(f64::from(0.65 - game.wheel_animation), 18.0, 5);
|
{
|
||||||
draw_texture_ex(
|
self.draw_target_rotation(game, state);
|
||||||
&self.assets.wheel,
|
|
||||||
80.0,
|
|
||||||
49.0,
|
|
||||||
WHITE,
|
|
||||||
DrawTextureParams {
|
|
||||||
dest_size: Some(vec2(68.0, 60.0)),
|
|
||||||
source: Some(Rect::new(frame as f32 * 68.0, 0.0, 68.0, 60.0)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
for (index, bumper) in BUMPERS.into_iter().enumerate() {
|
for (index, bumper) in BUMPERS.into_iter().enumerate() {
|
||||||
if game.bumper_flash[index] > 0.0 {
|
if game.bumper_flash[index] > 0.0 {
|
||||||
@@ -630,6 +628,43 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
fn draw_target_rotation(&self, game: &Game, state: u8) {
|
||||||
|
if state == 0 || state > 6 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if state < 6 {
|
||||||
|
draw_texture_region(
|
||||||
|
&self.assets.wheel,
|
||||||
|
79.0,
|
||||||
|
33.0,
|
||||||
|
91.0,
|
||||||
|
90.0,
|
||||||
|
f32::from(state - 1) * 91.0,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
draw_texture_region(
|
||||||
|
&self.assets.inactive_table,
|
||||||
|
79.0,
|
||||||
|
33.0,
|
||||||
|
91.0,
|
||||||
|
90.0,
|
||||||
|
79.0,
|
||||||
|
33.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (active, (x, y)) in game
|
||||||
|
.wheel_holes
|
||||||
|
.iter()
|
||||||
|
.zip(TARGET_POSITIONS[usize::from(state - 1)])
|
||||||
|
{
|
||||||
|
if *active {
|
||||||
|
draw_texture(&self.assets.panel_target, 79.0 + x, 33.0 + y, WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
fn draw_displays(&self, game: &Game) {
|
fn draw_displays(&self, game: &Game) {
|
||||||
for region in [
|
for region in [
|
||||||
@@ -898,11 +933,6 @@ fn draw_texture_region(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
||||||
fn animation_frame(elapsed: f64, frames_per_second: f64, frame_count: usize) -> usize {
|
|
||||||
((elapsed.max(0.0) * frames_per_second) as usize) % frame_count
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_centered(text: &str, baseline: f32, font_size: u16, color: Color) {
|
fn draw_centered(text: &str, baseline: f32, font_size: u16, color: Color) {
|
||||||
draw_centered_at(text, WIDTH * 0.5, baseline, font_size, color);
|
draw_centered_at(text, WIDTH * 0.5, baseline, font_size, color);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-6
@@ -15,6 +15,7 @@ pub struct Assets {
|
|||||||
pub panel_pair_narrow_b: Texture2D,
|
pub panel_pair_narrow_b: Texture2D,
|
||||||
pub panel_pair_wide_a: Texture2D,
|
pub panel_pair_wide_a: Texture2D,
|
||||||
pub panel_pair_wide_b: Texture2D,
|
pub panel_pair_wide_b: Texture2D,
|
||||||
|
pub panel_target: Texture2D,
|
||||||
pub robot: Texture2D,
|
pub robot: Texture2D,
|
||||||
pub plunger: Texture2D,
|
pub plunger: Texture2D,
|
||||||
pub ball: Texture2D,
|
pub ball: Texture2D,
|
||||||
@@ -54,10 +55,11 @@ impl Assets {
|
|||||||
];
|
];
|
||||||
let wheel_bytes = include_bytes!("../assets/original/images/dat_00600.png");
|
let wheel_bytes = include_bytes!("../assets/original/images/dat_00600.png");
|
||||||
let wheel = texture(wheel_bytes);
|
let wheel = texture(wheel_bytes);
|
||||||
let panel_pair_narrow_a = masked_atlas_pair(wheel_bytes, 0, 45, 45, 90);
|
let panel_pair_narrow_a = masked_atlas_pair(wheel_bytes, 0, 90, 45, 90, 45, 90);
|
||||||
let panel_pair_narrow_b = masked_atlas_pair(wheel_bytes, 90, 135, 45, 90);
|
let panel_pair_narrow_b = masked_atlas_pair(wheel_bytes, 90, 90, 135, 90, 45, 90);
|
||||||
let panel_pair_wide_a = masked_atlas_pair(wheel_bytes, 271, 362, 91, 90);
|
let panel_pair_wide_a = masked_atlas_pair(wheel_bytes, 271, 90, 362, 90, 91, 90);
|
||||||
let panel_pair_wide_b = masked_atlas_pair(wheel_bytes, 453, 362, 91, 90);
|
let panel_pair_wide_b = masked_atlas_pair(wheel_bytes, 453, 90, 362, 90, 91, 90);
|
||||||
|
let panel_target = masked_atlas_pair(wheel_bytes, 455, 0, 455, 17, 17, 17);
|
||||||
let robot = texture(include_bytes!("../assets/original/images/dat_00900.png"));
|
let robot = texture(include_bytes!("../assets/original/images/dat_00900.png"));
|
||||||
let plunger = texture(include_bytes!("../assets/original/images/dat_00901.png"));
|
let plunger = texture(include_bytes!("../assets/original/images/dat_00901.png"));
|
||||||
let ball = masked_texture(
|
let ball = masked_texture(
|
||||||
@@ -152,6 +154,7 @@ impl Assets {
|
|||||||
panel_pair_narrow_b,
|
panel_pair_narrow_b,
|
||||||
panel_pair_wide_a,
|
panel_pair_wide_a,
|
||||||
panel_pair_wide_b,
|
panel_pair_wide_b,
|
||||||
|
panel_target,
|
||||||
robot,
|
robot,
|
||||||
plunger,
|
plunger,
|
||||||
ball,
|
ball,
|
||||||
@@ -223,7 +226,9 @@ fn monochrome_texture(bytes: &[u8]) -> Texture2D {
|
|||||||
fn masked_atlas_pair(
|
fn masked_atlas_pair(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
image_x: usize,
|
image_x: usize,
|
||||||
|
image_y: usize,
|
||||||
mask_x: usize,
|
mask_x: usize,
|
||||||
|
mask_y: usize,
|
||||||
width: usize,
|
width: usize,
|
||||||
height: usize,
|
height: usize,
|
||||||
) -> Texture2D {
|
) -> Texture2D {
|
||||||
@@ -237,8 +242,8 @@ fn masked_atlas_pair(
|
|||||||
for y in 0..height {
|
for y in 0..height {
|
||||||
for x in 0..width {
|
for x in 0..width {
|
||||||
let output_index = (y * width + x) * 4;
|
let output_index = (y * width + x) * 4;
|
||||||
let image_index = ((y + 90) * atlas_width + image_x + x) * 4;
|
let image_index = ((y + image_y) * atlas_width + image_x + x) * 4;
|
||||||
let mask_index = ((y + 90) * atlas_width + mask_x + x) * 4;
|
let mask_index = ((y + mask_y) * atlas_width + mask_x + x) * 4;
|
||||||
output.bytes[output_index..output_index + 3]
|
output.bytes[output_index..output_index + 3]
|
||||||
.copy_from_slice(&atlas.bytes[image_index..image_index + 3]);
|
.copy_from_slice(&atlas.bytes[image_index..image_index + 3]);
|
||||||
output.bytes[output_index + 3] = u8::MAX - atlas.bytes[mask_index];
|
output.bytes[output_index + 3] = u8::MAX - atlas.bytes[mask_index];
|
||||||
|
|||||||
+52
-8
@@ -268,7 +268,7 @@ pub struct Game {
|
|||||||
flipper_inputs: Flippers,
|
flipper_inputs: Flippers,
|
||||||
pub claw: Claw,
|
pub claw: Claw,
|
||||||
pub bumper_flash: [f32; 3],
|
pub bumper_flash: [f32; 3],
|
||||||
pub wheel_animation: f32,
|
pub target_rotation_state: Option<u8>,
|
||||||
pub panel_frame: Option<u16>,
|
pub panel_frame: Option<u16>,
|
||||||
pub nudge_shake: f32,
|
pub nudge_shake: f32,
|
||||||
pub launcher_charge: f32,
|
pub launcher_charge: f32,
|
||||||
@@ -312,7 +312,7 @@ impl Game {
|
|||||||
flipper_inputs: Flippers::default(),
|
flipper_inputs: Flippers::default(),
|
||||||
claw: Claw::default(),
|
claw: Claw::default(),
|
||||||
bumper_flash: [0.0; 3],
|
bumper_flash: [0.0; 3],
|
||||||
wheel_animation: 0.0,
|
target_rotation_state: None,
|
||||||
panel_frame: None,
|
panel_frame: None,
|
||||||
nudge_shake: 0.0,
|
nudge_shake: 0.0,
|
||||||
launcher_charge: 0.0,
|
launcher_charge: 0.0,
|
||||||
@@ -456,6 +456,7 @@ impl Game {
|
|||||||
|
|
||||||
fn timer_tick(&mut self, elapsed: f32, substeps: u8, events: &mut Vec<Event>) {
|
fn timer_tick(&mut self, elapsed: f32, substeps: u8, events: &mut Vec<Event>) {
|
||||||
self.update_claw(elapsed, events);
|
self.update_claw(elapsed, events);
|
||||||
|
self.update_target_rotation(events);
|
||||||
let panel_active = self.update_panel_completion(events);
|
let panel_active = self.update_panel_completion(events);
|
||||||
if !self.claw.ball_suspended && !panel_active {
|
if !self.claw.ball_suspended && !panel_active {
|
||||||
for _ in 0..substeps {
|
for _ in 0..substeps {
|
||||||
@@ -493,6 +494,29 @@ impl Game {
|
|||||||
self.apply_flipper_kicks();
|
self.apply_flipper_kicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_target_rotation(&mut self, events: &mut Vec<Event>) {
|
||||||
|
let Some(state) = self.target_rotation_state else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if state >= 6 {
|
||||||
|
self.target_rotation_state = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let next = state + 1;
|
||||||
|
self.target_rotation_state = Some(next);
|
||||||
|
if next == 1 {
|
||||||
|
events.push(Event::Sound(2011));
|
||||||
|
}
|
||||||
|
if next == 6 {
|
||||||
|
self.wheel_holes.rotate_left(1);
|
||||||
|
let first = self.record_contacts[129];
|
||||||
|
for record_id in 129..133 {
|
||||||
|
self.record_contacts[record_id] = self.record_contacts[record_id + 1];
|
||||||
|
}
|
||||||
|
self.record_contacts[133] = first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn update_panel_completion(&mut self, events: &mut Vec<Event>) -> bool {
|
fn update_panel_completion(&mut self, events: &mut Vec<Event>) -> bool {
|
||||||
let Some(frame) = self.panel_frame else {
|
let Some(frame) = self.panel_frame else {
|
||||||
return false;
|
return false;
|
||||||
@@ -591,7 +615,6 @@ impl Game {
|
|||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) {
|
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) {
|
||||||
self.bumper_cooldown = (self.bumper_cooldown - dt).max(0.0);
|
self.bumper_cooldown = (self.bumper_cooldown - dt).max(0.0);
|
||||||
self.wheel_animation = (self.wheel_animation - dt).max(0.0);
|
|
||||||
self.nudge_shake = (self.nudge_shake - dt).max(0.0);
|
self.nudge_shake = (self.nudge_shake - dt).max(0.0);
|
||||||
for flash in &mut self.bumper_flash {
|
for flash in &mut self.bumper_flash {
|
||||||
*flash = (*flash - dt).max(0.0);
|
*flash = (*flash - dt).max(0.0);
|
||||||
@@ -898,9 +921,11 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if wall.flags & 0x0400 != 0 {
|
if wall.flags & 0x0400 != 0
|
||||||
self.wheel_animation = 0.65;
|
&& self.target_rotation_state.is_none()
|
||||||
events.push(Event::Sound(2011));
|
&& self.panel_frame.is_none()
|
||||||
|
{
|
||||||
|
self.target_rotation_state = Some(0);
|
||||||
}
|
}
|
||||||
if wall.flags & 0x0004 != 0 {
|
if wall.flags & 0x0004 != 0 {
|
||||||
events.push(Event::Sound(2012));
|
events.push(Event::Sound(2012));
|
||||||
@@ -1081,7 +1106,6 @@ impl Game {
|
|||||||
CaptureStep::Complete => {}
|
CaptureStep::Complete => {}
|
||||||
}
|
}
|
||||||
self.wheel_holes.fill(false);
|
self.wheel_holes.fill(false);
|
||||||
self.wheel_animation = 0.65;
|
|
||||||
self.multiball_state = MultiballState::Ready;
|
self.multiball_state = MultiballState::Ready;
|
||||||
self.reset_ball_to_launcher();
|
self.reset_ball_to_launcher();
|
||||||
events.push(Event::Wheel);
|
events.push(Event::Wheel);
|
||||||
@@ -1384,7 +1408,7 @@ impl Game {
|
|||||||
self.tilt_counter = 0;
|
self.tilt_counter = 0;
|
||||||
self.tilt_counter_accumulator = 0.0;
|
self.tilt_counter_accumulator = 0.0;
|
||||||
self.bumper_flash.fill(0.0);
|
self.bumper_flash.fill(0.0);
|
||||||
self.wheel_animation = 0.0;
|
self.target_rotation_state = None;
|
||||||
self.panel_frame = None;
|
self.panel_frame = None;
|
||||||
self.claw = Claw::default();
|
self.claw = Claw::default();
|
||||||
self.flipper_inputs = Flippers::default();
|
self.flipper_inputs = Flippers::default();
|
||||||
@@ -2475,6 +2499,26 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn target_rotation_uses_six_callbacks_and_rotates_player_state() {
|
||||||
|
let mut game = Game::new(1);
|
||||||
|
game.wheel_holes = [true, false, true, false, false];
|
||||||
|
game.record_contacts[129..=133].copy_from_slice(&[11, 22, 33, 44, 55]);
|
||||||
|
game.target_rotation_state = Some(0);
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
for expected in 1..=6 {
|
||||||
|
game.timer_tick(0.030, 0, &mut events);
|
||||||
|
assert_eq!(game.target_rotation_state, Some(expected));
|
||||||
|
}
|
||||||
|
assert_eq!(events, [Event::Sound(2011)]);
|
||||||
|
assert_eq!(game.wheel_holes, [false, true, false, false, true]);
|
||||||
|
assert_eq!(&game.record_contacts[129..=133], &[22, 33, 44, 55, 11]);
|
||||||
|
|
||||||
|
game.timer_tick(0.030, 0, &mut events);
|
||||||
|
assert_eq!(game.target_rotation_state, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn claw_release_table_decodes_the_original_thousandth_pixel_coordinates() {
|
fn claw_release_table_decodes_the_original_thousandth_pixel_coordinates() {
|
||||||
for (frame, expected_position, expected_velocity) in [
|
for (frame, expected_position, expected_velocity) in [
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub enum Scenario {
|
|||||||
Launcher,
|
Launcher,
|
||||||
Flippers,
|
Flippers,
|
||||||
Panel,
|
Panel,
|
||||||
|
Targets,
|
||||||
Claw1,
|
Claw1,
|
||||||
Claw6,
|
Claw6,
|
||||||
Claw7,
|
Claw7,
|
||||||
@@ -30,6 +31,7 @@ impl Scenario {
|
|||||||
Self::Launcher => "launcher",
|
Self::Launcher => "launcher",
|
||||||
Self::Flippers => "flippers",
|
Self::Flippers => "flippers",
|
||||||
Self::Panel => "panel",
|
Self::Panel => "panel",
|
||||||
|
Self::Targets => "targets",
|
||||||
Self::Claw1 => "claw-1",
|
Self::Claw1 => "claw-1",
|
||||||
Self::Claw6 => "claw-6",
|
Self::Claw6 => "claw-6",
|
||||||
Self::Claw7 => "claw-7",
|
Self::Claw7 => "claw-7",
|
||||||
@@ -43,7 +45,7 @@ impl Scenario {
|
|||||||
Self::Claw6 => Some(6),
|
Self::Claw6 => Some(6),
|
||||||
Self::Claw7 => Some(7),
|
Self::Claw7 => Some(7),
|
||||||
Self::Claw18 => Some(18),
|
Self::Claw18 => Some(18),
|
||||||
Self::Autoplay | Self::Launcher | Self::Flippers | Self::Panel => None,
|
Self::Autoplay | Self::Launcher | Self::Flippers | Self::Panel | Self::Targets => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,12 +59,13 @@ impl FromStr for Scenario {
|
|||||||
"launcher" => Ok(Self::Launcher),
|
"launcher" => Ok(Self::Launcher),
|
||||||
"flippers" => Ok(Self::Flippers),
|
"flippers" => Ok(Self::Flippers),
|
||||||
"panel" => Ok(Self::Panel),
|
"panel" => Ok(Self::Panel),
|
||||||
|
"targets" => Ok(Self::Targets),
|
||||||
"claw-1" => Ok(Self::Claw1),
|
"claw-1" => Ok(Self::Claw1),
|
||||||
"claw-6" => Ok(Self::Claw6),
|
"claw-6" => Ok(Self::Claw6),
|
||||||
"claw-7" => Ok(Self::Claw7),
|
"claw-7" => Ok(Self::Claw7),
|
||||||
"claw-18" => Ok(Self::Claw18),
|
"claw-18" => Ok(Self::Claw18),
|
||||||
_ => Err(format!(
|
_ => Err(format!(
|
||||||
"unknown scenario {value:?}; expected autoplay, launcher, flippers, panel, claw-1, claw-6, claw-7, or claw-18"
|
"unknown scenario {value:?}; expected autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18"
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +149,7 @@ fn seconds_to_step(value: &str) -> Result<u64, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const fn usage() -> &'static str {
|
pub const fn usage() -> &'static str {
|
||||||
"Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: autoplay, launcher, flippers, panel, claw-1, claw-6, claw-7, claw-18"
|
"Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, PartialEq)]
|
#[derive(Debug, Serialize, PartialEq)]
|
||||||
@@ -202,6 +205,10 @@ impl Simulation {
|
|||||||
game.panel_frame = Some(0);
|
game.panel_frame = Some(0);
|
||||||
game.wheel_holes.fill(true);
|
game.wheel_holes.fill(true);
|
||||||
}
|
}
|
||||||
|
if scenario == Scenario::Targets {
|
||||||
|
game.target_rotation_state = Some(0);
|
||||||
|
game.wheel_holes = [true, false, true, false, false];
|
||||||
|
}
|
||||||
let initial_events = scenario
|
let initial_events = scenario
|
||||||
.claw_terminal()
|
.claw_terminal()
|
||||||
.map_or_else(Vec::new, |terminal| game.begin_claw_scenario(terminal));
|
.map_or_else(Vec::new, |terminal| game.begin_claw_scenario(terminal));
|
||||||
@@ -326,7 +333,12 @@ fn controls_for(scenario: Scenario, step: u64) -> Controls {
|
|||||||
..Controls::default()
|
..Controls::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Scenario::Panel | Scenario::Claw1 | Scenario::Claw6 | Scenario::Claw7 | Scenario::Claw18 => {
|
Scenario::Panel
|
||||||
|
| Scenario::Targets
|
||||||
|
| Scenario::Claw1
|
||||||
|
| Scenario::Claw6
|
||||||
|
| Scenario::Claw7
|
||||||
|
| Scenario::Claw18 => {
|
||||||
Controls::default()
|
Controls::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,6 +473,20 @@ mod tests {
|
|||||||
assert_eq!(events.iter().filter(|event| **event == "Sound(2012)").count(), 2);
|
assert_eq!(events.iter().filter(|event| **event == "Sound(2012)").count(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn target_scenario_runs_all_six_rotation_states() {
|
||||||
|
let mut simulation = Simulation::new(Scenario::Targets, 1);
|
||||||
|
simulation.advance_to(4);
|
||||||
|
assert_eq!(simulation.game.target_rotation_state, Some(1));
|
||||||
|
assert_eq!(simulation.trace[4].events, ["Sound(2011)"]);
|
||||||
|
|
||||||
|
simulation.advance_to(22);
|
||||||
|
assert_eq!(simulation.game.target_rotation_state, Some(6));
|
||||||
|
assert_eq!(simulation.game.wheel_holes, [false, true, false, false, true]);
|
||||||
|
simulation.advance_to(26);
|
||||||
|
assert_eq!(simulation.game.target_rotation_state, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_claw_scenario_completes_its_seeded_capture() {
|
fn every_claw_scenario_completes_its_seeded_capture() {
|
||||||
for scenario in [
|
for scenario in [
|
||||||
|
|||||||
Reference in New Issue
Block a user