fix(render): restore animated playfield artwork

The gameplay renderer drew the inactive table and then placed thin line flippers
and procedural circles over baked resting artwork. This left two flipper poses
visible at once, made the ball difficult to see, and ignored recovered animation
resources already embedded with the reconstruction.

Build an alpha-masked texture from the original ball bitmap and mask, erase the
baked flipper pose before drawing collision-aligned moving flippers, and animate
the recovered wheel, robot, and magnet sheets from their gameplay events. Add
bumper flashes, exact lit-table pixels for TILT, and a short visual table shake
for nudges so contacts and machine state have immediate feedback.

Test Plan:
- `cargo fmt -- --check` -- passed
- `cargo test --all-targets` -- passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- launched the Linux window and inspected attract, gameplay, resting flippers,
  raised flipper cleanup, and the masked ball sprite -- passed
- `git diff --check` -- passed
This commit is contained in:
2026-08-22 16:58:32 +02:00
parent 7da0f4d171
commit cf2c839f83
4 changed files with 167 additions and 22 deletions
+34 -1
View File
@@ -1,6 +1,6 @@
use macroquad::{
audio::{PlaySoundParams, Sound, load_sound_from_bytes, play_sound},
prelude::{FilterMode, Texture2D},
prelude::{FilterMode, Image, Texture2D},
};
pub struct Assets {
@@ -10,6 +10,10 @@ pub struct Assets {
pub help: [Texture2D; 5],
pub media: [Texture2D; 4],
pub diamond: [Texture2D; 9],
pub wheel: Texture2D,
pub robot: Texture2D,
pub magnet: Texture2D,
pub ball: Texture2D,
sounds: Vec<(u16, Sound)>,
}
@@ -43,6 +47,13 @@ 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 robot = texture(include_bytes!("../assets/original/images/dat_00900.png"));
let magnet = texture(include_bytes!("../assets/original/images/dat_00901.png"));
let ball = masked_texture(
include_bytes!("../assets/original/images/bitmap_00101.png"),
include_bytes!("../assets/original/images/bitmap_00102.png"),
);
let sound_bytes: [(u16, &[u8]); 16] = [
(
@@ -124,6 +135,10 @@ impl Assets {
help,
media,
diamond,
wheel,
robot,
magnet,
ball,
sounds,
}
}
@@ -149,3 +164,21 @@ fn texture(bytes: &[u8]) -> Texture2D {
texture.set_filter(FilterMode::Nearest);
texture
}
fn masked_texture(color_bytes: &[u8], mask_bytes: &[u8]) -> Texture2D {
let mut color =
Image::from_file_with_format(color_bytes, None).expect("embedded color bitmap must decode");
let mask =
Image::from_file_with_format(mask_bytes, None).expect("embedded mask bitmap must decode");
assert_eq!((color.width, color.height), (mask.width, mask.height));
let (color_pixels, color_remainder) = color.bytes.as_chunks_mut::<4>();
let (mask_pixels, mask_remainder) = mask.bytes.as_chunks::<4>();
assert!(color_remainder.is_empty() && mask_remainder.is_empty());
for (pixel, mask_pixel) in color_pixels.iter_mut().zip(mask_pixels) {
pixel[3] = u8::MAX - mask_pixel[0];
}
let texture = Texture2D::from_image(&color);
texture.set_filter(FilterMode::Nearest);
texture
}