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:
2026-08-23 17:53:40 +02:00
parent 4b84460c93
commit 096d923c79
7 changed files with 151 additions and 43 deletions
+50 -20
View File
@@ -11,6 +11,14 @@ use std::path::Path;
const WIDTH: f32 = 640.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)]
enum Screen {
@@ -369,21 +377,11 @@ impl App {
if let Some(frame) = game.panel_frame {
self.draw_panel_animation(frame);
}
if game.wheel_animation > 0.0 {
let frame = animation_frame(f64::from(0.65 - game.wheel_animation), 18.0, 5);
draw_texture_ex(
&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()
},
);
} else if let Some(state) = game
.target_rotation_state
.or_else(|| game.wheel_holes.iter().any(|filled| *filled).then_some(6))
{
self.draw_target_rotation(game, state);
}
for (index, bumper) in BUMPERS.into_iter().enumerate() {
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)]
fn draw_displays(&self, game: &Game) {
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) {
draw_centered_at(text, WIDTH * 0.5, baseline, font_size, color);
}