Add the entities.png manifest, paste-in template and sprite loader
The sheet becomes a fixed grid (one row per entity, one column slot per animation state) described by a sidecar entities.toml that only maps names to rows; the loader reads present states and frame counts off the pixels, so assembling the sheet by hand needs no bookkeeping. tools/mktemplate.py renders the labelled template with low-alpha slot guides. The CGA atlas is kept for larger UI text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+113
-42
@@ -154,9 +154,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.0"
|
version = "3.20.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
@@ -180,9 +180,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "calloop"
|
name = "calloop"
|
||||||
@@ -469,13 +469,39 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "game"
|
name = "game"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pbio",
|
"pbio",
|
||||||
"png",
|
"png",
|
||||||
|
"serde",
|
||||||
"sim",
|
"sim",
|
||||||
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -702,11 +728,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.82"
|
version = "0.3.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
|
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -921,19 +948,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum"
|
name = "num_enum"
|
||||||
version = "0.7.6"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num_enum_derive",
|
"num_enum_derive",
|
||||||
"rustversion",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum_derive"
|
name = "num_enum_derive"
|
||||||
version = "0.7.6"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1306,9 +1332,9 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.11.1"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic-util"
|
name = "portable-atomic-util"
|
||||||
@@ -1327,10 +1353,11 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "2.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"toml_datetime 0.6.3",
|
||||||
"toml_edit",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1535,6 +1562,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -1729,24 +1765,44 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "0.9.12+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde_core",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
|
"toml_parser",
|
||||||
|
"toml_writer",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.7.5+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.6+spec-1.1.0"
|
version = "0.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0db3bae107c9522f86d361697dee1d7386a2ddcf659d5aea5159819a21a3c4a7"
|
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime",
|
"toml_datetime 0.6.3",
|
||||||
"toml_parser",
|
"winnow 0.5.40",
|
||||||
"winnow",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1755,9 +1811,15 @@ version = "1.1.2+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_writer"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
@@ -1825,9 +1887,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.105"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
|
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -1838,22 +1900,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.55"
|
version = "0.4.76"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
|
checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"once_cell",
|
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.105"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
|
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -1861,9 +1920,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.105"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
|
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1874,9 +1933,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.105"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
|
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -1992,9 +2051,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.82"
|
version = "0.3.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
|
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -2385,13 +2444,25 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.3"
|
version = "0.5.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 310 B After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,116 @@
|
|||||||
|
# Sidecar manifest for entities.png.
|
||||||
|
#
|
||||||
|
# The sheet is a grid of 8×8 cells: one entity per row, one fixed column slot per
|
||||||
|
# state. States are Idle, Sit, Walk, Action 1, Action 2, Hit, Dead; a slot holds up to
|
||||||
|
# `max` frames, left to right. Frames present in a slot are detected from the PNG
|
||||||
|
# (a fully transparent cell ends the animation, an empty slot means the entity has no
|
||||||
|
# such state and Idle is shown instead) — so this file only says WHERE things are,
|
||||||
|
# never how many frames exist.
|
||||||
|
#
|
||||||
|
# `assets/tilesets/entities-template.png` (from `tools/mktemplate.py`) shows the
|
||||||
|
# same layout with labels and slot guides: paste sprites into it, save it as
|
||||||
|
# entities.png. The label column and header are outside `origin` and never loaded.
|
||||||
|
|
||||||
|
[layout]
|
||||||
|
cell = 8
|
||||||
|
origin = [64, 16] # x, y of the first sprite cell (right of the labels, below the header)
|
||||||
|
|
||||||
|
# state = [first column (cells, relative to origin), max frames]
|
||||||
|
[layout.states]
|
||||||
|
idle = [0, 2]
|
||||||
|
sit = [2, 2]
|
||||||
|
walk = [4, 4]
|
||||||
|
action1 = [8, 4]
|
||||||
|
action2 = [12, 2]
|
||||||
|
hit = [14, 2]
|
||||||
|
dead = [16, 2]
|
||||||
|
|
||||||
|
# name = row (0-based). Order is free, rows must be unique.
|
||||||
|
[entities]
|
||||||
|
player = 0
|
||||||
|
|
||||||
|
ghost1 = 1
|
||||||
|
ghost2 = 2
|
||||||
|
mutant_rat = 3
|
||||||
|
slime1 = 4
|
||||||
|
slime2 = 5
|
||||||
|
slime3 = 6
|
||||||
|
ent = 7
|
||||||
|
shroom = 8
|
||||||
|
floating_eye = 9
|
||||||
|
|
||||||
|
chick = 10
|
||||||
|
hen = 11
|
||||||
|
rooster = 12
|
||||||
|
fox_kit = 13
|
||||||
|
fox = 14
|
||||||
|
rabbit_kit = 15
|
||||||
|
rabbit = 16
|
||||||
|
mouse_pup = 17
|
||||||
|
mouse = 18
|
||||||
|
kitten1 = 19
|
||||||
|
cat1 = 20
|
||||||
|
kitten2 = 21
|
||||||
|
cat2 = 22
|
||||||
|
puppy = 23
|
||||||
|
dog = 24
|
||||||
|
foal1 = 25
|
||||||
|
horse1 = 26
|
||||||
|
foal2 = 27
|
||||||
|
horse2 = 28
|
||||||
|
foal3 = 29
|
||||||
|
horse3 = 30
|
||||||
|
foal4 = 31
|
||||||
|
horse4 = 32
|
||||||
|
|
||||||
|
baby_bird1 = 33
|
||||||
|
bird1 = 34
|
||||||
|
baby_bird2 = 35
|
||||||
|
bird2 = 36
|
||||||
|
cardinal_chick = 37
|
||||||
|
cardinal = 38
|
||||||
|
baby_eagle = 39
|
||||||
|
eagle = 40
|
||||||
|
baby_crow = 41
|
||||||
|
crow = 42
|
||||||
|
|
||||||
|
piglet = 43
|
||||||
|
pig = 44
|
||||||
|
calf = 45
|
||||||
|
cattle = 46
|
||||||
|
kid = 47
|
||||||
|
goat = 48
|
||||||
|
lamb = 49
|
||||||
|
sheep = 50
|
||||||
|
duckling = 51
|
||||||
|
duck = 52
|
||||||
|
wolf_pup = 53
|
||||||
|
wolf = 54
|
||||||
|
doe = 55
|
||||||
|
buck = 56
|
||||||
|
boar_piglet = 57
|
||||||
|
boar = 58
|
||||||
|
bear_cub = 59
|
||||||
|
bear = 60
|
||||||
|
squirrel_kit = 61
|
||||||
|
squirrel = 62
|
||||||
|
turkey_chick = 63
|
||||||
|
turkey = 64
|
||||||
|
opossum_joey = 65
|
||||||
|
opossum = 66
|
||||||
|
skunk_kit = 67
|
||||||
|
skunk = 68
|
||||||
|
raccoon_cub = 69
|
||||||
|
raccoon = 70
|
||||||
|
|
||||||
|
fish1 = 71
|
||||||
|
fish2 = 72
|
||||||
|
fish3 = 73
|
||||||
|
fish4 = 74
|
||||||
|
bees = 75
|
||||||
|
bugs1 = 76
|
||||||
|
bugs2 = 77
|
||||||
|
butterfly1 = 78
|
||||||
|
butterfly2 = 79
|
||||||
|
butterfly3 = 80
|
||||||
|
butterfly4 = 81
|
||||||
@@ -7,3 +7,5 @@ edition = "2024"
|
|||||||
sim = { path = "../sim" }
|
sim = { path = "../sim" }
|
||||||
pbio = { git = "https://git.bnd.wtf/irrlicht/pbio.git", branch = "truecolor16" }
|
pbio = { git = "https://git.bnd.wtf/irrlicht/pbio.git", branch = "truecolor16" }
|
||||||
png = "0.18.1"
|
png = "0.18.1"
|
||||||
|
toml = "0.9"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
+14
-9
@@ -41,25 +41,30 @@ impl Image {
|
|||||||
Image { width: info.width, height: info.height, pixels, opaque }
|
Image { width: info.width, height: info.height, pixels, opaque }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Split this image into 8×8 tiles, row-major. Tile ID = flat index into the Vec.
|
/// The 8×8 tile whose top-left pixel is `(x, y)`.
|
||||||
pub fn to_tileset(&self) -> Vec<Tile> {
|
pub fn tile_at(&self, x: usize, y: usize) -> Tile {
|
||||||
let w = self.width as usize;
|
let w = self.width as usize;
|
||||||
let cols = w / 8;
|
|
||||||
let rows = self.height as usize / 8;
|
|
||||||
let mut tiles = Vec::with_capacity(rows * cols);
|
|
||||||
for row in 0..rows {
|
|
||||||
for col in 0..cols {
|
|
||||||
let mut tile = Tile { px: [0; 64], mask: 0 };
|
let mut tile = Tile { px: [0; 64], mask: 0 };
|
||||||
for tr in 0..8 {
|
for tr in 0..8 {
|
||||||
for tc in 0..8 {
|
for tc in 0..8 {
|
||||||
let i = w * (row * 8 + tr) + col * 8 + tc;
|
let i = w * (y + tr) + x + tc;
|
||||||
tile.px[tr * 8 + tc] = self.pixels[i];
|
tile.px[tr * 8 + tc] = self.pixels[i];
|
||||||
if self.opaque[i] {
|
if self.opaque[i] {
|
||||||
tile.mask |= 1 << (tr * 8 + tc);
|
tile.mask |= 1 << (tr * 8 + tc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tiles.push(tile);
|
tile
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split this image into 8×8 tiles, row-major. Tile ID = flat index into the Vec.
|
||||||
|
pub fn to_tileset(&self) -> Vec<Tile> {
|
||||||
|
let cols = self.width as usize / 8;
|
||||||
|
let rows = self.height as usize / 8;
|
||||||
|
let mut tiles = Vec::with_capacity(rows * cols);
|
||||||
|
for row in 0..rows {
|
||||||
|
for col in 0..cols {
|
||||||
|
tiles.push(self.tile_at(col * 8, row * 8));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tiles
|
tiles
|
||||||
|
|||||||
+26
-24
@@ -4,6 +4,7 @@ use std::collections::{HashMap, VecDeque};
|
|||||||
|
|
||||||
use crate::assets::{Image, Tile};
|
use crate::assets::{Image, Tile};
|
||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
|
use crate::sprites::{Sprites, State};
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
use crate::input::{GameAction, InputState};
|
use crate::input::{GameAction, InputState};
|
||||||
use sim::entity::{Brain, Entity, EntityKind, Prey};
|
use sim::entity::{Brain, Entity, EntityKind, Prey};
|
||||||
@@ -49,11 +50,6 @@ const CAM_SNAP_PX: f32 = 96.0;
|
|||||||
const HEN_COUNT: usize = 150;
|
const HEN_COUNT: usize = 150;
|
||||||
const FOX_COUNT: usize = 6;
|
const FOX_COUNT: usize = 6;
|
||||||
|
|
||||||
/// Sprite indices in `entities.png`.
|
|
||||||
const SPRITE_PLAYER: u16 = 0;
|
|
||||||
const SPRITE_HEN: u16 = 1;
|
|
||||||
const SPRITE_FOX: u16 = 2;
|
|
||||||
|
|
||||||
/// HUD panel: the 80 px strip right of the viewport.
|
/// HUD panel: the 80 px strip right of the viewport.
|
||||||
const HUD_X: i32 = VIEW_PX;
|
const HUD_X: i32 = VIEW_PX;
|
||||||
const HUD_W: i32 = 320 - VIEW_PX;
|
const HUD_W: i32 = 320 - VIEW_PX;
|
||||||
@@ -80,8 +76,11 @@ struct EntityLerp {
|
|||||||
|
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
tileset: Vec<Tile>,
|
tileset: Vec<Tile>,
|
||||||
entity_tileset: Vec<Tile>,
|
sprites: Sprites,
|
||||||
font: Font,
|
font: Font,
|
||||||
|
/// Sprite ids (= entity `type_id`) resolved from the manifest by name.
|
||||||
|
id_hen: u16,
|
||||||
|
id_fox: u16,
|
||||||
sim: Sim,
|
sim: Sim,
|
||||||
player_id: u32,
|
player_id: u32,
|
||||||
/// Frame time not yet consumed by whole ticks.
|
/// Frame time not yet consumed by whole ticks.
|
||||||
@@ -107,7 +106,8 @@ pub struct Game {
|
|||||||
impl Game {
|
impl Game {
|
||||||
pub fn start(map_path: &str) -> Self {
|
pub fn start(map_path: &str) -> Self {
|
||||||
let tileset = Image::from_png("assets/tilesets/overworld.png").to_tileset();
|
let tileset = Image::from_png("assets/tilesets/overworld.png").to_tileset();
|
||||||
let entity_tileset = Image::from_png("assets/tilesets/entities.png").to_tileset();
|
let sprites = Sprites::load("assets/tilesets/entities.png", "assets/tilesets/entities.toml");
|
||||||
|
let (id_player, id_hen, id_fox) = (sprites.id("player"), sprites.id("hen"), sprites.id("fox"));
|
||||||
let font = Font::from_png("assets/fonts/micro-chat-8x9.png", 8, 9);
|
let font = Font::from_png("assets/fonts/micro-chat-8x9.png", 8, 9);
|
||||||
|
|
||||||
let t0 = std::time::Instant::now();
|
let t0 = std::time::Instant::now();
|
||||||
@@ -117,7 +117,7 @@ impl Game {
|
|||||||
map.width, map.height, t0.elapsed().as_secs_f64() * 1e3);
|
map.width, map.height, t0.elapsed().as_secs_f64() * 1e3);
|
||||||
let mut sim = Sim::new(world);
|
let mut sim = Sim::new(world);
|
||||||
let player_id = sim.world.spawn_entity(
|
let player_id = sim.world.spawn_entity(
|
||||||
EntityKind::Player, SPRITE_PLAYER, Brain::Idle, (0, 0), 100, 3);
|
EntityKind::Player, id_player, Brain::Idle, (0, 0), 100, 3);
|
||||||
|
|
||||||
// Scatter animals over free tiles. A cheap LCG is plenty for placement.
|
// Scatter animals over free tiles. A cheap LCG is plenty for placement.
|
||||||
let mut seed: u32 = 12345;
|
let mut seed: u32 = 12345;
|
||||||
@@ -133,13 +133,15 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
scatter(&mut sim, HEN_COUNT, SPRITE_HEN, Brain::Wander, 5, 1);
|
scatter(&mut sim, HEN_COUNT, id_hen, Brain::Wander, 5, 1);
|
||||||
scatter(&mut sim, FOX_COUNT, SPRITE_FOX, Brain::Hunt(Prey::Kind(SPRITE_HEN)), 8, 2);
|
scatter(&mut sim, FOX_COUNT, id_fox, Brain::Hunt(Prey::Kind(id_hen)), 8, 2);
|
||||||
|
|
||||||
let mut game = Game {
|
let mut game = Game {
|
||||||
tileset,
|
tileset,
|
||||||
entity_tileset,
|
sprites,
|
||||||
font,
|
font,
|
||||||
|
id_hen,
|
||||||
|
id_fox,
|
||||||
sim,
|
sim,
|
||||||
player_id,
|
player_id,
|
||||||
tick_accum_ms: 0.0,
|
tick_accum_ms: 0.0,
|
||||||
@@ -209,7 +211,15 @@ impl Game {
|
|||||||
|
|
||||||
/// How an entity is referred to in the log. Ids only carry meaning for debugging.
|
/// How an entity is referred to in the log. Ids only carry meaning for debugging.
|
||||||
fn name(&self, id: u32, type_id: u16) -> &'static str {
|
fn name(&self, id: u32, type_id: u16) -> &'static str {
|
||||||
if id == self.player_id { "you" } else { species(type_id) }
|
if id == self.player_id { "you" } else { self.species(type_id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn species(&self, type_id: u16) -> &'static str {
|
||||||
|
match type_id {
|
||||||
|
t if t == self.id_hen => "hen",
|
||||||
|
t if t == self.id_fox => "fox",
|
||||||
|
_ => "something",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// React to what the sim reported for the last window. Returns `true` once the
|
/// React to what the sim reported for the last window. Returns `true` once the
|
||||||
@@ -232,7 +242,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
Event::Died { id, .. } if id == self.player_id => dead = true,
|
Event::Died { id, .. } if id == self.player_id => dead = true,
|
||||||
Event::Died { type_id, by, .. } if by == self.player_id => {
|
Event::Died { type_id, by, .. } if by == self.player_id => {
|
||||||
self.push_log(format!("you kill the {}", species(type_id)));
|
self.push_log(format!("you kill the {}", self.species(type_id)));
|
||||||
}
|
}
|
||||||
Event::Attacked { .. } | Event::Died { .. } => {}
|
Event::Attacked { .. } | Event::Died { .. } => {}
|
||||||
}
|
}
|
||||||
@@ -493,7 +503,7 @@ impl Game {
|
|||||||
let px = ex - cx;
|
let px = ex - cx;
|
||||||
let py = ey - cy;
|
let py = ey - cy;
|
||||||
if px <= -TILE_PX || px >= VIEW_PX || py <= -TILE_PX || py >= VIEW_PX { continue; }
|
if px <= -TILE_PX || px >= VIEW_PX || py <= -TILE_PX || py >= VIEW_PX { continue; }
|
||||||
if let Some(tile) = self.entity_tileset.get(e.type_id as usize) {
|
if let Some(tile) = self.sprites.frame(e.type_id, State::Idle, 0) {
|
||||||
pixelhelper::blit_tile(frame, W, px, py, tile);
|
pixelhelper::blit_tile(frame, W, px, py, tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -507,14 +517,6 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn species(type_id: u16) -> &'static str {
|
|
||||||
match type_id {
|
|
||||||
SPRITE_HEN => "hen",
|
|
||||||
SPRITE_FOX => "fox",
|
|
||||||
_ => "something",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
fn render_hud(&self, frame: &mut [u16]) {
|
fn render_hud(&self, frame: &mut [u16]) {
|
||||||
const W: usize = 320;
|
const W: usize = 320;
|
||||||
@@ -535,7 +537,7 @@ impl Game {
|
|||||||
self.font.draw(frame, W, x, y, HUD_DIM, &hp);
|
self.font.draw(frame, W, x, y, HUD_DIM, &hp);
|
||||||
y += LINE_H;
|
y += LINE_H;
|
||||||
|
|
||||||
for (label, sprite) in [("hens", SPRITE_HEN), ("foxes", SPRITE_FOX)] {
|
for (label, sprite) in [("hens", self.id_hen), ("foxes", self.id_fox)] {
|
||||||
let n = self.count(sprite).to_string();
|
let n = self.count(sprite).to_string();
|
||||||
self.font.draw(frame, W, x, y, HUD_DIM, label);
|
self.font.draw(frame, W, x, y, HUD_DIM, label);
|
||||||
self.font.draw(frame, W, x + inner_w - self.font.text_width(&n), y, HUD_DIM, &n);
|
self.font.draw(frame, W, x + inner_w - self.font.text_width(&n), y, HUD_DIM, &n);
|
||||||
@@ -574,7 +576,7 @@ mod snapshot {
|
|||||||
for _ in 0..secs * 60 {
|
for _ in 0..secs * 60 {
|
||||||
g.update(&mut frame, 16, &input);
|
g.update(&mut frame, 16, &input);
|
||||||
}
|
}
|
||||||
println!("after {secs}s: {} hens, {} foxes", g.count(super::SPRITE_HEN), g.count(super::SPRITE_FOX));
|
println!("after {secs}s: {} hens, {} foxes", g.count(g.id_hen), g.count(g.id_fox));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a frame headlessly to the PNG named by `$SNAPSHOT` — a way to eyeball
|
/// Renders a frame headlessly to the PNG named by `$SNAPSHOT` — a way to eyeball
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ mod assets;
|
|||||||
mod font;
|
mod font;
|
||||||
mod game;
|
mod game;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod sprites;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
//! Entity sprite sheet with its sidecar manifest (`entities.toml`).
|
||||||
|
//!
|
||||||
|
//! The sheet is a grid of 8×8 cells, one entity per row, one fixed column slot per
|
||||||
|
//! animation state. The manifest gives the layout and `name → row`; which states a
|
||||||
|
//! row has and how many frames each holds is read off the pixels: frames are the
|
||||||
|
//! leading non-empty cells of a slot, an empty slot means "no such state".
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::assets::{Image, Tile};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum State {
|
||||||
|
Idle,
|
||||||
|
Sit,
|
||||||
|
Walk,
|
||||||
|
Action1,
|
||||||
|
Action2,
|
||||||
|
Hit,
|
||||||
|
Dead,
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATES: [(State, &str); 7] = [
|
||||||
|
(State::Idle, "idle"), (State::Sit, "sit"), (State::Walk, "walk"),
|
||||||
|
(State::Action1, "action1"), (State::Action2, "action2"),
|
||||||
|
(State::Hit, "hit"), (State::Dead, "dead"),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Manifest {
|
||||||
|
layout: Layout,
|
||||||
|
entities: HashMap<String, u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Layout {
|
||||||
|
cell: u32,
|
||||||
|
origin: (u32, u32),
|
||||||
|
states: HashMap<String, (u16, u8)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One state's frames on a row: first cell index into `Sprites::tiles` and count.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Frames {
|
||||||
|
start: usize,
|
||||||
|
count: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Sprites {
|
||||||
|
tiles: Vec<Tile>,
|
||||||
|
/// Per row, per `STATES` index.
|
||||||
|
rows: Vec<[Option<Frames>; 7]>,
|
||||||
|
ids: HashMap<String, u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sprites {
|
||||||
|
pub fn load(sheet: &str, manifest: &str) -> Sprites {
|
||||||
|
let text = std::fs::read_to_string(manifest)
|
||||||
|
.unwrap_or_else(|e| panic!("failed to read {manifest}: {e}"));
|
||||||
|
let m: Manifest = toml::from_str(&text)
|
||||||
|
.unwrap_or_else(|e| panic!("bad manifest {manifest}: {e}"));
|
||||||
|
assert_eq!(m.layout.cell, 8, "{manifest}: only 8 px cells are supported");
|
||||||
|
|
||||||
|
let slots: Vec<(u16, u8)> = STATES.iter()
|
||||||
|
.map(|(_, name)| *m.layout.states.get(*name)
|
||||||
|
.unwrap_or_else(|| panic!("{manifest}: layout.states.{name} missing")))
|
||||||
|
.collect();
|
||||||
|
let cols = slots.iter().map(|&(c, n)| c as usize + n as usize).max().unwrap_or(0);
|
||||||
|
let row_count = m.entities.values().map(|&r| r as usize + 1).max().unwrap_or(0);
|
||||||
|
|
||||||
|
let img = Image::from_png(sheet);
|
||||||
|
let (ox, oy) = (m.layout.origin.0 as usize, m.layout.origin.1 as usize);
|
||||||
|
assert!(img.width as usize >= ox + cols * 8 && img.height as usize >= oy + row_count * 8,
|
||||||
|
"{sheet}: {}×{} is too small for the manifest's {cols}×{row_count} cells",
|
||||||
|
img.width, img.height);
|
||||||
|
|
||||||
|
let mut tiles = Vec::with_capacity(cols * row_count);
|
||||||
|
for row in 0..row_count {
|
||||||
|
for col in 0..cols {
|
||||||
|
tiles.push(img.tile_at(ox + col * 8, oy + row * 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = (0..row_count).map(|row| {
|
||||||
|
let mut states = [None; 7];
|
||||||
|
for (i, &(col, max)) in slots.iter().enumerate() {
|
||||||
|
let start = row * cols + col as usize;
|
||||||
|
let count = (0..max as usize)
|
||||||
|
.take_while(|&f| tiles[start + f].mask != 0)
|
||||||
|
.count() as u8;
|
||||||
|
if count > 0 {
|
||||||
|
states[i] = Some(Frames { start, count });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
states
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Sprites { tiles, rows, ids: m.entities }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sprite id (row) for a manifest name. Panics on unknown names — a typo in
|
||||||
|
/// game code, not a runtime condition.
|
||||||
|
pub fn id(&self, name: &str) -> u16 {
|
||||||
|
*self.ids.get(name).unwrap_or_else(|| panic!("no sprite named {name:?} in the manifest"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame `i` (wrapping) of `state`, falling back to `Idle` for a missing state.
|
||||||
|
/// `None` only for a sprite that has no idle frame either — an empty row.
|
||||||
|
pub fn frame(&self, sprite: u16, state: State, i: usize) -> Option<&Tile> {
|
||||||
|
let row = self.rows.get(sprite as usize)?;
|
||||||
|
let f = row[state as usize].or(row[State::Idle as usize])?;
|
||||||
|
Some(&self.tiles[f.start + i % f.count as usize])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn sprites() -> Sprites {
|
||||||
|
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/tilesets/");
|
||||||
|
Sprites::load(&format!("{root}entities.png"), &format!("{root}entities.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn manifest_names_resolve_and_bootstrap_sprites_have_idle() {
|
||||||
|
let s = sprites();
|
||||||
|
for name in ["player", "hen", "fox"] {
|
||||||
|
let id = s.id(name);
|
||||||
|
let idle = s.frame(id, State::Idle, 0).unwrap();
|
||||||
|
assert_ne!(idle.mask, 0, "{name}");
|
||||||
|
// Only one idle frame so far: index 1 wraps to it. No walk yet: falls back.
|
||||||
|
assert_eq!(s.frame(id, State::Idle, 1).unwrap().px, idle.px);
|
||||||
|
assert_eq!(s.frame(id, State::Walk, 0).unwrap().px, idle.px, "{name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_rows_have_nothing() {
|
||||||
|
let s = sprites();
|
||||||
|
assert!(s.frame(s.id("butterfly4"), State::Idle, 0).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-4
@@ -10,9 +10,21 @@ branch `truecolor16`),
|
|||||||
not in this repo.
|
not in this repo.
|
||||||
|
|
||||||
Assets: `assets/packs/` holds the source packs untouched (Mini Medieval family by
|
Assets: `assets/packs/` holds the source packs untouched (Mini Medieval family by
|
||||||
VEXED, CC-BY); `assets/tilesets/` is what the game actually loads — sheets assembled
|
VEXED, CC-BY — "documented" sheets: labels and state columns are pixel text in the
|
||||||
from the packs (`entities.png`: player, hen, fox); `assets/fonts/` the font sources and
|
PNGs, readable by eye, not by script); `assets/tilesets/` is what the game actually
|
||||||
the repacked atlas; `assets/maps/` the Tiled CSV maps.
|
loads; `assets/fonts/` the font sources and repacked atlases (Micro Chat for the HUD,
|
||||||
|
CGA 8×8 kept for menus/titles); `assets/maps/` the Tiled CSV maps.
|
||||||
|
|
||||||
|
Entity sprites: `tilesets/entities.png` is assembled by hand from the packs into a
|
||||||
|
fixed grid described by the sidecar `entities.toml` — one row per entity, one column
|
||||||
|
slot per state (idle 2, sit 2, walk 4, action1 4, action2 2, hit 2, dead 2 frames
|
||||||
|
max). The manifest only maps `name → row` and the slot layout; which states and how
|
||||||
|
many frames a row has, the loader (`game/src/sprites.rs`) reads off the pixels
|
||||||
|
(leading non-empty cells; empty slot → falls back to idle). `tools/mktemplate.py`
|
||||||
|
renders the paste-in template `entities-template.png` (labels, header, slot guides at
|
||||||
|
alpha 48 — invisible to the loader) and carries existing sprites over, so adding rows
|
||||||
|
is: edit the manifest, rerun the tool, paste, save as `entities.png`. Game code refers
|
||||||
|
to sprites by name (`sprites.id("hen")`); the id doubles as the entity's `type_id`.
|
||||||
|
|
||||||
History: this fork started from a client/server multiplayer architecture (authoritative UDP
|
History: this fork started from a client/server multiplayer architecture (authoritative UDP
|
||||||
server, thin client with prediction). The scope was cut to single player in September 2026;
|
server, thin client with prediction). The scope was cut to single player in September 2026;
|
||||||
@@ -37,7 +49,10 @@ crate's `tick`/`set_action` interface is the seam a server could be wrapped arou
|
|||||||
dimensions), `load_world` slices it into chunks, invisible solid border at the map rim.
|
dimensions), `load_world` slices it into chunks, invisible solid border at the map rim.
|
||||||
See `notes/07-map-loader.md`
|
See `notes/07-map-loader.md`
|
||||||
- [x] Tile rendering: chunk-based world rendering straight from `World` (`render_viewport`)
|
- [x] Tile rendering: chunk-based world rendering straight from `World` (`render_viewport`)
|
||||||
- [x] Sprite-based entity rendering: entity `type_id` → tile in `entities.png`, blitted with transparency
|
- [x] Sprite-based entity rendering: entity `type_id` → sprite row in `entities.png`
|
||||||
|
(via the `entities.toml` manifest, see Assets above), idle frame blitted with
|
||||||
|
transparency. Animation (walk while the lerp runs, hit/dead from events) is the
|
||||||
|
next step — the sheet layout and loader already carry the frames.
|
||||||
- [x] Hybrid tick loop: 24 Hz base tick, movement in `entity_tick` every 4th tick (6 Hz);
|
- [x] Hybrid tick loop: 24 Hz base tick, movement in `entity_tick` every 4th tick (6 Hz);
|
||||||
the game loop drives the sim with a fixed-timestep accumulator. See `notes/06-hybrid-tick-loop.md`
|
the game loop drives the sim with a fixed-timestep accumulator. See `notes/06-hybrid-tick-loop.md`
|
||||||
- [x] Intent model: one scheduled action per entity per movement window (`Sim::set_action`),
|
- [x] Intent model: one scheduled action per entity per movement window (`Sim::set_action`),
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Draw the paste-in template for entities.png from its manifest.
|
||||||
|
|
||||||
|
Reads assets/tilesets/entities.toml and writes entities-template.png: a label
|
||||||
|
column and header (outside the loaded area), and per-state slot guides tinted at
|
||||||
|
low alpha — below the loader's opacity threshold, so guides left under a sprite are
|
||||||
|
harmless. If entities.png already exists in this layout, its sprites are carried
|
||||||
|
over, so the template can be regenerated after adding rows to the manifest.
|
||||||
|
|
||||||
|
python3 tools/mktemplate.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
MANIFEST = "assets/tilesets/entities.toml"
|
||||||
|
SHEET = "assets/tilesets/entities.png"
|
||||||
|
OUT = "assets/tilesets/entities-template.png"
|
||||||
|
FONT = "assets/fonts/micro-chat-8x9.png"
|
||||||
|
|
||||||
|
GUIDE_ALPHA = 48 # < 128: transparent for the game
|
||||||
|
GUIDE_TINTS = [(90, 160, 255), (255, 200, 90), (120, 230, 120), (255, 120, 120),
|
||||||
|
(200, 120, 255), (255, 160, 60), (160, 160, 160)]
|
||||||
|
LABEL_COLOR = (0, 0, 0, 255)
|
||||||
|
GRID_COLOR = (0, 0, 0, 90)
|
||||||
|
|
||||||
|
|
||||||
|
class Font:
|
||||||
|
"""Minimal reader for the CP437 atlas in font.rs's layout (32 columns)."""
|
||||||
|
def __init__(self, path, gw, gh):
|
||||||
|
self.img = Image.open(path).convert("L")
|
||||||
|
self.gw, self.gh = gw, gh
|
||||||
|
self.left, self.width = {}, {}
|
||||||
|
for code in range(256):
|
||||||
|
cx, cy = (code % 32) * gw, (code // 32) * gh
|
||||||
|
xs = [x for y in range(gh) for x in range(gw) if self.img.getpixel((cx + x, cy + y)) > 128]
|
||||||
|
if xs:
|
||||||
|
self.left[code], self.width[code] = min(xs), max(xs) - min(xs) + 1
|
||||||
|
|
||||||
|
def draw(self, dst, x, y, text, color):
|
||||||
|
for ch in text:
|
||||||
|
code = ord(ch) if ord(ch) < 128 else ord("?")
|
||||||
|
w = self.width.get(code, 0)
|
||||||
|
if w:
|
||||||
|
cx, cy = (code % 32) * self.gw + self.left[code], (code // 32) * self.gh
|
||||||
|
for gy in range(self.gh):
|
||||||
|
for gx in range(w):
|
||||||
|
if self.img.getpixel((cx + gx, cy + gy)) > 128 \
|
||||||
|
and x + gx < dst.size[0] and y + gy < dst.size[1]:
|
||||||
|
dst.putpixel((x + gx, y + gy), color)
|
||||||
|
x += w + 1 if w else self.gw // 2
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
m = tomllib.load(open(MANIFEST, "rb"))
|
||||||
|
cell = m["layout"]["cell"]
|
||||||
|
ox, oy = m["layout"]["origin"]
|
||||||
|
states = list(m["layout"]["states"].items())
|
||||||
|
entities = sorted(m["entities"].items(), key=lambda kv: kv[1])
|
||||||
|
rows = max(r for _, r in entities) + 1
|
||||||
|
cols = max(col + n for _, (col, n) in states)
|
||||||
|
|
||||||
|
w, h = ox + cols * cell, oy + rows * cell
|
||||||
|
# A little margin right of the sprite area so the last header label fits.
|
||||||
|
out = Image.new("RGBA", (w + 8, h), (0, 0, 0, 0))
|
||||||
|
font = Font(FONT, 8, 9)
|
||||||
|
|
||||||
|
# Slot guides: one tint per state, alternating rows slightly for readability.
|
||||||
|
for i, (name, (col, n)) in enumerate(states):
|
||||||
|
r, g, b = GUIDE_TINTS[i % len(GUIDE_TINTS)]
|
||||||
|
for row in range(rows):
|
||||||
|
a = GUIDE_ALPHA if row % 2 == 0 else GUIDE_ALPHA * 2 // 3
|
||||||
|
x0, y0 = ox + col * cell, oy + row * cell
|
||||||
|
for y in range(y0, y0 + cell):
|
||||||
|
for x in range(x0, x0 + n * cell):
|
||||||
|
out.putpixel((x, y), (r, g, b, a))
|
||||||
|
# Header label, staggered over two lines so narrow slots don't collide, and a
|
||||||
|
# separator line at the slot's left edge.
|
||||||
|
label = {"action1": "act1", "action2": "act2"}.get(name, name)
|
||||||
|
font.draw(out, ox + col * cell + 1, 1 if i % 2 == 0 else 8, label, LABEL_COLOR)
|
||||||
|
for y in range(0, h):
|
||||||
|
out.putpixel((ox + col * cell, y), GRID_COLOR)
|
||||||
|
for y in range(0, h):
|
||||||
|
out.putpixel((w, y), GRID_COLOR)
|
||||||
|
|
||||||
|
# Cell grid lines (every cell, faint) inside the sprite area.
|
||||||
|
for x in range(ox, w, cell):
|
||||||
|
for y in range(oy, h):
|
||||||
|
if out.getpixel((x, y))[3] < GRID_COLOR[3]:
|
||||||
|
out.putpixel((x, y), GRID_COLOR)
|
||||||
|
for y in range(oy, h, cell):
|
||||||
|
for x in range(ox, w):
|
||||||
|
out.putpixel((x, y), GRID_COLOR)
|
||||||
|
|
||||||
|
# Row labels.
|
||||||
|
for name, row in entities:
|
||||||
|
font.draw(out, 1, oy + row * cell, name.replace("_", " "), LABEL_COLOR)
|
||||||
|
|
||||||
|
# Carry over sprites from an existing sheet in this layout.
|
||||||
|
try:
|
||||||
|
old = Image.open(SHEET).convert("RGBA")
|
||||||
|
if old.size[0] >= w and old.size[1] >= oy:
|
||||||
|
region = old.crop((ox, oy, w, min(h, old.size[1])))
|
||||||
|
out.paste(region, (ox, oy), region)
|
||||||
|
print(f"carried over sprites from {SHEET}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
out.save(OUT)
|
||||||
|
print(f"wrote {OUT}: {w}×{h}, {rows} rows, {cols} columns")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user