Add 2D endless runner game with pickups and coyote time
- Platformer endless runner: fixed player x, world scrolls left - Logarithmic speed curve: initial + factor * ln(1 + t / time_scale) - Enemies stomped from above; side/bottom contact kills player - Procedural level generation with StdRng seeded from SystemTime - Object pooling via Vec::retain + frontier-based generator - Coyote time: grace window after leaving platform edge - Data-driven pickup system with trait-based effects (ActiveEffect) - Invulnerability, JumpBoost, ScoreMultiplier — extend via config - Score accumulates with per-effect multiplier; stomp bonuses scaled - Game over screen with score, best score, restart prompt - HUD: score, speed bar, active effect timer bars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
636
Cargo.lock
generated
Normal file
636
Cargo.lock
generated
Normal file
@@ -0,0 +1,636 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "endless_runner"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"raylib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.182"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||
|
||||
[[package]]
|
||||
name = "raylib"
|
||||
version = "5.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5c54335590d1b6e6fbdbccee09dafdfd76a1111fc3c709eca949e71e81f7a8a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"paste",
|
||||
"raylib-sys",
|
||||
"seq-macro",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raylib-sys"
|
||||
version = "5.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ce5adc950b042db67f1f78f24f7e76563652ce24db032afe9ca9e534d8b7a13"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"cmake",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "seq-macro"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "endless_runner"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.10.0"
|
||||
raylib = { version = "5.5.1", features = ["wayland"] }
|
||||
129
src/config.rs
Normal file
129
src/config.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use crate::pickup::{PickupDef, PickupEffectType};
|
||||
|
||||
/// All game parameters in one place — change here to tune the game.
|
||||
/// Pickup types are also registered here; add a new `PickupDef` entry to extend.
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
pub screen_width: i32,
|
||||
pub screen_height: i32,
|
||||
|
||||
// Player
|
||||
pub player_x: f32,
|
||||
pub player_width: f32,
|
||||
pub player_height: f32,
|
||||
pub gravity: f32,
|
||||
pub jump_velocity: f32,
|
||||
pub stomp_bounce_velocity: f32,
|
||||
/// Grace window (seconds) after leaving a platform where jumping is still allowed.
|
||||
pub coyote_time: f32,
|
||||
|
||||
// Speed curve: speed(t) = initial + factor * ln(1 + t / time_scale)
|
||||
pub initial_speed: f32,
|
||||
pub speed_log_factor: f32,
|
||||
pub speed_time_scale: f32,
|
||||
|
||||
// Platforms
|
||||
pub platform_height: f32,
|
||||
pub platform_min_width: f32,
|
||||
pub platform_max_width: f32,
|
||||
pub platform_gap_min: f32,
|
||||
pub platform_gap_max: f32,
|
||||
pub platform_y_min: f32,
|
||||
pub platform_y_max: f32,
|
||||
pub platform_y_max_delta: f32,
|
||||
|
||||
// Starting platform
|
||||
pub start_platform_y: f32,
|
||||
|
||||
// Enemies
|
||||
pub enemy_width: f32,
|
||||
pub enemy_height: f32,
|
||||
pub enemy_spawn_chance: f32,
|
||||
pub enemy_min_platform_width: f32,
|
||||
|
||||
// Pickups
|
||||
pub pickup_size: f32,
|
||||
pub pickup_spawn_chance: f32,
|
||||
pub pickup_hover_gap: f32, // pixels above platform surface
|
||||
pub pickup_defs: Vec<PickupDef>,
|
||||
|
||||
// Scoring
|
||||
pub enemy_stomp_bonus: f32,
|
||||
pub score_per_second: f32,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Current scroll speed given elapsed game time.
|
||||
pub fn scroll_speed(&self, elapsed: f32) -> f32 {
|
||||
self.initial_speed
|
||||
+ self.speed_log_factor * (1.0 + elapsed / self.speed_time_scale).ln()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let screen_height = 720;
|
||||
let start_platform_y = screen_height as f32 - 110.0;
|
||||
Self {
|
||||
screen_width: 1280,
|
||||
screen_height,
|
||||
|
||||
player_x: 200.0,
|
||||
player_width: 38.0,
|
||||
player_height: 48.0,
|
||||
gravity: 1900.0,
|
||||
jump_velocity: -800.0,
|
||||
stomp_bounce_velocity: -550.0,
|
||||
coyote_time: 0.12,
|
||||
|
||||
initial_speed: 280.0,
|
||||
speed_log_factor: 150.0,
|
||||
speed_time_scale: 30.0,
|
||||
|
||||
platform_height: 20.0,
|
||||
platform_min_width: 130.0,
|
||||
platform_max_width: 360.0,
|
||||
platform_gap_min: 60.0,
|
||||
platform_gap_max: 210.0,
|
||||
platform_y_min: 260.0,
|
||||
platform_y_max: start_platform_y,
|
||||
platform_y_max_delta: 130.0,
|
||||
|
||||
start_platform_y,
|
||||
|
||||
enemy_width: 40.0,
|
||||
enemy_height: 40.0,
|
||||
enemy_spawn_chance: 0.35,
|
||||
enemy_min_platform_width: 120.0,
|
||||
|
||||
pickup_size: 28.0,
|
||||
pickup_spawn_chance: 0.28,
|
||||
pickup_hover_gap: 12.0,
|
||||
|
||||
// ── Register new pickup types here ────────────────────────────────
|
||||
pickup_defs: vec![
|
||||
PickupDef {
|
||||
label: "INVINCIBLE",
|
||||
color: (255, 200, 50),
|
||||
duration: 5.0,
|
||||
effect: PickupEffectType::Invulnerability,
|
||||
},
|
||||
PickupDef {
|
||||
label: "JUMP BOOST",
|
||||
color: (80, 180, 255),
|
||||
duration: 6.0,
|
||||
effect: PickupEffectType::JumpBoost { factor: 1.45 },
|
||||
},
|
||||
PickupDef {
|
||||
label: "2X SCORE",
|
||||
color: (180, 80, 255),
|
||||
duration: 8.0,
|
||||
effect: PickupEffectType::ScoreMultiplier { factor: 2.0 },
|
||||
},
|
||||
],
|
||||
|
||||
enemy_stomp_bonus: 100.0,
|
||||
score_per_second: 80.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
124
src/effects.rs
Normal file
124
src/effects.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use raylib::prelude::Color;
|
||||
|
||||
/// Behaviour that an active pickup effect applies each frame.
|
||||
/// Add a new impl here to introduce a new effect type — no other
|
||||
/// code changes required beyond registering it in `pickup.rs`.
|
||||
pub trait ActiveEffect {
|
||||
/// Tick the effect. Returns `false` when the effect has expired.
|
||||
fn update(&mut self, dt: f32) -> bool;
|
||||
|
||||
fn label(&self) -> &'static str;
|
||||
fn tint(&self) -> Color;
|
||||
|
||||
/// Fraction of lifetime remaining (1.0 = just collected, 0.0 = expired).
|
||||
fn progress(&self) -> f32;
|
||||
|
||||
// --- per-system queries (default = no-op) ---
|
||||
fn is_invulnerable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn jump_multiplier(&self) -> f32 {
|
||||
1.0
|
||||
}
|
||||
fn score_multiplier(&self) -> f32 {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
// ── Concrete effects ──────────────────────────────────────────────────────────
|
||||
|
||||
pub struct InvulnerabilityEffect {
|
||||
timer: f32,
|
||||
total: f32,
|
||||
}
|
||||
|
||||
impl InvulnerabilityEffect {
|
||||
pub fn new(duration: f32) -> Self {
|
||||
Self { timer: duration, total: duration }
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveEffect for InvulnerabilityEffect {
|
||||
fn update(&mut self, dt: f32) -> bool {
|
||||
self.timer -= dt;
|
||||
self.timer > 0.0
|
||||
}
|
||||
fn label(&self) -> &'static str {
|
||||
"INVINCIBLE"
|
||||
}
|
||||
fn tint(&self) -> Color {
|
||||
Color::new(255, 200, 50, 255)
|
||||
}
|
||||
fn progress(&self) -> f32 {
|
||||
(self.timer / self.total).max(0.0)
|
||||
}
|
||||
fn is_invulnerable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct JumpBoostEffect {
|
||||
timer: f32,
|
||||
total: f32,
|
||||
factor: f32,
|
||||
}
|
||||
|
||||
impl JumpBoostEffect {
|
||||
pub fn new(duration: f32, factor: f32) -> Self {
|
||||
Self { timer: duration, total: duration, factor }
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveEffect for JumpBoostEffect {
|
||||
fn update(&mut self, dt: f32) -> bool {
|
||||
self.timer -= dt;
|
||||
self.timer > 0.0
|
||||
}
|
||||
fn label(&self) -> &'static str {
|
||||
"JUMP BOOST"
|
||||
}
|
||||
fn tint(&self) -> Color {
|
||||
Color::new(80, 180, 255, 255)
|
||||
}
|
||||
fn progress(&self) -> f32 {
|
||||
(self.timer / self.total).max(0.0)
|
||||
}
|
||||
fn jump_multiplier(&self) -> f32 {
|
||||
self.factor
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct ScoreMultiplierEffect {
|
||||
timer: f32,
|
||||
total: f32,
|
||||
factor: f32,
|
||||
}
|
||||
|
||||
impl ScoreMultiplierEffect {
|
||||
pub fn new(duration: f32, factor: f32) -> Self {
|
||||
Self { timer: duration, total: duration, factor }
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveEffect for ScoreMultiplierEffect {
|
||||
fn update(&mut self, dt: f32) -> bool {
|
||||
self.timer -= dt;
|
||||
self.timer > 0.0
|
||||
}
|
||||
fn label(&self) -> &'static str {
|
||||
"2X SCORE"
|
||||
}
|
||||
fn tint(&self) -> Color {
|
||||
Color::new(180, 80, 255, 255)
|
||||
}
|
||||
fn progress(&self) -> f32 {
|
||||
(self.timer / self.total).max(0.0)
|
||||
}
|
||||
fn score_multiplier(&self) -> f32 {
|
||||
self.factor
|
||||
}
|
||||
}
|
||||
34
src/enemy.rs
Normal file
34
src/enemy.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
#[derive(Clone)]
|
||||
pub struct Enemy {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub alive: bool,
|
||||
}
|
||||
|
||||
impl Enemy {
|
||||
pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
|
||||
Self { x, y, width, height, alive: true }
|
||||
}
|
||||
|
||||
pub fn scroll(&mut self, speed: f32, dt: f32) {
|
||||
self.x -= speed * dt;
|
||||
}
|
||||
|
||||
pub fn is_off_screen(&self) -> bool {
|
||||
self.x + self.width < 0.0
|
||||
}
|
||||
|
||||
pub fn top(&self) -> f32 {
|
||||
self.y
|
||||
}
|
||||
|
||||
pub fn bottom(&self) -> f32 {
|
||||
self.y + self.height
|
||||
}
|
||||
|
||||
pub fn right(&self) -> f32 {
|
||||
self.x + self.width
|
||||
}
|
||||
}
|
||||
143
src/game.rs
Normal file
143
src/game.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use raylib::prelude::*;
|
||||
|
||||
use crate::{config::Config, world::World};
|
||||
|
||||
pub enum GameState {
|
||||
Playing,
|
||||
GameOver,
|
||||
}
|
||||
|
||||
pub struct Game {
|
||||
cfg: Config,
|
||||
state: GameState,
|
||||
pub world: World,
|
||||
high_score: u64,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new(cfg: Config) -> Self {
|
||||
let world = World::new(&cfg);
|
||||
Self {
|
||||
state: GameState::Playing,
|
||||
world,
|
||||
high_score: 0,
|
||||
cfg,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, rl: &RaylibHandle, dt: f32) {
|
||||
match self.state {
|
||||
GameState::Playing => {
|
||||
let jump = rl.is_key_pressed(KeyboardKey::KEY_SPACE)
|
||||
|| rl.is_key_pressed(KeyboardKey::KEY_UP)
|
||||
|| rl.is_key_pressed(KeyboardKey::KEY_W);
|
||||
|
||||
self.world.update(dt, jump, &self.cfg);
|
||||
|
||||
if !self.world.player.alive {
|
||||
if self.world.score > self.high_score {
|
||||
self.high_score = self.world.score;
|
||||
}
|
||||
self.state = GameState::GameOver;
|
||||
}
|
||||
}
|
||||
|
||||
GameState::GameOver => {
|
||||
if rl.is_key_pressed(KeyboardKey::KEY_R)
|
||||
|| rl.is_key_pressed(KeyboardKey::KEY_SPACE)
|
||||
|| rl.is_key_pressed(KeyboardKey::KEY_ENTER)
|
||||
{
|
||||
self.world = World::new(&self.cfg);
|
||||
self.state = GameState::Playing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, d: &mut RaylibDrawHandle) {
|
||||
self.world.render(d, &self.cfg);
|
||||
self.draw_hud(d);
|
||||
|
||||
if let GameState::GameOver = self.state {
|
||||
self.draw_game_over(d);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_hud(&self, d: &mut RaylibDrawHandle) {
|
||||
let score_text = format!("Score {:>6}", self.world.score);
|
||||
d.draw_text(&score_text, 16, 14, 28, Color::WHITE);
|
||||
|
||||
let speed = self.cfg.scroll_speed(self.world.elapsed);
|
||||
let speed_text = format!("Speed {:.0} px/s", speed);
|
||||
let sw = self.cfg.screen_width;
|
||||
let tw = d.measure_text(&speed_text, 22);
|
||||
d.draw_text(&speed_text, sw - tw - 16, 16, 22, Color::new(160, 200, 255, 255));
|
||||
|
||||
// Speed bar
|
||||
let bar_w = 160;
|
||||
let bar_h = 8;
|
||||
let bar_x = sw - bar_w - 16;
|
||||
let bar_y = 44;
|
||||
let max_speed = self.cfg.initial_speed + self.cfg.speed_log_factor * 3.0;
|
||||
let fill = ((speed / max_speed).min(1.0) * bar_w as f32) as i32;
|
||||
d.draw_rectangle(bar_x, bar_y, bar_w, bar_h, Color::new(50, 50, 80, 255));
|
||||
d.draw_rectangle(bar_x, bar_y, fill, bar_h, Color::new(80, 180, 255, 255));
|
||||
|
||||
// Active effect timer bars
|
||||
self.world.draw_active_effects_hud(d, &self.cfg);
|
||||
}
|
||||
|
||||
fn draw_game_over(&self, d: &mut RaylibDrawHandle) {
|
||||
let sw = self.cfg.screen_width;
|
||||
let sh = self.cfg.screen_height;
|
||||
|
||||
// Dim overlay
|
||||
d.draw_rectangle(0, 0, sw, sh, Color::new(0, 0, 0, 160));
|
||||
|
||||
// Panel
|
||||
let panel_w = 520;
|
||||
let panel_h = 340;
|
||||
let panel_x = (sw - panel_w) / 2;
|
||||
let panel_y = (sh - panel_h) / 2;
|
||||
d.draw_rectangle(panel_x, panel_y, panel_w, panel_h, Color::new(20, 22, 44, 240));
|
||||
d.draw_rectangle_lines(panel_x, panel_y, panel_w, panel_h, Color::new(80, 100, 200, 255));
|
||||
|
||||
// Title
|
||||
let title = "GAME OVER";
|
||||
let ts = 56;
|
||||
let tw = d.measure_text(title, ts);
|
||||
d.draw_text(title, (sw - tw) / 2, panel_y + 30, ts, Color::new(220, 60, 60, 255));
|
||||
|
||||
// Score
|
||||
let score_str = format!("Score: {}", self.world.score);
|
||||
let ss = 32;
|
||||
let sw2 = d.measure_text(&score_str, ss);
|
||||
d.draw_text(&score_str, (sw - sw2) / 2, panel_y + 120, ss, Color::WHITE);
|
||||
|
||||
// High score
|
||||
let hi_str = format!("Best: {}", self.high_score);
|
||||
let hs = 28;
|
||||
let hw = d.measure_text(&hi_str, hs);
|
||||
d.draw_text(&hi_str, (sw - hw) / 2, panel_y + 170, hs, Color::GOLD);
|
||||
|
||||
// Separator
|
||||
d.draw_line(
|
||||
panel_x + 40,
|
||||
panel_y + 215,
|
||||
panel_x + panel_w - 40,
|
||||
panel_y + 215,
|
||||
Color::new(60, 70, 130, 255),
|
||||
);
|
||||
|
||||
// Restart prompt
|
||||
let prompt = "SPACE / R / ENTER to restart";
|
||||
let ps = 22;
|
||||
let pw = d.measure_text(prompt, ps);
|
||||
// Blinking effect based on elapsed time (we don't have time here, so just static)
|
||||
d.draw_text(prompt, (sw - pw) / 2, panel_y + 240, ps, Color::new(160, 200, 255, 255));
|
||||
|
||||
let ctrl = "SPACE / W / UP to jump";
|
||||
let cw = d.measure_text(ctrl, 18);
|
||||
d.draw_text(ctrl, (sw - cw) / 2, panel_y + 285, 18, Color::new(100, 130, 180, 255));
|
||||
}
|
||||
}
|
||||
109
src/level_gen.rs
Normal file
109
src/level_gen.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use rand::{RngExt, SeedableRng, rngs::StdRng};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::{config::Config, enemy::Enemy, pickup::Pickup, platform::Platform};
|
||||
|
||||
pub struct LevelGenerator {
|
||||
rng: StdRng,
|
||||
/// Screen-space X of the right edge of the last generated platform.
|
||||
frontier_x: f32,
|
||||
/// Y of the last generated platform (for smooth height transitions).
|
||||
frontier_y: f32,
|
||||
}
|
||||
|
||||
impl LevelGenerator {
|
||||
pub fn new(cfg: &Config) -> Self {
|
||||
Self {
|
||||
rng: StdRng::seed_from_u64(
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0xDEAD_BEEF),
|
||||
),
|
||||
frontier_x: cfg.screen_width as f32 * 0.65,
|
||||
frontier_y: cfg.start_platform_y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Move frontier left with the world scroll so new platforms land
|
||||
/// at the correct screen position.
|
||||
pub fn scroll(&mut self, speed: f32, dt: f32) {
|
||||
self.frontier_x -= speed * dt;
|
||||
}
|
||||
|
||||
/// Generate platforms until the frontier is at least one screen-width
|
||||
/// past the right edge.
|
||||
pub fn generate_if_needed(
|
||||
&mut self,
|
||||
cfg: &Config,
|
||||
platforms: &mut Vec<Platform>,
|
||||
enemies: &mut Vec<Enemy>,
|
||||
pickups: &mut Vec<Pickup>,
|
||||
) {
|
||||
let target = cfg.screen_width as f32 + 300.0;
|
||||
while self.frontier_x < target {
|
||||
self.place_next(cfg, platforms, enemies, pickups);
|
||||
}
|
||||
}
|
||||
|
||||
fn place_next(
|
||||
&mut self,
|
||||
cfg: &Config,
|
||||
platforms: &mut Vec<Platform>,
|
||||
enemies: &mut Vec<Enemy>,
|
||||
pickups: &mut Vec<Pickup>,
|
||||
) {
|
||||
let gap: f32 = self.rng.random_range(cfg.platform_gap_min..cfg.platform_gap_max);
|
||||
let width: f32 = self.rng.random_range(cfg.platform_min_width..cfg.platform_max_width);
|
||||
|
||||
let dy: f32 = self.rng.random_range(-cfg.platform_y_max_delta..cfg.platform_y_max_delta);
|
||||
let y = (self.frontier_y + dy)
|
||||
.max(cfg.platform_y_min)
|
||||
.min(cfg.platform_y_max);
|
||||
|
||||
let x = self.frontier_x + gap;
|
||||
|
||||
platforms.push(Platform::new(x, y, width, cfg.platform_height));
|
||||
|
||||
let mut has_enemy = false;
|
||||
|
||||
if width >= cfg.enemy_min_platform_width
|
||||
&& self.rng.random::<f32>() < cfg.enemy_spawn_chance
|
||||
{
|
||||
let margin = 15.0;
|
||||
let max_offset = width - cfg.enemy_width - margin * 2.0;
|
||||
if max_offset > 0.0 {
|
||||
let ex = x + margin + self.rng.random_range(0.0..max_offset);
|
||||
let ey = y - cfg.enemy_height;
|
||||
enemies.push(Enemy::new(ex, ey, cfg.enemy_width, cfg.enemy_height));
|
||||
has_enemy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !cfg.pickup_defs.is_empty() && self.rng.random::<f32>() < cfg.pickup_spawn_chance {
|
||||
let def_index = self.rng.random_range(0..cfg.pickup_defs.len());
|
||||
|
||||
let margin = 20.0;
|
||||
let px_min = x + margin;
|
||||
let px_max = x + width - cfg.pickup_size - margin;
|
||||
|
||||
if px_max > px_min {
|
||||
let mut px: f32 = self.rng.random_range(px_min..px_max);
|
||||
|
||||
if has_enemy {
|
||||
let enemy_cx = x + width * 0.5;
|
||||
if (px - enemy_cx).abs() < cfg.enemy_width + cfg.pickup_size {
|
||||
px = (px + cfg.enemy_width + cfg.pickup_size)
|
||||
.min(x + width - cfg.pickup_size - margin);
|
||||
}
|
||||
}
|
||||
|
||||
let py = y - cfg.pickup_size - cfg.pickup_hover_gap;
|
||||
pickups.push(Pickup::new(px, py, cfg.pickup_size, def_index));
|
||||
}
|
||||
}
|
||||
|
||||
self.frontier_x = x + width;
|
||||
self.frontier_y = y;
|
||||
}
|
||||
}
|
||||
30
src/main.rs
Normal file
30
src/main.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
mod config;
|
||||
mod effects;
|
||||
mod enemy;
|
||||
mod game;
|
||||
mod level_gen;
|
||||
mod pickup;
|
||||
mod platform;
|
||||
mod player;
|
||||
mod world;
|
||||
|
||||
fn main() {
|
||||
let cfg = config::Config::default();
|
||||
|
||||
let (mut rl, thread) = raylib::init()
|
||||
.size(cfg.screen_width, cfg.screen_height)
|
||||
.title("Endless Runner")
|
||||
.build();
|
||||
|
||||
rl.set_target_fps(60);
|
||||
|
||||
let mut game = game::Game::new(cfg);
|
||||
|
||||
while !rl.window_should_close() {
|
||||
let dt = rl.get_frame_time().min(0.05); // cap dt to avoid spiral of death
|
||||
game.update(&rl, dt);
|
||||
|
||||
let mut d = rl.begin_drawing(&thread);
|
||||
game.render(&mut d);
|
||||
}
|
||||
}
|
||||
71
src/pickup.rs
Normal file
71
src/pickup.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use crate::effects::{ActiveEffect, InvulnerabilityEffect, JumpBoostEffect, ScoreMultiplierEffect};
|
||||
|
||||
/// Which gameplay effect this pickup grants.
|
||||
/// Add a new variant here (and a matching `ActiveEffect` impl) to extend.
|
||||
#[derive(Clone)]
|
||||
pub enum PickupEffectType {
|
||||
Invulnerability,
|
||||
JumpBoost { factor: f32 },
|
||||
ScoreMultiplier { factor: f32 },
|
||||
}
|
||||
|
||||
/// Data-driven definition of a pickup type — lives entirely in `Config`.
|
||||
#[derive(Clone)]
|
||||
pub struct PickupDef {
|
||||
pub label: &'static str,
|
||||
/// RGB colour used for the pickup icon and effect HUD bar.
|
||||
pub color: (u8, u8, u8),
|
||||
pub duration: f32,
|
||||
pub effect: PickupEffectType,
|
||||
}
|
||||
|
||||
impl PickupDef {
|
||||
/// Instantiate the corresponding active effect.
|
||||
pub fn create_effect(&self) -> Box<dyn ActiveEffect> {
|
||||
match self.effect {
|
||||
PickupEffectType::Invulnerability => {
|
||||
Box::new(InvulnerabilityEffect::new(self.duration))
|
||||
}
|
||||
PickupEffectType::JumpBoost { factor } => {
|
||||
Box::new(JumpBoostEffect::new(self.duration, factor))
|
||||
}
|
||||
PickupEffectType::ScoreMultiplier { factor } => {
|
||||
Box::new(ScoreMultiplierEffect::new(self.duration, factor))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── In-world pickup instance ──────────────────────────────────────────────────
|
||||
|
||||
pub struct Pickup {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
/// Index into `Config::pickup_defs`.
|
||||
pub def_index: usize,
|
||||
pub collected: bool,
|
||||
}
|
||||
|
||||
impl Pickup {
|
||||
pub fn new(x: f32, y: f32, size: f32, def_index: usize) -> Self {
|
||||
Self { x, y, width: size, height: size, def_index, collected: false }
|
||||
}
|
||||
|
||||
pub fn scroll(&mut self, speed: f32, dt: f32) {
|
||||
self.x -= speed * dt;
|
||||
}
|
||||
|
||||
pub fn is_off_screen(&self) -> bool {
|
||||
self.x + self.width < 0.0
|
||||
}
|
||||
|
||||
pub fn right(&self) -> f32 {
|
||||
self.x + self.width
|
||||
}
|
||||
|
||||
pub fn bottom(&self) -> f32 {
|
||||
self.y + self.height
|
||||
}
|
||||
}
|
||||
29
src/platform.rs
Normal file
29
src/platform.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
#[derive(Clone)]
|
||||
pub struct Platform {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
|
||||
pub fn scroll(&mut self, speed: f32, dt: f32) {
|
||||
self.x -= speed * dt;
|
||||
}
|
||||
|
||||
pub fn is_off_screen(&self) -> bool {
|
||||
self.x + self.width < 0.0
|
||||
}
|
||||
|
||||
pub fn top(&self) -> f32 {
|
||||
self.y
|
||||
}
|
||||
|
||||
pub fn right(&self) -> f32 {
|
||||
self.x + self.width
|
||||
}
|
||||
}
|
||||
80
src/player.rs
Normal file
80
src/player.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use crate::config::Config;
|
||||
|
||||
pub struct Player {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub prev_y: f32,
|
||||
pub vy: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub on_ground: bool,
|
||||
pub alive: bool,
|
||||
/// Countdown from `coyote_time` to 0. Positive value = jump still allowed.
|
||||
pub coyote_timer: f32,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new(cfg: &Config) -> Self {
|
||||
let y = cfg.start_platform_y - cfg.player_height;
|
||||
Self {
|
||||
x: cfg.player_x,
|
||||
y,
|
||||
prev_y: y,
|
||||
vy: 0.0,
|
||||
width: cfg.player_width,
|
||||
height: cfg.player_height,
|
||||
on_ground: true,
|
||||
alive: true,
|
||||
coyote_timer: cfg.coyote_time,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply gravity and integrate position. Must be called before collision resolution.
|
||||
pub fn update(&mut self, dt: f32, cfg: &Config) {
|
||||
self.prev_y = self.y;
|
||||
|
||||
// Refresh coyote window while grounded; tick it down while airborne.
|
||||
if self.on_ground {
|
||||
self.coyote_timer = cfg.coyote_time;
|
||||
} else {
|
||||
self.coyote_timer = (self.coyote_timer - dt).max(0.0);
|
||||
}
|
||||
|
||||
self.vy += cfg.gravity * dt;
|
||||
self.y += self.vy * dt;
|
||||
|
||||
// Reset ground flag; platform collision will set it back.
|
||||
self.on_ground = false;
|
||||
|
||||
if self.y > cfg.screen_height as f32 + 50.0 {
|
||||
self.alive = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Jump if the coyote window is open (on ground OR just left a platform).
|
||||
/// `jump_multiplier` comes from active effects.
|
||||
pub fn jump(&mut self, cfg: &Config, jump_multiplier: f32) {
|
||||
if self.coyote_timer > 0.0 {
|
||||
self.vy = cfg.jump_velocity * jump_multiplier;
|
||||
self.coyote_timer = 0.0; // consume the window
|
||||
self.on_ground = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stomp_bounce(&mut self, cfg: &Config) {
|
||||
self.vy = cfg.stomp_bounce_velocity;
|
||||
self.on_ground = false;
|
||||
}
|
||||
|
||||
pub fn bottom(&self) -> f32 {
|
||||
self.y + self.height
|
||||
}
|
||||
|
||||
pub fn prev_bottom(&self) -> f32 {
|
||||
self.prev_y + self.height
|
||||
}
|
||||
|
||||
pub fn right(&self) -> f32 {
|
||||
self.x + self.width
|
||||
}
|
||||
}
|
||||
357
src/world.rs
Normal file
357
src/world.rs
Normal file
@@ -0,0 +1,357 @@
|
||||
use raylib::prelude::*;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
effects::ActiveEffect,
|
||||
enemy::Enemy,
|
||||
level_gen::LevelGenerator,
|
||||
pickup::Pickup,
|
||||
platform::Platform,
|
||||
player::Player,
|
||||
};
|
||||
|
||||
pub struct World {
|
||||
pub player: Player,
|
||||
pub platforms: Vec<Platform>,
|
||||
pub enemies: Vec<Enemy>,
|
||||
pub pickups: Vec<Pickup>,
|
||||
pub active_effects: Vec<Box<dyn ActiveEffect>>,
|
||||
pub score: u64,
|
||||
pub elapsed: f32,
|
||||
score_f: f32,
|
||||
level_gen: LevelGenerator,
|
||||
}
|
||||
|
||||
impl World {
|
||||
pub fn new(cfg: &Config) -> Self {
|
||||
let start_platform = Platform::new(
|
||||
0.0,
|
||||
cfg.start_platform_y,
|
||||
cfg.screen_width as f32 * 0.65,
|
||||
cfg.platform_height,
|
||||
);
|
||||
|
||||
let mut platforms = vec![start_platform];
|
||||
let mut enemies = Vec::new();
|
||||
let mut pickups = Vec::new();
|
||||
let mut level_gen = LevelGenerator::new(cfg);
|
||||
level_gen.generate_if_needed(cfg, &mut platforms, &mut enemies, &mut pickups);
|
||||
|
||||
Self {
|
||||
player: Player::new(cfg),
|
||||
platforms,
|
||||
enemies,
|
||||
pickups,
|
||||
active_effects: Vec::new(),
|
||||
score: 0,
|
||||
score_f: 0.0,
|
||||
elapsed: 0.0,
|
||||
level_gen,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, dt: f32, jump_pressed: bool, cfg: &Config) {
|
||||
self.elapsed += dt;
|
||||
let speed = cfg.scroll_speed(self.elapsed);
|
||||
|
||||
// 1. Physics
|
||||
self.player.update(dt, cfg);
|
||||
|
||||
// 2. Scroll world
|
||||
for p in &mut self.platforms {
|
||||
p.scroll(speed, dt);
|
||||
}
|
||||
for e in &mut self.enemies {
|
||||
e.scroll(speed, dt);
|
||||
}
|
||||
for pk in &mut self.pickups {
|
||||
pk.scroll(speed, dt);
|
||||
}
|
||||
self.level_gen.scroll(speed, dt);
|
||||
|
||||
// 3. Resolve collisions (sets on_ground / alive)
|
||||
self.resolve_platform_collisions();
|
||||
self.resolve_enemy_collisions(cfg);
|
||||
self.collect_pickups(cfg);
|
||||
|
||||
// 4. Jump input — uses coyote_timer set during update, and effect multiplier
|
||||
if jump_pressed {
|
||||
let mult = self.jump_multiplier();
|
||||
self.player.jump(cfg, mult);
|
||||
}
|
||||
|
||||
// 5. Tick active effects; remove expired ones
|
||||
self.active_effects.retain_mut(|e| e.update(dt));
|
||||
|
||||
// 6. Recycle off-screen objects
|
||||
self.platforms.retain(|p| !p.is_off_screen());
|
||||
self.enemies.retain(|e| !e.is_off_screen() && e.alive);
|
||||
self.pickups.retain(|pk| !pk.is_off_screen() && !pk.collected);
|
||||
|
||||
// 7. Generate new content ahead
|
||||
self.level_gen.generate_if_needed(
|
||||
cfg,
|
||||
&mut self.platforms,
|
||||
&mut self.enemies,
|
||||
&mut self.pickups,
|
||||
);
|
||||
|
||||
// 8. Accumulate score with current multiplier
|
||||
let mult = self.score_multiplier();
|
||||
self.score_f += cfg.score_per_second * mult * dt;
|
||||
self.score = self.score_f as u64;
|
||||
}
|
||||
|
||||
// ── Effect queries ────────────────────────────────────────────────────────
|
||||
|
||||
pub fn is_invulnerable(&self) -> bool {
|
||||
self.active_effects.iter().any(|e| e.is_invulnerable())
|
||||
}
|
||||
|
||||
fn jump_multiplier(&self) -> f32 {
|
||||
self.active_effects.iter().fold(1.0_f32, |acc, e| acc * e.jump_multiplier())
|
||||
}
|
||||
|
||||
fn score_multiplier(&self) -> f32 {
|
||||
self.active_effects.iter().fold(1.0_f32, |acc, e| acc * e.score_multiplier())
|
||||
}
|
||||
|
||||
// ── Collision resolution ──────────────────────────────────────────────────
|
||||
|
||||
fn resolve_platform_collisions(&mut self) {
|
||||
if !self.player.alive {
|
||||
return;
|
||||
}
|
||||
let ph = self.player.height;
|
||||
let px = self.player.x;
|
||||
|
||||
let prev_bottom = self.player.prev_bottom();
|
||||
let curr_bottom = self.player.bottom();
|
||||
|
||||
for platform in &self.platforms {
|
||||
let overlaps_x = self.player.right() > platform.x + 4.0
|
||||
&& px < platform.right() - 4.0;
|
||||
if !overlaps_x {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Top-landing: bottom crossed platform surface this frame while falling.
|
||||
if self.player.vy >= 0.0
|
||||
&& prev_bottom <= platform.top() + 1.0
|
||||
&& curr_bottom >= platform.top()
|
||||
{
|
||||
self.player.y = platform.top() - ph;
|
||||
self.player.vy = 0.0;
|
||||
self.player.on_ground = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_enemy_collisions(&mut self, cfg: &Config) {
|
||||
if !self.player.alive {
|
||||
return;
|
||||
}
|
||||
let px = self.player.x;
|
||||
let prev_bottom = self.player.prev_bottom();
|
||||
let curr_bottom = self.player.bottom();
|
||||
let player_top = self.player.y;
|
||||
// Precompute effect queries before the mutable borrow of self.enemies.
|
||||
let invulnerable = self.is_invulnerable();
|
||||
let score_mult = self.score_multiplier();
|
||||
|
||||
for enemy in &mut self.enemies {
|
||||
if !enemy.alive {
|
||||
continue;
|
||||
}
|
||||
|
||||
let overlaps_x = self.player.right() > enemy.x && px < enemy.right();
|
||||
if !overlaps_x {
|
||||
continue;
|
||||
}
|
||||
|
||||
let overlaps_y = curr_bottom > enemy.top() && player_top < enemy.bottom();
|
||||
if !overlaps_y {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stomp: player was above enemy top and crosses it while falling.
|
||||
let stomp = self.player.vy > 0.0
|
||||
&& prev_bottom <= enemy.top() + 8.0
|
||||
&& curr_bottom >= enemy.top();
|
||||
|
||||
if stomp {
|
||||
enemy.alive = false;
|
||||
self.player.stomp_bounce(cfg);
|
||||
self.score_f += cfg.enemy_stomp_bonus * score_mult;
|
||||
self.score = self.score_f as u64;
|
||||
} else if !invulnerable {
|
||||
self.player.alive = false;
|
||||
return;
|
||||
}
|
||||
// invulnerable + non-stomp → pass through
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_pickups(&mut self, cfg: &Config) {
|
||||
if !self.player.alive {
|
||||
return;
|
||||
}
|
||||
let px = self.player.x;
|
||||
let curr_bottom = self.player.bottom();
|
||||
let player_top = self.player.y;
|
||||
|
||||
for pickup in &mut self.pickups {
|
||||
if pickup.collected {
|
||||
continue;
|
||||
}
|
||||
|
||||
let overlaps_x = self.player.right() > pickup.x && px < pickup.right();
|
||||
let overlaps_y = curr_bottom > pickup.y && player_top < pickup.bottom();
|
||||
|
||||
if overlaps_x && overlaps_y {
|
||||
pickup.collected = true;
|
||||
let effect = cfg.pickup_defs[pickup.def_index].create_effect();
|
||||
self.active_effects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rendering ─────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn render(&self, d: &mut RaylibDrawHandle, cfg: &Config) {
|
||||
// Sky
|
||||
d.draw_rectangle(0, 0, cfg.screen_width, cfg.screen_height,
|
||||
Color::new(22, 24, 46, 255));
|
||||
d.draw_rectangle(0, cfg.screen_height / 2, cfg.screen_width, cfg.screen_height / 2,
|
||||
Color::new(18, 20, 38, 255));
|
||||
|
||||
self.draw_platforms(d);
|
||||
self.draw_pickups(d, cfg);
|
||||
self.draw_enemies(d);
|
||||
self.draw_player(d);
|
||||
}
|
||||
|
||||
/// Draw effect timer bars; called from game HUD so it layers above the world.
|
||||
pub fn draw_active_effects_hud(&self, d: &mut RaylibDrawHandle, cfg: &Config) {
|
||||
let bar_w = 180;
|
||||
let bar_h = 14;
|
||||
let x0 = 16;
|
||||
let mut y = 58;
|
||||
|
||||
for effect in &self.active_effects {
|
||||
let tint = effect.tint();
|
||||
let (r, g, b, _) = (tint.r, tint.g, tint.b, tint.a);
|
||||
|
||||
// Background
|
||||
d.draw_rectangle(x0, y, bar_w, bar_h, Color::new(30, 30, 55, 200));
|
||||
// Fill
|
||||
let fill = (effect.progress() * bar_w as f32) as i32;
|
||||
d.draw_rectangle(x0, y, fill, bar_h, Color::new(r, g, b, 200));
|
||||
// Border
|
||||
d.draw_rectangle_lines(x0, y, bar_w, bar_h, Color::new(r, g, b, 160));
|
||||
// Label
|
||||
let lbl = effect.label();
|
||||
d.draw_text(lbl, x0 + 4, y + 1, 11,
|
||||
Color::new(cfg.screen_width as u8, 255, 255, 255)); // reuse white
|
||||
d.draw_text(lbl, x0 + 4, y + 1, 11, Color::WHITE);
|
||||
|
||||
y += bar_h + 4;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_platforms(&self, d: &mut RaylibDrawHandle) {
|
||||
for p in &self.platforms {
|
||||
d.draw_rectangle(p.x as i32, p.y as i32, p.width as i32, p.height as i32,
|
||||
Color::new(58, 160, 72, 255));
|
||||
d.draw_rectangle(p.x as i32, p.y as i32, p.width as i32, 4,
|
||||
Color::new(100, 220, 110, 255));
|
||||
d.draw_rectangle(p.x as i32, (p.y + p.height - 4.0) as i32, p.width as i32, 4,
|
||||
Color::new(38, 110, 50, 255));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_pickups(&self, d: &mut RaylibDrawHandle, cfg: &Config) {
|
||||
let s = cfg.pickup_size as i32;
|
||||
// Animate a gentle bob using elapsed time
|
||||
let bob = (self.elapsed * 3.0).sin() * 3.0;
|
||||
|
||||
for pickup in &self.pickups {
|
||||
if pickup.collected {
|
||||
continue;
|
||||
}
|
||||
|
||||
let def = &cfg.pickup_defs[pickup.def_index];
|
||||
let (r, g, b) = def.color;
|
||||
let x = pickup.x as i32;
|
||||
let y = (pickup.y + bob) as i32;
|
||||
|
||||
// Glow shadow
|
||||
d.draw_rectangle(x - 2, y - 2, s + 4, s + 4, Color::new(r, g, b, 60));
|
||||
// Body
|
||||
d.draw_rectangle(x, y, s, s, Color::new(r, g, b, 220));
|
||||
// Shine
|
||||
d.draw_rectangle(x + 3, y + 3, s / 3, s / 3, Color::new(255, 255, 255, 100));
|
||||
// Border
|
||||
d.draw_rectangle_lines(x, y, s, s, Color::WHITE);
|
||||
// First letter of label
|
||||
let letter = &def.label[..1];
|
||||
let lw = d.measure_text(letter, 16);
|
||||
d.draw_text(letter, x + (s - lw) / 2, y + (s - 16) / 2, 16, Color::WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_enemies(&self, d: &mut RaylibDrawHandle) {
|
||||
for e in &self.enemies {
|
||||
if !e.alive {
|
||||
continue;
|
||||
}
|
||||
d.draw_rectangle(e.x as i32, e.y as i32, e.width as i32, e.height as i32,
|
||||
Color::new(210, 50, 50, 255));
|
||||
|
||||
let eye_y = (e.y + e.height * 0.28) as i32;
|
||||
d.draw_circle((e.x + e.width * 0.28) as i32, eye_y, 5.0, Color::WHITE);
|
||||
d.draw_circle((e.x + e.width * 0.72) as i32, eye_y, 5.0, Color::WHITE);
|
||||
d.draw_circle((e.x + e.width * 0.28) as i32 + 1, eye_y + 1, 2.5, Color::BLACK);
|
||||
d.draw_circle((e.x + e.width * 0.72) as i32 + 1, eye_y + 1, 2.5, Color::BLACK);
|
||||
|
||||
let bx = e.x as i32;
|
||||
let bw = e.width as i32;
|
||||
let by = eye_y - 10;
|
||||
d.draw_line(bx + 4, by - 2, bx + bw / 2 - 2, by + 4, Color::new(140, 20, 20, 255));
|
||||
d.draw_line(bx + bw / 2 + 2, by + 4, bx + bw - 4, by - 2, Color::new(140, 20, 20, 255));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_player(&self, d: &mut RaylibDrawHandle) {
|
||||
let p = &self.player;
|
||||
if !p.alive {
|
||||
return;
|
||||
}
|
||||
|
||||
let x = p.x as i32;
|
||||
let y = p.y as i32;
|
||||
let w = p.width as i32;
|
||||
let h = p.height as i32;
|
||||
|
||||
// Invulnerability pulsing outline
|
||||
if self.is_invulnerable() {
|
||||
let pulse = ((self.elapsed * 8.0).sin() * 0.5 + 0.5) as f32;
|
||||
let alpha = (150.0 + pulse * 105.0) as u8;
|
||||
d.draw_rectangle(x - 3, y - 3, w + 6, h + 6, Color::new(255, 200, 50, alpha));
|
||||
}
|
||||
|
||||
let body_color = if p.on_ground {
|
||||
Color::new(60, 120, 220, 255)
|
||||
} else {
|
||||
Color::new(80, 160, 255, 255)
|
||||
};
|
||||
d.draw_rectangle(x, y + h / 4, w, h * 3 / 4, body_color);
|
||||
d.draw_rectangle(x + 4, y, w - 8, h / 2, Color::new(255, 210, 140, 255));
|
||||
d.draw_rectangle(x + 8, y + 8, 6, 6, Color::WHITE);
|
||||
d.draw_rectangle(x + w - 14, y + 8, 6, 6, Color::WHITE);
|
||||
d.draw_rectangle(x + 10, y + 10, 3, 3, Color::BLACK);
|
||||
d.draw_rectangle(x + w - 12, y + 10, 3, 3, Color::BLACK);
|
||||
d.draw_rectangle(x, y + h / 4, w, 5, Color::new(220, 60, 60, 255));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user