Compare commits

...

8 Commits

Author SHA1 Message Date
irrlicht 802f2bd34b Automatisches Ink-Build on Engine build 2026-06-13 02:48:04 +02:00
irrlicht d30ab97f13 Added inklecate to repo <3 2026-06-13 02:48:04 +02:00
irrlicht 7929e7349a Dialog Build-Script 2026-06-13 02:48:03 +02:00
irrlicht cdc27eccc3 Plan-Anpassung 2026-06-13 02:48:03 +02:00
irrlicht de853c22bb Actions statt Effects 2026-06-13 02:48:03 +02:00
irrlicht 1af93c897c Add signal command 2026-06-13 02:48:03 +02:00
irrlicht 97eff4f9eb Signal-System mit Inky und KV-Store 2026-06-13 02:48:03 +02:00
irrlicht ff80b3cc0f Plan für spätere Renderer-Implementierung 2026-06-13 02:48:03 +02:00
21 changed files with 1713 additions and 1 deletions
Generated
+582
View File
@@ -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"
+1
View File
@@ -4,3 +4,4 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
bladeink = "1.2.5"
+8
View File
@@ -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
+1
View File
@@ -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":{}}
+46
View File
@@ -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
+1
View File
@@ -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":{}}
+83
View File
@@ -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
+52
View File
@@ -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",
]
+59
View File
@@ -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"
);
}
}
}
+113
View File
@@ -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).
+26
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
}
}
+102
View File
@@ -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
View File
@@ -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
View File
@@ -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());
}
}
+33
View File
@@ -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)
}
+29
View File
@@ -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`
+83
View File
@@ -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 ]
BIN
View File
Binary file not shown.