fix(ui): restore original high-score windows
Replace modern overlays with the reconstructed HighScore child and DIALOG 32514 topology, exact DAT993 background, row coordinates, original strings, unpadded scores, and 21-character edit behavior. Restore OK/Cancel semantics, including Escape preserving the prefilled name and allowing an accepted empty name, with deterministic screenshot scenarios. 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 highscores --step 0 --screenshot /tmp/tdkpin-highscores.png - cargo run --quiet -- --simulate name-entry --step 0 --screenshot /tmp/tdkpin-name-entry.png - git diff --check
This commit is contained in:
+102
-45
@@ -193,6 +193,20 @@ impl App {
|
||||
self.loading_until = get_time() + 60.0;
|
||||
}
|
||||
|
||||
pub fn set_simulation_highscores(&mut self) {
|
||||
self.game = None;
|
||||
self.return_screen = Screen::Attract;
|
||||
self.screen = Screen::HighScores;
|
||||
}
|
||||
|
||||
pub fn set_simulation_name_entry(&mut self) {
|
||||
self.game = Some(Game::new(1));
|
||||
self.return_screen = Screen::Playing;
|
||||
self.pending_score = 6_537_393;
|
||||
"TDK Pinball Player".clone_into(&mut self.name);
|
||||
self.screen = Screen::NameEntry;
|
||||
}
|
||||
|
||||
pub fn render_simulation(&self) {
|
||||
self.draw_logical();
|
||||
}
|
||||
@@ -409,32 +423,46 @@ impl App {
|
||||
|
||||
fn update_name_entry(&mut self) {
|
||||
while let Some(character) = get_char_pressed() {
|
||||
if !character.is_control() && self.name.chars().count() < 21 {
|
||||
if !character.is_control()
|
||||
&& u32::from(character) <= 0xff
|
||||
&& self.name.chars().count() < 21
|
||||
{
|
||||
self.name.push(character);
|
||||
}
|
||||
}
|
||||
if is_key_pressed(KeyCode::Backspace) {
|
||||
self.name.pop();
|
||||
}
|
||||
if is_key_pressed(KeyCode::Enter) && !self.name.trim().is_empty() {
|
||||
insert_high_score(
|
||||
&mut self.saved.high_scores,
|
||||
HighScore {
|
||||
name: self.name.trim().to_owned(),
|
||||
score: self.pending_score,
|
||||
},
|
||||
);
|
||||
self.save();
|
||||
if self.game.as_ref().is_some_and(|game| game.finished) {
|
||||
self.game = None;
|
||||
self.return_screen = Screen::Attract;
|
||||
} else {
|
||||
self.return_screen = Screen::Playing;
|
||||
}
|
||||
self.screen = Screen::HighScores;
|
||||
let click = is_mouse_button_pressed(MouseButton::Left).then(Self::logical_mouse);
|
||||
let clicked_ok = click.is_some_and(|point| Rect::new(255.0, 256.0, 60.0, 23.0).contains(point));
|
||||
let clicked_cancel =
|
||||
click.is_some_and(|point| Rect::new(324.0, 256.0, 60.0, 23.0).contains(point));
|
||||
if is_key_pressed(KeyCode::Escape) || clicked_cancel {
|
||||
"TDK Pinball Player".clone_into(&mut self.name);
|
||||
self.accept_high_score_name();
|
||||
} else if is_key_pressed(KeyCode::Enter) || clicked_ok {
|
||||
self.accept_high_score_name();
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_high_score_name(&mut self) {
|
||||
insert_high_score(
|
||||
&mut self.saved.high_scores,
|
||||
HighScore {
|
||||
name: self.name.clone(),
|
||||
score: self.pending_score,
|
||||
},
|
||||
);
|
||||
self.save();
|
||||
if self.game.as_ref().is_some_and(|game| game.finished) {
|
||||
self.game = None;
|
||||
self.return_screen = Screen::Attract;
|
||||
} else {
|
||||
self.return_screen = Screen::Playing;
|
||||
}
|
||||
self.screen = Screen::HighScores;
|
||||
}
|
||||
|
||||
fn draw_logical(&self) {
|
||||
let mut camera = Camera2D::from_display_rect(Rect::new(0.0, 0.0, WIDTH, HEIGHT));
|
||||
camera.render_target = Some(self.render_target.clone());
|
||||
@@ -1079,36 +1107,47 @@ impl App {
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn draw_high_scores(&self) {
|
||||
draw_texture(&self.assets.inactive_table, 0.0, 0.0, WHITE);
|
||||
draw_panel(124.0, 32.0, 392.0, 398.0);
|
||||
draw_centered("TDK HIGHSCORES", 72.0, 28, BLACK);
|
||||
self.draw_modal_background();
|
||||
draw_rectangle(140.0, 58.0, 360.0, 22.0, Color::from_rgba(0, 0, 128, 255));
|
||||
draw_rectangle_lines(140.0, 58.0, 360.0, 22.0, 2.0, WHITE);
|
||||
draw_text("HighScore", 147.0, 74.0, 14.0, WHITE);
|
||||
draw_texture(&self.assets.highscore_background, 140.0, 80.0, WHITE);
|
||||
draw_centered_at("HIGH SCORES", 320.0, 122.0, 18, BLACK);
|
||||
for (index, entry) in self.saved.high_scores.iter().take(10).enumerate() {
|
||||
let y = 110.0 + index as f32 * 29.0;
|
||||
draw_text(format!("{:>2}.", index + 1), 153.0, y, 17.0, BLACK);
|
||||
draw_text(&entry.name, 190.0, y, 17.0, BLACK);
|
||||
let score = format!("{:09}", entry.score);
|
||||
draw_text(&score, 389.0, y, 17.0, Color::from_rgba(0, 72, 102, 255));
|
||||
let rank = index + 1;
|
||||
let y = 155.0 + index as f32 * 20.0;
|
||||
let prefix = if rank < 10 { " " } else { "" };
|
||||
draw_text(
|
||||
format!("{prefix}{rank}. {}", entry.name),
|
||||
170.0,
|
||||
y,
|
||||
16.0,
|
||||
BLACK,
|
||||
);
|
||||
let score = entry.score.to_string();
|
||||
let width = measure_text(&score, None, 16, 1.0).width;
|
||||
draw_text(&score, 460.0 - width, y, 16.0, BLACK);
|
||||
}
|
||||
draw_centered("ENTER / ESC TO RETURN", 409.0, 14, BLACK);
|
||||
}
|
||||
|
||||
fn draw_name_entry(&self) {
|
||||
draw_texture(&self.assets.active_table, 0.0, 0.0, WHITE);
|
||||
draw_panel(112.0, 145.0, 416.0, 170.0);
|
||||
draw_centered("NEW TDK HIGHSCORE", 183.0, 28, BLACK);
|
||||
draw_centered(
|
||||
&format!("SCORE {:09}", self.pending_score),
|
||||
218.0,
|
||||
21,
|
||||
BLACK,
|
||||
);
|
||||
draw_rectangle(
|
||||
153.0,
|
||||
235.0,
|
||||
334.0,
|
||||
38.0,
|
||||
Color::from_rgba(0, 191, 209, 255),
|
||||
self.draw_modal_background();
|
||||
let x = 185.0;
|
||||
let y = 168.0;
|
||||
draw_rectangle(x, y, 270.0, 124.0, Color::from_rgba(192, 192, 192, 255));
|
||||
draw_rectangle_lines(x, y, 270.0, 124.0, 2.0, WHITE);
|
||||
draw_rectangle_lines(x + 2.0, y + 2.0, 266.0, 120.0, 2.0, DARKGRAY);
|
||||
draw_rectangle(x + 4.0, y + 4.0, 262.0, 20.0, Color::from_rgba(0, 0, 128, 255));
|
||||
draw_text(
|
||||
"Congratulations! This is a Top Ten Score!",
|
||||
x + 9.0,
|
||||
y + 19.0,
|
||||
12.0,
|
||||
WHITE,
|
||||
);
|
||||
draw_text("Please input your name:", x + 15.0, y + 44.0, 14.0, BLACK);
|
||||
draw_rectangle(x + 15.0, y + 52.0, 240.0, 21.0, WHITE);
|
||||
draw_rectangle_lines(x + 14.0, y + 51.0, 242.0, 23.0, 2.0, DARKGRAY);
|
||||
draw_text(
|
||||
format!(
|
||||
"{}{}",
|
||||
@@ -1119,12 +1158,30 @@ impl App {
|
||||
""
|
||||
}
|
||||
),
|
||||
164.0,
|
||||
262.0,
|
||||
23.0,
|
||||
x + 18.0,
|
||||
y + 68.0,
|
||||
14.0,
|
||||
BLACK,
|
||||
);
|
||||
draw_centered("TYPE YOUR NAME, THEN PRESS ENTER", 296.0, 14, BLACK);
|
||||
Self::draw_dialog_button(x + 70.0, y + 88.0, "OK", true);
|
||||
Self::draw_dialog_button(x + 139.0, y + 88.0, "Cancel", false);
|
||||
}
|
||||
|
||||
fn draw_modal_background(&self) {
|
||||
if self.game.is_some() {
|
||||
self.draw_game();
|
||||
} else {
|
||||
self.draw_attract();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_dialog_button(x: f32, y: f32, label: &str, default: bool) {
|
||||
if default {
|
||||
draw_rectangle_lines(x - 2.0, y - 2.0, 64.0, 27.0, 1.0, BLACK);
|
||||
}
|
||||
draw_rectangle(x, y, 60.0, 23.0, Color::from_rgba(192, 192, 192, 255));
|
||||
draw_rectangle_lines(x, y, 60.0, 23.0, 2.0, WHITE);
|
||||
draw_centered_at(label, x + 30.0, y + 17.0, 13, BLACK);
|
||||
}
|
||||
|
||||
fn present(&self) {
|
||||
|
||||
@@ -8,6 +8,7 @@ pub struct Assets {
|
||||
pub inactive_table: Texture2D,
|
||||
pub loading: Texture2D,
|
||||
pub loading_progress: Texture2D,
|
||||
pub highscore_background: Texture2D,
|
||||
pub help: [Texture2D; 5],
|
||||
pub media: [Texture2D; 4],
|
||||
pub diamond: [Texture2D; 9],
|
||||
@@ -31,6 +32,7 @@ impl Assets {
|
||||
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")),
|
||||
@@ -149,6 +151,7 @@ impl Assets {
|
||||
inactive_table,
|
||||
loading,
|
||||
loading_progress,
|
||||
highscore_background,
|
||||
help,
|
||||
media,
|
||||
diamond,
|
||||
|
||||
@@ -63,6 +63,8 @@ async fn run_simulation(request: Request) -> Result<(), String> {
|
||||
match request.scenario {
|
||||
Scenario::Loading => app.set_simulation_loading(request.target_step),
|
||||
Scenario::Attract => app.set_simulation_attract(request.target_step),
|
||||
Scenario::HighScores => app.set_simulation_highscores(),
|
||||
Scenario::NameEntry => app.set_simulation_name_entry(),
|
||||
_ => app.set_simulation_game(simulation.game().clone()),
|
||||
}
|
||||
app.render_simulation();
|
||||
|
||||
@@ -228,4 +228,18 @@ mod tests {
|
||||
assert!(!scores.iter().any(|entry| entry.name == "WRAPPED"));
|
||||
assert!(!qualifies_high_score(&scores, u32::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn original_dialog_allows_an_accepted_empty_name() {
|
||||
let mut scores = parse_original_high_scores(ORIGINAL_HIGHSCORES);
|
||||
insert_high_score(
|
||||
&mut scores,
|
||||
HighScore {
|
||||
name: String::new(),
|
||||
score: 6_537_393,
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(scores[0].name, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ const MAX_SIMULATION_STEPS: u64 = 72_000;
|
||||
pub enum Scenario {
|
||||
Loading,
|
||||
Attract,
|
||||
HighScores,
|
||||
NameEntry,
|
||||
Autoplay,
|
||||
Launcher,
|
||||
Flippers,
|
||||
@@ -31,6 +33,8 @@ impl Scenario {
|
||||
match self {
|
||||
Self::Loading => "loading",
|
||||
Self::Attract => "attract",
|
||||
Self::HighScores => "highscores",
|
||||
Self::NameEntry => "name-entry",
|
||||
Self::Autoplay => "autoplay",
|
||||
Self::Launcher => "launcher",
|
||||
Self::Flippers => "flippers",
|
||||
@@ -51,6 +55,8 @@ impl Scenario {
|
||||
Self::Claw18 => Some(18),
|
||||
Self::Loading
|
||||
| Self::Attract
|
||||
| Self::HighScores
|
||||
| Self::NameEntry
|
||||
| Self::Autoplay
|
||||
| Self::Launcher
|
||||
| Self::Flippers
|
||||
@@ -67,6 +73,8 @@ impl FromStr for Scenario {
|
||||
match value {
|
||||
"loading" => Ok(Self::Loading),
|
||||
"attract" => Ok(Self::Attract),
|
||||
"highscores" => Ok(Self::HighScores),
|
||||
"name-entry" => Ok(Self::NameEntry),
|
||||
"autoplay" => Ok(Self::Autoplay),
|
||||
"launcher" => Ok(Self::Launcher),
|
||||
"flippers" => Ok(Self::Flippers),
|
||||
@@ -77,7 +85,7 @@ impl FromStr for Scenario {
|
||||
"claw-7" => Ok(Self::Claw7),
|
||||
"claw-18" => Ok(Self::Claw18),
|
||||
_ => Err(format!(
|
||||
"unknown scenario {value:?}; expected loading, attract, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18"
|
||||
"unknown scenario {value:?}; expected loading, attract, highscores, name-entry, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18"
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -161,7 +169,7 @@ fn seconds_to_step(value: &str) -> Result<u64, String> {
|
||||
}
|
||||
|
||||
pub const fn usage() -> &'static str {
|
||||
"Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: loading, attract, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18"
|
||||
"Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: loading, attract, highscores, name-entry, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18"
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
@@ -242,7 +250,10 @@ impl Simulation {
|
||||
} else {
|
||||
controls_for(self.scenario, self.step)
|
||||
};
|
||||
let events = if matches!(self.scenario, Scenario::Loading | Scenario::Attract) {
|
||||
let events = if matches!(
|
||||
self.scenario,
|
||||
Scenario::Loading | Scenario::Attract | Scenario::HighScores | Scenario::NameEntry
|
||||
) {
|
||||
Vec::new()
|
||||
} else {
|
||||
self.game.update(SIMULATION_DT, 3, controls)
|
||||
@@ -336,7 +347,6 @@ impl Simulation {
|
||||
|
||||
fn controls_for(scenario: Scenario, step: u64) -> Controls {
|
||||
match scenario {
|
||||
Scenario::Loading | Scenario::Attract => Controls::default(),
|
||||
Scenario::Autoplay => unreachable!("autoplay controls need mutable simulation state"),
|
||||
Scenario::Launcher => Controls {
|
||||
launch_down: step < u64::from(SIMULATION_HZ),
|
||||
@@ -350,7 +360,11 @@ fn controls_for(scenario: Scenario, step: u64) -> Controls {
|
||||
..Controls::default()
|
||||
}
|
||||
}
|
||||
Scenario::Panel
|
||||
Scenario::Loading
|
||||
| Scenario::Attract
|
||||
| Scenario::HighScores
|
||||
| Scenario::NameEntry
|
||||
| Scenario::Panel
|
||||
| Scenario::Targets
|
||||
| Scenario::Claw1
|
||||
| Scenario::Claw6
|
||||
|
||||
Reference in New Issue
Block a user