feat: rebuild TDK Pinball Machine in Rust
Replace the empty Rust scaffold with a playable native reconstruction of the 1995 Win16 game. Embed the complete decoded asset set, retain the original 640x460 presentation and localized help, and implement a resizable letterboxed Macroquad frontend for Linux, macOS, and Windows. Recreate the multiplayer ball flow, flippers, nudging, target banks, wheel, robot, lock, magnets, bumper progression, TDK diamond multiplier, media extra balls, sound dispatch, settings, and high-score entry. The physics engine uses a fixed time step and 101 static collision segments transcribed from the original 175-object table. Portable JSON persistence imports and decodes the original XOR-obfuscated high-score file on first run. Document the evidence boundary explicitly: artwork and PCM samples are exact, static geometry is recovered, and several numeric scoring/impulse values remain best-evidence behavioral tuning rather than bit-identical Win16 arithmetic. Add a native three-OS CI matrix and retain every decoded resource for future fidelity work. Test Plan: - `diff -qr original/assets/decoded tdkpin-rs/assets/original` with the two intentionally added legacy root files excluded -- passed - `cargo fmt --all -- --check` -- passed - `cargo check --all-targets` -- passed - `cargo test --all-targets` -- passed, 8 tests - `cargo clippy --all-targets -- -D warnings` -- passed - `cargo build --profile production` -- passed - `cargo check --all-targets --target x86_64-pc-windows-gnu` -- passed - `cargo check --all-targets --target x86_64-apple-darwin` -- passed - `cargo run` graphical start, launch, collision, and score smoke test -- passed; audible output was unavailable because the host has no ALSA device
This commit is contained in:
@@ -0,0 +1,683 @@
|
||||
use crate::{
|
||||
assets::Assets,
|
||||
game::{Controls, Event, Game},
|
||||
persistence::{HighScore, Language, Persistence, SavedData, insert_high_score},
|
||||
};
|
||||
use macroquad::prelude::*;
|
||||
|
||||
const WIDTH: f32 = 640.0;
|
||||
const HEIGHT: f32 = 460.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Screen {
|
||||
Loading,
|
||||
Attract,
|
||||
Playing,
|
||||
Help,
|
||||
Settings,
|
||||
HighScores,
|
||||
NameEntry,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
assets: Assets,
|
||||
persistence: Persistence,
|
||||
saved: SavedData,
|
||||
render_target: RenderTarget,
|
||||
screen: Screen,
|
||||
return_screen: Screen,
|
||||
game: Option<Game>,
|
||||
player_count: usize,
|
||||
setting_row: usize,
|
||||
loading_until: f64,
|
||||
last_help_click: f64,
|
||||
message: String,
|
||||
message_until: f64,
|
||||
name: String,
|
||||
pending_score: u32,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub async fn load() -> Self {
|
||||
let assets = Assets::load().await;
|
||||
let persistence = Persistence::new();
|
||||
let saved = persistence.load();
|
||||
let render_target = render_target(640, 460);
|
||||
render_target.texture.set_filter(FilterMode::Nearest);
|
||||
Self {
|
||||
assets,
|
||||
persistence,
|
||||
saved,
|
||||
render_target,
|
||||
screen: Screen::Loading,
|
||||
return_screen: Screen::Attract,
|
||||
game: None,
|
||||
player_count: 1,
|
||||
setting_row: 0,
|
||||
loading_until: get_time() + 1.1,
|
||||
last_help_click: -1.0,
|
||||
message: String::new(),
|
||||
message_until: 0.0,
|
||||
name: String::new(),
|
||||
pending_score: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frame(&mut self) {
|
||||
self.handle_global_input();
|
||||
match self.screen {
|
||||
Screen::Loading => {
|
||||
if get_time() >= self.loading_until {
|
||||
self.screen = Screen::Attract;
|
||||
}
|
||||
}
|
||||
Screen::Attract => self.update_attract(),
|
||||
Screen::Playing => self.update_game(),
|
||||
Screen::Help => self.update_help(),
|
||||
Screen::Settings => self.update_settings(),
|
||||
Screen::HighScores => self.update_high_scores(),
|
||||
Screen::NameEntry => self.update_name_entry(),
|
||||
}
|
||||
|
||||
self.draw_logical();
|
||||
self.present();
|
||||
}
|
||||
|
||||
fn handle_global_input(&mut self) {
|
||||
if is_key_pressed(KeyCode::F12) {
|
||||
self.saved.settings.sounds = !self.saved.settings.sounds;
|
||||
if self.saved.settings.sounds {
|
||||
"SOUND ON"
|
||||
} else {
|
||||
"SOUND OFF"
|
||||
}
|
||||
.clone_into(&mut self.message);
|
||||
self.message_until = get_time() + 1.2;
|
||||
self.save();
|
||||
}
|
||||
if is_key_pressed(KeyCode::F1) && self.screen != Screen::NameEntry {
|
||||
if self.screen == Screen::Help {
|
||||
self.screen = self.return_screen;
|
||||
} else {
|
||||
self.return_screen = self.base_screen();
|
||||
self.screen = Screen::Help;
|
||||
}
|
||||
}
|
||||
if is_key_pressed(KeyCode::F2) && self.screen != Screen::NameEntry {
|
||||
if self.screen == Screen::Settings {
|
||||
self.screen = self.return_screen;
|
||||
} else {
|
||||
self.return_screen = self.base_screen();
|
||||
self.screen = Screen::Settings;
|
||||
}
|
||||
}
|
||||
if is_key_pressed(KeyCode::F3) && self.screen != Screen::NameEntry {
|
||||
if self.screen == Screen::HighScores {
|
||||
self.screen = self.return_screen;
|
||||
} else {
|
||||
self.return_screen = self.base_screen();
|
||||
self.screen = Screen::HighScores;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn base_screen(&self) -> Screen {
|
||||
if self.game.is_some() {
|
||||
Screen::Playing
|
||||
} else {
|
||||
Screen::Attract
|
||||
}
|
||||
}
|
||||
|
||||
fn update_attract(&mut self) {
|
||||
if is_key_pressed(KeyCode::KpAdd) || is_key_pressed(KeyCode::Equal) {
|
||||
self.player_count = self.player_count % 4 + 1;
|
||||
self.assets.play(2012, self.saved.settings.sounds);
|
||||
}
|
||||
if is_key_pressed(KeyCode::Down) || is_key_pressed(KeyCode::Enter) {
|
||||
self.game = Some(Game::new(self.player_count));
|
||||
self.screen = Screen::Playing;
|
||||
self.assets.play(2008, self.saved.settings.sounds);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_game(&mut self) {
|
||||
let left_flipper = is_key_down(KeyCode::LeftControl)
|
||||
|| is_key_down(KeyCode::A)
|
||||
|| is_key_down(KeyCode::Left);
|
||||
let right_flipper = is_key_down(KeyCode::KpEnter)
|
||||
|| is_key_down(KeyCode::RightControl)
|
||||
|| is_key_down(KeyCode::D)
|
||||
|| is_key_down(KeyCode::Right);
|
||||
let nudge = if is_key_pressed(KeyCode::LeftShift) {
|
||||
-1.0
|
||||
} else if is_key_pressed(KeyCode::RightShift) || is_key_pressed(KeyCode::Kp3) {
|
||||
1.0
|
||||
} else if is_key_pressed(KeyCode::Space) {
|
||||
if self
|
||||
.game
|
||||
.as_ref()
|
||||
.is_some_and(|game| game.ball.position.x < WIDTH / 4.0)
|
||||
{
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let controls = Controls {
|
||||
left_flipper,
|
||||
right_flipper,
|
||||
launch_pressed: is_key_pressed(KeyCode::Down),
|
||||
nudge,
|
||||
};
|
||||
let events = self.game.as_mut().map_or_else(Vec::new, |game| {
|
||||
game.update(get_frame_time(), self.saved.settings.speed, controls)
|
||||
});
|
||||
for event in events {
|
||||
self.play_event(event);
|
||||
}
|
||||
|
||||
if self.game.as_ref().is_some_and(|game| game.finished) {
|
||||
self.finish_game();
|
||||
}
|
||||
}
|
||||
|
||||
fn play_event(&self, event: Event) {
|
||||
let id = match event {
|
||||
Event::Flipper => 2001,
|
||||
Event::Launch => 2016,
|
||||
Event::Bumper => 2002,
|
||||
Event::Target => 2006,
|
||||
Event::Wheel => 2004,
|
||||
Event::Robot => 2007,
|
||||
Event::Lock => 2017,
|
||||
Event::Media => 2022,
|
||||
Event::ExtraBall => 2019,
|
||||
Event::Nudge => 2021,
|
||||
Event::Drain => 2013,
|
||||
};
|
||||
self.assets.play(id, self.saved.settings.sounds);
|
||||
}
|
||||
|
||||
fn finish_game(&mut self) {
|
||||
self.pending_score = self
|
||||
.game
|
||||
.as_ref()
|
||||
.and_then(|game| game.players.iter().map(|player| player.score).max())
|
||||
.unwrap_or(0);
|
||||
let qualifies = self.saved.high_scores.len() < 10
|
||||
|| self
|
||||
.saved
|
||||
.high_scores
|
||||
.last()
|
||||
.is_some_and(|entry| self.pending_score > entry.score);
|
||||
if qualifies {
|
||||
self.name.clear();
|
||||
self.screen = Screen::NameEntry;
|
||||
} else {
|
||||
self.game = None;
|
||||
self.return_screen = Screen::Attract;
|
||||
self.screen = Screen::HighScores;
|
||||
}
|
||||
self.assets.play(2020, self.saved.settings.sounds);
|
||||
}
|
||||
|
||||
fn update_help(&mut self) {
|
||||
if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Enter) {
|
||||
self.screen = self.return_screen;
|
||||
}
|
||||
if is_mouse_button_pressed(MouseButton::Left) {
|
||||
let point = Self::logical_mouse();
|
||||
if point.x < 145.0 && point.y < 115.0 {
|
||||
let now = get_time();
|
||||
if now - self.last_help_click <= 0.40 {
|
||||
self.save();
|
||||
macroquad::miniquad::window::request_quit();
|
||||
}
|
||||
self.last_help_click = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_settings(&mut self) {
|
||||
if is_key_pressed(KeyCode::Escape) || is_key_pressed(KeyCode::Enter) {
|
||||
self.save();
|
||||
self.screen = self.return_screen;
|
||||
return;
|
||||
}
|
||||
if is_key_pressed(KeyCode::Up) {
|
||||
self.setting_row = (self.setting_row + 2) % 3;
|
||||
}
|
||||
if is_key_pressed(KeyCode::Down) {
|
||||
self.setting_row = (self.setting_row + 1) % 3;
|
||||
}
|
||||
let direction =
|
||||
i8::from(is_key_pressed(KeyCode::Right)) - i8::from(is_key_pressed(KeyCode::Left));
|
||||
if direction == 0 && !is_key_pressed(KeyCode::Space) {
|
||||
return;
|
||||
}
|
||||
match self.setting_row {
|
||||
0 => {
|
||||
let speed = i16::from(self.saved.settings.speed) + i16::from(direction);
|
||||
self.saved.settings.speed = u8::try_from(speed.clamp(1, 5)).unwrap_or(3);
|
||||
}
|
||||
1 => {
|
||||
self.saved.settings.sounds = !self.saved.settings.sounds;
|
||||
}
|
||||
_ => {
|
||||
let current = Language::ALL
|
||||
.iter()
|
||||
.position(|language| *language == self.saved.settings.language)
|
||||
.unwrap_or(0);
|
||||
let next = if direction < 0 {
|
||||
(current + Language::ALL.len() - 1) % Language::ALL.len()
|
||||
} else {
|
||||
(current + 1) % Language::ALL.len()
|
||||
};
|
||||
self.saved.settings.language = Language::ALL[next];
|
||||
}
|
||||
}
|
||||
self.assets.play(2012, self.saved.settings.sounds);
|
||||
self.save();
|
||||
}
|
||||
|
||||
fn update_high_scores(&mut self) {
|
||||
if is_key_pressed(KeyCode::Escape)
|
||||
|| is_key_pressed(KeyCode::Enter)
|
||||
|| is_key_pressed(KeyCode::F3)
|
||||
{
|
||||
self.screen = self.return_screen;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_name_entry(&mut self) {
|
||||
while let Some(character) = get_char_pressed() {
|
||||
if !character.is_control() && 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();
|
||||
self.game = None;
|
||||
self.return_screen = Screen::Attract;
|
||||
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());
|
||||
set_camera(&camera);
|
||||
clear_background(BLACK);
|
||||
|
||||
match self.screen {
|
||||
Screen::Loading => draw_texture(&self.assets.loading, 0.0, 0.0, WHITE),
|
||||
Screen::Help => {
|
||||
let index = Language::ALL
|
||||
.iter()
|
||||
.position(|language| *language == self.saved.settings.language)
|
||||
.unwrap_or(0);
|
||||
draw_texture(&self.assets.help[index], 0.0, 0.0, WHITE);
|
||||
}
|
||||
Screen::Attract => self.draw_attract(),
|
||||
Screen::Playing => self.draw_game(),
|
||||
Screen::Settings => self.draw_settings(),
|
||||
Screen::HighScores => self.draw_high_scores(),
|
||||
Screen::NameEntry => self.draw_name_entry(),
|
||||
}
|
||||
if get_time() < self.message_until {
|
||||
draw_panel(232.0, 205.0, 176.0, 48.0);
|
||||
draw_centered(&self.message, 229.0, 24, BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_attract(&self) {
|
||||
draw_texture(&self.assets.inactive_table, 0.0, 0.0, WHITE);
|
||||
draw_panel(342.0, 72.0, 267.0, 144.0);
|
||||
draw_centered_at("TDK PINBALL MACHINE", 475.5, 107.0, 22, BLACK);
|
||||
draw_centered_at(
|
||||
"PRESS DOWN ARROW",
|
||||
475.5,
|
||||
139.0,
|
||||
22,
|
||||
Color::from_rgba(0, 72, 102, 255),
|
||||
);
|
||||
draw_centered_at("TO START BALL", 475.5, 165.0, 18, BLACK);
|
||||
draw_centered_at(
|
||||
&format!("PLAYERS: {} [+] CHANGE", self.player_count),
|
||||
475.5,
|
||||
196.0,
|
||||
16,
|
||||
BLACK,
|
||||
);
|
||||
draw_text(
|
||||
"F1 HELP F2 SETTINGS F3 HIGHSCORES F12 SOUND",
|
||||
322.0,
|
||||
448.0,
|
||||
10.0,
|
||||
BLACK,
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
|
||||
fn draw_game(&self) {
|
||||
draw_texture(&self.assets.inactive_table, 0.0, 0.0, WHITE);
|
||||
let Some(game) = &self.game else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (index, center) in [vec2(205.0, 47.0), vec2(234.0, 50.0), vec2(262.0, 53.0)]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if game.top_targets[index] {
|
||||
draw_line(
|
||||
center.x - 7.0,
|
||||
center.y + 9.0,
|
||||
center.x + 5.0,
|
||||
center.y - 10.0,
|
||||
7.0,
|
||||
YELLOW,
|
||||
);
|
||||
}
|
||||
}
|
||||
let side_colors = [RED, ORANGE, YELLOW, GREEN, SKYBLUE];
|
||||
for (index, y) in [180.0, 195.0, 210.0, 225.0, 240.0].into_iter().enumerate() {
|
||||
let color = if game.side_targets[index] {
|
||||
side_colors[index]
|
||||
} else {
|
||||
Color::from_rgba(38, 38, 38, 255)
|
||||
};
|
||||
draw_circle(286.0, y, 5.0, color);
|
||||
}
|
||||
for (index, lit) in game.wheel_holes.iter().enumerate() {
|
||||
if *lit {
|
||||
let angle = index as f32 / 8.0 * std::f32::consts::TAU;
|
||||
draw_circle(
|
||||
114.0 + angle.cos() * 29.0,
|
||||
79.0 + angle.sin() * 29.0,
|
||||
6.0,
|
||||
YELLOW,
|
||||
);
|
||||
}
|
||||
}
|
||||
for index in 0..4 {
|
||||
draw_circle(
|
||||
155.0 + index as f32 * 14.0,
|
||||
214.0,
|
||||
5.0,
|
||||
if index < usize::from(game.lock_lights) {
|
||||
WHITE
|
||||
} else {
|
||||
DARKGRAY
|
||||
},
|
||||
);
|
||||
}
|
||||
if game.player().diamond_segments > 0 {
|
||||
let index = usize::from(game.player().diamond_segments.min(9) - 1);
|
||||
draw_texture(&self.assets.diamond[index], 123.0, 298.0, WHITE);
|
||||
}
|
||||
if game.magnets > 0.0 {
|
||||
for x in [18.0, 296.0] {
|
||||
draw_triangle(
|
||||
vec2(x, 345.0),
|
||||
vec2(x - 7.0, 359.0),
|
||||
vec2(x + 7.0, 359.0),
|
||||
YELLOW,
|
||||
);
|
||||
draw_triangle(
|
||||
vec2(x, 367.0),
|
||||
vec2(x - 7.0, 381.0),
|
||||
vec2(x + 7.0, 381.0),
|
||||
YELLOW,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let (left, right) = game.flippers();
|
||||
for flipper in [left, right] {
|
||||
draw_line(
|
||||
flipper.start.x,
|
||||
flipper.start.y,
|
||||
flipper.end.x,
|
||||
flipper.end.y,
|
||||
10.0,
|
||||
DARKGRAY,
|
||||
);
|
||||
draw_line(
|
||||
flipper.start.x,
|
||||
flipper.start.y - 1.0,
|
||||
flipper.end.x,
|
||||
flipper.end.y - 1.0,
|
||||
6.0,
|
||||
LIGHTGRAY,
|
||||
);
|
||||
draw_circle(flipper.start.x, flipper.start.y, 6.0, GRAY);
|
||||
}
|
||||
draw_circle(game.ball.position.x, game.ball.position.y, 6.0, DARKGRAY);
|
||||
draw_circle(
|
||||
game.ball.position.x - 1.0,
|
||||
game.ball.position.y - 1.0,
|
||||
4.5,
|
||||
LIGHTGRAY,
|
||||
);
|
||||
draw_circle(
|
||||
game.ball.position.x - 2.0,
|
||||
game.ball.position.y - 2.0,
|
||||
1.5,
|
||||
WHITE,
|
||||
);
|
||||
|
||||
self.draw_displays(game);
|
||||
if game.tilt > 1.15 {
|
||||
draw_centered("TILT", 263.0, 34, RED);
|
||||
}
|
||||
draw_text("F1 HELP F2 SETUP", 345.0, 448.0, 11.0, BLACK);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn draw_displays(&self, game: &Game) {
|
||||
draw_rectangle(
|
||||
371.0,
|
||||
235.0,
|
||||
222.0,
|
||||
31.0,
|
||||
Color::from_rgba(0, 191, 209, 255),
|
||||
);
|
||||
draw_text(
|
||||
format!("BONUS {:07}", game.bonus),
|
||||
374.0,
|
||||
258.0,
|
||||
24.0,
|
||||
BLACK,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
draw_text(
|
||||
format!("{:07} KBytes", game.player().score / 1_000),
|
||||
395.0,
|
||||
211.0,
|
||||
16.0,
|
||||
WHITE,
|
||||
);
|
||||
|
||||
for (index, player) in game.players.iter().enumerate() {
|
||||
let y = 274.0 + index as f32 * 44.0;
|
||||
let panel = if index == game.current_player {
|
||||
Color::from_rgba(0, 191, 209, 255)
|
||||
} else {
|
||||
Color::from_rgba(36, 82, 137, 255)
|
||||
};
|
||||
draw_rectangle(472.0, y, 121.0, 30.0, panel);
|
||||
draw_text(format!("{:09}", player.score), 478.0, y + 21.0, 18.0, BLACK);
|
||||
for ball in 0..6 {
|
||||
let lit = ball < usize::from(player.balls + player.extra_balls);
|
||||
draw_circle(
|
||||
397.0 + ball as f32 * 8.0,
|
||||
y + 27.0,
|
||||
2.4,
|
||||
if lit { GREEN } else { DARKGRAY },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn draw_settings(&self) {
|
||||
draw_texture(&self.assets.inactive_table, 0.0, 0.0, WHITE);
|
||||
draw_panel(145.0, 82.0, 350.0, 285.0);
|
||||
draw_centered("SETTINGS", 122.0, 30, BLACK);
|
||||
let rows = [
|
||||
format!("GRAPHICS DETAIL / SPEED: {}", self.saved.settings.speed),
|
||||
format!(
|
||||
"SOUND: {}",
|
||||
if self.saved.settings.sounds {
|
||||
"ON"
|
||||
} else {
|
||||
"OFF"
|
||||
}
|
||||
),
|
||||
format!("LANGUAGE: {}", self.saved.settings.language.label()),
|
||||
];
|
||||
for (index, row) in rows.iter().enumerate() {
|
||||
let y = 181.0 + index as f32 * 52.0;
|
||||
if index == self.setting_row {
|
||||
draw_rectangle(
|
||||
174.0,
|
||||
y - 27.0,
|
||||
292.0,
|
||||
38.0,
|
||||
Color::from_rgba(0, 191, 209, 255),
|
||||
);
|
||||
}
|
||||
draw_centered(row, y, 19, BLACK);
|
||||
}
|
||||
draw_centered("UP/DOWN SELECT LEFT/RIGHT CHANGE", 328.0, 14, BLACK);
|
||||
draw_centered("ENTER OR ESC TO RETURN", 349.0, 14, BLACK);
|
||||
}
|
||||
|
||||
#[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);
|
||||
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));
|
||||
}
|
||||
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),
|
||||
);
|
||||
draw_text(
|
||||
format!(
|
||||
"{}{}",
|
||||
self.name,
|
||||
if get_time().rem_euclid(1.0) < 0.5 {
|
||||
"_"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
),
|
||||
164.0,
|
||||
262.0,
|
||||
23.0,
|
||||
BLACK,
|
||||
);
|
||||
draw_centered("TYPE YOUR NAME, THEN PRESS ENTER", 296.0, 14, BLACK);
|
||||
}
|
||||
|
||||
fn present(&self) {
|
||||
set_default_camera();
|
||||
clear_background(BLACK);
|
||||
let scale = (screen_width() / WIDTH).min(screen_height() / HEIGHT);
|
||||
let width = WIDTH * scale;
|
||||
let height = HEIGHT * scale;
|
||||
let x = (screen_width() - width) * 0.5;
|
||||
let y = (screen_height() - height) * 0.5;
|
||||
draw_texture_ex(
|
||||
&self.render_target.texture,
|
||||
x,
|
||||
y,
|
||||
WHITE,
|
||||
DrawTextureParams {
|
||||
dest_size: Some(vec2(width, height)),
|
||||
flip_y: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn logical_mouse() -> Vec2 {
|
||||
let scale = (screen_width() / WIDTH).min(screen_height() / HEIGHT);
|
||||
let width = WIDTH * scale;
|
||||
let height = HEIGHT * scale;
|
||||
let offset = vec2(
|
||||
(screen_width() - width) * 0.5,
|
||||
(screen_height() - height) * 0.5,
|
||||
);
|
||||
(vec2(mouse_position().0, mouse_position().1) - offset) / scale
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
if let Err(error) = self.persistence.save(&self.saved) {
|
||||
eprintln!("could not save settings and highscores: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_panel(x: f32, y: f32, width: f32, height: f32) {
|
||||
draw_rectangle(x, y, width, height, Color::from_rgba(214, 214, 214, 248));
|
||||
draw_rectangle_lines(x, y, width, height, 3.0, WHITE);
|
||||
draw_rectangle_lines(x + 4.0, y + 4.0, width - 8.0, height - 8.0, 2.0, DARKGRAY);
|
||||
}
|
||||
|
||||
fn draw_centered(text: &str, baseline: f32, font_size: u16, color: Color) {
|
||||
draw_centered_at(text, WIDTH * 0.5, baseline, font_size, color);
|
||||
}
|
||||
|
||||
fn draw_centered_at(text: &str, center_x: f32, baseline: f32, font_size: u16, color: Color) {
|
||||
let dimensions = measure_text(text, None, font_size, 1.0);
|
||||
draw_text(
|
||||
text,
|
||||
center_x - dimensions.width * 0.5,
|
||||
baseline,
|
||||
f32::from(font_size),
|
||||
color,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user