fix(ui): restore exact score and status displays

Render 1008:0996's eight-place 130x40 score fields at x=478 through right edge 606, exact configured-player rows, current/status markers, and all six 5x5 ball-marker slots. Derive 1008:0b9e status images from strict signed score thresholds rather than unrelated media-award state.

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 launcher --step 0 --screenshot /tmp/tdkpin-display.png
- git diff --check
This commit is contained in:
2026-08-23 19:19:12 +02:00
parent 0e7208e03a
commit 0643138c0e
3 changed files with 116 additions and 83 deletions
+111 -82
View File
@@ -20,6 +20,39 @@ const TARGET_POSITIONS: [[(f32, f32); 5]; 6] = [
[(8.0, 35.0), (26.0, 8.0), (59.0, 18.0), (59.0, 50.0), (27.0, 61.0)],
];
fn visible_score_digits(mut value: u32) -> Vec<(u8, u8)> {
let mut digits = Vec::with_capacity(8);
for position in 0..8 {
let digit = u8::try_from(value % 10).unwrap_or(0);
value /= 10;
if digit != 0 || value > 0 || position == 0 {
digits.push((7 - position, digit));
}
}
digits
}
#[allow(clippy::cast_possible_wrap)]
fn status_level_for_score(score: u32) -> u8 {
let score = score as i32;
if score > 1_300_000 {
4
} else if score > 650_000 {
3
} else if score > 140_000 {
2
} else {
u8::from(score > 2_000)
}
}
fn displayed_player_markers(balls: u8, extra_balls: u8, is_current: bool) -> u8 {
balls
.saturating_add(extra_balls)
.saturating_sub(u8::from(is_current))
.min(6)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Screen {
Loading,
@@ -633,6 +666,19 @@ impl App {
);
}
#[allow(clippy::cast_precision_loss)]
fn draw_inactive_table_region(&self, x: i32, y: i32, width: i32, height: i32) {
draw_texture_region(
&self.assets.inactive_table,
x as f32,
y as f32,
width as f32,
height as f32,
x as f32,
y as f32,
);
}
#[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
fn draw_game(&self) {
// Resource 998 is the original game's resting playfield. Resource 997
@@ -958,104 +1004,62 @@ impl App {
#[allow(clippy::cast_precision_loss)]
fn draw_displays(&self, game: &Game) {
for region in [
Rect::new(383.0, 72.0, 140.0, 136.0),
Rect::new(545.0, 170.0, 9.0, 9.0),
Rect::new(372.0, 233.0, 238.0, 29.0),
] {
draw_texture_ex(
&self.assets.active_table,
region.x,
region.y,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(region.w, region.h)),
source: Some(region),
..Default::default()
},
);
}
self.draw_right_aligned_digits(&game.secondary_score_display().to_string(), 605.0, 233.0);
self.draw_active_table_region(372, 233, 238, 30);
self.draw_score_value(game.secondary_score_display(), 233.0);
let media_level = game.player().media_level;
if media_level > 0 {
let index = usize::from(media_level.saturating_sub(1).min(3));
draw_texture(&self.assets.media[index], 380.0, 72.0, WHITE);
}
let row_y = 277.0 + game.current_player as f32 * 44.0;
for region in [
Rect::new(373.0, row_y + 4.0, 9.0, 9.0),
Rect::new(473.0, row_y, 137.0, 29.0),
] {
draw_texture_ex(
&self.assets.active_table,
region.x,
region.y,
let status_level = status_level_for_score(game.player().score);
if status_level == 0 {
self.draw_active_table_region(380, 69, 146, 142);
} else {
draw_texture(
&self.assets.media[usize::from(status_level - 1)],
380.0,
69.0,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(region.w, region.h)),
source: Some(region),
..Default::default()
},
);
}
let displayed_balls = game
.player()
.balls
.saturating_sub(1)
.saturating_add(game.player().extra_balls)
.min(6);
for ball in 0..displayed_balls {
let x = 392.0 + f32::from(ball) * 8.0;
draw_texture_ex(
&self.assets.active_table,
x,
row_y + 22.0,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(3.0, 3.0)),
source: Some(Rect::new(x, row_y + 22.0, 3.0, 3.0)),
..Default::default()
},
);
}
self.draw_active_table_region(544, i32::from(4 - status_level) * 23 + 77, 11, 11);
for (index, player) in game.players.iter().enumerate() {
self.draw_right_aligned_digits(
&player.score.to_string(),
605.0,
277.0 + index as f32 * 44.0,
let y = 276 + i32::try_from(index).unwrap_or(0) * 44;
self.draw_active_table_region(472, y, 139, 31);
self.draw_score_value(player.score, (y + 1) as f32);
let displayed_balls = displayed_player_markers(
player.balls,
player.extra_balls,
index == game.current_player,
);
}
}
fn draw_right_aligned_digits(&self, digits: &str, right: f32, y: f32) {
let digit_count = u16::try_from(digits.bytes().filter(u8::is_ascii_digit).count())
.expect("a display value has at most ten digits");
let width = f32::from(digit_count) * 16.0;
self.draw_digits(digits, vec2(right - width, y), vec2(16.0, 28.0));
}
fn draw_digits(&self, digits: &str, position: Vec2, digit_size: Vec2) {
let mut x = position.x;
for digit in digits.bytes() {
if !digit.is_ascii_digit() {
continue;
for marker in 0..6 {
let x = 391 + i32::from(marker) * 8;
if marker < displayed_balls {
self.draw_active_table_region(x, y + 22, 5, 5);
} else {
self.draw_inactive_table_region(x, y + 22, 5, 5);
}
}
let source_x = f32::from(digit - b'0') * 16.0;
}
self.draw_active_table_region(
372,
280 + i32::try_from(game.current_player).unwrap_or(0) * 44,
11,
11,
);
}
fn draw_score_value(&self, value: u32, y: f32) {
for (slot, digit) in visible_score_digits(value) {
let x = 478.0 + f32::from(slot) * 16.0;
draw_texture_ex(
&self.assets.digits,
x,
position.y,
y,
BLACK,
DrawTextureParams {
dest_size: Some(digit_size),
source: Some(Rect::new(source_x, 0.0, 16.0, 28.0)),
dest_size: Some(vec2(16.0, 28.0)),
source: Some(Rect::new(f32::from(digit) * 16.0, 0.0, 16.0, 28.0)),
..Default::default()
},
);
x += digit_size.x;
}
}
@@ -1299,6 +1303,31 @@ mod tests {
assert_eq!(i32::from(App::loading_stage_at(99.0) - 1) * 7, 238);
}
#[test]
fn score_renderer_uses_eight_places_and_original_right_edge() {
assert_eq!(visible_score_digits(0), [(7, 0)]);
assert_eq!(visible_score_digits(123), [(7, 3), (6, 2), (5, 1)]);
let digits = visible_score_digits(123_456_789);
assert_eq!(digits.len(), 8);
assert_eq!(digits[0], (7, 9));
assert_eq!(digits[7], (0, 2));
assert_eq!(478 + i32::from(digits[0].0) * 16 + 16, 606);
}
#[test]
fn status_and_ball_markers_follow_original_display_thresholds() {
assert_eq!(status_level_for_score(2_000), 0);
assert_eq!(status_level_for_score(2_001), 1);
assert_eq!(status_level_for_score(140_000), 1);
assert_eq!(status_level_for_score(140_001), 2);
assert_eq!(status_level_for_score(650_001), 3);
assert_eq!(status_level_for_score(1_300_001), 4);
assert_eq!(status_level_for_score(u32::MAX), 0);
assert_eq!(displayed_player_markers(3, 0, true), 2);
assert_eq!(displayed_player_markers(3, 0, false), 3);
assert_eq!(displayed_player_markers(3, 4, true), 6);
}
#[test]
fn attract_phases_follow_the_reconstructed_tick_quotients() {
let at = |tick| AttractAnimation {