Files
tdkpin/tdkpin-rs/src/assets.rs
T
ddidderr c84df73857 fix(ui): restore effect and player display assets
Load and render DAT400-407 effect panels with the preserved wheel overlap, and add deterministic effect-7 coverage. Restore exact 1008:0996/0b9e score fields, configured-player rows, current/status indicators, signed status thresholds, and six 5x5 per-player ball-marker slots.

Test Plan:
- cargo test --all-targets
- cargo clippy --all-targets --all-features -- -D warnings
- rumdl check CHANGELOG.md RECONSTRUCTION.md README.md
- cargo run --quiet -- --simulate effect-7 --step 0 --screenshot /tmp/tdkpin-effect7.png
- cargo run --quiet -- --simulate launcher --step 0 --screenshot /tmp/tdkpin-display.png
- git diff --check
2026-08-23 19:21:28 +02:00

274 lines
10 KiB
Rust

use macroquad::{
audio::{PlaySoundParams, Sound, load_sound_from_bytes, play_sound, stop_sound},
prelude::{FilterMode, Image, Texture2D},
};
pub struct Assets {
pub active_table: Texture2D,
pub inactive_table: Texture2D,
pub loading: Texture2D,
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,
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 panel_target: Texture2D,
pub robot: Texture2D,
pub plunger: Texture2D,
pub ball: Texture2D,
pub digits: Texture2D,
sounds: Vec<(u16, Sound)>,
}
impl Assets {
#[allow(clippy::too_many_lines)]
pub async fn load() -> Self {
let active_table = texture(include_bytes!("../assets/original/images/dat_00997.png"));
let inactive_table = texture(include_bytes!("../assets/original/images/dat_00998.png"));
let loading = texture(include_bytes!("../assets/original/images/dat_00995.png"));
let loading_progress = texture(include_bytes!("../assets/original/images/dat_00994.png"));
let highscore_background = texture(include_bytes!("../assets/original/images/dat_00993.png"));
let help = [
texture(include_bytes!("../assets/original/images/dat_01001.png")),
texture(include_bytes!("../assets/original/images/dat_01002.png")),
texture(include_bytes!("../assets/original/images/dat_01003.png")),
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")),
texture(include_bytes!("../assets/original/images/dat_00703.png")),
texture(include_bytes!("../assets/original/images/dat_00704.png")),
];
let diamond = [
texture(include_bytes!("../assets/original/images/dat_00801.png")),
texture(include_bytes!("../assets/original/images/dat_00802.png")),
texture(include_bytes!("../assets/original/images/dat_00803.png")),
texture(include_bytes!("../assets/original/images/dat_00804.png")),
texture(include_bytes!("../assets/original/images/dat_00805.png")),
texture(include_bytes!("../assets/original/images/dat_00806.png")),
texture(include_bytes!("../assets/original/images/dat_00807.png")),
texture(include_bytes!("../assets/original/images/dat_00808.png")),
texture(include_bytes!("../assets/original/images/dat_00809.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, 90, 45, 90, 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, 90, 362, 90, 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 plunger = 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 digits =
monochrome_texture(include_bytes!("../assets/original/images/bitmap_00500.png"));
let sound_bytes: [(u16, &[u8]); 16] = [
(
2001,
include_bytes!("../assets/original/audio/wav_02001.wav"),
),
(
2002,
include_bytes!("../assets/original/audio/wav_02002.wav"),
),
(
2004,
include_bytes!("../assets/original/audio/wav_02004.wav"),
),
(
2006,
include_bytes!("../assets/original/audio/wav_02006.wav"),
),
(
2007,
include_bytes!("../assets/original/audio/wav_02007.wav"),
),
(
2008,
include_bytes!("../assets/original/audio/wav_02008.wav"),
),
(
2011,
include_bytes!("../assets/original/audio/wav_02011.wav"),
),
(
2012,
include_bytes!("../assets/original/audio/wav_02012.wav"),
),
(
2013,
include_bytes!("../assets/original/audio/wav_02013.wav"),
),
(
2015,
include_bytes!("../assets/original/audio/wav_02015.wav"),
),
(
2016,
include_bytes!("../assets/original/audio/wav_02016.wav"),
),
(
2017,
include_bytes!("../assets/original/audio/wav_02017.wav"),
),
(
2019,
include_bytes!("../assets/original/audio/wav_02019.wav"),
),
(
2020,
include_bytes!("../assets/original/audio/wav_02020.wav"),
),
(
2021,
include_bytes!("../assets/original/audio/wav_02021.wav"),
),
(
2022,
include_bytes!("../assets/original/audio/wav_02022.wav"),
),
];
let mut sounds = Vec::with_capacity(sound_bytes.len());
for (id, bytes) in sound_bytes {
if let Ok(sound) = load_sound_from_bytes(bytes).await {
sounds.push((id, sound));
}
}
Self {
active_table,
inactive_table,
loading,
loading_progress,
highscore_background,
help,
player_effects,
media,
diamond,
wheel,
panel_pair_narrow_a,
panel_pair_narrow_b,
panel_pair_wide_a,
panel_pair_wide_b,
panel_target,
robot,
plunger,
ball,
digits,
sounds,
}
}
pub fn play(&self, id: u16, enabled: bool) {
if !enabled {
return;
}
if let Some((_, sound)) = self.sounds.iter().find(|(sound_id, _)| *sound_id == id) {
play_sound(
sound,
PlaySoundParams {
looped: false,
volume: 0.72,
},
);
}
}
pub fn stop_all_sounds(&self) {
for (_, sound) in &self.sounds {
stop_sound(sound);
}
}
}
fn texture(bytes: &[u8]) -> Texture2D {
let texture = Texture2D::from_file_with_format(bytes, None);
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
}
fn monochrome_texture(bytes: &[u8]) -> Texture2D {
let mut image =
Image::from_file_with_format(bytes, None).expect("embedded monochrome bitmap must decode");
let (pixels, remainder) = image.bytes.as_chunks_mut::<4>();
assert!(remainder.is_empty());
for pixel in pixels {
pixel[3] = u8::MAX - pixel[0];
pixel[..3].fill(u8::MAX);
}
let texture = Texture2D::from_image(&image);
texture.set_filter(FilterMode::Nearest);
texture
}
fn masked_atlas_pair(
bytes: &[u8],
image_x: usize,
image_y: usize,
mask_x: usize,
mask_y: 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 + image_y) * atlas_width + image_x + x) * 4;
let mask_index = ((y + mask_y) * 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
}