diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index 7cef875..e966d2f 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to ### Fixed +- Load and draw all DAT400-407 player/effect sprites at `(55,79)`, preserving + the original saved 14x14 wheel overlap at `(84,77)`; selected bonuses, + collect-bonus, and release-ball states are now visible. - Restore display blits from `1008:0996`/`1008:0b9e`: 130x40 score fields at X=478, eight places ending at X=606, exact 139x31 configured-player rows, 11x11 active/status markers, 5x5 remaining-ball markers for every player, diff --git a/tdkpin-rs/README.md b/tdkpin-rs/README.md index 8b9de96..2220638 100644 --- a/tdkpin-rs/README.md +++ b/tdkpin-rs/README.md @@ -45,7 +45,7 @@ cargo run -- --simulate claw-6 --at 0.15 \ Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The available scenarios are `loading`, `attract`, `highscores`, `name-entry`, -`autoplay`, `launcher`, `flippers`, `panel`, `targets`, `claw-1`, `claw-6`, +`effect-7`, `autoplay`, `launcher`, `flippers`, `panel`, `targets`, `claw-1`, `claw-6`, `claw-7`, and `claw-18`; `--seed N` fixes random choices. `autoplay` charges each ball and operates the flippers from live ball position for long end-to-end validation runs. diff --git a/tdkpin-rs/RECONSTRUCTION.md b/tdkpin-rs/RECONSTRUCTION.md index 1c44252..9023b5e 100644 --- a/tdkpin-rs/RECONSTRUCTION.md +++ b/tdkpin-rs/RECONSTRUCTION.md @@ -19,7 +19,7 @@ implementation. | Subsystem | Rust status | Evidence and boundary | | --- | --- | --- | -| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, help, ball, wheel, robot, plunger, media, and item frames. Loading presents DAT995 and advances the original DAT994 strip at raw `1000:531e` destination `(202,328)`, height 13, and clipped width `(stage-1)*7` through stages 2-35. | +| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, help, ball, wheel, robot, plunger, media, and item frames. DAT400-407 expose the current per-player bonus/collect/release effect at `(55,79)` while preserving the saved 14x14 wheel overlap. Loading presents DAT995 and advances the original DAT994 strip at raw `1000:531e` destination `(202,328)`, height 13, and clipped width `(stage-1)*7` through stages 2-35. | | Audio | Exact samples and recovered dispatch | All 16 mono PCM WAV resources are embedded unchanged. Playback is emitted at the reconstructed call sites, including multi-sound bank completions and nudge-then-tilt ordering. WAV 2022 is loaded by the original generic resource loop but has no playback call and is therefore never played by Rust. | | Help and languages | Exact | Original resource images 1001-1005 are displayed 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. Every static record retains its `+0x49` layer mask; the scanner selects layer 1 below the old Y value 250,000 and layer 2 at or above it, while mask 3 records remain shared. Before detection, the raw `1000:9b69` predicted position must lie in the record bounds derived with the registered five-pixel ball margin. 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. Detection retains unresolved normal/material candidates so later scan-time motion changes can participate in the selected response. Raw stack slot `SS:...d8a2` makes the first detected record ID win; later nearer candidates are stored but never applied, and both C/Rust have two-candidate regression coverage for this quirk. 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. | diff --git a/tdkpin-rs/src/app.rs b/tdkpin-rs/src/app.rs index 8aa3162..754694e 100644 --- a/tdkpin-rs/src/app.rs +++ b/tdkpin-rs/src/app.rs @@ -688,6 +688,14 @@ impl App { return; }; + draw_texture( + &self.assets.player_effects[usize::from(game.player_effect())], + 55.0, + 79.0, + WHITE, + ); + self.draw_inactive_table_region(84, 77, 14, 14); + if let Some(frame) = game.panel_frame { self.draw_panel_animation(frame); } else if let Some(state) = game diff --git a/tdkpin-rs/src/assets.rs b/tdkpin-rs/src/assets.rs index aef9ff1..b998208 100644 --- a/tdkpin-rs/src/assets.rs +++ b/tdkpin-rs/src/assets.rs @@ -10,6 +10,7 @@ pub struct Assets { pub loading_progress: Texture2D, pub highscore_background: Texture2D, pub help: [Texture2D; 5], + pub player_effects: [Texture2D; 8], pub media: [Texture2D; 4], pub diamond: [Texture2D; 9], pub wheel: Texture2D, @@ -40,6 +41,16 @@ impl Assets { texture(include_bytes!("../assets/original/images/dat_01004.png")), texture(include_bytes!("../assets/original/images/dat_01005.png")), ]; + let player_effects = [ + texture(include_bytes!("../assets/original/images/dat_00400.png")), + texture(include_bytes!("../assets/original/images/dat_00401.png")), + texture(include_bytes!("../assets/original/images/dat_00402.png")), + texture(include_bytes!("../assets/original/images/dat_00403.png")), + texture(include_bytes!("../assets/original/images/dat_00404.png")), + texture(include_bytes!("../assets/original/images/dat_00405.png")), + texture(include_bytes!("../assets/original/images/dat_00406.png")), + texture(include_bytes!("../assets/original/images/dat_00407.png")), + ]; let media = [ texture(include_bytes!("../assets/original/images/dat_00701.png")), texture(include_bytes!("../assets/original/images/dat_00702.png")), @@ -153,6 +164,7 @@ impl Assets { loading_progress, highscore_background, help, + player_effects, media, diamond, wheel, diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 704a94e..47e0358 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -406,6 +406,11 @@ impl Game { events } + pub(crate) fn begin_effect_scenario(&mut self, effect: u8) { + self.target_effect = effect.min(7); + self.object_active[usize::from(EFFECT_SENSOR.id)] = self.target_effect != 0; + } + fn fire_launcher(&mut self) { self.launcher_velocity_milli = self .launcher_velocity_milli @@ -1801,6 +1806,10 @@ impl Game { self.object_active[usize::from(EFFECT_SENSOR.id)] } + pub fn player_effect(&self) -> u8 { + self.target_effect.min(7) + } + fn next_claw_terminal_frame(&mut self) -> u8 { CLAW_TERMINAL_FRAMES[usize::from(self.random.below(4))] } @@ -2450,6 +2459,7 @@ mod tests { assert!((1..=6).contains(&game.target_effect)); assert!(game.effect_target_active()); game.target_effect = 3; + assert_eq!(game.player_effect(), 3); game.ball.in_launcher = false; game.ball.position = EFFECT_SENSOR.center; game.check_sensor_objects( @@ -2462,6 +2472,7 @@ mod tests { assert_eq!(game.secondary_score_display(), 50_000); assert_eq!(game.player().score, 2_000); assert_eq!(game.target_effect, 0); + assert_eq!(game.player_effect(), 0); assert!(!game.effect_target_active()); } diff --git a/tdkpin-rs/src/simulation.rs b/tdkpin-rs/src/simulation.rs index 2041a6b..a747cf6 100644 --- a/tdkpin-rs/src/simulation.rs +++ b/tdkpin-rs/src/simulation.rs @@ -17,6 +17,7 @@ pub enum Scenario { Attract, HighScores, NameEntry, + Effect7, Autoplay, Launcher, Flippers, @@ -35,6 +36,7 @@ impl Scenario { Self::Attract => "attract", Self::HighScores => "highscores", Self::NameEntry => "name-entry", + Self::Effect7 => "effect-7", Self::Autoplay => "autoplay", Self::Launcher => "launcher", Self::Flippers => "flippers", @@ -57,6 +59,7 @@ impl Scenario { | Self::Attract | Self::HighScores | Self::NameEntry + | Self::Effect7 | Self::Autoplay | Self::Launcher | Self::Flippers @@ -75,6 +78,7 @@ impl FromStr for Scenario { "attract" => Ok(Self::Attract), "highscores" => Ok(Self::HighScores), "name-entry" => Ok(Self::NameEntry), + "effect-7" => Ok(Self::Effect7), "autoplay" => Ok(Self::Autoplay), "launcher" => Ok(Self::Launcher), "flippers" => Ok(Self::Flippers), @@ -85,7 +89,7 @@ impl FromStr for Scenario { "claw-7" => Ok(Self::Claw7), "claw-18" => Ok(Self::Claw18), _ => Err(format!( - "unknown scenario {value:?}; expected loading, attract, highscores, name-entry, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18" + "unknown scenario {value:?}; expected loading, attract, highscores, name-entry, effect-7, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18" )), } } @@ -169,7 +173,7 @@ fn seconds_to_step(value: &str) -> Result { } 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: loading, attract, highscores, name-entry, autoplay, launcher, flippers, panel, targets, 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: loading, attract, highscores, name-entry, effect-7, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18" } #[derive(Debug, Serialize, PartialEq)] @@ -229,6 +233,9 @@ impl Simulation { game.target_rotation_state = Some(0); game.wheel_holes = [true, false, true, false, false]; } + if scenario == Scenario::Effect7 { + game.begin_effect_scenario(7); + } let initial_events = scenario .claw_terminal() .map_or_else(Vec::new, |terminal| game.begin_claw_scenario(terminal)); @@ -364,6 +371,7 @@ fn controls_for(scenario: Scenario, step: u64) -> Controls { | Scenario::Attract | Scenario::HighScores | Scenario::NameEntry + | Scenario::Effect7 | Scenario::Panel | Scenario::Targets | Scenario::Claw1