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
@@ -0,0 +1,27 @@
|
||||
name: Build TDK Pinball
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: tdkpin-rs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- name: Install Linux development libraries
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y libasound2-dev libgl1-mesa-dev libxi-dev
|
||||
- run: cargo check --all-targets
|
||||
- run: cargo test --all-targets
|
||||
- run: cargo clippy --all-targets -- -D warnings
|
||||
@@ -0,0 +1 @@
|
||||
assets/original/TDKPIN.INI binary
|
||||
@@ -2,6 +2,584 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "audir-sles"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea47348666a8edb7ad80cbee3940eb2bccf70df0e6ce09009abe1a836cb779f5"
|
||||
|
||||
[[package]]
|
||||
name = "audrey"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58b92a84e89497e3cd25d3672cd5d1c288abaac02c18ff21283f17d118b889b8"
|
||||
dependencies = [
|
||||
"dasp_frame",
|
||||
"dasp_sample",
|
||||
"hound",
|
||||
"lewton",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "core_maths"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_frame"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6"
|
||||
dependencies = [
|
||||
"dasp_sample",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "fontdue"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7894823fa221401399e2598f8b63f81ac77ff5c63248b7656779bff1632d7d3d"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hound"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"num-traits",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "lewton"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d542c1a317036c45c2aa1cf10cc9d403ca91eb2d333ef1a4917e5cb10628bd0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ogg",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macroquad"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cbd54d99d4d3ac59deed7ee4927bbbf4eb33a55635530af3be9395a55746090"
|
||||
dependencies = [
|
||||
"fontdue",
|
||||
"glam",
|
||||
"image",
|
||||
"macroquad_macro",
|
||||
"miniquad",
|
||||
"quad-rand",
|
||||
"quad-snd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macroquad_macro"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64b1d96218903768c1ce078b657c0d5965465c95a60d2682fd97443c9d2483dd"
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maybe-uninit"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "miniquad"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcbaea978d741f3b60ced666b06a2a1e4a2bc81cb8b9dd29f4a40965e1f59e65"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"ndk-sys",
|
||||
"objc-rs",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-rs"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ogg"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13e571c3517af9e1729d4c63571a27edd660ade0667973bfc74a67c660c2b651"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quad-alsa-sys"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c66c2f04a6946293477973d85adc251d502da51c57b08cd9c997f0cfd8dcd4b5"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quad-rand"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40"
|
||||
|
||||
[[package]]
|
||||
name = "quad-snd"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cba0c4943fc67147fbe9d1eb731fb9e678bfc9d926507eebbbfe0103e154e5b0"
|
||||
dependencies = [
|
||||
"audir-sles",
|
||||
"audrey",
|
||||
"libc",
|
||||
"quad-alsa-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
|
||||
dependencies = [
|
||||
"maybe-uninit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tdkpin-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"directories",
|
||||
"macroquad",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
@@ -4,6 +4,10 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
directories = "6.0.0"
|
||||
macroquad = { version = "0.4.16", features = ["audio"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# TDK Pinball Machine for modern PCs
|
||||
|
||||
This is a native Rust reconstruction of the 1995 Windows game **TDK Pinball
|
||||
Machine**. It uses the extracted original artwork and sound, recreates the
|
||||
playfield as a fixed-step simulation, and runs from the same source on Linux,
|
||||
macOS, and Windows.
|
||||
|
||||
The original program is not required at runtime. All required game assets are
|
||||
embedded in the executable at build time.
|
||||
|
||||
## Run
|
||||
|
||||
Install a current stable Rust toolchain, then run:
|
||||
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
|
||||
For a distributable optimized binary:
|
||||
|
||||
```sh
|
||||
cargo build --profile production
|
||||
```
|
||||
|
||||
The binary is written below `target/production/`. On Linux, Macroquad's native
|
||||
development packages are also required (X11, OpenGL, and ALSA). Windows needs
|
||||
no extra runtime installation; macOS builds with the normal Apple developer
|
||||
command-line tools.
|
||||
|
||||
## Original and modern controls
|
||||
|
||||
| Action | Original key | Additional modern key |
|
||||
| --- | --- | --- |
|
||||
| Choose 1-4 players | `+` | `=` |
|
||||
| Start or launch ball | Down arrow | Enter starts a game |
|
||||
| Left flipper | Left Ctrl | `A` or Left arrow |
|
||||
| Right flipper | Keypad Enter | Right Ctrl, `D`, or Right arrow |
|
||||
| Nudge | Space, either Shift, keypad `3` | - |
|
||||
| Help | F1 | Enter/Escape returns |
|
||||
| Settings | F2 | - |
|
||||
| High scores | F3 | - |
|
||||
| Sound | F12 | - |
|
||||
|
||||
The five original speed choices are retained as simulation-detail settings.
|
||||
They change fixed-step resolution without changing the speed of play.
|
||||
|
||||
The help screen is the original artwork in English, German, French, Italian,
|
||||
or Spanish. As instructed on that screen, double-clicking its upper-left exit
|
||||
box closes the program.
|
||||
|
||||
## Saved data
|
||||
|
||||
Settings and the ten-entry high-score table are stored as `save.json` in the
|
||||
platform's normal per-user application-data directory. On first run, the table
|
||||
is imported from the original `HISCORES.DAT` included with this reconstruction.
|
||||
|
||||
## Reconstruction status
|
||||
|
||||
This repository distinguishes exact recovered material from behavioral
|
||||
reimplementation. See [RECONSTRUCTION.md](RECONSTRUCTION.md) for the evidence
|
||||
ledger, known inference boundaries, and validation performed.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Reconstruction ledger
|
||||
|
||||
## Source artifacts
|
||||
|
||||
The reconstruction was derived from the preserved files under `../original/`.
|
||||
The primary program fingerprint is:
|
||||
|
||||
```text
|
||||
a9022f1894e3e6e21fc42e8f6c932f7c549ca77f63aaa0c488bb9d55d9d0174c TDKPIN.EXE
|
||||
f1d9ac980c7bfba5dc53eaa9e7cb2c3cd9b82f879ee8962ad40bf863d641bb49 MMTIMER.DLL
|
||||
```
|
||||
|
||||
The Ghidra recovery covers all 301 named NE exports plus 62 internal function
|
||||
entries. Its raw output and coverage tables remain beside the binary, so later
|
||||
work can be checked against the same evidence rather than against this Rust
|
||||
implementation.
|
||||
|
||||
## Coverage by subsystem
|
||||
|
||||
| Subsystem | Rust status | Evidence and boundary |
|
||||
| --- | --- | --- |
|
||||
| Artwork | Exact | All 34 custom DIB images, three standard bitmaps, icon, and palette derivatives are preserved in `assets/original/`. The game uses the original 640x460 table, loading, help, media, and diamond frames. |
|
||||
| Audio | Exact samples | All 16 mono PCM WAV resources are embedded unchanged. Their trigger roles were recovered from resource use and gameplay context. |
|
||||
| Help and languages | Exact | Original resource images 1001-1005 are displayed directly. |
|
||||
| Playfield collision layout | Recovered | 101 active static line segments are transcribed from the original 175-object registration table. The original sideways coordinates are transformed with `screen = (y, 478 - x)`. Moving flippers and round bumpers use equivalent native Rust bodies. |
|
||||
| Physics arithmetic | Reimplemented | The Win16 fixed-point/timer engine is replaced by deterministic fixed-step floating-point integration. Restitution and impulses are tuned to the recovered table but are not instruction-for-instruction equivalents. |
|
||||
| Rules | Behaviorally recovered | Player count, controls, wheel holes, magnetic saves, robot grip, four-position ball lock, target banks, increasing bumper value, nine-part TDK diamond, permanent double scoring for a completed diamond, KByte media progression, and media extra balls follow the original help and code paths. |
|
||||
| Numeric scoring | Partly inferred | Visible 2000-6000 target values and recovered registration values are preserved. Some bumper, bank-completion, robot, wheel, lock, and media thresholds are best-evidence reconstructions because the decompiler did not recover meaningful names or a clean rule table. |
|
||||
| High scores | Compatible import | The original 276-byte table is decoded as ten `IWIK`-XOR-obfuscated little-endian scores plus ten 22-byte names, sorted, then migrated to portable JSON. |
|
||||
| Configuration | Behaviorally compatible | Sound, language, and five detail levels are retained. Storage moves from a local Win16 INI file to the platform user-data directory. |
|
||||
| Windows UI shell | Deliberately modernized | Win16 menus, modal dialogs, GDI blitting, and multimedia timers are replaced by a resizable, letterboxed native window with keyboard overlays. The visible game and original help remain at 640x460 logical pixels. |
|
||||
|
||||
## Extracted asset inventory
|
||||
|
||||
`assets/original/` contains 69 files used or preserved by the modern project:
|
||||
|
||||
- 40 image files, including all gameplay frames and five localized help pages;
|
||||
- 16 original WAV resources;
|
||||
- six decoded dialog descriptions;
|
||||
- the original icon in ICO and PNG form;
|
||||
- palette and version metadata;
|
||||
- the original high-score table and INI configuration sample.
|
||||
|
||||
The parent `original/assets/` directory remains the lossless extraction record,
|
||||
including raw resource blobs and the extraction manifest. The Rust copy is the
|
||||
decoded, build-ready subset; it does not replace that evidence archive.
|
||||
|
||||
## Validation levels
|
||||
|
||||
- Static coverage: all intended decoded visual/audio resources are preserved;
|
||||
the 101 recovered static collision segments are represented in Rust.
|
||||
- Build coverage: `cargo check`, unit tests, and strict Clippy complete on the
|
||||
host. Cross-target checks pass for `x86_64-pc-windows-gnu` and
|
||||
`x86_64-apple-darwin`; CI is configured to build and test natively on Linux,
|
||||
macOS, and Windows.
|
||||
- Runtime coverage: the Linux executable was launched through the real window
|
||||
backend, the attract screen was inspected, a game was started, a ball was
|
||||
launched, collision scoring was observed, and a rendered frame was captured.
|
||||
- Semantic boundary: no claim is made that every trajectory or score tick is
|
||||
bit-identical to the 16-bit executable. Remaining numeric inference is listed
|
||||
above instead of being presented as proven parity.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `app.rs`: screen flow, controls, rendering, audio dispatch, and overlays;
|
||||
- `assets.rs`: compile-time original asset embedding;
|
||||
- `game.rs`: fixed-step game state, recovered walls, rules, and scoring;
|
||||
- `geometry.rs`: segment/circle collision primitives;
|
||||
- `persistence.rs`: platform paths, settings, high scores, and legacy import.
|
||||
@@ -0,0 +1,27 @@
|
||||
; Pinball INI-File by KIWI Hamburg
|
||||
|
||||
[Settings]
|
||||
; The speed setting defines the "time resolution" of the graphics.
|
||||
; The higher the setting, the "smoother" the ball will run.
|
||||
; However, if your hardware is too slow for the current speed setting,
|
||||
; the pinball machine might "hang" or not react correctly.
|
||||
; The speed setting DOES NOT influence game speed.
|
||||
; In any case, try setting your Windows graphics resolution to
|
||||
; 640x480 pixel in 256 colours before changing this setting.
|
||||
; Speed=1 for slow graphics hardware
|
||||
; Speed=5 for very fast graphics hardware
|
||||
; Default: Speed=3
|
||||
Speed=3
|
||||
|
||||
; Sound=1 for Sounds on
|
||||
; Sound=0 for Sounds off
|
||||
; Default: Sounds=1
|
||||
Sounds=1
|
||||
|
||||
;Language=1 for english helpscreen
|
||||
;Language=2 for german helpscreen
|
||||
;Language=3 for french helpscreen
|
||||
;Language=4 for italian helpscreen
|
||||
;Language=5 for spanish helpscreen
|
||||
;Default: Language=1
|
||||
Language=2
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"style": "0x00c800c0",
|
||||
"bounds": {
|
||||
"x": 20,
|
||||
"y": 24,
|
||||
"width": 202,
|
||||
"height": 130
|
||||
},
|
||||
"menu": null,
|
||||
"class": null,
|
||||
"caption": "File Open",
|
||||
"declared_control_count": 10,
|
||||
"controls": [
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 6,
|
||||
"y": 8,
|
||||
"width": 36,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "File&name:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"bounds": {
|
||||
"x": 42,
|
||||
"y": 6,
|
||||
"width": 98,
|
||||
"height": 12
|
||||
},
|
||||
"style": "0x50810080",
|
||||
"class": {
|
||||
"class_atom": "EDIT"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 6,
|
||||
"y": 20,
|
||||
"width": 36,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "Directory:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"bounds": {
|
||||
"x": 42,
|
||||
"y": 20,
|
||||
"width": 98,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 6,
|
||||
"y": 32,
|
||||
"width": 64,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "&Files:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"bounds": {
|
||||
"x": 6,
|
||||
"y": 44,
|
||||
"width": 64,
|
||||
"height": 82
|
||||
},
|
||||
"style": "0x50a10003",
|
||||
"class": {
|
||||
"class_atom": "LISTBOX"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 76,
|
||||
"y": 32,
|
||||
"width": 64,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "&Directories:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"bounds": {
|
||||
"x": 76,
|
||||
"y": 44,
|
||||
"width": 64,
|
||||
"height": 82
|
||||
},
|
||||
"style": "0x50a10003",
|
||||
"class": {
|
||||
"class_atom": "LISTBOX"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"bounds": {
|
||||
"x": 146,
|
||||
"y": 5,
|
||||
"width": 50,
|
||||
"height": 14
|
||||
},
|
||||
"style": "0x50010001",
|
||||
"class": {
|
||||
"class_atom": "BUTTON"
|
||||
},
|
||||
"title": "OK",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"bounds": {
|
||||
"x": 146,
|
||||
"y": 23,
|
||||
"width": 50,
|
||||
"height": 14
|
||||
},
|
||||
"style": "0x50010000",
|
||||
"class": {
|
||||
"class_atom": "BUTTON"
|
||||
},
|
||||
"title": "Cancel",
|
||||
"extra_hex": ""
|
||||
}
|
||||
],
|
||||
"font": {
|
||||
"point_size": 8,
|
||||
"name": "Helv"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"style": "0x00c800c0",
|
||||
"bounds": {
|
||||
"x": 20,
|
||||
"y": 24,
|
||||
"width": 202,
|
||||
"height": 130
|
||||
},
|
||||
"menu": null,
|
||||
"class": null,
|
||||
"caption": "File Save As",
|
||||
"declared_control_count": 8,
|
||||
"controls": [
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 6,
|
||||
"y": 8,
|
||||
"width": 36,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "File&name:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"bounds": {
|
||||
"x": 42,
|
||||
"y": 6,
|
||||
"width": 98,
|
||||
"height": 12
|
||||
},
|
||||
"style": "0x50810080",
|
||||
"class": {
|
||||
"class_atom": "EDIT"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 6,
|
||||
"y": 20,
|
||||
"width": 36,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "Directory:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"bounds": {
|
||||
"x": 42,
|
||||
"y": 20,
|
||||
"width": 98,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"bounds": {
|
||||
"x": 6,
|
||||
"y": 44,
|
||||
"width": 64,
|
||||
"height": 82
|
||||
},
|
||||
"style": "0x50a10003",
|
||||
"class": {
|
||||
"class_atom": "LISTBOX"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 6,
|
||||
"y": 32,
|
||||
"width": 64,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "&Directories:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"bounds": {
|
||||
"x": 146,
|
||||
"y": 5,
|
||||
"width": 50,
|
||||
"height": 14
|
||||
},
|
||||
"style": "0x50010001",
|
||||
"class": {
|
||||
"class_atom": "BUTTON"
|
||||
},
|
||||
"title": "OK",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"bounds": {
|
||||
"x": 146,
|
||||
"y": 23,
|
||||
"width": 50,
|
||||
"height": 14
|
||||
},
|
||||
"style": "0x50010000",
|
||||
"class": {
|
||||
"class_atom": "BUTTON"
|
||||
},
|
||||
"title": "Cancel",
|
||||
"extra_hex": ""
|
||||
}
|
||||
],
|
||||
"font": {
|
||||
"point_size": 8,
|
||||
"name": "Helv"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"style": "0x80c00040",
|
||||
"bounds": {
|
||||
"x": 20,
|
||||
"y": 24,
|
||||
"width": 180,
|
||||
"height": 64
|
||||
},
|
||||
"menu": null,
|
||||
"class": null,
|
||||
"caption": null,
|
||||
"declared_control_count": 4,
|
||||
"controls": [
|
||||
{
|
||||
"id": 101,
|
||||
"bounds": {
|
||||
"x": 10,
|
||||
"y": 20,
|
||||
"width": 160,
|
||||
"height": 12
|
||||
},
|
||||
"style": "0x50810080",
|
||||
"class": {
|
||||
"class_atom": "EDIT"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"bounds": {
|
||||
"x": 10,
|
||||
"y": 8,
|
||||
"width": 160,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"bounds": {
|
||||
"x": 47,
|
||||
"y": 42,
|
||||
"width": 40,
|
||||
"height": 14
|
||||
},
|
||||
"style": "0x50010001",
|
||||
"class": {
|
||||
"class_atom": "BUTTON"
|
||||
},
|
||||
"title": "OK",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"bounds": {
|
||||
"x": 93,
|
||||
"y": 42,
|
||||
"width": 40,
|
||||
"height": 14
|
||||
},
|
||||
"style": "0x50010000",
|
||||
"class": {
|
||||
"class_atom": "BUTTON"
|
||||
},
|
||||
"title": "Cancel",
|
||||
"extra_hex": ""
|
||||
}
|
||||
],
|
||||
"font": {
|
||||
"point_size": 8,
|
||||
"name": "Helv"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"style": "0x80c800c0",
|
||||
"bounds": {
|
||||
"x": 24,
|
||||
"y": 19,
|
||||
"width": 199,
|
||||
"height": 135
|
||||
},
|
||||
"menu": null,
|
||||
"class": "BorDlg",
|
||||
"caption": "File Open",
|
||||
"declared_control_count": 14,
|
||||
"controls": [
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 8,
|
||||
"y": 8,
|
||||
"width": 36,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "File&name:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"bounds": {
|
||||
"x": 44,
|
||||
"y": 6,
|
||||
"width": 104,
|
||||
"height": 12
|
||||
},
|
||||
"style": "0x50810080",
|
||||
"class": {
|
||||
"class_atom": "EDIT"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 8,
|
||||
"y": 20,
|
||||
"width": 36,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "Directory:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"bounds": {
|
||||
"x": 44,
|
||||
"y": 20,
|
||||
"width": 104,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 8,
|
||||
"y": 39,
|
||||
"width": 21,
|
||||
"height": 8
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "&Files:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"bounds": {
|
||||
"x": 8,
|
||||
"y": 48,
|
||||
"width": 64,
|
||||
"height": 82
|
||||
},
|
||||
"style": "0x50a10003",
|
||||
"class": {
|
||||
"class_atom": "LISTBOX"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 84,
|
||||
"y": 39,
|
||||
"width": 40,
|
||||
"height": 8
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "&Directories:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"bounds": {
|
||||
"x": 84,
|
||||
"y": 48,
|
||||
"width": 64,
|
||||
"height": 82
|
||||
},
|
||||
"style": "0x50a10003",
|
||||
"class": {
|
||||
"class_atom": "LISTBOX"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"bounds": {
|
||||
"x": 4,
|
||||
"y": 4,
|
||||
"width": 148,
|
||||
"height": 28
|
||||
},
|
||||
"style": "0x50000001",
|
||||
"class": "BorShade",
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 105,
|
||||
"bounds": {
|
||||
"x": 4,
|
||||
"y": 36,
|
||||
"width": 72,
|
||||
"height": 96
|
||||
},
|
||||
"style": "0x50000001",
|
||||
"class": "BorShade",
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"bounds": {
|
||||
"x": 160,
|
||||
"y": 7,
|
||||
"width": 36,
|
||||
"height": 24
|
||||
},
|
||||
"style": "0x50010001",
|
||||
"class": "BorBtn",
|
||||
"title": "Button",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"bounds": {
|
||||
"x": 160,
|
||||
"y": 37,
|
||||
"width": 36,
|
||||
"height": 24
|
||||
},
|
||||
"style": "0x50010000",
|
||||
"class": "BorBtn",
|
||||
"title": "Button",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 106,
|
||||
"bounds": {
|
||||
"x": 156,
|
||||
"y": -1,
|
||||
"width": 4,
|
||||
"height": 136
|
||||
},
|
||||
"style": "0x50000003",
|
||||
"class": "BorShade",
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 107,
|
||||
"bounds": {
|
||||
"x": 80,
|
||||
"y": 36,
|
||||
"width": 72,
|
||||
"height": 96
|
||||
},
|
||||
"style": "0x50000001",
|
||||
"class": "BorShade",
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
}
|
||||
],
|
||||
"font": {
|
||||
"point_size": 8,
|
||||
"name": "Helv"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"style": "0x80c800c0",
|
||||
"bounds": {
|
||||
"x": 20,
|
||||
"y": 24,
|
||||
"width": 202,
|
||||
"height": 135
|
||||
},
|
||||
"menu": null,
|
||||
"class": "BorDlg",
|
||||
"caption": "File Save As",
|
||||
"declared_control_count": 11,
|
||||
"controls": [
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 8,
|
||||
"y": 8,
|
||||
"width": 36,
|
||||
"height": 12
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "File&name:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"bounds": {
|
||||
"x": 44,
|
||||
"y": 8,
|
||||
"width": 96,
|
||||
"height": 12
|
||||
},
|
||||
"style": "0x50810080",
|
||||
"class": {
|
||||
"class_atom": "EDIT"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 8,
|
||||
"y": 20,
|
||||
"width": 36,
|
||||
"height": 8
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "Directory:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"bounds": {
|
||||
"x": 44,
|
||||
"y": 20,
|
||||
"width": 98,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"bounds": {
|
||||
"x": 8,
|
||||
"y": 48,
|
||||
"width": 64,
|
||||
"height": 82
|
||||
},
|
||||
"style": "0x50a10003",
|
||||
"class": {
|
||||
"class_atom": "LISTBOX"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 8,
|
||||
"y": 37,
|
||||
"width": 64,
|
||||
"height": 8
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": "&Directories:",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 4,
|
||||
"y": 4,
|
||||
"width": 140,
|
||||
"height": 27
|
||||
},
|
||||
"style": "0x50000001",
|
||||
"class": "BorShade",
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 65535,
|
||||
"bounds": {
|
||||
"x": 4,
|
||||
"y": 36,
|
||||
"width": 72,
|
||||
"height": 96
|
||||
},
|
||||
"style": "0x50000001",
|
||||
"class": "BorShade",
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"bounds": {
|
||||
"x": 164,
|
||||
"y": 4,
|
||||
"width": 32,
|
||||
"height": 20
|
||||
},
|
||||
"style": "0x50010001",
|
||||
"class": "BorBtn",
|
||||
"title": "Button",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"bounds": {
|
||||
"x": 164,
|
||||
"y": 32,
|
||||
"width": 32,
|
||||
"height": 20
|
||||
},
|
||||
"style": "0x50010000",
|
||||
"class": "BorBtn",
|
||||
"title": "Button",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 105,
|
||||
"bounds": {
|
||||
"x": 160,
|
||||
"y": 0,
|
||||
"width": 2,
|
||||
"height": 136
|
||||
},
|
||||
"style": "0x50000003",
|
||||
"class": "BorShade",
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
}
|
||||
],
|
||||
"font": {
|
||||
"point_size": 8,
|
||||
"name": "Helv"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"style": "0x80c800c0",
|
||||
"bounds": {
|
||||
"x": 20,
|
||||
"y": 24,
|
||||
"width": 180,
|
||||
"height": 75
|
||||
},
|
||||
"menu": null,
|
||||
"class": "BorDlg",
|
||||
"caption": null,
|
||||
"declared_control_count": 5,
|
||||
"controls": [
|
||||
{
|
||||
"id": 101,
|
||||
"bounds": {
|
||||
"x": 10,
|
||||
"y": 20,
|
||||
"width": 160,
|
||||
"height": 12
|
||||
},
|
||||
"style": "0x50810080",
|
||||
"class": {
|
||||
"class_atom": "EDIT"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 100,
|
||||
"bounds": {
|
||||
"x": 10,
|
||||
"y": 8,
|
||||
"width": 160,
|
||||
"height": 10
|
||||
},
|
||||
"style": "0x50020000",
|
||||
"class": {
|
||||
"class_atom": "STATIC"
|
||||
},
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"bounds": {
|
||||
"x": 6,
|
||||
"y": 5,
|
||||
"width": 168,
|
||||
"height": 32
|
||||
},
|
||||
"style": "0x50000001",
|
||||
"class": "BorShade",
|
||||
"title": null,
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"bounds": {
|
||||
"x": 42,
|
||||
"y": 44,
|
||||
"width": 36,
|
||||
"height": 24
|
||||
},
|
||||
"style": "0x50010001",
|
||||
"class": "BorBtn",
|
||||
"title": "Button",
|
||||
"extra_hex": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"bounds": {
|
||||
"x": 102,
|
||||
"y": 44,
|
||||
"width": 36,
|
||||
"height": 24
|
||||
},
|
||||
"style": "0x50010000",
|
||||
"class": "BorBtn",
|
||||
"title": "Button",
|
||||
"extra_hex": ""
|
||||
}
|
||||
],
|
||||
"font": {
|
||||
"point_size": 8,
|
||||
"name": "Helv"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 766 B |
|
After Width: | Height: | Size: 373 B |
|
After Width: | Height: | Size: 246 B |
|
After Width: | Height: | Size: 202 B |
|
After Width: | Height: | Size: 246 B |
|
After Width: | Height: | Size: 167 B |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 772 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 880 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"printable_strings": [
|
||||
"VS_VERSION_INFO",
|
||||
"StringFileInfo",
|
||||
"040904E4",
|
||||
"CompanyName",
|
||||
"Lars Hammer / KIWI Hamburg",
|
||||
"FileDescription",
|
||||
"TDK Pinball Machine",
|
||||
"FileVersion",
|
||||
"1.00",
|
||||
"InternalName",
|
||||
"TDKPIN",
|
||||
"LegalCopyright",
|
||||
"Developed 1995 by KIWI Von Wendt Konzept GmbH.",
|
||||
"ProductName",
|
||||
"TDK Pinball Machine",
|
||||
"ProductVersion",
|
||||
"3.10.0.0",
|
||||
"VarFileInfo",
|
||||
"Translation"
|
||||
],
|
||||
"fixed_file_info": {
|
||||
"signature": "0xfeef04bd",
|
||||
"structure_version": "0x00010000",
|
||||
"file_version_ms": "0x00000000",
|
||||
"file_version_ls": "0x00010006",
|
||||
"product_version_ms": "0x00030001",
|
||||
"product_version_ls": "0x00000000",
|
||||
"file_flags_mask": "0x00000000",
|
||||
"file_flags": "0x00000000",
|
||||
"file_os": "0x00000001",
|
||||
"file_type": "0x00000002",
|
||||
"file_subtype": "0x00000000",
|
||||
"file_date_ms": "0x00000000",
|
||||
"file_date_ls": "0x00000000"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
GIMP Palette
|
||||
Name: TDKPIN resource 999
|
||||
Columns: 16
|
||||
#
|
||||
0 0 0 Index 0
|
||||
128 0 0 Index 1
|
||||
0 128 0 Index 2
|
||||
128 128 0 Index 3
|
||||
0 0 128 Index 4
|
||||
128 0 128 Index 5
|
||||
0 128 128 Index 6
|
||||
192 192 192 Index 7
|
||||
192 220 192 Index 8
|
||||
166 202 240 Index 9
|
||||
32 32 184 Index 10
|
||||
72 32 184 Index 11
|
||||
112 32 184 Index 12
|
||||
152 32 184 Index 13
|
||||
192 32 184 Index 14
|
||||
232 32 184 Index 15
|
||||
32 32 240 Index 16
|
||||
72 32 240 Index 17
|
||||
112 32 240 Index 18
|
||||
152 32 240 Index 19
|
||||
192 32 240 Index 20
|
||||
232 32 240 Index 21
|
||||
124 76 48 Index 22
|
||||
140 84 56 Index 23
|
||||
32 72 16 Index 24
|
||||
72 72 16 Index 25
|
||||
112 72 16 Index 26
|
||||
152 72 16 Index 27
|
||||
192 72 16 Index 28
|
||||
232 72 16 Index 29
|
||||
32 72 72 Index 30
|
||||
72 72 72 Index 31
|
||||
112 72 72 Index 32
|
||||
152 72 72 Index 33
|
||||
192 72 72 Index 34
|
||||
232 72 72 Index 35
|
||||
32 72 128 Index 36
|
||||
72 72 128 Index 37
|
||||
112 72 128 Index 38
|
||||
152 72 128 Index 39
|
||||
192 72 128 Index 40
|
||||
232 72 128 Index 41
|
||||
32 72 184 Index 42
|
||||
72 72 184 Index 43
|
||||
112 72 184 Index 44
|
||||
152 72 184 Index 45
|
||||
192 72 184 Index 46
|
||||
232 72 184 Index 47
|
||||
32 72 240 Index 48
|
||||
72 72 240 Index 49
|
||||
112 72 240 Index 50
|
||||
152 72 240 Index 51
|
||||
192 72 240 Index 52
|
||||
232 72 240 Index 53
|
||||
152 92 64 Index 54
|
||||
168 100 76 Index 55
|
||||
32 112 16 Index 56
|
||||
72 112 16 Index 57
|
||||
112 112 16 Index 58
|
||||
152 112 16 Index 59
|
||||
192 112 16 Index 60
|
||||
232 112 16 Index 61
|
||||
32 112 72 Index 62
|
||||
72 112 72 Index 63
|
||||
112 112 72 Index 64
|
||||
152 112 72 Index 65
|
||||
192 112 72 Index 66
|
||||
232 112 72 Index 67
|
||||
32 112 128 Index 68
|
||||
72 112 128 Index 69
|
||||
112 112 128 Index 70
|
||||
152 112 128 Index 71
|
||||
192 112 128 Index 72
|
||||
232 112 128 Index 73
|
||||
32 112 184 Index 74
|
||||
72 112 184 Index 75
|
||||
112 112 184 Index 76
|
||||
152 112 184 Index 77
|
||||
192 112 184 Index 78
|
||||
232 112 184 Index 79
|
||||
32 112 240 Index 80
|
||||
72 112 240 Index 81
|
||||
112 112 240 Index 82
|
||||
152 112 240 Index 83
|
||||
192 112 240 Index 84
|
||||
232 112 240 Index 85
|
||||
176 116 96 Index 86
|
||||
188 136 104 Index 87
|
||||
32 152 16 Index 88
|
||||
72 152 16 Index 89
|
||||
112 152 16 Index 90
|
||||
152 152 16 Index 91
|
||||
192 152 16 Index 92
|
||||
232 152 16 Index 93
|
||||
32 152 72 Index 94
|
||||
72 152 72 Index 95
|
||||
112 152 72 Index 96
|
||||
152 152 72 Index 97
|
||||
192 152 72 Index 98
|
||||
232 152 72 Index 99
|
||||
32 152 128 Index 100
|
||||
72 152 128 Index 101
|
||||
112 152 128 Index 102
|
||||
152 152 128 Index 103
|
||||
192 152 128 Index 104
|
||||
232 152 128 Index 105
|
||||
32 152 184 Index 106
|
||||
72 152 184 Index 107
|
||||
112 152 184 Index 108
|
||||
152 152 184 Index 109
|
||||
192 152 184 Index 110
|
||||
232 152 184 Index 111
|
||||
32 152 240 Index 112
|
||||
72 152 240 Index 113
|
||||
112 152 240 Index 114
|
||||
152 152 240 Index 115
|
||||
192 152 240 Index 116
|
||||
232 152 240 Index 117
|
||||
200 144 108 Index 118
|
||||
208 160 116 Index 119
|
||||
32 192 16 Index 120
|
||||
72 192 16 Index 121
|
||||
112 192 16 Index 122
|
||||
152 192 16 Index 123
|
||||
192 192 16 Index 124
|
||||
232 192 16 Index 125
|
||||
32 192 72 Index 126
|
||||
72 192 72 Index 127
|
||||
112 192 72 Index 128
|
||||
152 192 72 Index 129
|
||||
192 192 72 Index 130
|
||||
232 192 72 Index 131
|
||||
32 192 128 Index 132
|
||||
72 192 128 Index 133
|
||||
112 192 128 Index 134
|
||||
152 192 128 Index 135
|
||||
192 192 128 Index 136
|
||||
232 192 128 Index 137
|
||||
32 192 184 Index 138
|
||||
72 192 184 Index 139
|
||||
112 192 184 Index 140
|
||||
152 192 184 Index 141
|
||||
192 192 184 Index 142
|
||||
232 192 184 Index 143
|
||||
32 192 240 Index 144
|
||||
72 192 240 Index 145
|
||||
112 192 240 Index 146
|
||||
152 192 240 Index 147
|
||||
192 192 240 Index 148
|
||||
232 192 240 Index 149
|
||||
216 168 128 Index 150
|
||||
224 176 144 Index 151
|
||||
32 232 16 Index 152
|
||||
72 232 16 Index 153
|
||||
112 232 16 Index 154
|
||||
152 232 16 Index 155
|
||||
192 232 16 Index 156
|
||||
232 232 16 Index 157
|
||||
32 232 72 Index 158
|
||||
72 232 72 Index 159
|
||||
112 232 72 Index 160
|
||||
152 232 72 Index 161
|
||||
192 232 72 Index 162
|
||||
232 232 72 Index 163
|
||||
32 232 128 Index 164
|
||||
72 232 128 Index 165
|
||||
112 232 128 Index 166
|
||||
152 232 128 Index 167
|
||||
192 232 128 Index 168
|
||||
232 232 128 Index 169
|
||||
32 232 184 Index 170
|
||||
72 232 184 Index 171
|
||||
112 232 184 Index 172
|
||||
152 232 184 Index 173
|
||||
192 232 184 Index 174
|
||||
232 232 184 Index 175
|
||||
32 232 240 Index 176
|
||||
72 232 240 Index 177
|
||||
112 232 240 Index 178
|
||||
152 232 240 Index 179
|
||||
192 232 240 Index 180
|
||||
232 232 240 Index 181
|
||||
232 192 156 Index 182
|
||||
240 212 184 Index 183
|
||||
0 56 0 Index 184
|
||||
0 96 0 Index 185
|
||||
0 160 0 Index 186
|
||||
0 208 0 Index 187
|
||||
208 208 0 Index 188
|
||||
160 160 0 Index 189
|
||||
96 96 0 Index 190
|
||||
56 56 0 Index 191
|
||||
56 0 0 Index 192
|
||||
96 0 0 Index 193
|
||||
160 0 0 Index 194
|
||||
208 0 0 Index 195
|
||||
208 0 208 Index 196
|
||||
160 0 160 Index 197
|
||||
96 0 96 Index 198
|
||||
56 0 56 Index 199
|
||||
0 0 56 Index 200
|
||||
0 0 96 Index 201
|
||||
0 0 160 Index 202
|
||||
0 0 208 Index 203
|
||||
16 16 16 Index 204
|
||||
32 32 32 Index 205
|
||||
48 48 48 Index 206
|
||||
64 64 64 Index 207
|
||||
80 80 80 Index 208
|
||||
96 96 96 Index 209
|
||||
112 112 112 Index 210
|
||||
144 144 144 Index 211
|
||||
160 160 160 Index 212
|
||||
176 176 176 Index 213
|
||||
208 208 208 Index 214
|
||||
224 224 224 Index 215
|
||||
240 240 240 Index 216
|
||||
76 40 28 Index 217
|
||||
76 28 40 Index 218
|
||||
40 76 28 Index 219
|
||||
28 76 40 Index 220
|
||||
40 28 76 Index 221
|
||||
28 40 76 Index 222
|
||||
108 88 32 Index 223
|
||||
160 124 88 Index 224
|
||||
232 168 104 Index 225
|
||||
84 48 16 Index 226
|
||||
92 56 40 Index 227
|
||||
32 32 16 Index 228
|
||||
72 32 16 Index 229
|
||||
112 32 16 Index 230
|
||||
152 32 16 Index 231
|
||||
192 32 16 Index 232
|
||||
232 32 16 Index 233
|
||||
32 32 72 Index 234
|
||||
72 32 72 Index 235
|
||||
112 32 72 Index 236
|
||||
152 32 72 Index 237
|
||||
192 32 72 Index 238
|
||||
232 32 72 Index 239
|
||||
32 32 128 Index 240
|
||||
72 32 128 Index 241
|
||||
112 32 128 Index 242
|
||||
152 32 128 Index 243
|
||||
192 32 128 Index 244
|
||||
232 32 128 Index 245
|
||||
255 251 240 Index 246
|
||||
160 160 164 Index 247
|
||||
128 128 128 Index 248
|
||||
255 0 0 Index 249
|
||||
0 255 0 Index 250
|
||||
255 255 0 Index 251
|
||||
0 0 255 Index 252
|
||||
255 0 255 Index 253
|
||||
0 255 255 Index 254
|
||||
255 255 255 Index 255
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
use macroquad::{
|
||||
audio::{PlaySoundParams, Sound, load_sound_from_bytes, play_sound},
|
||||
prelude::{FilterMode, Texture2D},
|
||||
};
|
||||
|
||||
pub struct Assets {
|
||||
pub active_table: Texture2D,
|
||||
pub inactive_table: Texture2D,
|
||||
pub loading: Texture2D,
|
||||
pub help: [Texture2D; 5],
|
||||
pub media: [Texture2D; 4],
|
||||
pub diamond: [Texture2D; 9],
|
||||
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 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 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 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,
|
||||
help,
|
||||
media,
|
||||
diamond,
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn texture(bytes: &[u8]) -> Texture2D {
|
||||
let texture = Texture2D::from_file_with_format(bytes, None);
|
||||
texture.set_filter(FilterMode::Nearest);
|
||||
texture
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
use crate::geometry::{Segment, circle_collision, segment_collision};
|
||||
use macroquad::prelude::{Vec2, vec2};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
const BALL_RADIUS: f32 = 5.0;
|
||||
const GRAVITY: f32 = 135.0;
|
||||
const MAX_SPEED: f32 = 430.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Controls {
|
||||
pub left_flipper: bool,
|
||||
pub right_flipper: bool,
|
||||
pub launch_pressed: bool,
|
||||
pub nudge: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
Flipper,
|
||||
Launch,
|
||||
Bumper,
|
||||
Target,
|
||||
Wheel,
|
||||
Robot,
|
||||
Lock,
|
||||
Media,
|
||||
ExtraBall,
|
||||
Nudge,
|
||||
Drain,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Player {
|
||||
pub score: u32,
|
||||
pub balls: u8,
|
||||
pub extra_balls: u8,
|
||||
pub bumper_value: u32,
|
||||
pub diamond_segments: u8,
|
||||
pub media_level: u8,
|
||||
}
|
||||
|
||||
impl Default for Player {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
score: 0,
|
||||
balls: 3,
|
||||
extra_balls: 0,
|
||||
bumper_value: 1_000,
|
||||
diamond_segments: 0,
|
||||
media_level: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Ball {
|
||||
pub position: Vec2,
|
||||
pub velocity: Vec2,
|
||||
pub in_launcher: bool,
|
||||
}
|
||||
|
||||
impl Default for Ball {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: vec2(157.0, 426.0),
|
||||
velocity: Vec2::ZERO,
|
||||
in_launcher: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Game {
|
||||
pub players: Vec<Player>,
|
||||
pub current_player: usize,
|
||||
pub ball: Ball,
|
||||
pub bonus: u32,
|
||||
pub wheel_holes: [bool; 8],
|
||||
pub side_targets: [bool; 5],
|
||||
pub top_targets: [bool; 3],
|
||||
pub lock_lights: u8,
|
||||
pub magnets: f32,
|
||||
pub tilt: f32,
|
||||
pub left_flipper_angle: f32,
|
||||
pub right_flipper_angle: f32,
|
||||
pub finished: bool,
|
||||
accumulator: f32,
|
||||
target_cooldown: f32,
|
||||
bumper_cooldown: f32,
|
||||
nudge_cooldown: f32,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new(player_count: usize) -> Self {
|
||||
Self {
|
||||
players: vec![Player::default(); player_count.clamp(1, 4)],
|
||||
current_player: 0,
|
||||
ball: Ball::default(),
|
||||
bonus: 0,
|
||||
wheel_holes: [false; 8],
|
||||
side_targets: [false; 5],
|
||||
top_targets: [false; 3],
|
||||
lock_lights: 0,
|
||||
magnets: 0.0,
|
||||
tilt: 0.0,
|
||||
left_flipper_angle: -2.72,
|
||||
right_flipper_angle: -0.42,
|
||||
finished: false,
|
||||
accumulator: 0.0,
|
||||
target_cooldown: 0.0,
|
||||
bumper_cooldown: 0.0,
|
||||
nudge_cooldown: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn player(&self) -> &Player {
|
||||
&self.players[self.current_player]
|
||||
}
|
||||
|
||||
pub fn update(&mut self, frame_time: f32, detail: u8, controls: Controls) -> Vec<Event> {
|
||||
if self.finished {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut events = Vec::new();
|
||||
let desired_left = if controls.left_flipper { -2.18 } else { -2.72 };
|
||||
let desired_right = if controls.right_flipper { -0.96 } else { -0.42 };
|
||||
let flipper_rate = 14.0 * frame_time;
|
||||
let old_left = self.left_flipper_angle;
|
||||
let old_right = self.right_flipper_angle;
|
||||
self.left_flipper_angle +=
|
||||
(desired_left - self.left_flipper_angle).clamp(-flipper_rate, flipper_rate);
|
||||
self.right_flipper_angle +=
|
||||
(desired_right - self.right_flipper_angle).clamp(-flipper_rate, flipper_rate);
|
||||
if (controls.left_flipper && old_left < -2.68)
|
||||
|| (controls.right_flipper && old_right > -0.46)
|
||||
{
|
||||
events.push(Event::Flipper);
|
||||
}
|
||||
|
||||
if controls.launch_pressed && self.ball.in_launcher {
|
||||
self.ball.in_launcher = false;
|
||||
self.ball.velocity = vec2(12.0, -330.0);
|
||||
events.push(Event::Launch);
|
||||
}
|
||||
if controls.nudge.abs() > 0.1 && self.nudge_cooldown <= 0.0 {
|
||||
self.ball.velocity.x += controls.nudge * 55.0;
|
||||
self.tilt += controls.nudge.abs() * 0.34;
|
||||
self.nudge_cooldown = 0.22;
|
||||
events.push(Event::Nudge);
|
||||
}
|
||||
|
||||
self.accumulator = (self.accumulator + frame_time.min(0.05)).min(0.1);
|
||||
let steps_per_second =
|
||||
[60.0, 90.0, 120.0, 180.0, 240.0][usize::from(detail.clamp(1, 5) - 1)];
|
||||
let step = 1.0 / steps_per_second;
|
||||
while self.accumulator >= step {
|
||||
self.fixed_update(step, &mut events);
|
||||
self.accumulator -= step;
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) {
|
||||
self.target_cooldown = (self.target_cooldown - dt).max(0.0);
|
||||
self.bumper_cooldown = (self.bumper_cooldown - dt).max(0.0);
|
||||
self.nudge_cooldown = (self.nudge_cooldown - dt).max(0.0);
|
||||
self.magnets = (self.magnets - dt).max(0.0);
|
||||
self.tilt = (self.tilt - dt * 0.08).max(0.0);
|
||||
|
||||
if self.ball.in_launcher {
|
||||
self.ball.position = vec2(157.0, 426.0);
|
||||
self.ball.velocity = Vec2::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
self.ball.velocity.y += GRAVITY * dt;
|
||||
self.ball.velocity *= 1.0 - dt * 0.055;
|
||||
self.ball.velocity = self.ball.velocity.clamp_length_max(MAX_SPEED);
|
||||
self.ball.position += self.ball.velocity * dt;
|
||||
|
||||
for wall in table_walls() {
|
||||
segment_collision(
|
||||
&mut self.ball.position,
|
||||
&mut self.ball.velocity,
|
||||
BALL_RADIUS,
|
||||
*wall,
|
||||
);
|
||||
}
|
||||
|
||||
let left_flipper = flipper_segment(vec2(103.0, 394.0), self.left_flipper_angle);
|
||||
let right_flipper = flipper_segment(vec2(211.0, 394.0), self.right_flipper_angle);
|
||||
if segment_collision(
|
||||
&mut self.ball.position,
|
||||
&mut self.ball.velocity,
|
||||
BALL_RADIUS + 2.0,
|
||||
left_flipper,
|
||||
) && self.left_flipper_angle > -2.55
|
||||
{
|
||||
self.ball.velocity += vec2(-20.0, -115.0);
|
||||
}
|
||||
if segment_collision(
|
||||
&mut self.ball.position,
|
||||
&mut self.ball.velocity,
|
||||
BALL_RADIUS + 2.0,
|
||||
right_flipper,
|
||||
) && self.right_flipper_angle < -0.58
|
||||
{
|
||||
self.ball.velocity += vec2(20.0, -115.0);
|
||||
}
|
||||
|
||||
for center in [vec2(207.0, 109.0), vec2(167.0, 148.0), vec2(219.0, 167.0)] {
|
||||
if circle_collision(
|
||||
&mut self.ball.position,
|
||||
&mut self.ball.velocity,
|
||||
BALL_RADIUS,
|
||||
center,
|
||||
14.0,
|
||||
105.0,
|
||||
) && self.bumper_cooldown <= 0.0
|
||||
{
|
||||
self.add_score(self.player().bumper_value);
|
||||
self.bonus = self.bonus.saturating_add(100);
|
||||
self.bumper_cooldown = 0.08;
|
||||
events.push(Event::Bumper);
|
||||
}
|
||||
}
|
||||
|
||||
self.check_targets(events);
|
||||
self.check_media(events);
|
||||
|
||||
if self.ball.position.y > 454.0 {
|
||||
if self.magnets > 0.0 && (self.ball.position.x < 145.0 || self.ball.position.x > 175.0)
|
||||
{
|
||||
self.ball.position.y = 410.0;
|
||||
self.ball.velocity = vec2((157.0 - self.ball.position.x) * 2.0, -245.0);
|
||||
self.magnets = 0.0;
|
||||
} else {
|
||||
self.drain(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_targets(&mut self, events: &mut Vec<Event>) {
|
||||
if self.target_cooldown > 0.0 {
|
||||
return;
|
||||
}
|
||||
let position = self.ball.position;
|
||||
|
||||
let top = [vec2(205.0, 47.0), vec2(234.0, 50.0), vec2(262.0, 53.0)];
|
||||
for (index, center) in top.into_iter().enumerate() {
|
||||
if position.distance_squared(center) < 11.0_f32.powi(2) && !self.top_targets[index] {
|
||||
self.top_targets[index] = true;
|
||||
self.add_score(1_500);
|
||||
self.ball.velocity.y = self.ball.velocity.y.abs() + 65.0;
|
||||
self.target_cooldown = 0.12;
|
||||
events.push(Event::Target);
|
||||
}
|
||||
}
|
||||
if self.top_targets.iter().all(|target| *target) {
|
||||
self.top_targets.fill(false);
|
||||
self.magnets = 12.0;
|
||||
self.add_score(5_000);
|
||||
}
|
||||
|
||||
let side = [180.0, 195.0, 210.0, 225.0, 240.0];
|
||||
for (index, y) in side.into_iter().enumerate() {
|
||||
if position.distance_squared(vec2(286.0, y)) < 9.0_f32.powi(2)
|
||||
&& !self.side_targets[index]
|
||||
{
|
||||
self.side_targets[index] = true;
|
||||
self.add_score(2_000 + u32::try_from(index).unwrap_or(0) * 1_000);
|
||||
self.ball.velocity.x = -self.ball.velocity.x.abs() - 55.0;
|
||||
self.target_cooldown = 0.10;
|
||||
events.push(Event::Target);
|
||||
}
|
||||
}
|
||||
if self.side_targets.iter().all(|target| *target) {
|
||||
self.side_targets.fill(false);
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.bumper_value = (player.bumper_value + 1_000).min(10_000);
|
||||
player.diamond_segments = (player.diamond_segments + 1).min(9);
|
||||
self.add_score(10_000);
|
||||
}
|
||||
|
||||
let wheel_center = vec2(114.0, 79.0);
|
||||
let from_wheel = position - wheel_center;
|
||||
if (25.0..=43.0).contains(&from_wheel.length()) {
|
||||
let angle =
|
||||
(from_wheel.y.atan2(from_wheel.x) + std::f32::consts::TAU) % std::f32::consts::TAU;
|
||||
let sector_boundaries = [
|
||||
std::f32::consts::TAU / 16.0,
|
||||
std::f32::consts::TAU * 3.0 / 16.0,
|
||||
std::f32::consts::TAU * 5.0 / 16.0,
|
||||
std::f32::consts::TAU * 7.0 / 16.0,
|
||||
std::f32::consts::TAU * 9.0 / 16.0,
|
||||
std::f32::consts::TAU * 11.0 / 16.0,
|
||||
std::f32::consts::TAU * 13.0 / 16.0,
|
||||
std::f32::consts::TAU * 15.0 / 16.0,
|
||||
];
|
||||
let hole = sector_boundaries.partition_point(|boundary| angle >= *boundary) % 8;
|
||||
if !self.wheel_holes[hole] {
|
||||
self.wheel_holes[hole] = true;
|
||||
self.add_score(2_500);
|
||||
self.target_cooldown = 0.15;
|
||||
events.push(Event::Wheel);
|
||||
}
|
||||
if self.wheel_holes.iter().all(|hole| *hole) {
|
||||
self.wheel_holes.fill(false);
|
||||
self.add_score(25_000);
|
||||
}
|
||||
}
|
||||
|
||||
if (270.0..=302.0).contains(&position.x) && (45.0..=105.0).contains(&position.y) {
|
||||
self.add_score(7_500);
|
||||
self.ball.velocity = vec2(-125.0, 60.0);
|
||||
self.target_cooldown = 0.3;
|
||||
events.push(Event::Robot);
|
||||
}
|
||||
|
||||
if (151.0..=220.0).contains(&position.x) && (187.0..=205.0).contains(&position.y) {
|
||||
self.lock_lights = (self.lock_lights + 1).min(4);
|
||||
self.add_score(u32::from(self.lock_lights) * 5_000);
|
||||
self.ball.position = vec2(185.0, 181.0);
|
||||
self.ball.velocity = vec2(-40.0 + f32::from(self.lock_lights) * 18.0, -120.0);
|
||||
self.target_cooldown = 0.4;
|
||||
events.push(Event::Lock);
|
||||
}
|
||||
|
||||
for (center, value) in [
|
||||
(vec2(113.0, 285.0), 2_000),
|
||||
(vec2(132.0, 270.0), 3_000),
|
||||
(vec2(155.0, 258.0), 4_000),
|
||||
(vec2(181.0, 270.0), 5_000),
|
||||
(vec2(201.0, 285.0), 6_000),
|
||||
] {
|
||||
if position.distance_squared(center) < 10.0_f32.powi(2) {
|
||||
self.add_score(value);
|
||||
self.ball.velocity.y = -self.ball.velocity.y.abs() - 40.0;
|
||||
self.target_cooldown = 0.13;
|
||||
events.push(Event::Target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_media(&mut self, events: &mut Vec<Event>) {
|
||||
const THRESHOLDS: [u32; 5] = [360_000, 720_000, 1_440_000, 2_880_000, 5_760_000];
|
||||
let score = self.player().score;
|
||||
let level =
|
||||
u8::try_from(THRESHOLDS.partition_point(|threshold| score >= *threshold)).unwrap_or(5);
|
||||
if level > self.player().media_level {
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.media_level = level;
|
||||
player.extra_balls = player.extra_balls.saturating_add(1);
|
||||
events.push(Event::Media);
|
||||
events.push(Event::ExtraBall);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_score(&mut self, points: u32) {
|
||||
let multiplier = if self.player().diamond_segments == 9 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.score = player
|
||||
.score
|
||||
.saturating_add(points.saturating_mul(multiplier));
|
||||
}
|
||||
|
||||
fn drain(&mut self, events: &mut Vec<Event>) {
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.score = player.score.saturating_add(self.bonus);
|
||||
if player.extra_balls > 0 {
|
||||
player.extra_balls -= 1;
|
||||
} else {
|
||||
player.balls = player.balls.saturating_sub(1);
|
||||
}
|
||||
events.push(Event::Drain);
|
||||
self.bonus = 0;
|
||||
self.side_targets.fill(false);
|
||||
self.top_targets.fill(false);
|
||||
self.wheel_holes.fill(false);
|
||||
self.lock_lights = 0;
|
||||
self.magnets = 0.0;
|
||||
self.tilt = 0.0;
|
||||
|
||||
let mut next = (self.current_player + 1) % self.players.len();
|
||||
for _ in 0..self.players.len() {
|
||||
if self.players[next].balls > 0 || self.players[next].extra_balls > 0 {
|
||||
self.current_player = next;
|
||||
self.ball = Ball::default();
|
||||
return;
|
||||
}
|
||||
next = (next + 1) % self.players.len();
|
||||
}
|
||||
self.finished = true;
|
||||
}
|
||||
|
||||
pub fn flippers(&self) -> (Segment, Segment) {
|
||||
(
|
||||
flipper_segment(vec2(103.0, 394.0), self.left_flipper_angle),
|
||||
flipper_segment(vec2(211.0, 394.0), self.right_flipper_angle),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn flipper_segment(pivot: Vec2, angle: f32) -> Segment {
|
||||
Segment::new(pivot, pivot + vec2(angle.cos(), angle.sin()) * 49.0, 0.88)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn table_walls() -> &'static [Segment] {
|
||||
static WALLS: LazyLock<Vec<Segment>> = LazyLock::new(|| {
|
||||
vec![
|
||||
Segment::new(Vec2::new(125.0, 21.0), Vec2::new(7.0, 61.0), 0.82),
|
||||
Segment::new(Vec2::new(300.0, 3.0), Vec2::new(10.0, 3.0), 0.82),
|
||||
Segment::new(Vec2::new(120.0, 6.0), Vec2::new(120.0, 26.0), 0.82),
|
||||
Segment::new(Vec2::new(195.0, 26.0), Vec2::new(195.0, 6.0), 0.82),
|
||||
Segment::new(Vec2::new(134.0, 33.0), Vec2::new(134.0, 16.0), 0.82),
|
||||
Segment::new(Vec2::new(148.0, 11.0), Vec2::new(148.0, 33.0), 0.82),
|
||||
Segment::new(Vec2::new(164.0, 12.0), Vec2::new(165.0, 33.0), 0.82),
|
||||
Segment::new(Vec2::new(180.0, 11.0), Vec2::new(179.0, 33.0), 0.82),
|
||||
Segment::new(Vec2::new(304.0, 64.0), Vec2::new(183.0, 21.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 417.0), Vec2::new(298.0, 313.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 336.0), Vec2::new(310.0, 336.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 374.0), Vec2::new(291.0, 342.0), 0.82),
|
||||
Segment::new(Vec2::new(281.0, 378.0), Vec2::new(300.0, 369.0), 0.82),
|
||||
Segment::new(Vec2::new(275.0, 343.0), Vec2::new(290.0, 382.0), 0.82),
|
||||
Segment::new(Vec2::new(248.0, 364.0), Vec2::new(264.0, 339.0), 0.82),
|
||||
Segment::new(Vec2::new(257.0, 387.0), Vec2::new(249.0, 364.0), 0.82),
|
||||
Segment::new(Vec2::new(277.0, 429.0), Vec2::new(257.0, 387.0), 0.82),
|
||||
Segment::new(Vec2::new(287.0, 446.0), Vec2::new(277.0, 427.0), 0.82),
|
||||
Segment::new(Vec2::new(303.0, 430.0), Vec2::new(283.0, 436.0), 0.82),
|
||||
Segment::new(Vec2::new(320.0, 410.0), Vec2::new(302.0, 430.0), 0.82),
|
||||
Segment::new(Vec2::new(320.0, 21.0), Vec2::new(320.0, 410.0), 0.82),
|
||||
Segment::new(Vec2::new(307.0, 437.0), Vec2::new(316.0, 430.0), 0.82),
|
||||
Segment::new(Vec2::new(296.0, 442.0), Vec2::new(307.0, 437.0), 0.82),
|
||||
Segment::new(Vec2::new(285.0, 443.0), Vec2::new(296.0, 442.0), 0.82),
|
||||
Segment::new(Vec2::new(112.0, 443.0), Vec2::new(285.0, 443.0), 0.82),
|
||||
Segment::new(Vec2::new(99.0, 438.0), Vec2::new(121.0, 444.0), 0.82),
|
||||
Segment::new(Vec2::new(78.0, 427.0), Vec2::new(100.0, 438.0), 0.82),
|
||||
Segment::new(Vec2::new(66.0, 409.0), Vec2::new(79.0, 428.0), 0.82),
|
||||
Segment::new(Vec2::new(57.0, 385.0), Vec2::new(68.0, 413.0), 0.82),
|
||||
Segment::new(Vec2::new(59.0, 304.0), Vec2::new(59.0, 396.0), 0.82),
|
||||
Segment::new(Vec2::new(67.0, 288.0), Vec2::new(58.0, 311.0), 0.82),
|
||||
Segment::new(Vec2::new(41.0, 268.0), Vec2::new(64.0, 279.0), 0.82),
|
||||
Segment::new(Vec2::new(22.0, 293.0), Vec2::new(31.0, 272.0), 0.82),
|
||||
Segment::new(Vec2::new(22.0, 447.0), Vec2::new(22.0, 293.0), 0.82),
|
||||
Segment::new(Vec2::new(11.0, 445.0), Vec2::new(23.0, 445.0), 0.82),
|
||||
Segment::new(Vec2::new(13.0, 288.0), Vec2::new(13.0, 448.0), 0.82),
|
||||
Segment::new(Vec2::new(32.0, 228.0), Vec2::new(12.0, 293.0), 0.82),
|
||||
Segment::new(Vec2::new(10.0, 158.0), Vec2::new(33.0, 227.0), 0.82),
|
||||
Segment::new(Vec2::new(12.0, 51.0), Vec2::new(12.0, 197.0), 0.82),
|
||||
Segment::new(Vec2::new(37.0, 111.0), Vec2::new(37.0, 166.0), 0.82),
|
||||
Segment::new(Vec2::new(110.0, 74.0), Vec2::new(37.0, 111.0), 0.82),
|
||||
Segment::new(Vec2::new(19.0, 73.0), Vec2::new(92.0, 50.0), 0.82),
|
||||
Segment::new(Vec2::new(19.0, 167.0), Vec2::new(19.0, 73.0), 0.82),
|
||||
Segment::new(Vec2::new(141.0, 45.0), Vec2::new(112.0, 72.0), 0.82),
|
||||
Segment::new(Vec2::new(97.0, 47.0), Vec2::new(130.0, 31.0), 0.82),
|
||||
Segment::new(Vec2::new(0.0, 433.0), Vec2::new(7.0, 478.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 73.0), Vec2::new(292.0, 167.0), 0.82),
|
||||
Segment::new(Vec2::new(221.0, 50.0), Vec2::new(292.0, 73.0), 0.82),
|
||||
Segment::new(Vec2::new(275.0, 111.0), Vec2::new(205.0, 74.0), 0.82),
|
||||
Segment::new(Vec2::new(275.0, 166.0), Vec2::new(275.0, 111.0), 0.82),
|
||||
Segment::new(Vec2::new(177.0, 31.0), Vec2::new(216.0, 47.0), 0.82),
|
||||
Segment::new(Vec2::new(201.0, 72.0), Vec2::new(172.0, 45.0), 0.82),
|
||||
Segment::new(Vec2::new(40.0, 257.0), Vec2::new(73.0, 273.0), 0.82),
|
||||
Segment::new(Vec2::new(73.0, 273.0), Vec2::new(67.0, 285.0), 0.82),
|
||||
Segment::new(Vec2::new(34.0, 269.0), Vec2::new(40.0, 257.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 231.0), Vec2::new(292.0, 205.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 205.0), Vec2::new(302.0, 205.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 231.0), Vec2::new(292.0, 231.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 247.0), Vec2::new(292.0, 222.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 222.0), Vec2::new(302.0, 222.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 247.0), Vec2::new(292.0, 247.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 261.0), Vec2::new(292.0, 237.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 237.0), Vec2::new(302.0, 237.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 261.0), Vec2::new(292.0, 261.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 277.0), Vec2::new(292.0, 252.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 252.0), Vec2::new(302.0, 252.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 276.0), Vec2::new(292.0, 276.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 291.0), Vec2::new(292.0, 268.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 269.0), Vec2::new(302.0, 268.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 295.0), Vec2::new(292.0, 290.0), 0.82),
|
||||
Segment::new(Vec2::new(148.0, 254.0), Vec2::new(203.0, 236.0), 0.82),
|
||||
Segment::new(Vec2::new(215.0, 261.0), Vec2::new(158.0, 280.0), 0.82),
|
||||
Segment::new(Vec2::new(187.0, 232.0), Vec2::new(209.0, 225.0), 0.82),
|
||||
Segment::new(Vec2::new(209.0, 225.0), Vec2::new(214.0, 240.0), 0.82),
|
||||
Segment::new(Vec2::new(191.0, 244.0), Vec2::new(187.0, 232.0), 0.82),
|
||||
Segment::new(Vec2::new(169.0, 239.0), Vec2::new(198.0, 229.0), 0.82),
|
||||
Segment::new(Vec2::new(198.0, 229.0), Vec2::new(202.0, 241.0), 0.82),
|
||||
Segment::new(Vec2::new(173.0, 251.0), Vec2::new(169.0, 239.0), 0.82),
|
||||
Segment::new(Vec2::new(153.0, 245.0), Vec2::new(182.0, 235.0), 0.82),
|
||||
Segment::new(Vec2::new(182.0, 235.0), Vec2::new(186.0, 247.0), 0.82),
|
||||
Segment::new(Vec2::new(157.0, 257.0), Vec2::new(153.0, 245.0), 0.82),
|
||||
Segment::new(Vec2::new(139.0, 252.0), Vec2::new(163.0, 241.0), 0.82),
|
||||
Segment::new(Vec2::new(163.0, 241.0), Vec2::new(167.0, 253.0), 0.82),
|
||||
Segment::new(Vec2::new(144.0, 267.0), Vec2::new(139.0, 252.0), 0.82),
|
||||
Segment::new(Vec2::new(84.0, 318.0), Vec2::new(105.0, 329.0), 0.82),
|
||||
Segment::new(Vec2::new(105.0, 329.0), Vec2::new(101.0, 339.0), 0.82),
|
||||
Segment::new(Vec2::new(80.0, 328.0), Vec2::new(84.0, 318.0), 0.82),
|
||||
Segment::new(Vec2::new(87.0, 327.0), Vec2::new(111.0, 339.0), 0.82),
|
||||
Segment::new(Vec2::new(77.0, 370.0), Vec2::new(77.0, 331.0), 0.82),
|
||||
Segment::new(Vec2::new(87.0, 372.0), Vec2::new(77.0, 370.0), 0.82),
|
||||
Segment::new(Vec2::new(110.0, 337.0), Vec2::new(87.0, 372.0), 0.82),
|
||||
Segment::new(Vec2::new(184.0, 426.0), Vec2::new(172.0, 403.0), 0.82),
|
||||
Segment::new(Vec2::new(193.0, 396.0), Vec2::new(205.0, 417.0), 0.82),
|
||||
Segment::new(Vec2::new(213.0, 426.0), Vec2::new(201.0, 403.0), 0.82),
|
||||
Segment::new(Vec2::new(222.0, 396.0), Vec2::new(234.0, 417.0), 0.82),
|
||||
Segment::new(Vec2::new(242.0, 426.0), Vec2::new(230.0, 403.0), 0.82),
|
||||
Segment::new(Vec2::new(251.0, 396.0), Vec2::new(263.0, 417.0), 0.82),
|
||||
Segment::new(Vec2::new(291.0, 205.0), Vec2::new(298.0, 187.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 378.0), Vec2::new(298.0, 58.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 288.0), Vec2::new(298.0, 211.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 313.0), Vec2::new(291.0, 290.0), 0.82),
|
||||
]
|
||||
});
|
||||
&WALLS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn player_count_is_bounded() {
|
||||
assert_eq!(Game::new(0).players.len(), 1);
|
||||
assert_eq!(Game::new(99).players.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_is_deterministic() {
|
||||
let mut game = Game::new(1);
|
||||
let events = game.update(
|
||||
1.0 / 60.0,
|
||||
3,
|
||||
Controls {
|
||||
launch_pressed: true,
|
||||
..Controls::default()
|
||||
},
|
||||
);
|
||||
assert!(events.contains(&Event::Launch));
|
||||
assert!(!game.ball.in_launcher);
|
||||
assert!(game.ball.velocity.y < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_diamond_doubles_score() {
|
||||
let mut game = Game::new(1);
|
||||
game.players[0].diamond_segments = 9;
|
||||
game.add_score(1_000);
|
||||
assert_eq!(game.players[0].score, 2_000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
use macroquad::prelude::Vec2;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Segment {
|
||||
pub start: Vec2,
|
||||
pub end: Vec2,
|
||||
pub bounce: f32,
|
||||
}
|
||||
|
||||
impl Segment {
|
||||
pub const fn new(start: Vec2, end: Vec2, bounce: f32) -> Self {
|
||||
Self { start, end, bounce }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn closest_point(point: Vec2, segment: Segment) -> Vec2 {
|
||||
let line = segment.end - segment.start;
|
||||
let length_squared = line.length_squared();
|
||||
if length_squared <= f32::EPSILON {
|
||||
return segment.start;
|
||||
}
|
||||
let t = ((point - segment.start).dot(line) / length_squared).clamp(0.0, 1.0);
|
||||
segment.start + line * t
|
||||
}
|
||||
|
||||
pub fn segment_collision(
|
||||
position: &mut Vec2,
|
||||
velocity: &mut Vec2,
|
||||
radius: f32,
|
||||
segment: Segment,
|
||||
) -> bool {
|
||||
let point = closest_point(*position, segment);
|
||||
let offset = *position - point;
|
||||
let distance_squared = offset.length_squared();
|
||||
if distance_squared >= radius * radius {
|
||||
return false;
|
||||
}
|
||||
|
||||
let normal = if distance_squared > 0.000_001 {
|
||||
offset / distance_squared.sqrt()
|
||||
} else {
|
||||
let line = segment.end - segment.start;
|
||||
Vec2::new(-line.y, line.x).normalize_or_zero()
|
||||
};
|
||||
*position = point + normal * radius;
|
||||
let inward_speed = velocity.dot(normal);
|
||||
if inward_speed < 0.0 {
|
||||
*velocity -= normal * inward_speed * (1.0 + segment.bounce);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn circle_collision(
|
||||
position: &mut Vec2,
|
||||
velocity: &mut Vec2,
|
||||
radius: f32,
|
||||
center: Vec2,
|
||||
obstacle_radius: f32,
|
||||
kick: f32,
|
||||
) -> bool {
|
||||
let offset = *position - center;
|
||||
let minimum = radius + obstacle_radius;
|
||||
if offset.length_squared() >= minimum * minimum {
|
||||
return false;
|
||||
}
|
||||
let normal = if offset.length_squared() > 0.000_001 {
|
||||
offset.normalize()
|
||||
} else {
|
||||
Vec2::Y
|
||||
};
|
||||
*position = center + normal * minimum;
|
||||
let inward_speed = velocity.dot(normal);
|
||||
if inward_speed < 0.0 {
|
||||
*velocity -= normal * inward_speed * 1.8;
|
||||
}
|
||||
*velocity += normal * kick;
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn closest_point_is_clamped_to_segment() {
|
||||
let segment = Segment::new(Vec2::ZERO, Vec2::new(10.0, 0.0), 0.8);
|
||||
assert_eq!(
|
||||
closest_point(Vec2::new(12.0, 4.0), segment),
|
||||
Vec2::new(10.0, 0.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collision_reflects_velocity() {
|
||||
let mut position = Vec2::new(5.0, 1.0);
|
||||
let mut velocity = Vec2::new(0.0, -10.0);
|
||||
let hit = segment_collision(
|
||||
&mut position,
|
||||
&mut velocity,
|
||||
2.0,
|
||||
Segment::new(Vec2::ZERO, Vec2::new(10.0, 0.0), 1.0),
|
||||
);
|
||||
assert!(hit);
|
||||
assert!(velocity.y > 9.9);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,31 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
mod app;
|
||||
mod assets;
|
||||
mod game;
|
||||
mod geometry;
|
||||
mod persistence;
|
||||
|
||||
use app::App;
|
||||
use macroquad::prelude::*;
|
||||
|
||||
const LOGICAL_WIDTH: i32 = 640;
|
||||
const LOGICAL_HEIGHT: i32 = 460;
|
||||
|
||||
fn window_conf() -> Conf {
|
||||
Conf {
|
||||
window_title: "TDK Pinball Machine".to_owned(),
|
||||
window_width: LOGICAL_WIDTH,
|
||||
window_height: LOGICAL_HEIGHT,
|
||||
window_resizable: true,
|
||||
high_dpi: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[macroquad::main(window_conf)]
|
||||
async fn main() {
|
||||
let mut app = App::load().await;
|
||||
loop {
|
||||
app.frame();
|
||||
next_frame().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, fs, io, path::PathBuf};
|
||||
|
||||
const ORIGINAL_HIGHSCORES: &[u8] = include_bytes!("../assets/original/HISCORES.DAT");
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub enum Language {
|
||||
English,
|
||||
German,
|
||||
French,
|
||||
Italian,
|
||||
Spanish,
|
||||
}
|
||||
|
||||
impl Language {
|
||||
pub const ALL: [Self; 5] = [
|
||||
Self::English,
|
||||
Self::German,
|
||||
Self::French,
|
||||
Self::Italian,
|
||||
Self::Spanish,
|
||||
];
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::English => "English",
|
||||
Self::German => "Deutsch",
|
||||
Self::French => "Francais",
|
||||
Self::Italian => "Italiano",
|
||||
Self::Spanish => "Espanol",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub speed: u8,
|
||||
pub sounds: bool,
|
||||
pub language: Language,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
speed: 3,
|
||||
sounds: true,
|
||||
language: Language::English,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct HighScore {
|
||||
pub name: String,
|
||||
pub score: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct SavedData {
|
||||
pub settings: Settings,
|
||||
pub high_scores: Vec<HighScore>,
|
||||
}
|
||||
|
||||
impl Default for SavedData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
settings: Settings::default(),
|
||||
high_scores: parse_original_high_scores(ORIGINAL_HIGHSCORES),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Persistence {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Persistence {
|
||||
pub fn new() -> Self {
|
||||
let path = ProjectDirs::from("com", "kiwi-hamburg", "TDK Pinball Machine").map_or_else(
|
||||
|| PathBuf::from("tdkpin-save.json"),
|
||||
|dirs| dirs.data_local_dir().join("save.json"),
|
||||
);
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn load(&self) -> SavedData {
|
||||
fs::read_to_string(&self.path)
|
||||
.ok()
|
||||
.and_then(|text| serde_json::from_str(&text).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn save(&self, data: &SavedData) -> io::Result<()> {
|
||||
if let Some(parent) = self
|
||||
.path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
{
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let encoded = serde_json::to_vec_pretty(data).map_err(io::Error::other)?;
|
||||
let temporary = self.path.with_extension("json.tmp");
|
||||
fs::write(&temporary, encoded)?;
|
||||
#[cfg(target_os = "windows")]
|
||||
if self.path.exists() {
|
||||
fs::remove_file(&self.path)?;
|
||||
}
|
||||
fs::rename(temporary, &self.path)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_original_high_scores(bytes: &[u8]) -> Vec<HighScore> {
|
||||
const HEADER: usize = 16;
|
||||
const COUNT: usize = 10;
|
||||
const NAME_LENGTH: usize = 22;
|
||||
const SCORE_KEY: u32 = u32::from_le_bytes(*b"IWIK");
|
||||
if bytes.len() < HEADER + COUNT * 4 + COUNT * NAME_LENGTH
|
||||
|| !bytes.starts_with(b"TDK Highscores\0\0")
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let scores = &bytes[HEADER..HEADER + COUNT * 4];
|
||||
let names = &bytes[HEADER + COUNT * 4..];
|
||||
let mut result = (0..COUNT)
|
||||
.map(|index| {
|
||||
let score_start = index * 4;
|
||||
let score = u32::from_le_bytes([
|
||||
scores[score_start],
|
||||
scores[score_start + 1],
|
||||
scores[score_start + 2],
|
||||
scores[score_start + 3],
|
||||
]) ^ SCORE_KEY;
|
||||
let name_start = index * NAME_LENGTH;
|
||||
let name_bytes = &names[name_start..name_start + NAME_LENGTH];
|
||||
let length = name_bytes
|
||||
.iter()
|
||||
.position(|byte| *byte == 0)
|
||||
.unwrap_or(NAME_LENGTH);
|
||||
HighScore {
|
||||
name: String::from_utf8_lossy(&name_bytes[..length]).into_owned(),
|
||||
score,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
result.sort_by_key(|entry| Reverse(entry.score));
|
||||
result
|
||||
}
|
||||
|
||||
pub fn insert_high_score(scores: &mut Vec<HighScore>, entry: HighScore) {
|
||||
scores.push(entry);
|
||||
scores.sort_by_key(|entry| Reverse(entry.score));
|
||||
scores.truncate(10);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn imports_all_original_entries_in_score_order() {
|
||||
let scores = parse_original_high_scores(ORIGINAL_HIGHSCORES);
|
||||
assert_eq!(scores.len(), 10);
|
||||
assert_eq!(scores[0].score, 6_537_392);
|
||||
assert!(scores.windows(2).all(|pair| pair[0].score >= pair[1].score));
|
||||
assert!(
|
||||
scores
|
||||
.iter()
|
||||
.any(|entry| entry.name == "TDK Pinball Player")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_table_without_the_original_signature() {
|
||||
let mut corrupt = ORIGINAL_HIGHSCORES.to_vec();
|
||||
corrupt[0] = b'X';
|
||||
assert!(parse_original_high_scores(&corrupt).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_score_is_inserted_and_table_stays_bounded() {
|
||||
let mut scores = parse_original_high_scores(ORIGINAL_HIGHSCORES);
|
||||
insert_high_score(
|
||||
&mut scores,
|
||||
HighScore {
|
||||
name: "TEST".to_owned(),
|
||||
score: u32::MAX,
|
||||
},
|
||||
);
|
||||
assert_eq!(scores.len(), 10);
|
||||
assert_eq!(scores[0].name, "TEST");
|
||||
}
|
||||
}
|
||||