Replace the float nudge direction, cooldown, and decaying meter with the scan-code-specific Win16 behavior. Left Shift and keypad 3 now use the original random directional impulse, while Space uses the reconstructed random horizontal and vertical formulas with scalar 15 and wrapping millipixel velocities. Track the original 16-bit tilt counter: each nudge adds 25, compares against 30 + Random(10), and the selected detail timer decrements a nonzero counter once per callback. Allow the same input while a ball waits in the launcher, retain bonus state when tilt latches, clear flippers immediately, and keep Right Shift only as a documented modern alias for keypad 3. Test Plan: - `cargo test --all-targets` -- passed, 57 tests - `cargo clippy --all-targets -- -D warnings` -- passed - `rumdl check tdkpin-rs/CHANGELOG.md tdkpin-rs/README.md tdkpin-rs/RECONSTRUCTION.md` -- passed - `git diff --cached --check` -- passed
789 lines
26 KiB
Rust
789 lines
26 KiB
Rust
use crate::{
|
|
assets::Assets,
|
|
game::{Controls, Event, Game, Nudge},
|
|
persistence::{HighScore, Language, Persistence, SavedData, insert_high_score},
|
|
table::BUMPERS,
|
|
};
|
|
use macroquad::prelude::*;
|
|
use std::path::Path;
|
|
|
|
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>,
|
|
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,
|
|
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();
|
|
}
|
|
|
|
pub fn set_simulation_game(&mut self, game: Game) {
|
|
self.game = Some(game);
|
|
self.screen = Screen::Playing;
|
|
}
|
|
|
|
pub fn render_simulation(&self) {
|
|
self.draw_logical();
|
|
}
|
|
|
|
pub fn export_simulation_png(&self, path: &Path) -> Result<(), String> {
|
|
let path = path
|
|
.to_str()
|
|
.ok_or_else(|| format!("screenshot path is not valid UTF-8: {}", path.display()))?;
|
|
self.render_target
|
|
.texture
|
|
.get_texture_data()
|
|
.export_png(path);
|
|
Ok(())
|
|
}
|
|
|
|
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.game = Some(Game::new(1));
|
|
self.screen = Screen::Playing;
|
|
self.assets.play(2001, self.saved.settings.sounds);
|
|
}
|
|
}
|
|
|
|
fn update_game(&mut self) {
|
|
if (is_key_pressed(KeyCode::KpAdd) || is_key_pressed(KeyCode::Equal))
|
|
&& self
|
|
.game
|
|
.as_mut()
|
|
.is_some_and(Game::add_player_before_launch)
|
|
{
|
|
self.assets.play(2001, self.saved.settings.sounds);
|
|
}
|
|
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) {
|
|
Nudge::Left
|
|
} else if is_key_pressed(KeyCode::RightShift) || is_key_pressed(KeyCode::Kp3) {
|
|
Nudge::Right
|
|
} else if is_key_pressed(KeyCode::Space) {
|
|
Nudge::Center
|
|
} else {
|
|
Nudge::None
|
|
};
|
|
let controls = Controls {
|
|
left_flipper,
|
|
right_flipper,
|
|
launch_down: is_key_down(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) {
|
|
if let Some(resource) = event.sound_resource() {
|
|
self.assets.play(resource, 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 + TO START",
|
|
475.5,
|
|
139.0,
|
|
22,
|
|
Color::from_rgba(0, 72, 102, 255),
|
|
);
|
|
draw_centered_at("PRESS + AGAIN TO ADD PLAYERS", 475.5, 165.0, 15, BLACK);
|
|
draw_centered_at("THEN HOLD DOWN TO LAUNCH", 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) {
|
|
// Resource 998 is the original game's resting playfield. Resource 997
|
|
// contains exact lit-state patches used below for the active displays.
|
|
draw_texture(&self.assets.inactive_table, 0.0, 0.0, WHITE);
|
|
let Some(game) = &self.game else {
|
|
return;
|
|
};
|
|
|
|
if game.wheel_animation > 0.0 {
|
|
let frame = animation_frame(f64::from(0.65 - game.wheel_animation), 18.0, 5);
|
|
draw_texture_ex(
|
|
&self.assets.wheel,
|
|
80.0,
|
|
49.0,
|
|
WHITE,
|
|
DrawTextureParams {
|
|
dest_size: Some(vec2(68.0, 60.0)),
|
|
source: Some(Rect::new(frame as f32 * 68.0, 0.0, 68.0, 60.0)),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
for (index, bumper) in BUMPERS.into_iter().enumerate() {
|
|
if game.bumper_flash[index] > 0.0 {
|
|
draw_circle_lines(bumper.center.x, bumper.center.y, 17.0, 3.0, WHITE);
|
|
draw_circle_lines(bumper.center.x, bumper.center.y, 13.0, 2.0, YELLOW);
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
for (object_id, region) in [
|
|
(6, Rect::new(151.0, 389.0, 13.0, 49.0)),
|
|
(153, Rect::new(11.0, 344.0, 13.0, 49.0)),
|
|
(154, Rect::new(291.0, 346.0, 13.0, 49.0)),
|
|
] {
|
|
if game.magnetic_field_active(object_id) {
|
|
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()
|
|
},
|
|
);
|
|
}
|
|
}
|
|
if game.effect_target_active() {
|
|
let region = Rect::new(60.0, 57.0, 18.0, 17.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()
|
|
},
|
|
);
|
|
}
|
|
if game.ball.in_launcher {
|
|
draw_texture_ex(
|
|
&self.assets.plunger,
|
|
314.0,
|
|
426.0,
|
|
WHITE,
|
|
DrawTextureParams {
|
|
dest_size: Some(vec2(21.0, 18.0)),
|
|
source: Some(Rect::new(
|
|
game.launcher_frame() as f32 * 21.0,
|
|
0.0,
|
|
20.0,
|
|
17.0,
|
|
)),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
for (raised, region) in [
|
|
(
|
|
game.flippers.left_raised,
|
|
Rect::new(93.0, 373.0, 47.0, 52.0),
|
|
),
|
|
(
|
|
game.flippers.right_raised,
|
|
Rect::new(175.0, 372.0, 45.0, 53.0),
|
|
),
|
|
] {
|
|
if raised {
|
|
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()
|
|
},
|
|
);
|
|
}
|
|
}
|
|
if !game.claw.ball_suspended {
|
|
draw_texture(
|
|
&self.assets.ball,
|
|
game.ball.position.x - 8.0,
|
|
game.ball.position.y - 8.0,
|
|
WHITE,
|
|
);
|
|
}
|
|
if let Some(ball) = game.secondary_ball {
|
|
draw_texture(
|
|
&self.assets.ball,
|
|
ball.position.x - 8.0,
|
|
ball.position.y - 8.0,
|
|
WHITE,
|
|
);
|
|
}
|
|
if let Some(source) = game.claw.sprite_source() {
|
|
draw_texture_ex(
|
|
&self.assets.robot,
|
|
238.0,
|
|
47.0,
|
|
WHITE,
|
|
DrawTextureParams {
|
|
dest_size: Some(vec2(100.0, 74.0)),
|
|
source: Some(source),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
self.draw_displays(game);
|
|
if game.tilted {
|
|
draw_texture_ex(
|
|
&self.assets.active_table,
|
|
82.0,
|
|
238.0,
|
|
WHITE,
|
|
DrawTextureParams {
|
|
dest_size: Some(vec2(151.0, 33.0)),
|
|
source: Some(Rect::new(82.0, 238.0, 151.0, 33.0)),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
#[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.bonus.to_string(), 605.0, 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,
|
|
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()
|
|
},
|
|
);
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
let source_x = f32::from(digit - b'0') * 16.0;
|
|
draw_texture_ex(
|
|
&self.assets.digits,
|
|
x,
|
|
position.y,
|
|
BLACK,
|
|
DrawTextureParams {
|
|
dest_size: Some(digit_size),
|
|
source: Some(Rect::new(source_x, 0.0, 16.0, 28.0)),
|
|
..Default::default()
|
|
},
|
|
);
|
|
x += digit_size.x;
|
|
}
|
|
}
|
|
|
|
#[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;
|
|
let shake = self.game.as_ref().map_or(Vec2::ZERO, |game| {
|
|
if game.nudge_shake <= 0.0 {
|
|
return Vec2::ZERO;
|
|
}
|
|
let strength = (game.nudge_shake / 0.18).min(1.0) * 2.5 * scale;
|
|
let phase = game.nudge_shake * 950.0;
|
|
vec2(
|
|
phase.sin() * strength,
|
|
(phase * 0.73).cos() * strength * 0.45,
|
|
)
|
|
});
|
|
draw_texture_ex(
|
|
&self.render_target.texture,
|
|
x + shake.x,
|
|
y + shake.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);
|
|
}
|
|
|
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
fn animation_frame(elapsed: f64, frames_per_second: f64, frame_count: usize) -> usize {
|
|
((elapsed.max(0.0) * frames_per_second) as usize) % frame_count
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|