fix(game): restore the 281-frame panel sequence

Replace the immediate five-hole cleanup with the original 281-callback panel
state machine. Suspend physics, clear contact/item state at the recovered
boundaries, and emit WAVE 2013, WAVE 2012, and stop events at frames
1/45/58/66/129/159/222/230/238 before resuming after cleanup.

Decode DAT600's four AND/OR mask-image pairs into transparent native textures
and render all nine phase formulas with the original source rectangles,
destination offsets, variable heights, and opaque wheel copies. Add a `panel`
simulation scenario for deterministic trace and framebuffer validation.

Test Plan:
- `cargo test --all-targets` -- passed, 72 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check CHANGELOG.md README.md RECONSTRUCTION.md` -- passed
- rendered panel frames 1, 66, 159, 238, and completion -- visually verified
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 17:49:04 +02:00
parent 6337291461
commit 4b84460c93
7 changed files with 303 additions and 11 deletions
+50 -2
View File
@@ -1,5 +1,5 @@
use macroquad::{
audio::{PlaySoundParams, Sound, load_sound_from_bytes, play_sound},
audio::{PlaySoundParams, Sound, load_sound_from_bytes, play_sound, stop_sound},
prelude::{FilterMode, Image, Texture2D},
};
@@ -11,6 +11,10 @@ pub struct Assets {
pub media: [Texture2D; 4],
pub diamond: [Texture2D; 9],
pub wheel: Texture2D,
pub panel_pair_narrow_a: Texture2D,
pub panel_pair_narrow_b: Texture2D,
pub panel_pair_wide_a: Texture2D,
pub panel_pair_wide_b: Texture2D,
pub robot: Texture2D,
pub plunger: Texture2D,
pub ball: Texture2D,
@@ -48,7 +52,12 @@ impl Assets {
texture(include_bytes!("../assets/original/images/dat_00808.png")),
texture(include_bytes!("../assets/original/images/dat_00809.png")),
];
let wheel = texture(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 panel_pair_narrow_a = masked_atlas_pair(wheel_bytes, 0, 45, 45, 90);
let panel_pair_narrow_b = masked_atlas_pair(wheel_bytes, 90, 135, 45, 90);
let panel_pair_wide_a = masked_atlas_pair(wheel_bytes, 271, 362, 91, 90);
let panel_pair_wide_b = masked_atlas_pair(wheel_bytes, 453, 362, 91, 90);
let robot = texture(include_bytes!("../assets/original/images/dat_00900.png"));
let plunger = texture(include_bytes!("../assets/original/images/dat_00901.png"));
let ball = masked_texture(
@@ -139,6 +148,10 @@ impl Assets {
media,
diamond,
wheel,
panel_pair_narrow_a,
panel_pair_narrow_b,
panel_pair_wide_a,
panel_pair_wide_b,
robot,
plunger,
ball,
@@ -161,6 +174,12 @@ impl Assets {
);
}
}
pub fn stop_all_sounds(&self) {
for (_, sound) in &self.sounds {
stop_sound(sound);
}
}
}
fn texture(bytes: &[u8]) -> Texture2D {
@@ -200,3 +219,32 @@ fn monochrome_texture(bytes: &[u8]) -> Texture2D {
texture.set_filter(FilterMode::Nearest);
texture
}
fn masked_atlas_pair(
bytes: &[u8],
image_x: usize,
mask_x: usize,
width: usize,
height: usize,
) -> Texture2D {
let atlas = Image::from_file_with_format(bytes, None).expect("embedded DAT600 must decode");
let atlas_width = usize::from(atlas.width);
let mut output = Image {
bytes: vec![0; width * height * 4],
width: u16::try_from(width).unwrap_or(0),
height: u16::try_from(height).unwrap_or(0),
};
for y in 0..height {
for x in 0..width {
let output_index = (y * width + x) * 4;
let image_index = ((y + 90) * atlas_width + image_x + x) * 4;
let mask_index = ((y + 90) * atlas_width + mask_x + x) * 4;
output.bytes[output_index..output_index + 3]
.copy_from_slice(&atlas.bytes[image_index..image_index + 3]);
output.bytes[output_index + 3] = u8::MAX - atlas.bytes[mask_index];
}
}
let texture = Texture2D::from_image(&output);
texture.set_filter(FilterMode::Nearest);
texture
}