Compare commits
8 Commits
46494c29d9
...
802f2bd34b
| Author | SHA1 | Date | |
|---|---|---|---|
| 802f2bd34b | |||
| d30ab97f13 | |||
| 7929e7349a | |||
| cdc27eccc3 | |||
| de853c22bb | |||
| 1af93c897c | |||
| 97eff4f9eb | |||
| ff80b3cc0f |
Generated
+582
@@ -0,0 +1,582 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "as-any"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0f477b951e452a0b6b4a10b53ccd569042d1d01729b519e02074a9c0958a063"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bladeink"
|
||||||
|
version = "1.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29b7852038d956c822e9f9091cf8aa14954d54056c1fb242e9ee5e6a0882fbce"
|
||||||
|
dependencies = [
|
||||||
|
"as-any",
|
||||||
|
"getrandom",
|
||||||
|
"rand",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"strum",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[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 = "cpufeatures"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[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]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"rand_core",
|
||||||
|
"wasip2",
|
||||||
|
"wasip3",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[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.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[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.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "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.4+wasi-0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen 0.57.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 0.51.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.123"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.123"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.123"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.123"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "wds"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"bladeink",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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"
|
||||||
|
version = "0.57.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
|
|
||||||
|
[[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"
|
||||||
@@ -4,3 +4,4 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bladeink = "1.2.5"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
VAR cube_clicks = 0
|
||||||
|
|
||||||
|
Du hast den Würfel {cube_clicks} mal berührt.
|
||||||
|
|
||||||
|
{ cube_clicks >= 3:
|
||||||
|
Er fühlt sich langsam abgenutzt an.
|
||||||
|
}
|
||||||
|
-> END
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"inkVersion":21,"root":[["^Du hast den Würfel ","ev",{"VAR?":"cube_clicks"},"out","/ev","^ mal berührt.","\n","ev",{"VAR?":"cube_clicks"},3,">=","/ev",[{"->":".^.b","c":true},{"b":["\n","^Er fühlt sich langsam abgenutzt an.","\n",{"->":"0.13"},null]}],"nop","\n","end",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"cube_clicks"},"/ev","end",null]}],"listDefs":{}}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
VAR mushrooms_collected = 0
|
||||||
|
VAR quest_mushroom_active = false
|
||||||
|
VAR quest_mushroom_done = false
|
||||||
|
|
||||||
|
{
|
||||||
|
- quest_mushroom_done:
|
||||||
|
# color whisper
|
||||||
|
Danke nochmal für die Pilze. Das Rezept ist fast fertig.
|
||||||
|
-> ende
|
||||||
|
|
||||||
|
- quest_mushroom_active && mushrooms_collected >= 10:
|
||||||
|
# color whisper
|
||||||
|
Du hast {mushrooms_collected} Pilze dabei — mehr als genug!
|
||||||
|
* [Hier, nimm sie.]
|
||||||
|
~ quest_mushroom_done = true
|
||||||
|
# color whisper
|
||||||
|
Wunderbar, vielen Dank! Das wird ein gutes Süppchen.
|
||||||
|
-> ende
|
||||||
|
* [Lass mich noch ein paar behalten.]
|
||||||
|
# color whisper
|
||||||
|
Wie du willst. Komm wieder, wenn du sie loswerden möchtest.
|
||||||
|
-> ende
|
||||||
|
|
||||||
|
- quest_mushroom_active:
|
||||||
|
# color whisper
|
||||||
|
Du hast schon {mushrooms_collected} von 10 Pilzen. Mach ruhig weiter.
|
||||||
|
-> ende
|
||||||
|
|
||||||
|
- else:
|
||||||
|
# color whisper
|
||||||
|
TEST1 Hallo! Ich bräuchte 10 Pilze für ein Rezept. Hättest du Lust, welche zu sammeln?
|
||||||
|
* [Klar, ich helfe.]
|
||||||
|
~ quest_mushroom_active = true
|
||||||
|
# color whisper
|
||||||
|
Danke! Ich warte hier.
|
||||||
|
-> ende
|
||||||
|
* [Vielleicht später.]
|
||||||
|
# color whisper
|
||||||
|
Schade. Komm gerne nochmal vorbei.
|
||||||
|
-> ende
|
||||||
|
}
|
||||||
|
|
||||||
|
= ende
|
||||||
|
* [Bis dann]
|
||||||
|
# person_farewell
|
||||||
|
-> END
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"inkVersion":21,"root":[[["ev",{"VAR?":"quest_mushroom_done"},"/ev",{"->":".^.b","c":true},{"b":["\n","#","^color whisper","/#","^Danke nochmal für die Pilze. Das Rezept ist fast fertig.","\n",{"->":"ende"},{"->":"0.4"},null]}],["ev",{"VAR?":"quest_mushroom_active"},{"VAR?":"mushrooms_collected"},10,">=","&&","/ev",{"->":".^.b","c":true},{"b":["\n","#","^color whisper","/#","^Du hast ","ev",{"VAR?":"mushrooms_collected"},"out","/ev","^ Pilze dabei — mehr als genug!","\n","ev","str","^Hier, nimm sie.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Lass mich noch ein paar behalten.","/str","/ev",{"*":".^.c-1","flg":20},{"->":"0.4"},{"c-0":["\n","ev",true,"/ev",{"VAR=":"quest_mushroom_done","re":true},"#","^color whisper","/#","^Wunderbar, vielen Dank! Das wird ein gutes Süppchen.","\n",{"->":"ende"},{"#f":5}],"c-1":["\n","#","^color whisper","/#","^Wie du willst. Komm wieder, wenn du sie loswerden möchtest.","\n",{"->":"ende"},{"#f":5}]}]}],["ev",{"VAR?":"quest_mushroom_active"},"/ev",{"->":".^.b","c":true},{"b":["\n","#","^color whisper","/#","^Du hast schon ","ev",{"VAR?":"mushrooms_collected"},"out","/ev","^ von 10 Pilzen. Mach ruhig weiter.","\n",{"->":"ende"},{"->":"0.4"},null]}],[{"->":".^.b"},{"b":["\n","#","^color whisper","/#","^TEST1 Hallo! Ich bräuchte 10 Pilze für ein Rezept. Hättest du Lust, welche zu sammeln?","\n","ev","str","^Klar, ich helfe.","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Vielleicht später.","/str","/ev",{"*":".^.c-1","flg":20},{"->":"0.4"},{"c-0":["\n","ev",true,"/ev",{"VAR=":"quest_mushroom_active","re":true},"#","^color whisper","/#","^Danke! Ich warte hier.","\n",{"->":"ende"},{"#f":5}],"c-1":["\n","#","^color whisper","/#","^Schade. Komm gerne nochmal vorbei.","\n",{"->":"ende"},{"#f":5}]}]}],"nop","\n",["done",{"#n":"g-0"}],null],"done",{"ende":[["ev","str","^Bis dann","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n","#","^person_farewell","/#","end",{"#f":5}]}],null],"global decl":["ev",0,{"VAR=":"mushrooms_collected"},false,{"VAR=":"quest_mushroom_active"},false,{"VAR=":"quest_mushroom_done"},"/ev","end",null]}],"listDefs":{}}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# color suzanne
|
||||||
|
Hallo! Ich bin Suzanne, der Affe von Blender.
|
||||||
|
Möchtest du wissen, wie diese Engine funktioniert?
|
||||||
|
* [Klar, leg los]
|
||||||
|
-> menu
|
||||||
|
* [Nein, danke.]
|
||||||
|
-> ende
|
||||||
|
|
||||||
|
=== menu ===
|
||||||
|
# color suzanne
|
||||||
|
Worüber soll ich plaudern?
|
||||||
|
* [Ink by Inkle] -> story
|
||||||
|
* [Renderer & Pixel] -> render
|
||||||
|
* [Welt & Physik] -> welt
|
||||||
|
* [Interaktion & Picking] -> interakt
|
||||||
|
* [Audio] -> audio
|
||||||
|
* [Warum so wenig Pixel?] -> pixel
|
||||||
|
* [Nichts weiter, danke] -> ende
|
||||||
|
|
||||||
|
=== render ===
|
||||||
|
# color suzanne
|
||||||
|
Alles, was du siehst, wird als ein Pixelbuffer von der CPU gerendert. Der Pixelbuffer ist einfach eine Liste von Bytes, die einen Frame beschreibt. Jedes Pixel hat dabei genau einen Byte, also 8 Bit speicherplatz. Deshalb haben wir auch nur 256 unterschiedliche Farbwerte pro Frame.
|
||||||
|
|
||||||
|
Texturen sind 8-bit indexed TGAs, die durch eine 256-Farben-Palette laufen. Das ist ein simples Dateiformat, das viele Grafikprogramme unterstützen und das sehr einfach zu verarbeiten ist.
|
||||||
|
Farb-Paletten sind auch TGA-Dateien, mit 16x16 Pixeln im normalen 24 Bit RGB-Raum. Diese werden von der Engine gelesen und als Farbpalette festgelegt.
|
||||||
|
|
||||||
|
Schalte Mal den Wireframe-Modus ist im ESC-Menü ein!
|
||||||
|
|
||||||
|
* [Zurück zum Menü] -> menu
|
||||||
|
|
||||||
|
=== welt ===
|
||||||
|
# color suzanne
|
||||||
|
Geometrie kommt aus OBJ-Dateien, die z.B. in Blender bearbeitet werden. Dabei gibt es Konventionen:
|
||||||
|
|
||||||
|
Mesh-Namen mit Prefix steuern die Rolle: VIS_ ist nur sichtbar, COL_ ist nur
|
||||||
|
Kollision, INT_ ist nur Trigger (weder sichtbar, noch kollision). Alle anderen Meshes sind sichtbar und kollidieren.
|
||||||
|
|
||||||
|
Materialien werden in assets/materials/materials.toml definiert. Dort liegen auch die Texturen.
|
||||||
|
|
||||||
|
* [Zurück zum Menü] -> menu
|
||||||
|
|
||||||
|
=== interakt ===
|
||||||
|
# color suzanne
|
||||||
|
|
||||||
|
Diese Engine nutzt ein Signal-System mit Triggern und Actions.
|
||||||
|
|
||||||
|
In signals.toml werden Trigger definiert und Actions zugewiesen. Schau dir gern die Datei Mal an.
|
||||||
|
|
||||||
|
Jedes Objekt kann einen eigenen Trigger bei Spieler-Interaktion haben. Dieser Trigger heißt einfach wie das Objekt. VIS_mushroom hat also den Trigger [VIS_mushroom], der Actions auslöst.
|
||||||
|
|
||||||
|
* [Zurück zum Menü] -> menu
|
||||||
|
|
||||||
|
=== story ===
|
||||||
|
# color suzanne
|
||||||
|
Dialoge und Interaktionen werden in Inkscript geschrieben. Die Engine liest kompilierte .ink.json-Dateien.
|
||||||
|
|
||||||
|
Im backend läuft bladeink als Interpreter. Dadurch ist der komplette Inky-Standard unterstützt. Color-Tags ändern die Textfarbe. Alle anderen Tags werden als Signals in der Engine dispatched.
|
||||||
|
|
||||||
|
Die Engine hat einen Variablen-Speicher, der mit dem Variablenspeicher vor und nach jedem Dialog synchronisiert wird. Variablen im Inkscript sind also global verfügbar und können durch Signals gesteuert werden.
|
||||||
|
|
||||||
|
* [Zurück zum Menü] -> menu
|
||||||
|
|
||||||
|
=== audio ===
|
||||||
|
# color suzanne
|
||||||
|
Die Sound-Engine ist noch sehr rudimentär. WAV-Dateien werden durch die Engine geladen. Die Sounds können durch Signals gesteuert werden. Es gibt noch keine 3D-Effekte.
|
||||||
|
|
||||||
|
* [Zurück zum Menü] -> menu
|
||||||
|
|
||||||
|
=== pixel ===
|
||||||
|
# color suzanne
|
||||||
|
Diese Engine unterstützt beliebige Auflösung. Aktuell sind es 320x240px also qVGA. Ich find das schick!
|
||||||
|
Aber 640x480px haben auch noch einen guten Retro-Look. Bei 1920p brauchen wir dann schon bessere Assets, sonst wirds hässlich.
|
||||||
|
|
||||||
|
Starte die Engine im Terminal mit ./irl3d --resolution VGA
|
||||||
|
|
||||||
|
* [Zurück zum Menü] -> menu
|
||||||
|
|
||||||
|
=== ende ===
|
||||||
|
# color suzanne
|
||||||
|
Schön, dass du dich hier umschaust. Viel Spaß noch!
|
||||||
|
* [Bis dann]
|
||||||
|
# suzanne_farewell
|
||||||
|
-> END
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,52 @@
|
|||||||
|
# Signal → Action Mapping. Trigger feuern Signals (Strings); hier wird
|
||||||
|
# definiert, was bei jedem Signal passiert. Signal-Name = Trigger-Quelle:
|
||||||
|
# für Objekt-Interaktion ist es der Instance-Name ohne Blender-Suffix.
|
||||||
|
#
|
||||||
|
# Actions sind Strings im Format "verb args". Verben:
|
||||||
|
# start_ink <script> Story laden (Frontend treibt sie)
|
||||||
|
# debug_log <text> eprintln!
|
||||||
|
# set <name> <value> KV setzen (true/false/i32/f32/string)
|
||||||
|
# inc <name> [<delta>] KV-Integer +Delta (Default +1)
|
||||||
|
# clear <name> KV-Eintrag entfernen
|
||||||
|
# hide_object <name> deferred (Frontend): Instance ausblenden
|
||||||
|
# play_sound <file> deferred (Frontend): SFX abspielen
|
||||||
|
#
|
||||||
|
# Reserviertes Signal `[init]` feuert einmal vor der Game-Loop —
|
||||||
|
# idealer Ort für KV-Defaults.
|
||||||
|
|
||||||
|
[init]
|
||||||
|
actions = [
|
||||||
|
"set cube_clicks 0",
|
||||||
|
"set mushrooms_collected 0",
|
||||||
|
"set quest_mushroom_active false",
|
||||||
|
"set quest_mushroom_done false",
|
||||||
|
]
|
||||||
|
|
||||||
|
[Mushroom]
|
||||||
|
actions = [
|
||||||
|
"inc mushrooms_collected",
|
||||||
|
"play_sound pickup.wav",
|
||||||
|
"hide_object $self",
|
||||||
|
]
|
||||||
|
|
||||||
|
[Person]
|
||||||
|
actions = [
|
||||||
|
"play_sound greet.wav",
|
||||||
|
"start_ink person.ink.json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[Suzanne]
|
||||||
|
actions = [
|
||||||
|
"start_ink suzanne.ink.json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[Cube]
|
||||||
|
actions = [
|
||||||
|
"inc cube_clicks",
|
||||||
|
"start_ink cube.ink.json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[person_farewell]
|
||||||
|
actions = [
|
||||||
|
"debug_log Person hat sich verabschiedet",
|
||||||
|
]
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//! Build-Hook: kompiliert geänderte Ink-Dialoge automatisch beim Cargo-Build.
|
||||||
|
//!
|
||||||
|
//! Reines „Wann"-Stück — das eigentliche Bauen macht `tools/build-dialogues.sh`
|
||||||
|
//! (und bleibt von Hand aufrufbar). Hier melden wir Cargo nur, wann es uns
|
||||||
|
//! erneut ausführen soll, und stoßen das Skript an.
|
||||||
|
//!
|
||||||
|
//! Cargo führt build.rs ausschließlich dann erneut aus, wenn eine der unten
|
||||||
|
//! via `rerun-if-changed` gemeldeten Dateien sich ändert. Unveränderte
|
||||||
|
//! Rebuilds kosten also nichts; nur nach einer `.ink`-Änderung läuft
|
||||||
|
//! inklecate (inkrementell nur für die betroffene Datei).
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let root = env!("CARGO_MANIFEST_DIR");
|
||||||
|
let script = Path::new(root).join("tools/build-dialogues.sh");
|
||||||
|
let ink_dir = Path::new(root).join("assets/interactions");
|
||||||
|
|
||||||
|
// Wann soll Cargo build.rs erneut ausführen?
|
||||||
|
println!("cargo:rerun-if-changed=tools/build-dialogues.sh");
|
||||||
|
println!("cargo:rerun-if-changed=tools/inklecate");
|
||||||
|
// Verzeichnis-mtime fängt neu hinzugekommene/gelöschte .ink-Dateien.
|
||||||
|
println!("cargo:rerun-if-changed=assets/interactions");
|
||||||
|
// Inhaltsänderungen einzelner .ink-Dateien einzeln melden.
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&ink_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let p = entry.path();
|
||||||
|
if p.extension().and_then(|s| s.to_str()) == Some("ink") {
|
||||||
|
println!("cargo:rerun-if-changed={}", p.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skript ausführen. Ausgabe einsammeln, damit ein Dialog-Fehler sichtbar
|
||||||
|
// wird (Cargo blendet Build-Script-stdout sonst aus).
|
||||||
|
match Command::new("bash").arg(&script).output() {
|
||||||
|
Ok(out) if out.status.success() => { /* still bei Erfolg */ }
|
||||||
|
Ok(out) => {
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
for line in stdout.lines().chain(stderr.lines()) {
|
||||||
|
println!("cargo:warning={line}");
|
||||||
|
}
|
||||||
|
panic!(
|
||||||
|
"Dialog-Build fehlgeschlagen — siehe Warnungen oben. \
|
||||||
|
Einzeln debuggen: ./tools/build-dialogues.sh"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// bash/Skript nicht startbar (z.B. fremdes OS): den Build nicht
|
||||||
|
// blockieren, nur warnen — die committeten .ink.json reichen.
|
||||||
|
println!(
|
||||||
|
"cargo:warning=Dialog-Build übersprungen ({e}). \
|
||||||
|
Bei Bedarf von Hand: ./tools/build-dialogues.sh"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# wds — Renderer-Plan
|
||||||
|
|
||||||
|
Stand: 2026-06-12. Entscheidungen aus der irl3d-Analyse, festgehalten bevor
|
||||||
|
der Projekt-Einstieg über die Kern-Systeme (Signals/KV, CLI-Phase) läuft.
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
PS1-artiger Look, von vornherein nativ und simpel implementiert statt
|
||||||
|
emuliert: affines Texture-Mapping, Integer-Vertex-Koordinaten, 16-Bit-Farbe.
|
||||||
|
Niedrige interne Auflösung (320×240), hochskaliert aufs Fenster.
|
||||||
|
|
||||||
|
Referenz ist `../irl3d`: gleiche Engine-Idee (datengetrieben, eine
|
||||||
|
OBJ-Szene, Signals/KV/Ink), aber dort als 8-Bit-Palette-CPU-Softwarerenderer
|
||||||
|
in Q16.16. wds ersetzt den Grafikpfad durch echtes GPU-Rastern.
|
||||||
|
|
||||||
|
## Hardware-Target
|
||||||
|
|
||||||
|
Mittelklasse-Laptop von 2015 (Klasse: i5-5200U + Intel HD 5500).
|
||||||
|
Konsequenzen:
|
||||||
|
|
||||||
|
- Zwei schwache CPU-Kerne → Transformation gehört auf die GPU, die CPU
|
||||||
|
bleibt für Physik und Spiellogik frei.
|
||||||
|
- Per-Frame-Vertex-Streaming (CPU-Transform, GTE-Stil) wäre auf alten
|
||||||
|
iGPU-Treibern der riskante Pfad (Buffer-Upload-Stalls) → statische
|
||||||
|
Vertex-Buffer, einmal hochgeladen.
|
||||||
|
- wgpu-Backend: Vulkan primär (ANV unterstützt Gen8+), GL als Fallback
|
||||||
|
einplanen. Keine Features jenseits von Baseline-WebGPU benutzen.
|
||||||
|
|
||||||
|
## Entscheidungen
|
||||||
|
|
||||||
|
1. **wgpu direkt, kein pbio.** pbio ist eine Abstraktion für
|
||||||
|
Software-Renderer (8-Bit-Framebuffer + Palette-Blit) — für echtes
|
||||||
|
GPU-Rastern Verschwendung. winit + wgpu direkt im Projekt.
|
||||||
|
|
||||||
|
2. **Vertex-Shader-Transform mit Pixel-Snapping.** Model→View→Projektion im
|
||||||
|
Vertex-Shader. Danach `xy/w` auf das Pixelraster der internen Auflösung
|
||||||
|
runden und wieder mit `w` multiplizieren → Vertex-Jitter wie auf der PS1,
|
||||||
|
authentisch gerechnet, nicht nachgestellt.
|
||||||
|
|
||||||
|
3. **Affines Texture-Mapping via `@interpolate(linear)`.** WGSL kann die
|
||||||
|
perspektivische Korrektur pro Varying abschalten. UVs (und ggf.
|
||||||
|
Vertex-Colors) linear interpolieren → das Textur-Wobbeln kommt gratis.
|
||||||
|
Gegenmittel bleibt wie auf der PS1 das Unterteilen großer Flächen im
|
||||||
|
Asset, nicht Shader-Tricks.
|
||||||
|
|
||||||
|
4. **RGB555 + Ordered Dithering im Fragment-Shader.** wgpu hat kein
|
||||||
|
555-Render-Target-Format; Render-Target bleibt RGBA8 bei 320×240, aber
|
||||||
|
der Fragment-Shader quantisiert auf 5 Bit pro Kanal mit 4×4-Bayer-Dither.
|
||||||
|
Die Farbreduktion ist echt, nur das Speicherformat ist modern.
|
||||||
|
Das 16. Bit entspricht dem PS1-Maskbit → bei uns **1-Bit-Alpha**
|
||||||
|
(Alpha-Test, kein Blending) — Fortführung der Index-0-Transparenz
|
||||||
|
aus irl3d.
|
||||||
|
|
||||||
|
5. **Z-Buffer: ja.** Ordering-Tables wären nur eine technische Hürde, kein
|
||||||
|
ästhetischer Gewinn. Standard-Depth-Buffer auf dem internen Target.
|
||||||
|
|
||||||
|
6. **UVs aus dem OBJ (`vt`).** irl3d ignoriert `vt` und mappt planar über
|
||||||
|
Welt-Koordinaten; wds liest echte UVs. PS1-Spiele hatten handgemachte
|
||||||
|
UVs — das gehört zum Look.
|
||||||
|
|
||||||
|
7. **Licht: gebacken zuerst.** Nur Gouraud-Shading wäre zu einseitig.
|
||||||
|
Plan: Lichteffekte in die Szene backen (Blender → Vertex-Colors),
|
||||||
|
im Shader mit der Textur multiplizieren und mitquantisieren. Dynamische
|
||||||
|
per-Vertex-Punktlichter (im Vertex-Shader, additiv auf die gebackene
|
||||||
|
Farbe) als spätere Stufe. Kein Per-Pixel-Lighting.
|
||||||
|
|
||||||
|
## Pipeline-Skizze
|
||||||
|
|
||||||
|
```
|
||||||
|
statische VBOs (Pos i16/f32, UV, Vertex-Color)
|
||||||
|
→ Vertex-Shader: Model·View·Proj, dann Snap auf 320×240-Raster
|
||||||
|
→ Raster (Hardware), UV/Color @interpolate(linear)
|
||||||
|
→ Fragment: Texel-Fetch (nearest), × Vertex-Color,
|
||||||
|
Alpha-Test (1 Bit), RGB555-Quantisierung + 4×4-Bayer
|
||||||
|
→ RGBA8-Offscreen 320×240 + Depth
|
||||||
|
→ Blit-Pass: Nearest-Upscale mit Letterbox (4:3) aufs Fenster
|
||||||
|
```
|
||||||
|
|
||||||
|
Draw-Granularität wie irl3d: pro Instance ein Draw (Uniform: Pos + Yaw),
|
||||||
|
Material-Batching später, wenn es weh tut.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- **Asset-Pipeline:** Texture-Painting in Blender ist mühsam. Kandidaten
|
||||||
|
für Textur-pro-Fläche-Workflows: TrenchBroom (Map-Format-Import nötig),
|
||||||
|
Crocotile 3D, Blockbench. Entscheidung wenn der Szenen-Loader ansteht.
|
||||||
|
- **Texturformat:** direkte True-Color-Texturen statt 8-Bit-CLUT-TGA —
|
||||||
|
wir arbeiten durchgehend mit 15-Bit-True-Color, es gibt keine globale
|
||||||
|
Palette mehr (das `load_palette`-Verb ist entfernt). Offen bleibt nur
|
||||||
|
das Dateiformat (TGA/PNG) und ob Quantisierung beim Import oder erst
|
||||||
|
im Shader passiert.
|
||||||
|
- **Fog / Distance-Fade:** irl3d hat hartes Pop-In bei `CULL_DIST` —
|
||||||
|
in wds von Anfang an als Fade Richtung Sky-Farbe einplanen (im Shader
|
||||||
|
trivial, passt zur PS1).
|
||||||
|
- **Skybox:** Cubemap wie irl3d oder schlicht Far-Plane-Farbe + Fog.
|
||||||
|
- Interne Auflösung umschaltbar lassen (QVGA/VGA wie irl3d).
|
||||||
|
|
||||||
|
## Aus irl3d portierbar (renderer-unabhängig)
|
||||||
|
|
||||||
|
signals, kv, ink, story_ctrl, game (ActionCtx-Idee), physics, pick,
|
||||||
|
camera, obj-Loader (um `vt` erweitern), tga, wav, audio, assets,
|
||||||
|
fix/math (Q16.16 bleibt für Spiellogik/Physik sinnvoll; der Renderer
|
||||||
|
selbst rechnet f32 — die GPU kennt ohnehin nur Floats).
|
||||||
|
|
||||||
|
## Projektphasen
|
||||||
|
|
||||||
|
1. **CLI-Phase (läuft):** KV-Store, Signal/Action-Dispatcher,
|
||||||
|
Ink-Integration, Konsolen-REPL als erstes Frontend. Deferred Actions
|
||||||
|
als Daten (`hide_object`, `play_sound`), damit spätere Subsysteme sie
|
||||||
|
konsumieren, ohne dass der Kern sie kennt.
|
||||||
|
2. Datenmodell (Mesh/Instance/Material/Scene + TOML-Formate) headless.
|
||||||
|
3. Renderer nach diesem Plan, gegen das stehende Datenmodell.
|
||||||
|
4. Physik, Audio, Panel-UI andocken (Ports aus irl3d).
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//! Asset-Pfadauflösung: Distribution-Layout (assets/ neben der Binary)
|
||||||
|
//! oder Dev-Layout (assets/ im CWD, also Projektroot bei `cargo run`).
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
static BASE: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
|
||||||
|
fn base() -> &'static PathBuf {
|
||||||
|
BASE.get_or_init(|| {
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(dir) = exe.parent() {
|
||||||
|
if dir.join("assets").exists() {
|
||||||
|
return dir.to_path_buf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wandelt einen relativen Pfad wie `"assets/signals.toml"` in einen
|
||||||
|
/// absoluten Pfad relativ zum Asset-Basisverzeichnis um.
|
||||||
|
pub fn path(rel: &str) -> String {
|
||||||
|
base().join(rel).to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
//! Owner des dispatch-relevanten Spielzustands und der Borrow-View, durch
|
||||||
|
//! die Signal-Actions und Ink-Story-Steps darauf zugreifen.
|
||||||
|
//!
|
||||||
|
//! `Game` hält die Slots, die Actions lesen oder mutieren dürfen.
|
||||||
|
//! `ActionCtx` ist der schmale `&mut`-View; er wird über `Game::action_ctx()`
|
||||||
|
//! konstruiert und durch die Dispatch-Pipeline gereicht.
|
||||||
|
//!
|
||||||
|
//! Verben, deren Ziel-Subsystem (Szene, Audio, Renderer) noch nicht
|
||||||
|
//! existiert oder den Kern nichts angeht, produzieren [`Action`]-Werte
|
||||||
|
//! statt direkt zu wirken. Frontends konsumieren die Queue: die CLI druckt
|
||||||
|
//! sie, die Engine führt sie später aus. So bleibt der Kern headless
|
||||||
|
//! testbar und kennt keines der Subsysteme.
|
||||||
|
|
||||||
|
use crate::ink;
|
||||||
|
use crate::kv::Store;
|
||||||
|
use crate::signals::Signals;
|
||||||
|
|
||||||
|
/// Action, die der Kern nicht selbst ausführt, sondern ans Frontend
|
||||||
|
/// weiterreicht. Die Queue in `Game::actions` gehört nach jedem Dispatch
|
||||||
|
/// geleert (konsumiert).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Action {
|
||||||
|
/// Instance unsichtbar + kollisionslos + nicht mehr interagierbar machen.
|
||||||
|
HideObject(String),
|
||||||
|
/// WAV unter `assets/audio/<name>` als SFX abspielen.
|
||||||
|
PlaySound(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Game {
|
||||||
|
pub kv: Store,
|
||||||
|
pub signals: Signals,
|
||||||
|
pub story: Option<ink::Story>,
|
||||||
|
pub actions: Vec<Action>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActionCtx<'a> {
|
||||||
|
pub kv: &'a mut Store,
|
||||||
|
pub signals: &'a Signals,
|
||||||
|
pub story: &'a mut Option<ink::Story>,
|
||||||
|
pub actions: &'a mut Vec<Action>,
|
||||||
|
/// Auslöser-Identifier: bei Objekt-Interaktion der volle Instance-Name
|
||||||
|
/// (mit Blender-Suffix, z.B. `Mushroom.005`). `$self` in Action-Args
|
||||||
|
/// wird damit substituiert. `None` bei Signalen ohne Quell-Instance
|
||||||
|
/// (z.B. `init`, Story-Tags).
|
||||||
|
pub instance_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game {
|
||||||
|
pub fn new(signals: Signals) -> Self {
|
||||||
|
Self { kv: Store::new(), signals, story: None, actions: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn action_ctx(&mut self, instance_name: Option<String>) -> ActionCtx<'_> {
|
||||||
|
ActionCtx {
|
||||||
|
kv: &mut self.kv,
|
||||||
|
signals: &self.signals,
|
||||||
|
story: &mut self.story,
|
||||||
|
actions: &mut self.actions,
|
||||||
|
instance_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
//! Dünner Adapter um die `bladeink`-Crate, gekoppelt an unseren `kv::Store`.
|
||||||
|
//!
|
||||||
|
//! Bei jedem `cont()` läuft ein bidirektionaler Sync:
|
||||||
|
//! - vor `continue`: KV-Werte ins `variables_state` der Story spiegeln
|
||||||
|
//! (Skript sieht aktuelle Spielvariablen)
|
||||||
|
//! - nach `continue`: alle KV-Keys, die das Skript kennt, zurücklesen
|
||||||
|
//! (Ink-`~ var = …`-Writes landen im KV)
|
||||||
|
//!
|
||||||
|
//! Variablen die der KV kennt, das Skript aber nicht als `VAR` deklariert,
|
||||||
|
//! lehnt bladeink beim `set_variable` ab — wir ignorieren das still.
|
||||||
|
//! Variablen die nur im Skript existieren, werden beim Read-Back ignoriert
|
||||||
|
//! (KV bleibt minimal: nur was Rust kennt).
|
||||||
|
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
|
use bladeink::story::Story as BladeStory;
|
||||||
|
|
||||||
|
use crate::kv::Store;
|
||||||
|
|
||||||
|
pub enum StoryState {
|
||||||
|
Text(String),
|
||||||
|
Choice { prompt: String, options: Vec<String> },
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Story {
|
||||||
|
inner: BladeStory,
|
||||||
|
tags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Story {
|
||||||
|
pub fn load(path: &str, kv: &Store) -> Self {
|
||||||
|
let src = read_to_string(path)
|
||||||
|
.unwrap_or_else(|e| panic!("ink load {}: {}", path, e));
|
||||||
|
let inner = BladeStory::new(&src)
|
||||||
|
.unwrap_or_else(|e| panic!("ink parse {}: {:?}", path, e));
|
||||||
|
let mut s = Self { inner, tags: Vec::new() };
|
||||||
|
s.sync_from_kv(kv);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_tags(&mut self) -> Vec<String> { std::mem::take(&mut self.tags) }
|
||||||
|
|
||||||
|
pub fn cont(&mut self, kv: &mut Store) -> StoryState {
|
||||||
|
self.sync_from_kv(kv);
|
||||||
|
|
||||||
|
// continue_maximally ruft intern reset_output() vor jedem cont() —
|
||||||
|
// damit überschriebe der Tag der letzten Zeile alle vorherigen.
|
||||||
|
// Wir loopen selbst und sammeln Tags nach jedem Schritt ein, sonst
|
||||||
|
// gehen z.B. `# set has_key true` am Knoten-Anfang bei mehrzeiligem
|
||||||
|
// Text verloren.
|
||||||
|
let mut text = String::new();
|
||||||
|
while self.inner.can_continue() {
|
||||||
|
let chunk = self.inner.cont()
|
||||||
|
.unwrap_or_else(|e| panic!("ink cont: {:?}", e));
|
||||||
|
text.push_str(&chunk);
|
||||||
|
if let Ok(t) = self.inner.get_current_tags() {
|
||||||
|
self.tags.extend(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let text = text.trim().to_string();
|
||||||
|
|
||||||
|
self.sync_to_kv(kv);
|
||||||
|
|
||||||
|
let choices = self.inner.get_current_choices();
|
||||||
|
if !choices.is_empty() {
|
||||||
|
let options = choices.iter().map(|c| c.text.clone()).collect();
|
||||||
|
return StoryState::Choice { prompt: text, options };
|
||||||
|
}
|
||||||
|
if !text.is_empty() { return StoryState::Text(text); }
|
||||||
|
StoryState::End
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn choose(&mut self, i: usize) {
|
||||||
|
self.inner.choose_choice_index(i)
|
||||||
|
.unwrap_or_else(|e| panic!("ink choose {}: {:?}", i, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_from_kv(&mut self, kv: &Store) {
|
||||||
|
for (name, value) in kv {
|
||||||
|
// set_variable schlägt fehl wenn der Name kein deklariertes
|
||||||
|
// VAR im Skript ist — still ignorieren.
|
||||||
|
let _ = self.inner.set_variable(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_to_kv(&self, kv: &mut Store) {
|
||||||
|
let keys: Vec<String> = kv.keys().cloned().collect();
|
||||||
|
for name in keys {
|
||||||
|
if let Some(v) = self.inner.get_variable(&name) {
|
||||||
|
kv.insert(name, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
//! Persistenter Game-State Key-Value-Store.
|
||||||
|
//!
|
||||||
|
//! Single Source of Truth für alle Spielvariablen (Quest-Flags, Inventar-
|
||||||
|
//! Counter, Story-Beats). Ink-Skripte haben eigene `variables_state`-Stores;
|
||||||
|
//! die werden bei jedem `cont()` mit diesem KV synchronisiert (siehe
|
||||||
|
//! [`crate::ink`]). Damit der KV authoritativ bleibt: externe Actions
|
||||||
|
//! (`set`, `inc`, `clear`) ändern *immer* den KV, niemals direkt die Story.
|
||||||
|
//!
|
||||||
|
//! Wir nutzen `bladeink::ValueType` als Value, damit die Sync-Konvertierung
|
||||||
|
//! gratis ist (kein Mapping zwischen Rust- und Ink-Typen nötig).
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use bladeink::value_type::ValueType;
|
||||||
|
|
||||||
|
pub type Store = HashMap<String, ValueType>;
|
||||||
|
|
||||||
|
/// Parst einen Action-Arg-String zu einem `ValueType`. Reihenfolge:
|
||||||
|
/// `true`/`false` → Bool, dann i32, dann f32, sonst String.
|
||||||
|
pub fn parse_value(s: &str) -> ValueType {
|
||||||
|
let s = s.trim();
|
||||||
|
match s {
|
||||||
|
"true" => return ValueType::Bool(true),
|
||||||
|
"false" => return ValueType::Bool(false),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Ok(i) = s.parse::<i32>() { return ValueType::Int(i); }
|
||||||
|
if let Ok(f) = s.parse::<f32>() { return ValueType::Float(f); }
|
||||||
|
ValueType::new(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Menschenlesbare Darstellung für REPL-Dumps und Debug-Logs.
|
||||||
|
pub fn format_value(v: &ValueType) -> String {
|
||||||
|
match v {
|
||||||
|
ValueType::Bool(b) => b.to_string(),
|
||||||
|
ValueType::Int(i) => i.to_string(),
|
||||||
|
ValueType::Float(f) => f.to_string(),
|
||||||
|
other => other.coerce_to_string().unwrap_or_else(|_| "<?>".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_set(args: &str, store: &mut Store) {
|
||||||
|
let Some((name, value)) = args.split_once(char::is_whitespace) else {
|
||||||
|
eprintln!("[kv] set: expected 'name value', got {:?}", args);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
store.insert(name.trim().to_string(), parse_value(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_inc(args: &str, store: &mut Store) {
|
||||||
|
let (name, delta) = args.split_once(char::is_whitespace).unwrap_or((args, "1"));
|
||||||
|
let name = name.trim();
|
||||||
|
let delta = delta.trim().parse::<i32>().unwrap_or(1);
|
||||||
|
let cur = store.get(name).and_then(|v| v.coerce_to_int().ok()).unwrap_or(0);
|
||||||
|
store.insert(name.to_string(), ValueType::Int(cur + delta));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_clear(args: &str, store: &mut Store) {
|
||||||
|
store.remove(args.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_value_types() {
|
||||||
|
assert!(matches!(parse_value("true"), ValueType::Bool(true)));
|
||||||
|
assert!(matches!(parse_value("false"), ValueType::Bool(false)));
|
||||||
|
assert!(matches!(parse_value("42"), ValueType::Int(42)));
|
||||||
|
assert!(matches!(parse_value("-7"), ValueType::Int(-7)));
|
||||||
|
assert!(matches!(parse_value("1.5"), ValueType::Float(f) if f == 1.5));
|
||||||
|
assert!(matches!(parse_value("hallo"), ValueType::String(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_inc_clear_roundtrip() {
|
||||||
|
let mut s = Store::new();
|
||||||
|
apply_set("clicks 3", &mut s);
|
||||||
|
assert_eq!(s["clicks"].coerce_to_int().unwrap(), 3);
|
||||||
|
|
||||||
|
apply_inc("clicks", &mut s);
|
||||||
|
assert_eq!(s["clicks"].coerce_to_int().unwrap(), 4);
|
||||||
|
|
||||||
|
apply_inc("clicks -2", &mut s);
|
||||||
|
assert_eq!(s["clicks"].coerce_to_int().unwrap(), 2);
|
||||||
|
|
||||||
|
// inc auf unbekannten Key startet bei 0
|
||||||
|
apply_inc("fresh 5", &mut s);
|
||||||
|
assert_eq!(s["fresh"].coerce_to_int().unwrap(), 5);
|
||||||
|
|
||||||
|
apply_clear("clicks", &mut s);
|
||||||
|
assert!(!s.contains_key("clicks"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_without_value_is_noop() {
|
||||||
|
let mut s = Store::new();
|
||||||
|
apply_set("only_name", &mut s);
|
||||||
|
assert!(s.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+142
-1
@@ -1,3 +1,144 @@
|
|||||||
|
//! wds — Weirdcore Dating Simulator.
|
||||||
|
//!
|
||||||
|
//! CLI-Phase: Die Kern-Systeme (Signal/Action-Dispatcher, KV-Store,
|
||||||
|
//! Ink-Stories) laufen headless; diese REPL ist ihr erstes Frontend.
|
||||||
|
//! Der Renderer kommt später dazu (siehe notes/renderer-plan.md) und
|
||||||
|
//! konsumiert dieselben Schnittstellen — insbesondere `StoryState` und
|
||||||
|
//! die `Action`-Queue.
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
mod assets;
|
||||||
|
mod game;
|
||||||
|
mod ink;
|
||||||
|
mod kv;
|
||||||
|
mod signals;
|
||||||
|
mod story_ctrl;
|
||||||
|
|
||||||
|
use game::{Action, Game};
|
||||||
|
use ink::StoryState;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
let signals_path = assets::path("assets/signals.toml");
|
||||||
|
let signal_table = signals::load_signals(&signals_path);
|
||||||
|
println!("wds-Konsole — {} Signale aus {}. `help` für Befehle.",
|
||||||
|
signal_table.len(), signals_path);
|
||||||
|
let mut game = Game::new(signal_table);
|
||||||
|
|
||||||
|
// Reserviertes `[init]`-Signal: KV-Defaults setzen, bevor Stories laufen.
|
||||||
|
fire(&mut game, "init", None);
|
||||||
|
|
||||||
|
while let Some(line) = read_line("> ") {
|
||||||
|
let line = line.trim();
|
||||||
|
match line {
|
||||||
|
"" => {}
|
||||||
|
"quit" | "exit" => break,
|
||||||
|
"help" => print_help(),
|
||||||
|
"kv" => dump_kv(&game),
|
||||||
|
"reload" => {
|
||||||
|
game.signals = signals::load_signals(&signals_path);
|
||||||
|
println!("{} Signale geladen.", game.signals.len());
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(sig) = line.strip_prefix("signal ") {
|
||||||
|
fire(&mut game, sig.trim(), None);
|
||||||
|
} else if let Some(name) = line.strip_prefix("use ") {
|
||||||
|
// Objekt-Interaktion simulieren: Signal ist der gestrippte
|
||||||
|
// Name, $self der volle — wie der LMB-Klick-Pfad in irl3d.
|
||||||
|
let name = name.trim();
|
||||||
|
let key = signals::signal_key(name).to_string();
|
||||||
|
fire(&mut game, &key, Some(name.to_string()));
|
||||||
|
} else {
|
||||||
|
println!("unbekannter Befehl: {line:?} — `help` für Befehle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!(" signal <s> Signal feuern oder Action direkt ausführen");
|
||||||
|
println!(" (z.B. `signal Cube`, `signal set has_key true`)");
|
||||||
|
println!(" use <instance> Objekt-Interaktion simulieren — `use Mushroom.005`");
|
||||||
|
println!(" feuert Signal `Mushroom` mit $self = Mushroom.005");
|
||||||
|
println!(" kv KV-Store anzeigen");
|
||||||
|
println!(" reload signals.toml neu laden");
|
||||||
|
println!(" quit beenden");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal dispatchen, deferred Actions ausgeben und — falls eine Action
|
||||||
|
/// eine Story gestartet hat — den Dialog-Loop fahren.
|
||||||
|
fn fire(game: &mut Game, signal: &str, instance: Option<String>) {
|
||||||
|
{
|
||||||
|
let mut ctx = game.action_ctx(instance);
|
||||||
|
signals::dispatch(signal, &mut ctx);
|
||||||
|
}
|
||||||
|
drain_actions(game);
|
||||||
|
if game.story.is_some() {
|
||||||
|
run_story(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dialog-Loop: Story schrittweise treiben, Tags zurück in den Dispatcher,
|
||||||
|
/// Choices über nummerierte Eingabe. Läuft bis `End` (oder EOF).
|
||||||
|
fn run_story(game: &mut Game) {
|
||||||
|
let mut sel: Option<usize> = None;
|
||||||
|
loop {
|
||||||
|
let state = {
|
||||||
|
let mut ctx = game.action_ctx(None);
|
||||||
|
let (state, tags) = story_ctrl::advance(sel, &mut ctx);
|
||||||
|
for t in &tags { signals::dispatch(t, &mut ctx); }
|
||||||
|
state
|
||||||
|
};
|
||||||
|
drain_actions(game);
|
||||||
|
match state {
|
||||||
|
None | Some(StoryState::End) => break,
|
||||||
|
Some(StoryState::Text(text)) => {
|
||||||
|
println!("\n{text}");
|
||||||
|
if read_line(" [Enter] ").is_none() { break; }
|
||||||
|
sel = None;
|
||||||
|
}
|
||||||
|
Some(StoryState::Choice { prompt, options }) => {
|
||||||
|
if !prompt.is_empty() { println!("\n{prompt}"); }
|
||||||
|
for (i, o) in options.iter().enumerate() {
|
||||||
|
println!(" {}) {o}", i + 1);
|
||||||
|
}
|
||||||
|
sel = loop {
|
||||||
|
let Some(line) = read_line(" wahl> ") else { return; };
|
||||||
|
if let Ok(n) = line.trim().parse::<usize>() {
|
||||||
|
if (1..=options.len()).contains(&n) { break Some(n - 1); }
|
||||||
|
}
|
||||||
|
println!(" (1..{})", options.len());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain_actions(game: &mut Game) {
|
||||||
|
for a in game.actions.drain(..) {
|
||||||
|
match a {
|
||||||
|
Action::HideObject(n) => println!("[action] hide_object {n}"),
|
||||||
|
Action::PlaySound(n) => println!("[action] play_sound {n}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump_kv(game: &Game) {
|
||||||
|
let mut keys: Vec<&String> = game.kv.keys().collect();
|
||||||
|
keys.sort();
|
||||||
|
for k in keys {
|
||||||
|
println!(" {k} = {}", kv::format_value(&game.kv[k]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zeile von stdin lesen; `None` bei EOF (Ctrl-D) oder Lesefehler.
|
||||||
|
fn read_line(prompt: &str) -> Option<String> {
|
||||||
|
print!("{prompt}");
|
||||||
|
io::stdout().flush().ok();
|
||||||
|
let mut buf = String::new();
|
||||||
|
match io::stdin().read_line(&mut buf) {
|
||||||
|
Ok(0) | Err(_) => None,
|
||||||
|
Ok(_) => Some(buf.trim_end().to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+194
@@ -0,0 +1,194 @@
|
|||||||
|
//! Signal → Action Dispatcher.
|
||||||
|
//!
|
||||||
|
//! Triggers (Objekt-Interaktion, Zone-Enter, Ink-Tag, Konsole) feuern
|
||||||
|
//! *Signals* als Strings. Eine zentrale Table in `assets/signals.toml` mappt
|
||||||
|
//! jedes Signal auf eine Liste von Actions. Actions sind selbst wieder
|
||||||
|
//! Strings im Format `"verb args"` — dieselbe Vokabular-Familie wie
|
||||||
|
//! Ink-Tags, sodass beide auf dieselbe Execute-Pipeline laufen.
|
||||||
|
//!
|
||||||
|
//! Builtin-Verben:
|
||||||
|
//! `start_ink <script>` Story aus `assets/interactions/` in den
|
||||||
|
//! Story-Slot laden; das Frontend treibt sie
|
||||||
|
//! dann über `story_ctrl::advance`.
|
||||||
|
//! `debug_log <text>` eprintln!
|
||||||
|
//! `set <name> <value>` KV setzen (true/false/i32/f32/string)
|
||||||
|
//! `inc <name> [<delta>]` KV-Integer inkrementieren (Default +1)
|
||||||
|
//! `clear <name>` KV-Eintrag entfernen
|
||||||
|
//! `hide_object <name>` → Action::HideObject (deferred)
|
||||||
|
//! `play_sound <file>` → Action::PlaySound (deferred)
|
||||||
|
//!
|
||||||
|
//! Parameter-Substitution: vor dem Parsen ersetzt `execute` `$self` in den
|
||||||
|
//! Action-Args durch `ctx.instance_name`. Damit kann eine generische Action
|
||||||
|
//! wie `hide_object $self` für viele duplizierte Instances wirken, ohne pro
|
||||||
|
//! Instance einen Eintrag in signals.toml zu brauchen.
|
||||||
|
//!
|
||||||
|
//! Dispatch-Reihenfolge: erst Signal-Lookup; steht der Name in der Table,
|
||||||
|
//! laufen alle gemappten Actions. Sonst wird der String direkt als
|
||||||
|
//! Builtin-Aufruf probiert — so wirken Ink-Tags wie `# set has_key true`
|
||||||
|
//! ohne Umweg über signals.toml. Unbekannte Verben werden still ignoriert.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
|
use crate::game::{Action, ActionCtx};
|
||||||
|
use crate::kv;
|
||||||
|
use crate::story_ctrl;
|
||||||
|
|
||||||
|
pub type Signals = HashMap<String, Vec<String>>;
|
||||||
|
|
||||||
|
/// Strippt Blenders Duplikat-Suffix (`.NNN` mit reinen Ziffern am Ende),
|
||||||
|
/// damit `Mushroom.001`, `Mushroom.042` etc. alle auf `[Mushroom]` in
|
||||||
|
/// signals.toml mappen. Andere Punkte im Namen bleiben unberührt, solange
|
||||||
|
/// der letzte Token nicht rein numerisch ist.
|
||||||
|
pub fn signal_key(name: &str) -> &str {
|
||||||
|
if let Some(dot) = name.rfind('.') {
|
||||||
|
let tail = &name[dot + 1..];
|
||||||
|
if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
return &name[..dot];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch(signal: &str, ctx: &mut ActionCtx) {
|
||||||
|
// Signal-Name in der Table? Dann alle gemappten Actions ausführen.
|
||||||
|
if let Some(actions) = ctx.signals.get(signal) {
|
||||||
|
let actions = actions.clone(); // entkoppelt vom ctx.signals-Borrow
|
||||||
|
for a in &actions { execute(a, ctx); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Sonst direkt als Builtin probieren (Ink-Tag-Pfad).
|
||||||
|
execute(signal, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(cmd: &str, ctx: &mut ActionCtx) {
|
||||||
|
// $self → ctx.instance_name. Substituieren bevor wir Verb/Args splitten,
|
||||||
|
// damit Tokens wie `hide_object $self` einheitlich funktionieren. Ohne
|
||||||
|
// instance_name (z.B. `init`-Signal) bleibt `$self` stehen — die Action
|
||||||
|
// zielt dann ins Leere, was für deferred Actions ein No-Op beim
|
||||||
|
// Konsumenten ist.
|
||||||
|
let cmd_owned: String;
|
||||||
|
let cmd_ref: &str = if cmd.contains("$self") {
|
||||||
|
if let Some(name) = ctx.instance_name.as_deref() {
|
||||||
|
cmd_owned = cmd.replace("$self", name);
|
||||||
|
&cmd_owned
|
||||||
|
} else { cmd }
|
||||||
|
} else { cmd };
|
||||||
|
let (verb, args) = cmd_ref.split_once(char::is_whitespace).unwrap_or((cmd_ref, ""));
|
||||||
|
let args = args.trim();
|
||||||
|
match verb {
|
||||||
|
"start_ink" => story_ctrl::start(args, ctx),
|
||||||
|
"debug_log" => eprintln!("[signal] {args}"),
|
||||||
|
"set" => kv::apply_set(args, ctx.kv),
|
||||||
|
"inc" => kv::apply_inc(args, ctx.kv),
|
||||||
|
"clear" => kv::apply_clear(args, ctx.kv),
|
||||||
|
"hide_object" => ctx.actions.push(Action::HideObject(args.to_string())),
|
||||||
|
"play_sound" => ctx.actions.push(Action::PlaySound(args.to_string())),
|
||||||
|
_ => { /* Unbekannter Verb/Tag → still ignorieren */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parst das signals.toml-Subset: `[signal]`-Header gefolgt von einem
|
||||||
|
/// `actions = [ "…", … ]`-Array. Bewusst handgerollt statt toml-Crate —
|
||||||
|
/// das Format ist trivial und die Engine bleibt dependency-arm.
|
||||||
|
pub fn parse_signals(src: &str) -> Signals {
|
||||||
|
let mut map: Signals = HashMap::new();
|
||||||
|
let mut cur = String::new();
|
||||||
|
let mut in_arr = false;
|
||||||
|
for line in src.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') { continue; }
|
||||||
|
if !in_arr && line.starts_with('[') {
|
||||||
|
cur = line.trim_matches(|c| c == '[' || c == ']').to_string();
|
||||||
|
map.entry(cur.clone()).or_default();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !in_arr && line.starts_with("actions") && line.contains('[') {
|
||||||
|
in_arr = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if in_arr {
|
||||||
|
if line.starts_with(']') { in_arr = false; continue; }
|
||||||
|
let s = line.trim_end_matches(',').trim().trim_matches('"');
|
||||||
|
if s.is_empty() { continue; }
|
||||||
|
map.entry(cur.clone()).or_default().push(s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt `signals.toml`. Fehlende Datei → leere Table (kein Fehler), damit
|
||||||
|
/// ein Projekt ohne Signals lauffähig bleibt.
|
||||||
|
pub fn load_signals(path: &str) -> Signals {
|
||||||
|
match read_to_string(path) {
|
||||||
|
Ok(src) => parse_signals(&src),
|
||||||
|
Err(_) => Signals::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::game::Game;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signal_key_strips_blender_suffix() {
|
||||||
|
assert_eq!(signal_key("Mushroom.005"), "Mushroom");
|
||||||
|
assert_eq!(signal_key("Mushroom"), "Mushroom");
|
||||||
|
assert_eq!(signal_key("my.thing.with.dots"), "my.thing.with.dots");
|
||||||
|
assert_eq!(signal_key("name."), "name.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_signals_table() {
|
||||||
|
let src = r#"
|
||||||
|
# Kommentar
|
||||||
|
[init]
|
||||||
|
actions = [
|
||||||
|
"set foo 1",
|
||||||
|
"debug_log hi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[Empty]
|
||||||
|
actions = [
|
||||||
|
]
|
||||||
|
"#;
|
||||||
|
let s = parse_signals(src);
|
||||||
|
assert_eq!(s["init"], vec!["set foo 1", "debug_log hi"]);
|
||||||
|
assert!(s["Empty"].is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dispatch_runs_actions_and_queues_deferred() {
|
||||||
|
let mut signals = Signals::new();
|
||||||
|
signals.insert("pickup".into(), vec![
|
||||||
|
"inc items".into(),
|
||||||
|
"play_sound pickup.wav".into(),
|
||||||
|
"hide_object $self".into(),
|
||||||
|
]);
|
||||||
|
let mut game = Game::new(signals);
|
||||||
|
|
||||||
|
let mut ctx = game.action_ctx(Some("Mushroom.005".into()));
|
||||||
|
dispatch("pickup", &mut ctx);
|
||||||
|
|
||||||
|
assert_eq!(game.kv["items"].coerce_to_int().unwrap(), 1);
|
||||||
|
assert_eq!(game.actions, vec![
|
||||||
|
Action::PlaySound("pickup.wav".into()),
|
||||||
|
Action::HideObject("Mushroom.005".into()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_signal_falls_through_to_builtin() {
|
||||||
|
let mut game = Game::new(Signals::new());
|
||||||
|
let mut ctx = game.action_ctx(None);
|
||||||
|
// Ink-Tag-Pfad: kein Table-Eintrag, direkt als Verb ausführen.
|
||||||
|
dispatch("set has_key true", &mut ctx);
|
||||||
|
assert!(game.kv["has_key"].coerce_to_bool().unwrap());
|
||||||
|
|
||||||
|
// Unbekanntes Verb ist ein stilles No-Op.
|
||||||
|
let mut ctx = game.action_ctx(None);
|
||||||
|
dispatch("color story_a", &mut ctx);
|
||||||
|
assert!(game.actions.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//! Story-Lifecycle: Ink-Skript in den Story-Slot laden und schrittweise
|
||||||
|
//! treiben. Frontend-agnostisch — anders als in irl3d, wo `StoryState`
|
||||||
|
//! direkt in UI-Panels gegossen wurde, konsumiert hier der Aufrufer
|
||||||
|
//! (CLI-REPL jetzt, Panel-UI später) den Zustand selbst.
|
||||||
|
//!
|
||||||
|
//! Tags eines Schritts werden als `Vec<String>` zurückgegeben — der
|
||||||
|
//! Aufrufer entscheidet, ob/wie er sie via `signals::dispatch` weiterreicht
|
||||||
|
//! (vermeidet den Modul-Zyklus signals → story_ctrl → signals).
|
||||||
|
|
||||||
|
use crate::assets;
|
||||||
|
use crate::game::ActionCtx;
|
||||||
|
use crate::ink::{Story, StoryState};
|
||||||
|
|
||||||
|
/// Lädt ein Ink-Skript (Pfad relativ zu `assets/interactions/`) in den
|
||||||
|
/// Story-Slot. Der erste Schritt passiert noch nicht — das Frontend ruft
|
||||||
|
/// dafür `advance(None, …)`.
|
||||||
|
pub fn start(script: &str, ctx: &mut ActionCtx) {
|
||||||
|
let path = assets::path(&format!("assets/interactions/{script}"));
|
||||||
|
*ctx.story = Some(Story::load(&path, ctx.kv));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Treibt die laufende Story einen Schritt weiter. `sel` ist die
|
||||||
|
/// Choice-Auswahl bzw. `None` beim ersten Schritt oder Text-Weiterklicken.
|
||||||
|
/// Bei `End` wird der Story-Slot geleert. Rückgabe: neuer Zustand (oder
|
||||||
|
/// `None` wenn gar keine Story läuft) plus Tags zum Dispatchen.
|
||||||
|
pub fn advance(sel: Option<usize>, ctx: &mut ActionCtx) -> (Option<StoryState>, Vec<String>) {
|
||||||
|
let Some(st) = ctx.story.as_mut() else { return (None, Vec::new()); };
|
||||||
|
if let Some(i) = sel { st.choose(i); }
|
||||||
|
let state = st.cont(ctx.kv);
|
||||||
|
let tags = st.take_tags();
|
||||||
|
if matches!(state, StoryState::End) { *ctx.story = None; }
|
||||||
|
(Some(state), tags)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# tools/
|
||||||
|
|
||||||
|
Hilfsskripte für die Asset-Pipeline. Sie sind **nicht** Teil der Engine —
|
||||||
|
die Engine liest nur die fertig gebauten Dateien unter `assets/`.
|
||||||
|
|
||||||
|
## build-dialogues.sh
|
||||||
|
|
||||||
|
Baut alle Ink-Dialoge zu den `.ink.json`, die das Spiel liest.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./tools/build-dialogues.sh # nur Geändertes neu bauen
|
||||||
|
./tools/build-dialogues.sh -f # alles neu bauen
|
||||||
|
```
|
||||||
|
|
||||||
|
Workflow für die Autorin: `.ink`-Dateien in `assets/interactions/`
|
||||||
|
schreiben, Skript laufen lassen, fertig. Die `.ink.json` landen direkt
|
||||||
|
neben den `.ink` und werden mit ins Repo eingecheckt, damit ein frischer
|
||||||
|
Clone ohne Toolchain spielbar ist.
|
||||||
|
|
||||||
|
**Automatisch beim Build:** `cargo build`/`cargo run` ruft dieses Skript
|
||||||
|
über `build.rs` selbst auf, sobald sich eine `.ink` geändert hat.
|
||||||
|
|
||||||
|
### inklecate
|
||||||
|
|
||||||
|
Das Skript sucht den Ink-Compiler in dieser Reihenfolge:
|
||||||
|
|
||||||
|
1. `$INKLECATE` (Pfad zum Binary)
|
||||||
|
2. `tools/inklecate` (mitgeliefert)
|
||||||
|
3. `inklecate` im `PATH`
|
||||||
Executable
+83
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Baut alle Ink-Dialoge zu .ink.json, die das Spiel lesen kann.
|
||||||
|
#
|
||||||
|
# Für die Autorin: Schreib deine Dialoge als .ink-Dateien in
|
||||||
|
# assets/interactions/
|
||||||
|
# und führ danach dieses Skript aus:
|
||||||
|
# ./tools/build-dialogues.sh
|
||||||
|
# Fertig — das Spiel liest die erzeugten .ink.json.
|
||||||
|
#
|
||||||
|
# Standardmäßig wird nur neu gebaut, was sich seit dem letzten Mal
|
||||||
|
# geändert hat. Mit -f / --force wird alles neu gebaut.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Repo-Wurzel relativ zum Skript bestimmen — egal, von wo aufgerufen.
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
root="$(cd "$script_dir/.." && pwd)"
|
||||||
|
ink_dir="$root/assets/interactions"
|
||||||
|
|
||||||
|
force=0
|
||||||
|
case "${1:-}" in
|
||||||
|
-f|--force) force=1 ;;
|
||||||
|
"") ;;
|
||||||
|
*) echo "Unbekannte Option: $1 (nur -f/--force)" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# inklecate finden: $INKLECATE → mitgeliefertes tools/inklecate → PATH.
|
||||||
|
find_inklecate() {
|
||||||
|
if [ -n "${INKLECATE:-}" ] && [ -x "$INKLECATE" ]; then
|
||||||
|
echo "$INKLECATE"; return 0
|
||||||
|
fi
|
||||||
|
if [ -x "$script_dir/inklecate" ]; then
|
||||||
|
echo "$script_dir/inklecate"; return 0
|
||||||
|
fi
|
||||||
|
if command -v inklecate >/dev/null 2>&1; then
|
||||||
|
command -v inklecate; return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! inklecate="$(find_inklecate)"; then
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
Fehler: inklecate nicht gefunden.
|
||||||
|
Lege das Linux-Binary nach tools/inklecate (chmod +x),
|
||||||
|
setze $INKLECATE auf den Pfad, oder installiere es in den PATH.
|
||||||
|
Quelle: https://github.com/inkle/ink/releases
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$ink_dir" ]; then
|
||||||
|
echo "Kein Dialog-Ordner: $ink_dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
built=0 skipped=0 failed=0
|
||||||
|
shopt -s nullglob
|
||||||
|
for src in "$ink_dir"/*.ink; do
|
||||||
|
out="${src%.ink}.ink.json"
|
||||||
|
name="$(basename "$src")"
|
||||||
|
|
||||||
|
# Inkrementell: überspringen, wenn Ausgabe existiert und neuer ist.
|
||||||
|
if [ "$force" -eq 0 ] && [ -f "$out" ] && [ "$out" -nt "$src" ]; then
|
||||||
|
printf ' aktuell %s\n' "$name"
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# inklecate-Ausgabe einsammeln; nur bei Problemen/Warnungen zeigen.
|
||||||
|
if log="$("$inklecate" -o "$out" "$src" 2>&1)"; then
|
||||||
|
printf ' gebaut %s\n' "$name"
|
||||||
|
built=$((built + 1))
|
||||||
|
[ -n "$log" ] && printf '%s\n' "$log" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
printf ' FEHLER %s\n' "$name"
|
||||||
|
printf '%s\n' "$log" | sed 's/^/ /'
|
||||||
|
failed=$((failed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "—— $built gebaut, $skipped aktuell, $failed fehlgeschlagen ——"
|
||||||
|
[ "$failed" -eq 0 ]
|
||||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user