fix: client hooks, wip story rework
This commit is contained in:
parent
01c61c143e
commit
7fc90eba74
|
|
@ -26,12 +26,6 @@ version = "1.0.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anymap2"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -160,12 +154,6 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "boolinator"
|
|
||||||
version = "2.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.0"
|
version = "3.19.0"
|
||||||
|
|
@ -255,18 +243,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.8.0"
|
version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.9.0"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
@ -507,74 +495,23 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo"
|
|
||||||
version = "0.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d"
|
|
||||||
dependencies = [
|
|
||||||
"gloo-console 0.2.3",
|
|
||||||
"gloo-dialogs 0.1.1",
|
|
||||||
"gloo-events 0.1.2",
|
|
||||||
"gloo-file 0.2.3",
|
|
||||||
"gloo-history 0.1.5",
|
|
||||||
"gloo-net 0.3.1",
|
|
||||||
"gloo-render 0.1.1",
|
|
||||||
"gloo-storage 0.2.2",
|
|
||||||
"gloo-timers 0.2.6",
|
|
||||||
"gloo-utils 0.1.7",
|
|
||||||
"gloo-worker 0.2.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cd35526c28cc55c1db77aed6296de58677dbab863b118483a27845631d870249"
|
|
||||||
dependencies = [
|
|
||||||
"gloo-console 0.3.0",
|
|
||||||
"gloo-dialogs 0.2.0",
|
|
||||||
"gloo-events 0.2.0",
|
|
||||||
"gloo-file 0.3.0",
|
|
||||||
"gloo-history 0.2.2",
|
|
||||||
"gloo-net 0.4.0",
|
|
||||||
"gloo-render 0.2.0",
|
|
||||||
"gloo-storage 0.3.0",
|
|
||||||
"gloo-timers 0.3.0",
|
|
||||||
"gloo-utils 0.2.0",
|
|
||||||
"gloo-worker 0.4.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo"
|
name = "gloo"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372"
|
checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gloo-console 0.3.0",
|
"gloo-console",
|
||||||
"gloo-dialogs 0.2.0",
|
"gloo-dialogs",
|
||||||
"gloo-events 0.2.0",
|
"gloo-events",
|
||||||
"gloo-file 0.3.0",
|
"gloo-file",
|
||||||
"gloo-history 0.2.2",
|
"gloo-history",
|
||||||
"gloo-net 0.5.0",
|
"gloo-net",
|
||||||
"gloo-render 0.2.0",
|
"gloo-render",
|
||||||
"gloo-storage 0.3.0",
|
"gloo-storage",
|
||||||
"gloo-timers 0.3.0",
|
"gloo-timers",
|
||||||
"gloo-utils 0.2.0",
|
"gloo-utils",
|
||||||
"gloo-worker 0.5.0",
|
"gloo-worker",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-console"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
|
|
||||||
dependencies = [
|
|
||||||
"gloo-utils 0.1.7",
|
|
||||||
"js-sys",
|
|
||||||
"serde",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -583,23 +520,13 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261"
|
checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gloo-utils 0.2.0",
|
"gloo-utils",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"serde",
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-dialogs"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
|
|
||||||
dependencies = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-dialogs"
|
name = "gloo-dialogs"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -610,16 +537,6 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-events"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
|
|
||||||
dependencies = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-events"
|
name = "gloo-events"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -630,18 +547,6 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-file"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
|
|
||||||
dependencies = [
|
|
||||||
"gloo-events 0.1.2",
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-file"
|
name = "gloo-file"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -649,28 +554,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f"
|
checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"gloo-events 0.2.0",
|
"gloo-events",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-history"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f"
|
|
||||||
dependencies = [
|
|
||||||
"gloo-events 0.1.2",
|
|
||||||
"gloo-utils 0.1.7",
|
|
||||||
"serde",
|
|
||||||
"serde-wasm-bindgen 0.5.0",
|
|
||||||
"serde_urlencoded",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-history"
|
name = "gloo-history"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -678,58 +567,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6"
|
checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
"gloo-events 0.2.0",
|
"gloo-events",
|
||||||
"gloo-utils 0.2.0",
|
"gloo-utils",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen 0.6.5",
|
"serde-wasm-bindgen",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-net"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-sink",
|
|
||||||
"gloo-utils 0.1.7",
|
|
||||||
"http 0.2.12",
|
|
||||||
"js-sys",
|
|
||||||
"pin-project",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-net"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-sink",
|
|
||||||
"gloo-utils 0.2.0",
|
|
||||||
"http 0.2.12",
|
|
||||||
"js-sys",
|
|
||||||
"pin-project",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-net"
|
name = "gloo-net"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
@ -739,7 +586,7 @@ dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"gloo-utils 0.2.0",
|
"gloo-utils",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
|
|
@ -751,16 +598,6 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-render"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
|
|
||||||
dependencies = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-render"
|
name = "gloo-render"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -771,28 +608,13 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-storage"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
|
|
||||||
dependencies = [
|
|
||||||
"gloo-utils 0.1.7",
|
|
||||||
"js-sys",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-storage"
|
name = "gloo-storage"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
|
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gloo-utils 0.2.0",
|
"gloo-utils",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -801,16 +623,6 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-timers"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-timers"
|
name = "gloo-timers"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -823,19 +635,6 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-utils"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-utils"
|
name = "gloo-utils"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -849,42 +648,6 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-worker"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a"
|
|
||||||
dependencies = [
|
|
||||||
"anymap2",
|
|
||||||
"bincode",
|
|
||||||
"gloo-console 0.2.3",
|
|
||||||
"gloo-utils 0.1.7",
|
|
||||||
"js-sys",
|
|
||||||
"serde",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gloo-worker"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "76495d3dd87de51da268fa3a593da118ab43eb7f8809e17eb38d3319b424e400"
|
|
||||||
dependencies = [
|
|
||||||
"bincode",
|
|
||||||
"futures",
|
|
||||||
"gloo-utils 0.2.0",
|
|
||||||
"gloo-worker-macros",
|
|
||||||
"js-sys",
|
|
||||||
"pinned",
|
|
||||||
"serde",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-worker"
|
name = "gloo-worker"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
@ -893,7 +656,7 @@ checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"futures",
|
"futures",
|
||||||
"gloo-utils 0.2.0",
|
"gloo-utils",
|
||||||
"gloo-worker-macros",
|
"gloo-worker-macros",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"pinned",
|
"pinned",
|
||||||
|
|
@ -1190,9 +953,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "implicit-clone"
|
name = "implicit-clone"
|
||||||
version = "0.4.9"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84"
|
checksum = "1689b939ee35e3a075b0834b5672efd43aec8a6e81a1c6002b76a5ca2f211ae0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"implicit-clone-derive",
|
"implicit-clone-derive",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
|
@ -1511,23 +1274,6 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "prokio"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488"
|
|
||||||
dependencies = [
|
|
||||||
"futures",
|
|
||||||
"gloo 0.8.1",
|
|
||||||
"num_cpus",
|
|
||||||
"once_cell",
|
|
||||||
"pin-project",
|
|
||||||
"pinned",
|
|
||||||
"tokio",
|
|
||||||
"tokio-stream",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.42"
|
version = "1.0.42"
|
||||||
|
|
@ -1656,17 +1402,6 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde-wasm-bindgen"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"serde",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde-wasm-bindgen"
|
name = "serde-wasm-bindgen"
|
||||||
version = "0.6.5"
|
version = "0.6.5"
|
||||||
|
|
@ -1936,6 +1671,23 @@ dependencies = [
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokise"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "decf97738ce15b9e9cc1671ea29b0f6c56538719e1a092d19cc2134bf144e40e"
|
||||||
|
dependencies = [
|
||||||
|
"futures",
|
||||||
|
"gloo",
|
||||||
|
"num_cpus",
|
||||||
|
"once_cell",
|
||||||
|
"pin-project",
|
||||||
|
"pinned",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.11"
|
version = "0.6.11"
|
||||||
|
|
@ -2209,10 +1961,10 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"ciborium",
|
"ciborium",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.10.0",
|
||||||
"futures",
|
"futures",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"gloo 0.11.0",
|
"gloo",
|
||||||
"instant",
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -2537,22 +2289,22 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yew"
|
name = "yew"
|
||||||
version = "0.21.0"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac"
|
checksum = "3346273ed61b636f5d84e6c696d40f380045b5565b36c5c47f8fc634b8bf5be6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"futures",
|
"futures",
|
||||||
"gloo 0.10.0",
|
"gloo",
|
||||||
"implicit-clone",
|
"implicit-clone",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"prokio",
|
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"serde",
|
"serde",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror 1.0.69",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokise",
|
||||||
"tracing",
|
"tracing",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
|
@ -2562,26 +2314,26 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yew-macro"
|
name = "yew-macro"
|
||||||
version = "0.21.0"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2"
|
checksum = "479e94d645dde3749e81d488c1d32987509dd3b8c31650fcf6e3af1f370e913b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"boolinator",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
"rustversion",
|
||||||
"syn 2.0.111",
|
"syn 2.0.111",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yew-router"
|
name = "yew-router"
|
||||||
version = "0.18.0"
|
version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ca1d5052c96e6762b4d6209a8aded597758d442e6c479995faf0c7b5538e0c6"
|
checksum = "415cb628900ddf1eaf55ebd04163adf1ea80d3f5a9832a876554f9c0fdd4c282"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gloo 0.10.0",
|
"gloo",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"route-recognizer",
|
"route-recognizer",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -2596,9 +2348,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yew-router-macro"
|
name = "yew-router-macro"
|
||||||
version = "0.18.0"
|
version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42bfd190a07ca8cfde7cd4c52b3ac463803dc07323db8c34daa697e86365978c"
|
checksum = "9e87a3ce33434ab66a700edbaf2cc8a417d9b89f00a6fd8216fd6ac83b0e7b1c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ pub mod story;
|
||||||
mod village;
|
mod village;
|
||||||
|
|
||||||
use core::{
|
use core::{
|
||||||
|
cmp::Ordering,
|
||||||
fmt::{Debug, Display},
|
fmt::{Debug, Display},
|
||||||
num::NonZeroU8,
|
num::NonZeroU8,
|
||||||
ops::{Deref, Range, RangeBounds},
|
ops::{Deref, Range, RangeBounds},
|
||||||
|
|
@ -379,6 +380,35 @@ pub enum GameTime {
|
||||||
Night { number: u8 },
|
Night { number: u8 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for GameTime {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for GameTime {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
match (self, other) {
|
||||||
|
(GameTime::Day { number: l }, GameTime::Day { number: r }) => l.cmp(r),
|
||||||
|
(GameTime::Day { number: l }, GameTime::Night { number: r }) => {
|
||||||
|
if *r >= l.get() {
|
||||||
|
Ordering::Less
|
||||||
|
} else {
|
||||||
|
Ordering::Greater
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(GameTime::Night { number: l }, GameTime::Day { number: r }) => {
|
||||||
|
if *l > r.get() {
|
||||||
|
Ordering::Greater
|
||||||
|
} else {
|
||||||
|
Ordering::Less
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(GameTime::Night { number: l }, GameTime::Night { number: r }) => l.cmp(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for GameTime {
|
impl Display for GameTime {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
|
|
@ -209,12 +209,17 @@ pub enum StoryActionPrompt {
|
||||||
character_id: CharacterId,
|
character_id: CharacterId,
|
||||||
chosen: CharacterId,
|
chosen: CharacterId,
|
||||||
},
|
},
|
||||||
|
BeholderWakes {
|
||||||
|
character_id: CharacterId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoryActionPrompt {
|
impl StoryActionPrompt {
|
||||||
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
||||||
Some(match prompt {
|
Some(match prompt {
|
||||||
ActionPrompt::BeholderWakes { .. } => return None, // TODO: rework story anyway
|
ActionPrompt::BeholderWakes { character_id } => Self::BeholderWakes {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
},
|
||||||
ActionPrompt::Bloodletter {
|
ActionPrompt::Bloodletter {
|
||||||
character_id,
|
character_id,
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
|
|
@ -425,6 +430,35 @@ impl StoryActionPrompt {
|
||||||
| ActionPrompt::CoverOfDarkness => return None,
|
| ActionPrompt::CoverOfDarkness => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn character_id(&self) -> Option<CharacterId> {
|
||||||
|
match self {
|
||||||
|
StoryActionPrompt::MasonsWake { .. } | StoryActionPrompt::WolfPackKill { .. } => None,
|
||||||
|
StoryActionPrompt::Seer { character_id, .. }
|
||||||
|
| StoryActionPrompt::Protector { character_id, .. }
|
||||||
|
| StoryActionPrompt::Arcanist { character_id, .. }
|
||||||
|
| StoryActionPrompt::Gravedigger { character_id, .. }
|
||||||
|
| StoryActionPrompt::Hunter { character_id, .. }
|
||||||
|
| StoryActionPrompt::Militia { character_id, .. }
|
||||||
|
| StoryActionPrompt::MapleWolf { character_id, .. }
|
||||||
|
| StoryActionPrompt::Guardian { character_id, .. }
|
||||||
|
| StoryActionPrompt::Adjudicator { character_id, .. }
|
||||||
|
| StoryActionPrompt::PowerSeer { character_id, .. }
|
||||||
|
| StoryActionPrompt::Mortician { character_id, .. }
|
||||||
|
| StoryActionPrompt::Beholder { character_id, .. }
|
||||||
|
| StoryActionPrompt::MasonLeaderRecruit { character_id, .. }
|
||||||
|
| StoryActionPrompt::Empath { character_id, .. }
|
||||||
|
| StoryActionPrompt::Vindicator { character_id, .. }
|
||||||
|
| StoryActionPrompt::PyreMaster { character_id, .. }
|
||||||
|
| StoryActionPrompt::Shapeshifter { character_id, .. }
|
||||||
|
| StoryActionPrompt::AlphaWolf { character_id, .. }
|
||||||
|
| StoryActionPrompt::DireWolf { character_id, .. }
|
||||||
|
| StoryActionPrompt::LoneWolfKill { character_id, .. }
|
||||||
|
| StoryActionPrompt::Insomniac { character_id, .. }
|
||||||
|
| StoryActionPrompt::Bloodletter { character_id, .. }
|
||||||
|
| StoryActionPrompt::BeholderWakes { character_id, .. } => Some(*character_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,16 @@ web-sys = { version = "0.3", features = [
|
||||||
"HtmlImageElement",
|
"HtmlImageElement",
|
||||||
"HtmlDivElement",
|
"HtmlDivElement",
|
||||||
"HtmlSelectElement",
|
"HtmlSelectElement",
|
||||||
|
"HtmlDialogElement",
|
||||||
|
"DomRect",
|
||||||
] }
|
] }
|
||||||
wasm-bindgen = { version = "=0.2.100" }
|
wasm-bindgen = { version = "=0.2.100" }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rand = { version = "0.9", features = ["small_rng"] }
|
rand = { version = "0.9", features = ["small_rng"] }
|
||||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
uuid = { version = "*", features = ["js"] }
|
uuid = { version = "*", features = ["js"] }
|
||||||
yew = { version = "0.21", features = ["csr"] }
|
yew = { version = "0.22", features = ["csr"] }
|
||||||
yew-router = "0.18"
|
yew-router = "0.19"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = { version = "1.0", optional = true }
|
serde_json = { version = "1.0", optional = true }
|
||||||
gloo = "0.11"
|
gloo = "0.11"
|
||||||
|
|
@ -33,7 +35,7 @@ werewolves-proto = { path = "../werewolves-proto" }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
thiserror = { version = "2" }
|
thiserror = { version = "2" }
|
||||||
convert_case = { version = "0.8" }
|
convert_case = { version = "0.10" }
|
||||||
ciborium = { version = "0.2", optional = true }
|
ciborium = { version = "0.2", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
||||||
|
|
@ -208,8 +208,8 @@ nav.host-nav {
|
||||||
block-size: max-content;
|
block-size: max-content;
|
||||||
|
|
||||||
&>button {
|
&>button {
|
||||||
width: 100%;
|
width: 160px;
|
||||||
height: 100%;
|
height: 75px;
|
||||||
border: 1px solid $disconnected_color;
|
border: 1px solid $disconnected_color;
|
||||||
background-color: color.change($disconnected_color, $alpha: 0.15);
|
background-color: color.change($disconnected_color, $alpha: 0.15);
|
||||||
color: $disconnected_color;
|
color: $disconnected_color;
|
||||||
|
|
@ -977,17 +977,30 @@ error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
// input {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
// background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
// color: white;
|
||||||
|
// border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
// margin: 10px;
|
||||||
|
// }
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
|
background-color: rgba(255, 255, 255, 0.07);
|
||||||
color: white;
|
color: white;
|
||||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
font-size: 1em;
|
||||||
margin: 10px;
|
|
||||||
|
&:focus {
|
||||||
|
outline: 1px solid white;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-update {
|
.info-update {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
padding: 30px 0px 30px 0px;
|
padding: 30px 0px 30px 0px;
|
||||||
font-size: 2rem;
|
// font-size: 2rem;
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -1799,25 +1812,52 @@ li.choice {
|
||||||
}
|
}
|
||||||
|
|
||||||
.signin {
|
.signin {
|
||||||
@extend .row-list;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
& label {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.full-height {
|
&.full-height {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
& input {
|
.signin-box {
|
||||||
height: 2rem;
|
display: flex;
|
||||||
text-align: center;
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
// justify-content: center;
|
||||||
|
// text-align: center;
|
||||||
|
|
||||||
#number {
|
.field {
|
||||||
font-size: 2rem;
|
display: flex;
|
||||||
max-width: 50vw;
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& label {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& input {
|
||||||
|
height: 2em;
|
||||||
|
// max-width: 80%;
|
||||||
|
width: 70%;
|
||||||
|
|
||||||
|
&#number {
|
||||||
|
text-align: center;
|
||||||
|
// font-size: 2rem;
|
||||||
|
// width: 20%;
|
||||||
|
width: 3ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&>button {
|
||||||
|
margin-top: 7px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1832,67 +1872,67 @@ li.choice {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.story {
|
// .story {
|
||||||
.cast {
|
// .cast {
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: row;
|
// flex-direction: row;
|
||||||
flex-wrap: wrap;
|
// flex-wrap: wrap;
|
||||||
gap: 10px;
|
// gap: 10px;
|
||||||
justify-content: center;
|
// justify-content: center;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.time-period {
|
// .time-period {
|
||||||
user-select: text;
|
// user-select: text;
|
||||||
|
|
||||||
.day {
|
// .day {
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: column;
|
// flex-direction: column;
|
||||||
flex-wrap: wrap;
|
// flex-wrap: wrap;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
|
|
||||||
.executed {
|
// .executed {
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: row;
|
// flex-direction: row;
|
||||||
flex-wrap: wrap;
|
// flex-wrap: wrap;
|
||||||
gap: 10px;
|
// gap: 10px;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
.night {
|
// .night {
|
||||||
&>label {
|
// &>label {
|
||||||
margin-left: 10vw;
|
// margin-left: 10vw;
|
||||||
font-size: 2rem;
|
// font-size: 2rem;
|
||||||
font-weight: lighter;
|
// font-weight: lighter;
|
||||||
}
|
// }
|
||||||
|
|
||||||
ul.changes,
|
// ul.changes,
|
||||||
ul.choices {
|
// ul.choices {
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: column;
|
// flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
// flex-wrap: nowrap;
|
||||||
gap: 10px;
|
// gap: 10px;
|
||||||
|
|
||||||
&>li {
|
// &>li {
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: row;
|
// flex-direction: row;
|
||||||
flex-wrap: wrap;
|
// flex-wrap: wrap;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
|
|
||||||
gap: 10px;
|
// gap: 10px;
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
& span {
|
// & span {
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: row;
|
// flex-direction: row;
|
||||||
flex-wrap: wrap;
|
// flex-wrap: wrap;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
gap: 10px;
|
// gap: 10px;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
.attribute-span {
|
.attribute-span {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -2209,13 +2249,6 @@ li.choice {
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-player {
|
|
||||||
background-color: black;
|
|
||||||
border: 1px solid white;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.joined {
|
.joined {
|
||||||
$joined_color: rgba(0, 255, 0, 0.7);
|
$joined_color: rgba(0, 255, 0, 0.7);
|
||||||
$joined_border: color.change($joined_color, $alpha: 1);
|
$joined_border: color.change($joined_color, $alpha: 1);
|
||||||
|
|
@ -2370,6 +2403,7 @@ li.choice {
|
||||||
left: 0;
|
left: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
|
||||||
.dialog-box {
|
.dialog-box {
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
|
|
@ -2395,6 +2429,22 @@ li.choice {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-dialog {
|
||||||
|
align-self: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid $wolves_border;
|
||||||
|
color: $wolves_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $wolves_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.about {
|
.about {
|
||||||
|
|
@ -2648,3 +2698,110 @@ li.choice {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.story {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
// width: 100vw;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
row-gap: 5px;
|
||||||
|
margin: 5vh 10vw 0px 10vw;
|
||||||
|
|
||||||
|
.character-headline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 3px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon-spacer {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 0.2em 1em 0.2em 1em;
|
||||||
|
min-width: 5cm;
|
||||||
|
|
||||||
|
.identity {
|
||||||
|
text-align: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-details {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&.shown {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-top: none;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 3px;
|
||||||
|
padding: 2px 3px 2px 3px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-time {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.time {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 3px 0px 3px 0px;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
backdrop-filter: brightness(150%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&.shown {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-submenu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.object {
|
||||||
|
width: 100%;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 3px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&>button,
|
||||||
|
&>div,
|
||||||
|
&>div>button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,9 @@ use yew::prelude::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
clients::client::connection::{Connection2, ConnectionError},
|
clients::client::connection::{Connection2, ConnectionError},
|
||||||
components::{
|
components::{
|
||||||
Button, CoverOfDarkness, Footer, Identity, Story,
|
Button, CoverOfDarkness, Footer, Identity,
|
||||||
client::{ClientNav, Signin},
|
client::{ClientNav, Signin},
|
||||||
|
story::Story,
|
||||||
},
|
},
|
||||||
storage::StorageKey,
|
storage::StorageKey,
|
||||||
};
|
};
|
||||||
|
|
@ -39,6 +40,7 @@ use crate::WerewolfError;
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
pub enum ClientEvent2 {
|
pub enum ClientEvent2 {
|
||||||
|
Signin,
|
||||||
Disconnected,
|
Disconnected,
|
||||||
Connecting,
|
Connecting,
|
||||||
ShowRole(RoleTitle),
|
ShowRole(RoleTitle),
|
||||||
|
|
@ -54,7 +56,6 @@ pub enum ClientEvent2 {
|
||||||
#[derive(Default, Clone, PartialEq)]
|
#[derive(Default, Clone, PartialEq)]
|
||||||
pub struct ClientContext {
|
pub struct ClientContext {
|
||||||
pub error_cb: Callback<Option<WerewolfError>>,
|
pub error_cb: Callback<Option<WerewolfError>>,
|
||||||
pub forced_identity: Option<Identification>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static LOST_FOCUS: AtomicI64 = AtomicI64::new(0);
|
static LOST_FOCUS: AtomicI64 = AtomicI64::new(0);
|
||||||
|
|
@ -73,6 +74,8 @@ pub(super) fn time_spent_unfocused() -> Option<TimeDelta> {
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
||||||
|
let ident_state = use_state(|| Option::<(PlayerId, PublicIdentity)>::None);
|
||||||
|
|
||||||
if gloo::utils::window().onfocus().is_none() {
|
if gloo::utils::window().onfocus().is_none() {
|
||||||
let on_focus = {
|
let on_focus = {
|
||||||
Closure::wrap(Box::new(move || {
|
Closure::wrap(Box::new(move || {
|
||||||
|
|
@ -93,45 +96,67 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
||||||
}
|
}
|
||||||
|
|
||||||
let client_state = use_state(|| ClientEvent2::Connecting);
|
let client_state = use_state(|| ClientEvent2::Connecting);
|
||||||
let ClientContext {
|
let ClientContext { error_cb } = use_context::<ClientContext>().unwrap_or_default();
|
||||||
error_cb,
|
// let force = use_force_update();
|
||||||
forced_identity,
|
let (send, recv) = yew::platform::pinned::mpsc::unbounded();
|
||||||
} = use_context::<ClientContext>().unwrap_or_default();
|
let send = use_state(|| send);
|
||||||
let force = use_force_update();
|
let recv = use_mut_ref(|| recv);
|
||||||
|
let connection =
|
||||||
|
use_mut_ref(|| Connection2::new(client_state.setter(), ident_state.clone(), recv));
|
||||||
|
|
||||||
let ident = if let Some(Identification { player_id, public }) = forced_identity {
|
let on_signin = {
|
||||||
(player_id, public)
|
let current_ident = ident_state.setter();
|
||||||
|
let client_state = client_state.setter();
|
||||||
|
Callback::from(move |ident: PublicIdentity| {
|
||||||
|
let pid = PlayerId::new();
|
||||||
|
pid.save_to_storage().expect("saving player id");
|
||||||
|
ident.save_to_storage().expect("saving ident");
|
||||||
|
|
||||||
|
current_ident.set(Some((pid, ident)));
|
||||||
|
client_state.set(ClientEvent2::Connecting);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if let ClientEvent2::Signin = &*client_state {
|
||||||
|
return html! {
|
||||||
|
<Signin callback={on_signin} />
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let ident = if let Some(current_ident) = ident_state.as_ref() {
|
||||||
|
current_ident.clone()
|
||||||
} else {
|
} else {
|
||||||
match PlayerId::load_from_storage()
|
match PlayerId::load_from_storage()
|
||||||
.and_then(|pid| PublicIdentity::load_from_storage().map(|ident| (pid, ident)))
|
.and_then(|pid| PublicIdentity::load_from_storage().map(|ident| (pid, ident)))
|
||||||
{
|
{
|
||||||
Ok((pid, ident)) => (pid, ident),
|
Ok((pid, ident)) => {
|
||||||
|
ident_state.set(Some((pid, ident.clone())));
|
||||||
|
(pid, ident)
|
||||||
|
}
|
||||||
Err(StorageError::KeyNotFound(_)) => {
|
Err(StorageError::KeyNotFound(_)) => {
|
||||||
let on_signin = Callback::from(move |ident: PublicIdentity| {
|
client_state.set(ClientEvent2::Signin);
|
||||||
PlayerId::new().save_to_storage().expect("saving player id");
|
|
||||||
ident.save_to_storage().expect("saving ident");
|
|
||||||
force.force_update();
|
|
||||||
});
|
|
||||||
return html! {
|
return html! {
|
||||||
<Signin callback={on_signin}/>
|
// <Signin callback={on_signin}/>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
log::error!("storage error: {err}");
|
||||||
error_cb.emit(Some(err.into()));
|
error_cb.emit(Some(err.into()));
|
||||||
PlayerId::delete();
|
PlayerId::delete();
|
||||||
PublicIdentity::delete();
|
PublicIdentity::delete();
|
||||||
force.force_update();
|
// force.force_update();
|
||||||
return html! {};
|
|
||||||
|
// client_state.set(ClientEvent2::Connecting);
|
||||||
|
client_state.set(ClientEvent2::Signin);
|
||||||
|
return html! {
|
||||||
|
// <Signin callback={on_signin} />
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let ident = use_state(|| ident);
|
|
||||||
let (send, recv) = yew::platform::pinned::mpsc::unbounded();
|
|
||||||
let send = use_state(|| send);
|
|
||||||
let recv = use_mut_ref(|| recv);
|
|
||||||
let connection = use_mut_ref(|| Connection2::new(client_state.setter(), ident.clone(), recv));
|
|
||||||
|
|
||||||
let content = match &*client_state {
|
let content = match &*client_state {
|
||||||
|
ClientEvent2::Signin => html! {
|
||||||
|
<Signin callback={on_signin} />
|
||||||
|
},
|
||||||
ClientEvent2::GameInProgress => html! {
|
ClientEvent2::GameInProgress => html! {
|
||||||
<CoverOfDarkness message={"game in progress".to_string()}/>
|
<CoverOfDarkness message={"game in progress".to_string()}/>
|
||||||
},
|
},
|
||||||
|
|
@ -245,7 +270,7 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<ClientNav identity={ident.clone()} message_callback={client_nav_msg_cb} />
|
<ClientNav identity={ident_state.clone()} message_callback={client_nav_msg_cb} />
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ fn url() -> String {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Connection2 {
|
pub struct Connection2 {
|
||||||
state: UseStateSetter<ClientEvent2>,
|
state: UseStateSetter<ClientEvent2>,
|
||||||
ident: UseStateHandle<(PlayerId, PublicIdentity)>,
|
ident: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||||
receiver: Rc<RefCell<UnboundedReceiver<ClientMessage>>>,
|
receiver: Rc<RefCell<UnboundedReceiver<ClientMessage>>>,
|
||||||
active: Rc<RefCell<()>>,
|
active: Rc<RefCell<()>>,
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ pub struct Connection2 {
|
||||||
impl Connection2 {
|
impl Connection2 {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
state: UseStateSetter<ClientEvent2>,
|
state: UseStateSetter<ClientEvent2>,
|
||||||
ident: UseStateHandle<(PlayerId, PublicIdentity)>,
|
ident: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||||
receiver: Rc<RefCell<UnboundedReceiver<ClientMessage>>>,
|
receiver: Rc<RefCell<UnboundedReceiver<ClientMessage>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -64,9 +64,19 @@ impl Connection2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn identification(&self) -> Identification {
|
fn identification(&self) -> Identification {
|
||||||
Identification {
|
match self.ident.as_ref() {
|
||||||
player_id: self.ident.0,
|
Some(ident) => Identification {
|
||||||
public: self.ident.1.clone(),
|
player_id: ident.0,
|
||||||
|
public: ident.1.clone(),
|
||||||
|
},
|
||||||
|
None => Identification {
|
||||||
|
player_id: PlayerId::from_u128(0),
|
||||||
|
public: PublicIdentity {
|
||||||
|
name: String::new(),
|
||||||
|
pronouns: None,
|
||||||
|
number: None,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn connect_ws() -> WebSocket {
|
async fn connect_ws() -> WebSocket {
|
||||||
|
|
@ -108,7 +118,10 @@ impl Connection2 {
|
||||||
yew::platform::spawn_local(async move {
|
yew::platform::spawn_local(async move {
|
||||||
let active = conn.active.clone();
|
let active = conn.active.clone();
|
||||||
conn.active = Rc::new(RefCell::new(()));
|
conn.active = Rc::new(RefCell::new(()));
|
||||||
let active_borrow = active.borrow_mut();
|
let Ok(active_borrow) = active.try_borrow_mut() else {
|
||||||
|
log::warn!("active connection already borrowed; exiting");
|
||||||
|
return;
|
||||||
|
};
|
||||||
conn.run().await;
|
conn.run().await;
|
||||||
core::mem::drop(active_borrow);
|
core::mem::drop(active_borrow);
|
||||||
});
|
});
|
||||||
|
|
@ -274,9 +287,11 @@ impl Connection2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
ServerMessage::Update(PlayerUpdate::Number(new_num)) => {
|
ServerMessage::Update(PlayerUpdate::Number(new_num)) => {
|
||||||
let (pid, mut ident) = (*self.ident).clone();
|
let Some((pid, mut ident)) = (*self.ident).clone() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
ident.number = Some(new_num);
|
ident.number = Some(new_num);
|
||||||
self.ident.set((pid, ident));
|
self.ident.set(Some((pid, ident)));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
ServerMessage::GameInProgress => ClientEvent2::GameInProgress,
|
ServerMessage::GameInProgress => ClientEvent2::GameInProgress,
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,11 @@ use yew::{html::Scope, prelude::*};
|
||||||
use crate::{
|
use crate::{
|
||||||
callback,
|
callback,
|
||||||
components::{
|
components::{
|
||||||
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Story, Victory,
|
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Victory,
|
||||||
action::{ActionResultView, Prompt},
|
action::{ActionResultView, Prompt},
|
||||||
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup},
|
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup},
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
|
story::Story,
|
||||||
},
|
},
|
||||||
pages::RolePage,
|
pages::RolePage,
|
||||||
storage::StorageKey,
|
storage::StorageKey,
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,11 @@ const BASE_URL: &str = match option_env!("BASE_URL") {
|
||||||
Some(base_url) => base_url,
|
Some(base_url) => base_url,
|
||||||
None => "ws://192.168.1.162:8080",
|
None => "ws://192.168.1.162:8080",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use yew::prelude::*;
|
||||||
|
#[function_component]
|
||||||
|
pub fn StoryTest() -> Html {
|
||||||
|
html! {
|
||||||
|
<crate::components::story::Story story={crate::components::story::generate_story()}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ use crate::{
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct ClientNavProps {
|
pub struct ClientNavProps {
|
||||||
pub identity: UseStateHandle<(PlayerId, PublicIdentity)>,
|
pub identity: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||||
pub message_callback: Callback<ClientMessage>,
|
pub message_callback: Callback<ClientMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,18 +38,19 @@ pub fn ClientNav(
|
||||||
message_callback,
|
message_callback,
|
||||||
}: &ClientNavProps,
|
}: &ClientNavProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
|
const MUST_HAVE_IDENTITY: &str = "client nav must have identity";
|
||||||
let pronouns = identity
|
let pronouns = identity
|
||||||
.1
|
|
||||||
.pronouns
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|pronouns| {
|
.and_then(|identity| {
|
||||||
html! {
|
identity.1.pronouns.as_ref().map(|pronouns| {
|
||||||
<div>{"("}{pronouns.as_str()}{")"}</div>
|
html! {
|
||||||
}
|
<span>{"("}{pronouns.as_str()}{")"}</span>
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
html! {
|
html! {
|
||||||
<div>{"(None)"}</div>
|
<span class="faint">{"(None)"}</span>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -62,10 +63,13 @@ pub fn ClientNav(
|
||||||
|
|
||||||
let submit_ident = identity.clone();
|
let submit_ident = identity.clone();
|
||||||
let current_num = identity
|
let current_num = identity
|
||||||
.1
|
.as_ref()
|
||||||
.number
|
.and_then(|identity| identity.1.number.map(|v| html! {{v.to_string()}}))
|
||||||
.map(|v| v.to_string())
|
.unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| String::from("???"));
|
html! {
|
||||||
|
<span class="red">{"???"}</span>
|
||||||
|
}
|
||||||
|
});
|
||||||
let open_set = number_open.setter();
|
let open_set = number_open.setter();
|
||||||
let on_submit = {
|
let on_submit = {
|
||||||
let val = current_value.clone();
|
let val = current_value.clone();
|
||||||
|
|
@ -74,13 +78,16 @@ pub fn ClientNav(
|
||||||
Some(num) => num,
|
Some(num) => num,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
let Some(submit_ident_ref) = submit_ident.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Number(num)));
|
message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Number(num)));
|
||||||
let new_ident = PublicIdentity {
|
let new_ident = PublicIdentity {
|
||||||
name: submit_ident.1.name.clone(),
|
name: submit_ident_ref.1.name.clone(),
|
||||||
pronouns: submit_ident.1.pronouns.clone(),
|
pronouns: submit_ident_ref.1.pronouns.clone(),
|
||||||
number: Some(num),
|
number: Some(num),
|
||||||
};
|
};
|
||||||
submit_ident.set((submit_ident.0, new_ident.clone()));
|
submit_ident.set(Some((submit_ident_ref.0, new_ident.clone())));
|
||||||
if let Err(err) = new_ident.save_to_storage() {
|
if let Err(err) = new_ident.save_to_storage() {
|
||||||
log::error!("saving public identity after change: {err}");
|
log::error!("saving public identity after change: {err}");
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +121,7 @@ pub fn ClientNav(
|
||||||
let ident = identity.clone();
|
let ident = identity.clone();
|
||||||
let message_callback = message_callback.clone();
|
let message_callback = message_callback.clone();
|
||||||
Callback::from(move |value: String| -> Option<PublicIdentity> {
|
Callback::from(move |value: String| -> Option<PublicIdentity> {
|
||||||
|
let ident = ident.as_ref().expect(MUST_HAVE_IDENTITY);
|
||||||
value.trim().is_empty().not().then(|| {
|
value.trim().is_empty().not().then(|| {
|
||||||
let name = value.trim().to_string();
|
let name = value.trim().to_string();
|
||||||
message_callback
|
message_callback
|
||||||
|
|
@ -134,6 +142,12 @@ pub fn ClientNav(
|
||||||
pronouns_open.set(false);
|
pronouns_open.set(false);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
let name_str = identity
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| html! {{i.1.name.to_string()}})
|
||||||
|
.unwrap_or(html! {
|
||||||
|
<span class="red">{"???"}</span>
|
||||||
|
});
|
||||||
html! {
|
html! {
|
||||||
<ClickableTextEdit
|
<ClickableTextEdit
|
||||||
value={name.clone()}
|
value={name.clone()}
|
||||||
|
|
@ -144,7 +158,7 @@ pub fn ClientNav(
|
||||||
on_open={close_others}
|
on_open={close_others}
|
||||||
label={String::from("name")}
|
label={String::from("name")}
|
||||||
>
|
>
|
||||||
<div class="name">{identity.1.name.as_str()}</div>
|
<div class="name">{name_str}</div>
|
||||||
</ClickableTextEdit>
|
</ClickableTextEdit>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -154,15 +168,18 @@ pub fn ClientNav(
|
||||||
let on_submit = {
|
let on_submit = {
|
||||||
let ident = identity.clone();
|
let ident = identity.clone();
|
||||||
let message_callback = message_callback.clone();
|
let message_callback = message_callback.clone();
|
||||||
|
let pronuns_state = pronuns_state.clone();
|
||||||
Callback::from(move |value: String| -> Option<PublicIdentity> {
|
Callback::from(move |value: String| -> Option<PublicIdentity> {
|
||||||
let pronouns = value.trim().is_empty().not().then_some(value);
|
let pronouns = value.trim().is_empty().not().then_some(value);
|
||||||
message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Pronouns(
|
message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Pronouns(
|
||||||
pronouns.clone(),
|
pronouns.clone(),
|
||||||
)));
|
)));
|
||||||
Some(PublicIdentity {
|
pronuns_state.set(String::new());
|
||||||
pronouns,
|
ident.as_ref().map(|id| {
|
||||||
name: ident.1.name.clone(),
|
let mut public = id.1.clone();
|
||||||
number: ident.1.number,
|
public.pronouns = pronouns;
|
||||||
|
ident.set(Some((id.0, public.clone())));
|
||||||
|
public
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
@ -196,14 +213,10 @@ pub fn ClientNav(
|
||||||
cb.emit(ClientMessage::Goodbye);
|
cb.emit(ClientMessage::Goodbye);
|
||||||
let _ = gloo::utils::window().location().reload();
|
let _ = gloo::utils::window().location().reload();
|
||||||
};
|
};
|
||||||
let host_click = Callback::from(|_| {
|
|
||||||
if let Some(loc) = gloo::utils::document().location() {
|
|
||||||
let _ = loc.replace("/host");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Button on_click={host_click}>{"host"}</Button>
|
<a href="/host"><Button on_click={Callback::noop()}>{"host"}</Button></a>
|
||||||
<Button on_click={forgor}>{"forgor 💀"}</Button>
|
<Button on_click={forgor}>{"forgor 💀"}</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
@ -223,7 +236,7 @@ struct ClickableTextEditProps {
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub children: Html,
|
pub children: Html,
|
||||||
pub value: UseStateHandle<String>,
|
pub value: UseStateHandle<String>,
|
||||||
pub submit_ident: UseStateHandle<(PlayerId, PublicIdentity)>,
|
pub submit_ident: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||||
pub on_submit: Callback<String, Option<PublicIdentity>>,
|
pub on_submit: Callback<String, Option<PublicIdentity>>,
|
||||||
pub field_name: &'static str,
|
pub field_name: &'static str,
|
||||||
pub state: UseStateHandle<bool>,
|
pub state: UseStateHandle<bool>,
|
||||||
|
|
@ -257,8 +270,10 @@ fn ClickableTextEdit(
|
||||||
let submit = {
|
let submit = {
|
||||||
let submit_ident = submit_ident.clone();
|
let submit_ident = submit_ident.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
if let Some(new_ident) = message_callback.emit(value.trim().to_string()) {
|
if let Some(new_ident) = message_callback.emit(value.trim().to_string())
|
||||||
submit_ident.set((submit_ident.0, new_ident.clone()));
|
&& let Some(ident) = submit_ident.as_ref()
|
||||||
|
{
|
||||||
|
submit_ident.set(Some((ident.0, new_ident.clone())));
|
||||||
if let Err(err) = new_ident.save_to_storage() {
|
if let Err(err) = new_ident.save_to_storage() {
|
||||||
log::error!("saving public identity after change: {err}");
|
log::error!("saving public identity after change: {err}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ use crate::components::Button;
|
||||||
#[derive(Debug, PartialEq, Properties)]
|
#[derive(Debug, PartialEq, Properties)]
|
||||||
pub struct SigninProps {
|
pub struct SigninProps {
|
||||||
pub callback: Callback<PublicIdentity>,
|
pub callback: Callback<PublicIdentity>,
|
||||||
|
#[prop_or(true)]
|
||||||
|
pub full_height: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
@ -60,15 +62,34 @@ pub fn Signin(props: &SigninProps) -> Html {
|
||||||
});
|
});
|
||||||
|
|
||||||
let on_change = crate::components::input_element_number_oninput(num_value);
|
let on_change = crate::components::input_element_number_oninput(num_value);
|
||||||
|
let full_height = props.full_height.then_some("full-height");
|
||||||
html! {
|
html! {
|
||||||
<div class="signin full-height">
|
<div class={classes!("signin", full_height)}>
|
||||||
<div class="column-list">
|
<div class="signin-box">
|
||||||
<label for="name">{"Name"}</label>
|
<div class="field">
|
||||||
<input oninput={name_on_input} name="name" id="name" type="text"/>
|
<label for="number">{"Seat Number"}</label>
|
||||||
<label for="pronouns">{"Pronouns"}</label>
|
<input
|
||||||
<input oninput={pronouns_on_input} name="pronouns" id="pronouns" type="text"/>
|
oninput={on_change}
|
||||||
<label for="number">{"Number"}</label>
|
type="text"
|
||||||
<input oninput={on_change} type="text" name="number" id="number"/>
|
name="number"
|
||||||
|
id="number"
|
||||||
|
// autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="name">{"Name"}</label>
|
||||||
|
<input
|
||||||
|
oninput={name_on_input}
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
// autocomplete="name nickname username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="pronouns">{"Pronouns"}</label>
|
||||||
|
<input oninput={pronouns_on_input} name="pronouns" id="pronouns" type="text"/>
|
||||||
|
</div>
|
||||||
<Button on_click={on_click}>{"Submit"}</Button>
|
<Button on_click={on_click}>{"Submit"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -109,18 +109,25 @@ pub fn ClickableNumberEdit(
|
||||||
label,
|
label,
|
||||||
}: &ClickableNumberEditProps,
|
}: &ClickableNumberEditProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let on_input = crate::components::input_element_string_oninput(value.setter(), 20);
|
// let on_input = crate::components::input_element_string_oninput(value.setter(), 20);
|
||||||
let on_submit = on_submit.clone();
|
let on_submit = on_submit.clone();
|
||||||
let label = label.is_empty().not().then_some(html! {
|
// let label = label.is_empty().not().then_some(html! {
|
||||||
<label>{label}</label>
|
// <label>{label}</label>
|
||||||
});
|
// });
|
||||||
|
|
||||||
let options = html! {
|
let options = html! {
|
||||||
<div class="info-update">
|
// <div class="info-update">
|
||||||
{label}
|
// {label}
|
||||||
<input type="text" oninput={on_input} name={*field_name} autofocus=true/>
|
// <input type="text" oninput={on_input} name={*field_name} autofocus=true/>
|
||||||
<Button on_click={on_submit.clone()}>{"ok"}</Button>
|
// <Button on_click={on_submit.clone()}>{"ok"}</Button>
|
||||||
</div>
|
// </div>
|
||||||
|
<NumberEdit
|
||||||
|
value={value.clone()}
|
||||||
|
on_submit={on_submit.clone()}
|
||||||
|
field_name={field_name}
|
||||||
|
label={label.clone()}
|
||||||
|
>
|
||||||
|
</NumberEdit>
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
||||||
|
|
@ -128,3 +135,36 @@ pub fn ClickableNumberEdit(
|
||||||
</ClickableField>
|
</ClickableField>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct NumberEditProps {
|
||||||
|
pub value: UseStateHandle<String>,
|
||||||
|
pub on_submit: Callback<()>,
|
||||||
|
pub field_name: &'static str,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn NumberEdit(
|
||||||
|
NumberEditProps {
|
||||||
|
value,
|
||||||
|
on_submit,
|
||||||
|
field_name,
|
||||||
|
label,
|
||||||
|
}: &NumberEditProps,
|
||||||
|
) -> Html {
|
||||||
|
let on_input = crate::components::input_element_string_oninput(value.setter(), 20);
|
||||||
|
let on_submit = on_submit.clone();
|
||||||
|
let label = label.is_empty().not().then_some(html! {
|
||||||
|
<label>{label}</label>
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="info-update">
|
||||||
|
{label}
|
||||||
|
<input type="text" oninput={on_input} name={*field_name} autofocus=true/>
|
||||||
|
<Button on_click={on_submit.clone()}>{"ok"}</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,10 @@ use core::num::NonZeroU8;
|
||||||
use werewolves_proto::{message::PlayerState, player::PlayerId};
|
use werewolves_proto::{message::PlayerState, player::PlayerId};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Button, ClickableField, ClickableNumberEdit, Identity};
|
use crate::components::{
|
||||||
|
Button, ClickableField, ClickableNumberEdit, Identity, NumberEdit,
|
||||||
|
modal::{Dialog, SubmenuDialog},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Properties)]
|
#[derive(Debug, PartialEq, Properties)]
|
||||||
pub struct LobbyPlayerProps {
|
pub struct LobbyPlayerProps {
|
||||||
|
|
@ -35,7 +38,11 @@ pub enum LobbyPlayerAction {
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) -> Html {
|
pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) -> Html {
|
||||||
let open = use_state(|| false);
|
let open = use_state(|| false);
|
||||||
let class = player.connected.then_some("connected");
|
let class = if player.connected {
|
||||||
|
"connected"
|
||||||
|
} else {
|
||||||
|
"disconnected"
|
||||||
|
};
|
||||||
let pid = player.identification.player_id;
|
let pid = player.identification.player_id;
|
||||||
let action_open = open.clone();
|
let action_open = open.clone();
|
||||||
let action = |action: LobbyPlayerAction| {
|
let action = |action: LobbyPlayerAction| {
|
||||||
|
|
@ -65,9 +72,22 @@ pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) ->
|
||||||
on_action.emit((pid, LobbyPlayerAction::SetNumber(number)));
|
on_action.emit((pid, LobbyPlayerAction::SetNumber(number)));
|
||||||
open.set(false);
|
open.set(false);
|
||||||
});
|
});
|
||||||
|
const NUMBER_DIALOG_ID: &str = "player-set-number-dialog";
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Button on_click={(action)(LobbyPlayerAction::Kick)}>{"kick"}</Button>
|
<Button on_click={(action)(LobbyPlayerAction::Kick)}>{"kick"}</Button>
|
||||||
|
// <Dialog
|
||||||
|
// id={NUMBER_DIALOG_ID.to_string()}
|
||||||
|
// button={html!{"set number"}}
|
||||||
|
// >
|
||||||
|
// <NumberEdit
|
||||||
|
// value={number}
|
||||||
|
// on_submit={on_number_submit}
|
||||||
|
// field_name="number"
|
||||||
|
// >
|
||||||
|
// </NumberEdit>
|
||||||
|
// </Dialog>
|
||||||
<ClickableNumberEdit
|
<ClickableNumberEdit
|
||||||
state={number_open}
|
state={number_open}
|
||||||
value={number}
|
value={number}
|
||||||
|
|
@ -79,8 +99,17 @@ pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) ->
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let object = html! {
|
||||||
|
<div class={classes!("player", class, "column-list")}>
|
||||||
|
<Identity ident={player.identification.public.clone()}/>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
html! {
|
html! {
|
||||||
|
// <SubmenuDialog
|
||||||
|
// object={object}
|
||||||
|
// >
|
||||||
|
// {submenu}
|
||||||
|
// </SubmenuDialog>
|
||||||
<ClickableField
|
<ClickableField
|
||||||
state={open}
|
state={open}
|
||||||
options={submenu}
|
options={submenu}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
// Copyright (C) 2026 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::HtmlDialogElement;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::Button;
|
||||||
|
|
||||||
|
pub fn show_modal_by_id(id: &str) {
|
||||||
|
if let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
&& let Err(err) = dialog.show_modal()
|
||||||
|
{
|
||||||
|
gloo::console::error!(format!("show modal [#{}]:", id), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_modal_by_id(id: &str) {
|
||||||
|
if let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
{
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_modal_by_id_callback<ARG>(id: &str) -> Callback<ARG> {
|
||||||
|
let id = id.to_string();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
show_modal_by_id(&id);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct DialogProps {
|
||||||
|
#[prop_or_default]
|
||||||
|
pub id: String,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Html,
|
||||||
|
#[prop_or(true)]
|
||||||
|
pub close_backdrop: bool,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub button: Html,
|
||||||
|
#[prop_or(true)]
|
||||||
|
pub close_button: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn Dialog(
|
||||||
|
DialogProps {
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
close_backdrop,
|
||||||
|
button,
|
||||||
|
close_button,
|
||||||
|
}: &DialogProps,
|
||||||
|
) -> Html {
|
||||||
|
// use generated id if not supplied
|
||||||
|
let id = if id.is_empty() {
|
||||||
|
uuid::Uuid::new_v4().to_string()
|
||||||
|
} else {
|
||||||
|
id.to_string()
|
||||||
|
};
|
||||||
|
let close_cb = {
|
||||||
|
let id = id.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
if let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(&id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
{
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let close = close_button.then_some({
|
||||||
|
html! {
|
||||||
|
<Button on_click={close_cb.clone()} classes={classes!("close-dialog")}>{"close"}</Button>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_backdrop_click = close_backdrop.then_some({
|
||||||
|
let id = id.clone();
|
||||||
|
let close_cb = close_cb.clone();
|
||||||
|
Callback::from(move |ev: MouseEvent| {
|
||||||
|
let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(&id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(Some(dialog)) = dialog.query_selector(".dialog-box") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let rect = dialog.get_bounding_client_rect();
|
||||||
|
|
||||||
|
let is_in_dialog = rect.top() as i32 <= ev.client_y()
|
||||||
|
&& ev.client_y() <= rect.top() as i32 + rect.height() as i32
|
||||||
|
&& rect.left() as i32 <= ev.client_x()
|
||||||
|
&& ev.client_x() <= rect.left() as i32 + rect.width() as i32;
|
||||||
|
|
||||||
|
if !is_in_dialog {
|
||||||
|
close_cb.emit(());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let modal = html! {
|
||||||
|
<dialog id={id.clone()} onclick={on_backdrop_click}>
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-box">
|
||||||
|
{close}
|
||||||
|
{children.clone()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
};
|
||||||
|
let button = (*button != html! {}).then_some({
|
||||||
|
let on_click = show_modal_by_id_callback(&id);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<Button on_click={on_click}>{button.clone()}</Button>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="dialog-modal">
|
||||||
|
{button}
|
||||||
|
{modal}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright (C) 2026 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use crate::components::Button;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::HtmlDialogElement;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct SubmenuProps {
|
||||||
|
#[prop_or_default]
|
||||||
|
pub id: String,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Html,
|
||||||
|
#[prop_or(true)]
|
||||||
|
pub close_backdrop: bool,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub object: Html,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn SubmenuDialog(
|
||||||
|
SubmenuProps {
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
close_backdrop,
|
||||||
|
object,
|
||||||
|
}: &SubmenuProps,
|
||||||
|
) -> Html {
|
||||||
|
// use generated id if not supplied
|
||||||
|
let id = if id.is_empty() {
|
||||||
|
uuid::Uuid::new_v4().to_string()
|
||||||
|
} else {
|
||||||
|
id.to_string()
|
||||||
|
};
|
||||||
|
let close_cb = {
|
||||||
|
let id = id.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
if let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(&id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
{
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_backdrop_click = close_backdrop.then_some({
|
||||||
|
let id = id.clone();
|
||||||
|
let close_cb = close_cb.clone();
|
||||||
|
Callback::from(move |ev: MouseEvent| {
|
||||||
|
let Some(dialog) = gloo::utils::document()
|
||||||
|
.get_element_by_id(&id)
|
||||||
|
.and_then(|elem| elem.dyn_into::<HtmlDialogElement>().ok())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(Some(dialog)) = dialog.query_selector(".object-submenu") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let rect = dialog.get_bounding_client_rect();
|
||||||
|
|
||||||
|
let is_in_dialog = rect.top() as i32 <= ev.client_y()
|
||||||
|
&& ev.client_y() <= rect.top() as i32 + rect.height() as i32
|
||||||
|
&& rect.left() as i32 <= ev.client_x()
|
||||||
|
&& ev.client_x() <= rect.left() as i32 + rect.width() as i32;
|
||||||
|
|
||||||
|
if !is_in_dialog {
|
||||||
|
close_cb.emit(());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let modal = html! {
|
||||||
|
<dialog id={id.clone()} onclick={on_backdrop_click}>
|
||||||
|
<div class="object-submenu">
|
||||||
|
<div class="object">
|
||||||
|
{object.clone()}
|
||||||
|
</div>
|
||||||
|
<menu>
|
||||||
|
{children.clone()}
|
||||||
|
</menu>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
};
|
||||||
|
|
||||||
|
let open = crate::components::modal::show_modal_by_id_callback(&id);
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<div class="opener" onclick={open}>
|
||||||
|
{object.clone()}
|
||||||
|
</div>
|
||||||
|
{modal}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ use crate::{
|
||||||
components::{
|
components::{
|
||||||
Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon,
|
Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon,
|
||||||
client::Signin,
|
client::Signin,
|
||||||
|
modal::Dialog,
|
||||||
settings::{AddRoleCategory, SettingSlotAction, SettingsSlot},
|
settings::{AddRoleCategory, SettingSlotAction, SettingsSlot},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -274,17 +275,19 @@ pub fn Settings(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let add_player_open = use_state(|| false);
|
|
||||||
let add_player_opts = html! {
|
|
||||||
<div class="add-player">
|
|
||||||
<Signin callback={on_add_player.clone()}/>
|
|
||||||
</div>
|
|
||||||
};
|
|
||||||
let current_roles = settings.slots().len();
|
let current_roles = settings.slots().len();
|
||||||
let min_roles = settings.min_players_needed();
|
let min_roles = settings.min_players_needed();
|
||||||
let min_roles_class = (current_roles < min_roles).then_some("red");
|
let min_roles_class = (current_roles < min_roles).then_some("red");
|
||||||
let player_count = players_in_lobby.len();
|
let player_count = players_in_lobby.len();
|
||||||
let player_count_class = (player_count != current_roles).then_some("red");
|
let player_count_class = (player_count != current_roles).then_some("red");
|
||||||
|
const ADD_PLAYER_DIALOG_ID: &str = "add-player-dialog";
|
||||||
|
let add_player_dialog_cb = {
|
||||||
|
let on_add_player = on_add_player.clone();
|
||||||
|
Callback::from(move |ident| {
|
||||||
|
on_add_player.emit(ident);
|
||||||
|
crate::components::modal::close_modal_by_id(ADD_PLAYER_DIALOG_ID);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
|
|
@ -295,12 +298,16 @@ pub fn Settings(
|
||||||
{clear_all_assignments}
|
{clear_all_assignments}
|
||||||
{clear_bad_assigned}
|
{clear_bad_assigned}
|
||||||
</div>
|
</div>
|
||||||
<ClickableField
|
<Dialog
|
||||||
options={add_player_opts}
|
id={ADD_PLAYER_DIALOG_ID.to_string()}
|
||||||
state={add_player_open}
|
button={html!{"add player"}}
|
||||||
|
close_button=false
|
||||||
>
|
>
|
||||||
{"add player"}
|
<div>
|
||||||
</ClickableField>
|
<h3>{"manually add a player"}</h3>
|
||||||
|
<Signin callback={add_player_dialog_cb} full_height=false/>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<div class="roles-add-list">
|
<div class="roles-add-list">
|
||||||
{add_roles_buttons}
|
{add_roles_buttons}
|
||||||
|
|
|
||||||
|
|
@ -1,613 +0,0 @@
|
||||||
// Copyright (C) 2025 Emilis Bliūdžius
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as
|
|
||||||
// published by the Free Software Foundation, either version 3 of the
|
|
||||||
// License, or (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
use core::ops::Not;
|
|
||||||
use std::{collections::HashMap, rc::Rc};
|
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
|
||||||
use werewolves_proto::{
|
|
||||||
aura::AuraTitle, character::{Character, CharacterId}, game::{
|
|
||||||
GameTime, SetupRole,
|
|
||||||
night::changes::NightChange,
|
|
||||||
story::{
|
|
||||||
DayDetail, GameActions, GameStory, NightChoice, StoryActionPrompt, StoryActionResult,
|
|
||||||
},
|
|
||||||
}, role::Alignment
|
|
||||||
};
|
|
||||||
use yew::prelude::*;
|
|
||||||
|
|
||||||
use crate::components::{
|
|
||||||
AuraSpan, CharacterCard, Icon, IconSource, IconType, PartialAssociatedIcon, attributes::{
|
|
||||||
AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
|
||||||
pub struct StoryProps {
|
|
||||||
pub story: GameStory,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component]
|
|
||||||
pub fn Story(StoryProps { story }: &StoryProps) -> Html {
|
|
||||||
let final_characters =
|
|
||||||
story
|
|
||||||
.final_village()
|
|
||||||
.unwrap_or_else(|_| story.starting_village.clone())
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.map(|c| {
|
|
||||||
let dead =c.alive().not();
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={c} dead={dead}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
let bits = story
|
|
||||||
.iter()
|
|
||||||
.map(|(time, changes)| {
|
|
||||||
let characters = story
|
|
||||||
.village_at(match time {
|
|
||||||
GameTime::Day { .. } => {
|
|
||||||
time
|
|
||||||
},
|
|
||||||
GameTime::Night { .. } => {
|
|
||||||
time.previous().unwrap_or(time)
|
|
||||||
},
|
|
||||||
}).ok().flatten()
|
|
||||||
.map(|v| Rc::new(v.characters().into_iter()
|
|
||||||
.map(|c| (c.character_id(), c))
|
|
||||||
.collect::<HashMap<CharacterId, Character>>()))
|
|
||||||
.unwrap_or_else(||
|
|
||||||
Rc::new(story.starting_village
|
|
||||||
.characters().into_iter()
|
|
||||||
.map(|c| (c.character_id(), c))
|
|
||||||
.collect::<HashMap<_, _>>()));
|
|
||||||
let changes = match changes {
|
|
||||||
GameActions::DayDetails(day_changes) => {
|
|
||||||
let execute_list =
|
|
||||||
day_changes
|
|
||||||
.iter()
|
|
||||||
.map(|c| match c {
|
|
||||||
DayDetail::Execute(target) => *target,
|
|
||||||
})
|
|
||||||
.filter_map(|c| story.starting_village.character_by_id(c).ok())
|
|
||||||
.map(|c| {
|
|
||||||
html! {
|
|
||||||
<CharacterCard faint=true char={c.clone()}/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
|
|
||||||
day_changes.is_empty().not().then_some(html! {
|
|
||||||
<div class="day">
|
|
||||||
<h3>{"village executed"}</h3>
|
|
||||||
<div class="executed">
|
|
||||||
{execute_list}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
GameActions::NightDetails(details) => details.choices.is_empty().not().then_some({
|
|
||||||
let choices = details
|
|
||||||
.choices
|
|
||||||
.iter()
|
|
||||||
.map(|c| {
|
|
||||||
html! {
|
|
||||||
<StoryNightChoice choice={c.clone()} characters={characters.clone()}/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
|
|
||||||
let changes = details
|
|
||||||
.changes
|
|
||||||
.iter()
|
|
||||||
.map(|c| {
|
|
||||||
html! {
|
|
||||||
<li class="change">
|
|
||||||
<StoryNightChange
|
|
||||||
change={c.clone()}
|
|
||||||
characters={characters.clone()}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<div class="night">
|
|
||||||
<label>{"choices"}</label>
|
|
||||||
<ul class="choices">
|
|
||||||
{choices}
|
|
||||||
</ul>
|
|
||||||
<label>{"changes"}</label>
|
|
||||||
<ul class="changes">
|
|
||||||
{changes}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
changes
|
|
||||||
.map(|changes| {
|
|
||||||
html! {
|
|
||||||
<div class="time-period">
|
|
||||||
<h1>{"on "}{time.to_string()}{"..."}</h1>
|
|
||||||
{changes}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
html! {
|
|
||||||
<div class="story">
|
|
||||||
<div class="cast">
|
|
||||||
{final_characters}
|
|
||||||
</div>
|
|
||||||
{bits}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
|
||||||
struct StoryNightChangeProps {
|
|
||||||
change: NightChange,
|
|
||||||
characters: Rc<HashMap<CharacterId, Character>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component]
|
|
||||||
fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html {
|
|
||||||
match change {
|
|
||||||
NightChange::LostAura { character, aura } => characters.get(character).map(|character| html!{
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={character.clone()}/>
|
|
||||||
{"lost the"}
|
|
||||||
<AuraSpan aura={aura.title()}/>
|
|
||||||
{"aura"}
|
|
||||||
</>
|
|
||||||
}).unwrap_or_default(),
|
|
||||||
NightChange::ApplyAura { source, target, aura } => characters.get(source).and_then(|source| characters.get(target).map(|target| (source, target))).map(|(source, target)| {
|
|
||||||
html!{
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={target.clone()}/>
|
|
||||||
<span class="story-text">{"gained the"}</span>
|
|
||||||
<AuraSpan aura={aura.title()}/>
|
|
||||||
<span class="story-text">{"aura from"}</span>
|
|
||||||
<CharacterCard faint=true char={source.clone()}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}).unwrap_or_default(),
|
|
||||||
NightChange::RoleChange(character_id, role_title) => characters
|
|
||||||
.get(character_id)
|
|
||||||
.map(|char| {
|
|
||||||
let mut new_char = char.clone();
|
|
||||||
let _ = new_char.role_change(*role_title, GameTime::Night { number: 0 });
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={char.clone()}/>
|
|
||||||
<span class="story-text">{"is now"}</span>
|
|
||||||
<CharacterCard faint=true char={new_char.clone()}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
|
|
||||||
NightChange::Kill { target, died_to } => {
|
|
||||||
|
|
||||||
characters
|
|
||||||
.get(target)
|
|
||||||
.map(|target| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<Icon source={IconSource::Skull} icon_type={IconType::Small}/>
|
|
||||||
<CharacterCard faint=true char={target.clone()}/>
|
|
||||||
<span class="story-text">{"died to"}</span>
|
|
||||||
<DiedToSpan died_to={died_to.title()}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
},
|
|
||||||
NightChange::RoleBlock { source, target, .. } => characters
|
|
||||||
.get(source)
|
|
||||||
.and_then(|s| characters.get(target).map(|t| (s, t)))
|
|
||||||
.map(|(source, target)| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={source.clone()}/>
|
|
||||||
<span class="story-text">{"role blocked"}</span>
|
|
||||||
<CharacterCard faint=true char={target.clone()}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
NightChange::Shapeshift { source, into } => characters
|
|
||||||
.get(source)
|
|
||||||
.and_then(|s| characters.get(into).map(|i| (s, i)))
|
|
||||||
.map(|(source, into)| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={source.clone()}/>
|
|
||||||
<span class="story-text">{"shapeshifted into"}</span>
|
|
||||||
<CharacterCard faint=true char={into.clone()}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
|
|
||||||
NightChange::ElderReveal { elder } => characters
|
|
||||||
.get(elder)
|
|
||||||
.map(|elder| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={elder.clone()}/>
|
|
||||||
<span class="story-text">{"learned they are the Elder"}</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
NightChange::EmpathFoundScapegoat { empath, scapegoat } => characters
|
|
||||||
.get(empath)
|
|
||||||
.and_then(|e| characters.get(scapegoat).map(|s| (e, s)))
|
|
||||||
.map(|(empath, scapegoat)| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={empath.clone()}/>
|
|
||||||
<span class="story-text">{"found the scapegoat in"}</span>
|
|
||||||
<CharacterCard faint=true char={scapegoat.clone()}/>
|
|
||||||
<span class="story-text">{"and took on their curse"}</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
|
||||||
|
|
||||||
NightChange::HunterTarget { .. }
|
|
||||||
| NightChange::MasonRecruit { .. }
|
|
||||||
| NightChange::Protection { .. } => html! {}, // sorted in prompt side
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
|
||||||
struct StoryNightResultProps {
|
|
||||||
result: StoryActionResult,
|
|
||||||
characters: Rc<HashMap<CharacterId, Character>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component]
|
|
||||||
fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html {
|
|
||||||
match result {
|
|
||||||
StoryActionResult::ShiftFailed => html!{
|
|
||||||
<span class="story-text">{"but it failed"}</span>
|
|
||||||
},
|
|
||||||
StoryActionResult::Drunk => html! {
|
|
||||||
<>
|
|
||||||
<span class="story-text">{"but got "}</span>
|
|
||||||
<AuraSpan aura={AuraTitle::Drunk}/>
|
|
||||||
<span class="story-text">{" instead"}</span>
|
|
||||||
</>
|
|
||||||
},
|
|
||||||
StoryActionResult::BeholderSawEverything => html!{
|
|
||||||
<span class="story-text">{"and saw everything 👁️"}</span>
|
|
||||||
},
|
|
||||||
StoryActionResult::BeholderSawNothing => html!{
|
|
||||||
<span class="story-text">{"but saw nothing"}</span>
|
|
||||||
},
|
|
||||||
StoryActionResult::RoleBlocked => html! {
|
|
||||||
<span class="story-text">{"but was role blocked"}</span>
|
|
||||||
},
|
|
||||||
StoryActionResult::Seer(alignment) => {
|
|
||||||
html! {
|
|
||||||
<span>
|
|
||||||
<span class="story-text">{"and saw"}</span>
|
|
||||||
<AlignmentSpan alignment={*alignment}/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StoryActionResult::PowerSeer { powerful } => {
|
|
||||||
html! {
|
|
||||||
<span>
|
|
||||||
<span class="story-text">{"and discovered they are"}</span>
|
|
||||||
<PowerfulSpan powerful={*powerful}/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StoryActionResult::Adjudicator { killer } => html! {
|
|
||||||
<span>
|
|
||||||
<span class="story-text">{"and saw"}</span>
|
|
||||||
<KillerSpan killer={*killer}/>
|
|
||||||
</span>
|
|
||||||
},
|
|
||||||
StoryActionResult::Arcanist(same) => html! {
|
|
||||||
<span>
|
|
||||||
<span class="story-text">{"and saw"}</span>
|
|
||||||
<AlignmentComparisonSpan comparison={*same}/>
|
|
||||||
</span>
|
|
||||||
},
|
|
||||||
StoryActionResult::GraveDigger(None) => html! {
|
|
||||||
<span class="story-text">
|
|
||||||
{"found an empty grave"}
|
|
||||||
</span>
|
|
||||||
},
|
|
||||||
StoryActionResult::GraveDigger(Some(role_title)) => {
|
|
||||||
let category = Into::<SetupRole>::into(*role_title).category();
|
|
||||||
html! {
|
|
||||||
<span>
|
|
||||||
<span class="story-text">{"found the body of a"}</span>
|
|
||||||
<CategorySpan category={category} icon={role_title.icon()}>
|
|
||||||
{role_title.to_string().to_case(Case::Title)}
|
|
||||||
</CategorySpan>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StoryActionResult::Mortician(died_to_title) => html! {
|
|
||||||
<>
|
|
||||||
<span class="story-text">{"and found the cause of death to be"}</span>
|
|
||||||
<DiedToSpan died_to={*died_to_title}/>
|
|
||||||
</>
|
|
||||||
},
|
|
||||||
StoryActionResult::Insomniac { visits } => {
|
|
||||||
let visitors = visits
|
|
||||||
.iter()
|
|
||||||
.filter_map(|c| characters.get(c))
|
|
||||||
.map(|c| {
|
|
||||||
html! {
|
|
||||||
<CharacterCard faint=true char={c.clone()}/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
html! {
|
|
||||||
{visitors}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StoryActionResult::Empath { scapegoat: false } => html! {
|
|
||||||
<>
|
|
||||||
<span class="story-text">{"and saw that they are"}</span>
|
|
||||||
<span class="attribute-span faint">
|
|
||||||
<div class="inactive">
|
|
||||||
<Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
|
||||||
</div>
|
|
||||||
{"Not The Scapegoat"}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
},
|
|
||||||
StoryActionResult::Empath { scapegoat: true } => html! {
|
|
||||||
<>
|
|
||||||
<span class="story-text">{"and saw that they are"}</span>
|
|
||||||
<span class="attribute-span faint wolves">
|
|
||||||
<div>
|
|
||||||
<Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
|
||||||
</div>
|
|
||||||
{"The Scapegoat"}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
|
||||||
struct StoryNightChoiceProps {
|
|
||||||
choice: NightChoice,
|
|
||||||
characters: Rc<HashMap<CharacterId, Character>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component]
|
|
||||||
fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoiceProps) -> Html {
|
|
||||||
let generate = |character_id: &CharacterId, chosen: &CharacterId, action: &'static str| {
|
|
||||||
characters
|
|
||||||
.get(character_id)
|
|
||||||
.and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
|
||||||
.map(|(char, chosen)| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={char.clone()}/>
|
|
||||||
<span class="story-text">{action}</span>
|
|
||||||
<CharacterCard faint=true char={chosen.clone()}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let choice_body = match &choice.prompt {
|
|
||||||
StoryActionPrompt::Arcanist {
|
|
||||||
character_id,
|
|
||||||
chosen: (chosen1, chosen2),
|
|
||||||
} => characters
|
|
||||||
.get(character_id)
|
|
||||||
.and_then(|arcanist| characters.get(chosen1).map(|c| (arcanist, c)))
|
|
||||||
.and_then(|(arcanist, chosen1)| {
|
|
||||||
characters
|
|
||||||
.get(chosen2)
|
|
||||||
.map(|chosen2| (arcanist, chosen1, chosen2))
|
|
||||||
})
|
|
||||||
.map(|(arcanist, chosen1, chosen2)| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={arcanist.clone()}/>
|
|
||||||
<span class="story-text">{"compared"}</span>
|
|
||||||
<CharacterCard faint=true char={chosen1.clone()}/>
|
|
||||||
<span class="story-text">{"and"}</span>
|
|
||||||
<CharacterCard faint=true char={chosen2.clone()}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
StoryActionPrompt::MasonsWake { leader, masons } => characters.get(leader).map(|leader| {
|
|
||||||
let masons = masons
|
|
||||||
.iter()
|
|
||||||
.filter_map(|m| characters.get(m))
|
|
||||||
.map(|c| {
|
|
||||||
html! {
|
|
||||||
<CharacterCard faint=true char={c.clone()}/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={leader.clone()}/>
|
|
||||||
<span class="story-text">{"'s masons"}</span>
|
|
||||||
{masons}
|
|
||||||
<span class="story-text">{"convened in secret"}</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
StoryActionPrompt::Bloodletter { character_id, chosen } => generate(character_id, chosen, "spilt wolf blood on"),
|
|
||||||
StoryActionPrompt::Vindicator {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
}
|
|
||||||
| StoryActionPrompt::Protector {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "protected"),
|
|
||||||
StoryActionPrompt::Gravedigger {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "dug up"),
|
|
||||||
StoryActionPrompt::Adjudicator {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
}
|
|
||||||
| StoryActionPrompt::PowerSeer {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
}
|
|
||||||
| StoryActionPrompt::Empath {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
}
|
|
||||||
| StoryActionPrompt::Seer {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "checked"),
|
|
||||||
StoryActionPrompt::Hunter {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "set a trap for"),
|
|
||||||
StoryActionPrompt::Militia {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "shot"),
|
|
||||||
StoryActionPrompt::MapleWolf {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => characters
|
|
||||||
.get(character_id)
|
|
||||||
.and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
|
||||||
.map(|(char, chosen)| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={char.clone()}/>
|
|
||||||
<span class="story-text">{"invited"}</span>
|
|
||||||
<CharacterCard faint=true char={chosen.clone()}/>
|
|
||||||
<span class="story-text">{"for dinner"}</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
StoryActionPrompt::Guardian {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
guarding,
|
|
||||||
} => generate(
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
if *guarding { "guarded" } else { "protected" },
|
|
||||||
),
|
|
||||||
StoryActionPrompt::Mortician {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "examined"),
|
|
||||||
StoryActionPrompt::Beholder {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "👁️"),
|
|
||||||
|
|
||||||
StoryActionPrompt::MasonLeaderRecruit {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "tried recruiting"),
|
|
||||||
StoryActionPrompt::PyreMaster {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "torched"),
|
|
||||||
StoryActionPrompt::WolfPackKill { chosen } => {
|
|
||||||
characters.get(chosen).map(|chosen: &Character| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<AlignmentSpan alignment={Alignment::Wolves}/>
|
|
||||||
<span class="story-text">{"attempted a kill on"}</span>
|
|
||||||
<CharacterCard faint=true char={chosen.clone()} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
StoryActionPrompt::Shapeshifter { character_id } => {
|
|
||||||
if choice.result.is_none() {
|
|
||||||
return html!{};
|
|
||||||
}
|
|
||||||
characters.get(character_id).map(|shifter| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={shifter.clone()} />
|
|
||||||
<span class="story-text">{"decided to shapeshift into the wolf kill target"}</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
StoryActionPrompt::AlphaWolf {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "took a stab at"),
|
|
||||||
StoryActionPrompt::DireWolf {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "roleblocked"),
|
|
||||||
StoryActionPrompt::LoneWolfKill {
|
|
||||||
character_id,
|
|
||||||
chosen,
|
|
||||||
} => generate(character_id, chosen, "sought vengeance from"),
|
|
||||||
StoryActionPrompt::Insomniac { character_id } => {
|
|
||||||
characters.get(character_id).map(|insomniac| {
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<CharacterCard faint=true char={insomniac.clone()} />
|
|
||||||
<span class="story-text">{"witnessed visits from"}</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let result = choice.result.as_ref().map(|result| {
|
|
||||||
html! {
|
|
||||||
<StoryNightResult result={result.clone()} characters={characters.clone()}/>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
choice_body
|
|
||||||
.map(|choice_body| {
|
|
||||||
html! {
|
|
||||||
<li class="choice">
|
|
||||||
<Icon
|
|
||||||
source={IconSource::ListItem}
|
|
||||||
icon_type={IconType::Small}
|
|
||||||
classes={classes!("li-icon")}
|
|
||||||
/>
|
|
||||||
{choice_body}
|
|
||||||
{result}
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
// Copyright (C) 2026 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
|
use werewolves_proto::{
|
||||||
|
character::{Character, CharacterId},
|
||||||
|
game::{
|
||||||
|
GameTime, SetupRole,
|
||||||
|
night::changes::NightChange,
|
||||||
|
story::{NightChoice, StoryActionPrompt},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Icon, IconSource, IconType, Identity, PartialAssociatedIcon};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct CharacterStoryProps {
|
||||||
|
pub character: Character,
|
||||||
|
pub actions: Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn CharacterStory(CharacterStoryProps { character, actions }: &CharacterStoryProps) -> Html {
|
||||||
|
let mut by_time = actions.clone();
|
||||||
|
by_time.sort_by_key(|s| s.0);
|
||||||
|
let by_time = by_time
|
||||||
|
.into_iter()
|
||||||
|
.map(|(time, changes, choices)| {
|
||||||
|
html! {
|
||||||
|
<CharacterStoryTime
|
||||||
|
character={character.clone()}
|
||||||
|
time={time}
|
||||||
|
changes={changes}
|
||||||
|
choices={choices}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
html! {
|
||||||
|
<div class="character-story">
|
||||||
|
<CharacterStoryContainer character={character.clone()}>
|
||||||
|
{by_time}
|
||||||
|
</CharacterStoryContainer>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
struct CharacterStoryTimeProps {
|
||||||
|
pub character: Character,
|
||||||
|
pub time: GameTime,
|
||||||
|
pub changes: Box<[NightChange]>,
|
||||||
|
pub choices: Box<[NightChoice]>,
|
||||||
|
}
|
||||||
|
#[function_component]
|
||||||
|
fn CharacterStoryTime(
|
||||||
|
CharacterStoryTimeProps {
|
||||||
|
character,
|
||||||
|
time,
|
||||||
|
changes,
|
||||||
|
choices,
|
||||||
|
}: &CharacterStoryTimeProps,
|
||||||
|
) -> Html {
|
||||||
|
let open = use_state(|| false);
|
||||||
|
let time_text = match time {
|
||||||
|
GameTime::Day { number } => format!("day {number}"),
|
||||||
|
GameTime::Night { number } => format!("night {number}"),
|
||||||
|
};
|
||||||
|
let shown = open.then_some("shown");
|
||||||
|
let on_click = {
|
||||||
|
let open = open.clone();
|
||||||
|
Callback::from(move |_| open.set(!*open))
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<div class="story-time">
|
||||||
|
<span class={classes!("time")} onclick={on_click}>
|
||||||
|
{time_text}
|
||||||
|
</span>
|
||||||
|
<div class={classes!("details", shown)}>
|
||||||
|
{"hello"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
struct ChoiceProps {
|
||||||
|
all_characters: Rc<[Character]>,
|
||||||
|
character: Character,
|
||||||
|
choice: NightChoice,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn Choice(
|
||||||
|
ChoiceProps {
|
||||||
|
all_characters,
|
||||||
|
character,
|
||||||
|
choice,
|
||||||
|
}: &ChoiceProps,
|
||||||
|
) -> Html {
|
||||||
|
let generate_prompt = |chosen: CharacterId| -> Html { todo!() };
|
||||||
|
let prompt = match &choice.prompt {
|
||||||
|
StoryActionPrompt::Guardian {
|
||||||
|
chosen, guarding, ..
|
||||||
|
} => todo!(),
|
||||||
|
StoryActionPrompt::Seer { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Protector { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Arcanist { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Gravedigger { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Hunter { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Militia { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::MapleWolf { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Adjudicator { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::PowerSeer { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Mortician { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Beholder { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::MasonsWake { leader, masons } => todo!(),
|
||||||
|
StoryActionPrompt::MasonLeaderRecruit { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Empath { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Vindicator { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::PyreMaster { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::WolfPackKill { chosen } => todo!(),
|
||||||
|
StoryActionPrompt::Shapeshifter { .. } => todo!(),
|
||||||
|
StoryActionPrompt::AlphaWolf { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::DireWolf { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::LoneWolfKill { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::Insomniac { .. } => todo!(),
|
||||||
|
StoryActionPrompt::Bloodletter { chosen, .. } => todo!(),
|
||||||
|
StoryActionPrompt::BeholderWakes { character_id } => todo!(),
|
||||||
|
};
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
struct ChangeProps {
|
||||||
|
all_characters: Rc<[Character]>,
|
||||||
|
change: NightChange,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn Change(
|
||||||
|
ChangeProps {
|
||||||
|
all_characters,
|
||||||
|
change,
|
||||||
|
}: &ChangeProps,
|
||||||
|
) -> Html {
|
||||||
|
match change {
|
||||||
|
NightChange::RoleChange(role_title, ..) => todo!(),
|
||||||
|
NightChange::Kill { target, died_to } => todo!(),
|
||||||
|
NightChange::RoleBlock {
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
block_type,
|
||||||
|
} => todo!(),
|
||||||
|
NightChange::Shapeshift { source, into } => todo!(),
|
||||||
|
NightChange::Protection { target, protection } => todo!(),
|
||||||
|
NightChange::ElderReveal { .. } => todo!(),
|
||||||
|
NightChange::MasonRecruit {
|
||||||
|
mason_leader,
|
||||||
|
recruiting,
|
||||||
|
} => todo!(),
|
||||||
|
NightChange::ApplyAura { source, aura, .. } => todo!(),
|
||||||
|
NightChange::LostAura { aura, .. } => todo!(),
|
||||||
|
NightChange::EmpathFoundScapegoat { .. } | NightChange::HunterTarget { .. } => {
|
||||||
|
return html! {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html! {
|
||||||
|
<div class="change">
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
struct CharacterStoryContainerProps {
|
||||||
|
pub character: Character,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Html,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn CharacterStoryContainer(
|
||||||
|
CharacterStoryContainerProps {
|
||||||
|
character,
|
||||||
|
children,
|
||||||
|
}: &CharacterStoryContainerProps,
|
||||||
|
) -> Html {
|
||||||
|
let open = use_state(|| true);
|
||||||
|
let role_class = Into::<SetupRole>::into(character.role_title())
|
||||||
|
.category()
|
||||||
|
.class();
|
||||||
|
let icon = character
|
||||||
|
.role_title()
|
||||||
|
.icon()
|
||||||
|
.map(|source| {
|
||||||
|
html! {
|
||||||
|
<Icon source={source} icon_type={IconType::Small}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(html! {
|
||||||
|
<div class="icon-spacer"/>
|
||||||
|
});
|
||||||
|
let dead_icon = character
|
||||||
|
.died_to()
|
||||||
|
.map(|_| {
|
||||||
|
html! {
|
||||||
|
<Icon source={IconSource::Skull} icon_type={IconType::Small}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(html! {
|
||||||
|
<div class="icon-spacer"/>
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_click = {
|
||||||
|
let open = open.clone();
|
||||||
|
Callback::from(move |_| open.set(!*open))
|
||||||
|
};
|
||||||
|
let shown = open.then_some("shown");
|
||||||
|
html! {
|
||||||
|
<div class="character-story-card">
|
||||||
|
<div class={classes!("character-headline", role_class, "faint")} onclick={on_click}>
|
||||||
|
{icon}
|
||||||
|
<Identity ident={character.identity().into_public()}/>
|
||||||
|
{dead_icon}
|
||||||
|
</div>
|
||||||
|
<div class={classes!("character-details", role_class, "faint", shown)}>
|
||||||
|
{children.clone()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright (C) 2026 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct StoryErrorProps {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn StoryError(StoryErrorProps { error }: &StoryErrorProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="story-error">
|
||||||
|
{error.clone()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,696 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use core::ops::Not;
|
||||||
|
use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use werewolves_proto::{
|
||||||
|
aura::AuraTitle,
|
||||||
|
character::{Character, CharacterId},
|
||||||
|
game::{
|
||||||
|
GameTime, SetupRole,
|
||||||
|
night::changes::NightChange,
|
||||||
|
story::{
|
||||||
|
DayDetail, GameActions, GameStory, NightChoice, StoryActionPrompt, StoryActionResult,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
role::Alignment,
|
||||||
|
};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{
|
||||||
|
AuraSpan, CharacterCard, Icon, IconSource, IconType, PartialAssociatedIcon,
|
||||||
|
attributes::{
|
||||||
|
AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
||||||
|
},
|
||||||
|
story::{CharacterStory, StoryError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct StoryProps {
|
||||||
|
pub story: GameStory,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn Story(StoryProps { story }: &StoryProps) -> Html {
|
||||||
|
let village = match story.final_village() {
|
||||||
|
Ok(village) => village,
|
||||||
|
Err(err) => {
|
||||||
|
return html! {
|
||||||
|
<StoryError error={err.to_string()}/>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let actions_by_character = village
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| {
|
||||||
|
let actions = story
|
||||||
|
.changes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(time, act)| {
|
||||||
|
let cid = c.character_id();
|
||||||
|
match act {
|
||||||
|
GameActions::DayDetails(_) => None,
|
||||||
|
GameActions::NightDetails(night) => Some((
|
||||||
|
*time,
|
||||||
|
night
|
||||||
|
.changes
|
||||||
|
.iter()
|
||||||
|
.filter(|nc| {
|
||||||
|
nc.target().map(|target| target == cid).unwrap_or_default()
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Box<[_]>>(),
|
||||||
|
night
|
||||||
|
.choices
|
||||||
|
.iter()
|
||||||
|
.filter(|nc| {
|
||||||
|
nc.prompt
|
||||||
|
.character_id()
|
||||||
|
.map(|c| c == cid)
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Box<[_]>>(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
|
||||||
|
(c, actions)
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
|
||||||
|
let chars = actions_by_character
|
||||||
|
.into_iter()
|
||||||
|
.map(|(char, actions)| {
|
||||||
|
html! {
|
||||||
|
<CharacterStory character={char} actions={actions} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="story">
|
||||||
|
{chars}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[function_component]
|
||||||
|
// pub fn OldStory(StoryProps { story }: &StoryProps) -> Html {
|
||||||
|
// let final_characters =
|
||||||
|
// story
|
||||||
|
// .final_village()
|
||||||
|
// .unwrap_or_else(|_| story.starting_village.clone())
|
||||||
|
// .characters()
|
||||||
|
// .into_iter()
|
||||||
|
// .map(|c| {
|
||||||
|
// let dead =c.alive().not();
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={c} dead={dead}/>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .collect::<Html>();
|
||||||
|
// let bits = story
|
||||||
|
// .iter()
|
||||||
|
// .map(|(time, changes)| {
|
||||||
|
// let characters = story
|
||||||
|
// .village_at(match time {
|
||||||
|
// GameTime::Day { .. } => {
|
||||||
|
// time
|
||||||
|
// },
|
||||||
|
// GameTime::Night { .. } => {
|
||||||
|
// time.previous().unwrap_or(time)
|
||||||
|
// },
|
||||||
|
// }).ok().flatten()
|
||||||
|
// .map(|v| Rc::new(v.characters().into_iter()
|
||||||
|
// .map(|c| (c.character_id(), c))
|
||||||
|
// .collect::<HashMap<CharacterId, Character>>()))
|
||||||
|
// .unwrap_or_else(||
|
||||||
|
// Rc::new(story.starting_village
|
||||||
|
// .characters().into_iter()
|
||||||
|
// .map(|c| (c.character_id(), c))
|
||||||
|
// .collect::<HashMap<_, _>>()));
|
||||||
|
// let changes = match changes {
|
||||||
|
// GameActions::DayDetails(day_changes) => {
|
||||||
|
// let execute_list =
|
||||||
|
// day_changes
|
||||||
|
// .iter()
|
||||||
|
// .map(|c| match c {
|
||||||
|
// DayDetail::Execute(target) => *target,
|
||||||
|
// })
|
||||||
|
// .filter_map(|c| story.starting_village.character_by_id(c).ok())
|
||||||
|
// .map(|c| {
|
||||||
|
// html! {
|
||||||
|
// <CharacterCard faint=true char={c.clone()}/>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .collect::<Html>();
|
||||||
|
|
||||||
|
// day_changes.is_empty().not().then_some(html! {
|
||||||
|
// <div class="day">
|
||||||
|
// <h3>{"village executed"}</h3>
|
||||||
|
// <div class="executed">
|
||||||
|
// {execute_list}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// GameActions::NightDetails(details) => details.choices.is_empty().not().then_some({
|
||||||
|
// let choices = details
|
||||||
|
// .choices
|
||||||
|
// .iter()
|
||||||
|
// .map(|c| {
|
||||||
|
// html! {
|
||||||
|
// <StoryNightChoice choice={c.clone()} characters={characters.clone()}/>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .collect::<Html>();
|
||||||
|
|
||||||
|
// let changes = details
|
||||||
|
// .changes
|
||||||
|
// .iter()
|
||||||
|
// .map(|c| {
|
||||||
|
// html! {
|
||||||
|
// <li class="change">
|
||||||
|
// <StoryNightChange
|
||||||
|
// change={c.clone()}
|
||||||
|
// characters={characters.clone()}
|
||||||
|
// />
|
||||||
|
// </li>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .collect::<Html>();
|
||||||
|
|
||||||
|
// html! {
|
||||||
|
// <div class="night">
|
||||||
|
// <label>{"choices"}</label>
|
||||||
|
// <ul class="choices">
|
||||||
|
// {choices}
|
||||||
|
// </ul>
|
||||||
|
// <label>{"changes"}</label>
|
||||||
|
// <ul class="changes">
|
||||||
|
// {changes}
|
||||||
|
// </ul>
|
||||||
|
// </div>
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// };
|
||||||
|
|
||||||
|
// changes
|
||||||
|
// .map(|changes| {
|
||||||
|
// html! {
|
||||||
|
// <div class="time-period">
|
||||||
|
// <h1>{"on "}{time.to_string()}{"..."}</h1>
|
||||||
|
// {changes}
|
||||||
|
// </div>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .unwrap_or_default()
|
||||||
|
// })
|
||||||
|
// .collect::<Html>();
|
||||||
|
// html! {
|
||||||
|
// <div class="story">
|
||||||
|
// <div class="cast">
|
||||||
|
// {final_characters}
|
||||||
|
// </div>
|
||||||
|
// {bits}
|
||||||
|
// </div>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
// struct StoryNightChangeProps {
|
||||||
|
// change: NightChange,
|
||||||
|
// characters: Rc<HashMap<CharacterId, Character>>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[function_component]
|
||||||
|
// fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html {
|
||||||
|
// match change {
|
||||||
|
// NightChange::LostAura { character, aura } => characters.get(character).map(|character| html!{
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={character.clone()}/>
|
||||||
|
// {"lost the"}
|
||||||
|
// <AuraSpan aura={aura.title()}/>
|
||||||
|
// {"aura"}
|
||||||
|
// </>
|
||||||
|
// }).unwrap_or_default(),
|
||||||
|
// NightChange::ApplyAura { source, target, aura } => characters.get(source).and_then(|source| characters.get(target).map(|target| (source, target))).map(|(source, target)| {
|
||||||
|
// html!{
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={target.clone()}/>
|
||||||
|
// <span class="story-text">{"gained the"}</span>
|
||||||
|
// <AuraSpan aura={aura.title()}/>
|
||||||
|
// <span class="story-text">{"aura from"}</span>
|
||||||
|
// <CharacterCard faint=true char={source.clone()}/>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// }).unwrap_or_default(),
|
||||||
|
// NightChange::RoleChange(character_id, role_title) => characters
|
||||||
|
// .get(character_id)
|
||||||
|
// .map(|char| {
|
||||||
|
// let mut new_char = char.clone();
|
||||||
|
// let _ = new_char.role_change(*role_title, GameTime::Night { number: 0 });
|
||||||
|
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={char.clone()}/>
|
||||||
|
// <span class="story-text">{"is now"}</span>
|
||||||
|
// <CharacterCard faint=true char={new_char.clone()}/>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .unwrap_or_default(),
|
||||||
|
|
||||||
|
// NightChange::Kill { target, died_to } => {
|
||||||
|
|
||||||
|
// characters
|
||||||
|
// .get(target)
|
||||||
|
// .map(|target| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <Icon source={IconSource::Skull} icon_type={IconType::Small}/>
|
||||||
|
// <CharacterCard faint=true char={target.clone()}/>
|
||||||
|
// <span class="story-text">{"died to"}</span>
|
||||||
|
// <DiedToSpan died_to={died_to.title()}/>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .unwrap_or_default()
|
||||||
|
// },
|
||||||
|
// NightChange::RoleBlock { source, target, .. } => characters
|
||||||
|
// .get(source)
|
||||||
|
// .and_then(|s| characters.get(target).map(|t| (s, t)))
|
||||||
|
// .map(|(source, target)| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={source.clone()}/>
|
||||||
|
// <span class="story-text">{"role blocked"}</span>
|
||||||
|
// <CharacterCard faint=true char={target.clone()}/>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .unwrap_or_default(),
|
||||||
|
// NightChange::Shapeshift { source, into } => characters
|
||||||
|
// .get(source)
|
||||||
|
// .and_then(|s| characters.get(into).map(|i| (s, i)))
|
||||||
|
// .map(|(source, into)| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={source.clone()}/>
|
||||||
|
// <span class="story-text">{"shapeshifted into"}</span>
|
||||||
|
// <CharacterCard faint=true char={into.clone()}/>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .unwrap_or_default(),
|
||||||
|
|
||||||
|
// NightChange::ElderReveal { elder } => characters
|
||||||
|
// .get(elder)
|
||||||
|
// .map(|elder| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={elder.clone()}/>
|
||||||
|
// <span class="story-text">{"learned they are the Elder"}</span>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .unwrap_or_default(),
|
||||||
|
// NightChange::EmpathFoundScapegoat { empath, scapegoat } => characters
|
||||||
|
// .get(empath)
|
||||||
|
// .and_then(|e| characters.get(scapegoat).map(|s| (e, s)))
|
||||||
|
// .map(|(empath, scapegoat)| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={empath.clone()}/>
|
||||||
|
// <span class="story-text">{"found the scapegoat in"}</span>
|
||||||
|
// <CharacterCard faint=true char={scapegoat.clone()}/>
|
||||||
|
// <span class="story-text">{"and took on their curse"}</span>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .unwrap_or_default(),
|
||||||
|
|
||||||
|
// NightChange::HunterTarget { .. }
|
||||||
|
// | NightChange::MasonRecruit { .. }
|
||||||
|
// | NightChange::Protection { .. } => html! {}, // sorted in prompt side
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
// struct StoryNightResultProps {
|
||||||
|
// result: StoryActionResult,
|
||||||
|
// characters: Rc<HashMap<CharacterId, Character>>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[function_component]
|
||||||
|
// fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html {
|
||||||
|
// match result {
|
||||||
|
// StoryActionResult::ShiftFailed => html!{
|
||||||
|
// <span class="story-text">{"but it failed"}</span>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::Drunk => html! {
|
||||||
|
// <>
|
||||||
|
// <span class="story-text">{"but got "}</span>
|
||||||
|
// <AuraSpan aura={AuraTitle::Drunk}/>
|
||||||
|
// <span class="story-text">{" instead"}</span>
|
||||||
|
// </>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::BeholderSawEverything => html!{
|
||||||
|
// <span class="story-text">{"and saw everything 👁️"}</span>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::BeholderSawNothing => html!{
|
||||||
|
// <span class="story-text">{"but saw nothing"}</span>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::RoleBlocked => html! {
|
||||||
|
// <span class="story-text">{"but was role blocked"}</span>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::Seer(alignment) => {
|
||||||
|
// html! {
|
||||||
|
// <span>
|
||||||
|
// <span class="story-text">{"and saw"}</span>
|
||||||
|
// <AlignmentSpan alignment={*alignment}/>
|
||||||
|
// </span>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// StoryActionResult::PowerSeer { powerful } => {
|
||||||
|
// html! {
|
||||||
|
// <span>
|
||||||
|
// <span class="story-text">{"and discovered they are"}</span>
|
||||||
|
// <PowerfulSpan powerful={*powerful}/>
|
||||||
|
// </span>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// StoryActionResult::Adjudicator { killer } => html! {
|
||||||
|
// <span>
|
||||||
|
// <span class="story-text">{"and saw"}</span>
|
||||||
|
// <KillerSpan killer={*killer}/>
|
||||||
|
// </span>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::Arcanist(same) => html! {
|
||||||
|
// <span>
|
||||||
|
// <span class="story-text">{"and saw"}</span>
|
||||||
|
// <AlignmentComparisonSpan comparison={*same}/>
|
||||||
|
// </span>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::GraveDigger(None) => html! {
|
||||||
|
// <span class="story-text">
|
||||||
|
// {"found an empty grave"}
|
||||||
|
// </span>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::GraveDigger(Some(role_title)) => {
|
||||||
|
// let category = Into::<SetupRole>::into(*role_title).category();
|
||||||
|
// html! {
|
||||||
|
// <span>
|
||||||
|
// <span class="story-text">{"found the body of a"}</span>
|
||||||
|
// <CategorySpan category={category} icon={role_title.icon()}>
|
||||||
|
// {role_title.to_string().to_case(Case::Title)}
|
||||||
|
// </CategorySpan>
|
||||||
|
// </span>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// StoryActionResult::Mortician(died_to_title) => html! {
|
||||||
|
// <>
|
||||||
|
// <span class="story-text">{"and found the cause of death to be"}</span>
|
||||||
|
// <DiedToSpan died_to={*died_to_title}/>
|
||||||
|
// </>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::Insomniac { visits } => {
|
||||||
|
// let visitors = visits
|
||||||
|
// .iter()
|
||||||
|
// .filter_map(|c| characters.get(c))
|
||||||
|
// .map(|c| {
|
||||||
|
// html! {
|
||||||
|
// <CharacterCard faint=true char={c.clone()}/>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .collect::<Html>();
|
||||||
|
// html! {
|
||||||
|
// {visitors}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// StoryActionResult::Empath { scapegoat: false } => html! {
|
||||||
|
// <>
|
||||||
|
// <span class="story-text">{"and saw that they are"}</span>
|
||||||
|
// <span class="attribute-span faint">
|
||||||
|
// <div class="inactive">
|
||||||
|
// <Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
||||||
|
// </div>
|
||||||
|
// {"Not The Scapegoat"}
|
||||||
|
// </span>
|
||||||
|
// </>
|
||||||
|
// },
|
||||||
|
// StoryActionResult::Empath { scapegoat: true } => html! {
|
||||||
|
// <>
|
||||||
|
// <span class="story-text">{"and saw that they are"}</span>
|
||||||
|
// <span class="attribute-span faint wolves">
|
||||||
|
// <div>
|
||||||
|
// <Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
||||||
|
// </div>
|
||||||
|
// {"The Scapegoat"}
|
||||||
|
// </span>
|
||||||
|
// </>
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
// struct StoryNightChoiceProps {
|
||||||
|
// choice: NightChoice,
|
||||||
|
// characters: Rc<HashMap<CharacterId, Character>>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[function_component]
|
||||||
|
// fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoiceProps) -> Html {
|
||||||
|
// let generate = |character_id: &CharacterId, chosen: &CharacterId, action: &'static str| {
|
||||||
|
// characters
|
||||||
|
// .get(character_id)
|
||||||
|
// .and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
||||||
|
// .map(|(char, chosen)| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={char.clone()}/>
|
||||||
|
// <span class="story-text">{action}</span>
|
||||||
|
// <CharacterCard faint=true char={chosen.clone()}/>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// };
|
||||||
|
// let choice_body = match &choice.prompt {
|
||||||
|
// StoryActionPrompt::Arcanist {
|
||||||
|
// character_id,
|
||||||
|
// chosen: (chosen1, chosen2),
|
||||||
|
// } => characters
|
||||||
|
// .get(character_id)
|
||||||
|
// .and_then(|arcanist| characters.get(chosen1).map(|c| (arcanist, c)))
|
||||||
|
// .and_then(|(arcanist, chosen1)| {
|
||||||
|
// characters
|
||||||
|
// .get(chosen2)
|
||||||
|
// .map(|chosen2| (arcanist, chosen1, chosen2))
|
||||||
|
// })
|
||||||
|
// .map(|(arcanist, chosen1, chosen2)| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={arcanist.clone()}/>
|
||||||
|
// <span class="story-text">{"compared"}</span>
|
||||||
|
// <CharacterCard faint=true char={chosen1.clone()}/>
|
||||||
|
// <span class="story-text">{"and"}</span>
|
||||||
|
// <CharacterCard faint=true char={chosen2.clone()}/>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// StoryActionPrompt::MasonsWake { leader, masons } => characters.get(leader).map(|leader| {
|
||||||
|
// let masons = masons
|
||||||
|
// .iter()
|
||||||
|
// .filter_map(|m| characters.get(m))
|
||||||
|
// .map(|c| {
|
||||||
|
// html! {
|
||||||
|
// <CharacterCard faint=true char={c.clone()}/>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .collect::<Html>();
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={leader.clone()}/>
|
||||||
|
// <span class="story-text">{"'s masons"}</span>
|
||||||
|
// {masons}
|
||||||
|
// <span class="story-text">{"convened in secret"}</span>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// StoryActionPrompt::Bloodletter { character_id, chosen } => generate(character_id, chosen, "spilt wolf blood on"),
|
||||||
|
|
||||||
|
// StoryActionPrompt::BeholderWakes { character_id }=>characters
|
||||||
|
// .get(character_id)
|
||||||
|
// .map(|char| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={char.clone()}/>
|
||||||
|
// <span class="story-text">{"woke up and saw"}</span>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// StoryActionPrompt::Vindicator {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// }
|
||||||
|
// | StoryActionPrompt::Protector {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "protected"),
|
||||||
|
// StoryActionPrompt::Gravedigger {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "dug up"),
|
||||||
|
// StoryActionPrompt::Adjudicator {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// }
|
||||||
|
// | StoryActionPrompt::PowerSeer {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// }
|
||||||
|
// | StoryActionPrompt::Empath {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// }
|
||||||
|
// | StoryActionPrompt::Seer {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "checked"),
|
||||||
|
// StoryActionPrompt::Hunter {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "set a trap for"),
|
||||||
|
// StoryActionPrompt::Militia {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "shot"),
|
||||||
|
// StoryActionPrompt::MapleWolf {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => characters
|
||||||
|
// .get(character_id)
|
||||||
|
// .and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
||||||
|
// .map(|(char, chosen)| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={char.clone()}/>
|
||||||
|
// <span class="story-text">{"invited"}</span>
|
||||||
|
// <CharacterCard faint=true char={chosen.clone()}/>
|
||||||
|
// <span class="story-text">{"for dinner"}</span>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// StoryActionPrompt::Guardian {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// guarding,
|
||||||
|
// } => generate(
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// if *guarding { "guarded" } else { "protected" },
|
||||||
|
// ),
|
||||||
|
// StoryActionPrompt::Mortician {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "examined"),
|
||||||
|
// StoryActionPrompt::Beholder {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "👁️"),
|
||||||
|
|
||||||
|
// StoryActionPrompt::MasonLeaderRecruit {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "tried recruiting"),
|
||||||
|
// StoryActionPrompt::PyreMaster {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "torched"),
|
||||||
|
// StoryActionPrompt::WolfPackKill { chosen } => {
|
||||||
|
// characters.get(chosen).map(|chosen: &Character| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <AlignmentSpan alignment={Alignment::Wolves}/>
|
||||||
|
// <span class="story-text">{"attempted a kill on"}</span>
|
||||||
|
// <CharacterCard faint=true char={chosen.clone()} />
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// StoryActionPrompt::Shapeshifter { character_id } => {
|
||||||
|
// if choice.result.is_none() {
|
||||||
|
// return html!{};
|
||||||
|
// }
|
||||||
|
// characters.get(character_id).map(|shifter| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={shifter.clone()} />
|
||||||
|
// <span class="story-text">{"decided to shapeshift into the wolf kill target"}</span>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// StoryActionPrompt::AlphaWolf {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "took a stab at"),
|
||||||
|
// StoryActionPrompt::DireWolf {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "roleblocked"),
|
||||||
|
// StoryActionPrompt::LoneWolfKill {
|
||||||
|
// character_id,
|
||||||
|
// chosen,
|
||||||
|
// } => generate(character_id, chosen, "sought vengeance from"),
|
||||||
|
// StoryActionPrompt::Insomniac { character_id } => {
|
||||||
|
// characters.get(character_id).map(|insomniac| {
|
||||||
|
// html! {
|
||||||
|
// <>
|
||||||
|
// <CharacterCard faint=true char={insomniac.clone()} />
|
||||||
|
// <span class="story-text">{"witnessed visits from"}</span>
|
||||||
|
// </>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// let result = choice.result.as_ref().map(|result| {
|
||||||
|
// html! {
|
||||||
|
// <StoryNightResult result={result.clone()} characters={characters.clone()}/>
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// choice_body
|
||||||
|
// .map(|choice_body| {
|
||||||
|
// html! {
|
||||||
|
// <li class="choice">
|
||||||
|
// <Icon
|
||||||
|
// source={IconSource::ListItem}
|
||||||
|
// icon_type={IconType::Small}
|
||||||
|
// classes={classes!("li-icon")}
|
||||||
|
// />
|
||||||
|
// {choice_body}
|
||||||
|
// {result}
|
||||||
|
// </li>
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .unwrap_or_default()
|
||||||
|
// }
|
||||||
|
|
@ -19,6 +19,9 @@ mod storage;
|
||||||
mod test_util;
|
mod test_util;
|
||||||
mod components {
|
mod components {
|
||||||
werewolves_macros::include_path!("werewolves/src/components");
|
werewolves_macros::include_path!("werewolves/src/components");
|
||||||
|
pub mod modal {
|
||||||
|
werewolves_macros::include_path!("werewolves/src/components/modal");
|
||||||
|
}
|
||||||
pub mod attributes {
|
pub mod attributes {
|
||||||
werewolves_macros::include_path!("werewolves/src/components/attributes");
|
werewolves_macros::include_path!("werewolves/src/components/attributes");
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +37,9 @@ mod components {
|
||||||
pub mod settings {
|
pub mod settings {
|
||||||
werewolves_macros::include_path!("werewolves/src/components/settings");
|
werewolves_macros::include_path!("werewolves/src/components/settings");
|
||||||
}
|
}
|
||||||
|
pub mod story {
|
||||||
|
werewolves_macros::include_path!("werewolves/src/components/story");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mod pages {
|
mod pages {
|
||||||
werewolves_macros::include_path!("werewolves/src/pages");
|
werewolves_macros::include_path!("werewolves/src/pages");
|
||||||
|
|
@ -81,13 +87,14 @@ fn main() {
|
||||||
} else {
|
} else {
|
||||||
host.send_message(HostEvent::SetErrorCallback(error_callback));
|
host.send_message(HostEvent::SetErrorCallback(error_callback));
|
||||||
}
|
}
|
||||||
|
} else if path.starts_with("/story") {
|
||||||
|
let story = yew::Renderer::<clients::StoryTest>::with_root(app_element).render();
|
||||||
} else {
|
} else {
|
||||||
yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props(
|
yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props(
|
||||||
app_element,
|
app_element,
|
||||||
ContextProviderProps {
|
ContextProviderProps {
|
||||||
context: ClientContext {
|
context: ClientContext {
|
||||||
error_cb: error_callback.clone(),
|
error_cb: error_callback.clone(),
|
||||||
forced_identity: None,
|
|
||||||
},
|
},
|
||||||
children: html! {
|
children: html! {
|
||||||
<Client2 auto_join=false/>
|
<Client2 auto_join=false/>
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ fn FalselyAppearsAs(
|
||||||
<div class="bottom-bound">
|
<div class="bottom-bound">
|
||||||
<h5>
|
<h5>
|
||||||
{"ROLES THAT FALSELY APPEAR AS "}
|
{"ROLES THAT FALSELY APPEAR AS "}
|
||||||
<span class="yellow">{alignment_text}</span>
|
<span class="yellow">{*alignment_text}</span>
|
||||||
</h5>
|
</h5>
|
||||||
<div class="false-positives yellow">
|
<div class="false-positives yellow">
|
||||||
{false_positives}
|
{false_positives}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue