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"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "anymap2"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
|
|
@ -160,12 +154,6 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boolinator"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
|
|
@ -255,18 +243,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
|
@ -507,74 +495,23 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gloo"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372"
|
||||
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.5.0",
|
||||
"gloo-render 0.2.0",
|
||||
"gloo-storage 0.3.0",
|
||||
"gloo-timers 0.3.0",
|
||||
"gloo-utils 0.2.0",
|
||||
"gloo-worker 0.5.0",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"gloo-console",
|
||||
"gloo-dialogs",
|
||||
"gloo-events",
|
||||
"gloo-file",
|
||||
"gloo-history",
|
||||
"gloo-net",
|
||||
"gloo-render",
|
||||
"gloo-storage",
|
||||
"gloo-timers",
|
||||
"gloo-utils",
|
||||
"gloo-worker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -583,23 +520,13 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261"
|
||||
dependencies = [
|
||||
"gloo-utils 0.2.0",
|
||||
"gloo-utils",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"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]]
|
||||
name = "gloo-dialogs"
|
||||
version = "0.2.0"
|
||||
|
|
@ -610,16 +537,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gloo-events"
|
||||
version = "0.2.0"
|
||||
|
|
@ -630,18 +547,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gloo-file"
|
||||
version = "0.3.0"
|
||||
|
|
@ -649,28 +554,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"gloo-events 0.2.0",
|
||||
"gloo-events",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"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]]
|
||||
name = "gloo-history"
|
||||
version = "0.2.2"
|
||||
|
|
@ -678,58 +567,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"gloo-events 0.2.0",
|
||||
"gloo-utils 0.2.0",
|
||||
"gloo-events",
|
||||
"gloo-utils",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.6.5",
|
||||
"serde-wasm-bindgen",
|
||||
"serde_urlencoded",
|
||||
"thiserror 1.0.69",
|
||||
"wasm-bindgen",
|
||||
"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]]
|
||||
name = "gloo-net"
|
||||
version = "0.5.0"
|
||||
|
|
@ -739,7 +586,7 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"gloo-utils 0.2.0",
|
||||
"gloo-utils",
|
||||
"http 0.2.12",
|
||||
"js-sys",
|
||||
"pin-project",
|
||||
|
|
@ -751,16 +598,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gloo-render"
|
||||
version = "0.2.0"
|
||||
|
|
@ -771,28 +608,13 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gloo-storage"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
|
||||
dependencies = [
|
||||
"gloo-utils 0.2.0",
|
||||
"gloo-utils",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -801,16 +623,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
|
|
@ -823,19 +635,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gloo-utils"
|
||||
version = "0.2.0"
|
||||
|
|
@ -849,42 +648,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gloo-worker"
|
||||
version = "0.5.0"
|
||||
|
|
@ -893,7 +656,7 @@ checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d"
|
|||
dependencies = [
|
||||
"bincode",
|
||||
"futures",
|
||||
"gloo-utils 0.2.0",
|
||||
"gloo-utils",
|
||||
"gloo-worker-macros",
|
||||
"js-sys",
|
||||
"pinned",
|
||||
|
|
@ -1190,9 +953,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "implicit-clone"
|
||||
version = "0.4.9"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84"
|
||||
checksum = "1689b939ee35e3a075b0834b5672efd43aec8a6e81a1c6002b76a5ca2f211ae0"
|
||||
dependencies = [
|
||||
"implicit-clone-derive",
|
||||
"indexmap",
|
||||
|
|
@ -1511,23 +1274,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
|
|
@ -1656,17 +1402,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "serde-wasm-bindgen"
|
||||
version = "0.6.5"
|
||||
|
|
@ -1936,6 +1671,23 @@ dependencies = [
|
|||
"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]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
|
|
@ -2209,10 +1961,10 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"chrono",
|
||||
"ciborium",
|
||||
"convert_case 0.8.0",
|
||||
"convert_case 0.10.0",
|
||||
"futures",
|
||||
"getrandom 0.3.4",
|
||||
"gloo 0.11.0",
|
||||
"gloo",
|
||||
"instant",
|
||||
"log",
|
||||
"once_cell",
|
||||
|
|
@ -2537,22 +2289,22 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
|||
|
||||
[[package]]
|
||||
name = "yew"
|
||||
version = "0.21.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac"
|
||||
checksum = "3346273ed61b636f5d84e6c696d40f380045b5565b36c5c47f8fc634b8bf5be6"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"futures",
|
||||
"gloo 0.10.0",
|
||||
"gloo",
|
||||
"implicit-clone",
|
||||
"indexmap",
|
||||
"js-sys",
|
||||
"prokio",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"slab",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokise",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
|
|
@ -2562,26 +2314,26 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "yew-macro"
|
||||
version = "0.21.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2"
|
||||
checksum = "479e94d645dde3749e81d488c1d32987509dd3b8c31650fcf6e3af1f370e913b"
|
||||
dependencies = [
|
||||
"boolinator",
|
||||
"once_cell",
|
||||
"prettyplease",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yew-router"
|
||||
version = "0.18.0"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca1d5052c96e6762b4d6209a8aded597758d442e6c479995faf0c7b5538e0c6"
|
||||
checksum = "415cb628900ddf1eaf55ebd04163adf1ea80d3f5a9832a876554f9c0fdd4c282"
|
||||
dependencies = [
|
||||
"gloo 0.10.0",
|
||||
"gloo",
|
||||
"js-sys",
|
||||
"route-recognizer",
|
||||
"serde",
|
||||
|
|
@ -2596,9 +2348,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "yew-router-macro"
|
||||
version = "0.18.0"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42bfd190a07ca8cfde7cd4c52b3ac463803dc07323db8c34daa697e86365978c"
|
||||
checksum = "9e87a3ce33434ab66a700edbaf2cc8a417d9b89f00a6fd8216fd6ac83b0e7b1c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub mod story;
|
|||
mod village;
|
||||
|
||||
use core::{
|
||||
cmp::Ordering,
|
||||
fmt::{Debug, Display},
|
||||
num::NonZeroU8,
|
||||
ops::{Deref, Range, RangeBounds},
|
||||
|
|
@ -379,6 +380,35 @@ pub enum GameTime {
|
|||
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 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -209,12 +209,17 @@ pub enum StoryActionPrompt {
|
|||
character_id: CharacterId,
|
||||
chosen: CharacterId,
|
||||
},
|
||||
BeholderWakes {
|
||||
character_id: CharacterId,
|
||||
},
|
||||
}
|
||||
|
||||
impl StoryActionPrompt {
|
||||
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
||||
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 {
|
||||
character_id,
|
||||
marked: Some(marked),
|
||||
|
|
@ -425,6 +430,35 @@ impl StoryActionPrompt {
|
|||
| 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)]
|
||||
|
|
|
|||
|
|
@ -13,14 +13,16 @@ web-sys = { version = "0.3", features = [
|
|||
"HtmlImageElement",
|
||||
"HtmlDivElement",
|
||||
"HtmlSelectElement",
|
||||
"HtmlDialogElement",
|
||||
"DomRect",
|
||||
] }
|
||||
wasm-bindgen = { version = "=0.2.100" }
|
||||
log = "0.4"
|
||||
rand = { version = "0.9", features = ["small_rng"] }
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
uuid = { version = "*", features = ["js"] }
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
yew-router = "0.18"
|
||||
yew = { version = "0.22", features = ["csr"] }
|
||||
yew-router = "0.19"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
gloo = "0.11"
|
||||
|
|
@ -33,7 +35,7 @@ werewolves-proto = { path = "../werewolves-proto" }
|
|||
futures = "0.3"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
thiserror = { version = "2" }
|
||||
convert_case = { version = "0.8" }
|
||||
convert_case = { version = "0.10" }
|
||||
ciborium = { version = "0.2", optional = true }
|
||||
|
||||
[features]
|
||||
|
|
|
|||
|
|
@ -208,8 +208,8 @@ nav.host-nav {
|
|||
block-size: max-content;
|
||||
|
||||
&>button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 160px;
|
||||
height: 75px;
|
||||
border: 1px solid $disconnected_color;
|
||||
background-color: color.change($disconnected_color, $alpha: 0.15);
|
||||
color: $disconnected_color;
|
||||
|
|
@ -977,17 +977,30 @@ error {
|
|||
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
// input {
|
||||
// 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;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
margin: 10px;
|
||||
font-size: 1em;
|
||||
|
||||
&:focus {
|
||||
outline: 1px solid white;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.info-update {
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
padding: 30px 0px 30px 0px;
|
||||
font-size: 2rem;
|
||||
// font-size: 2rem;
|
||||
align-content: stretch;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
|
|
@ -1799,25 +1812,52 @@ li.choice {
|
|||
}
|
||||
|
||||
.signin {
|
||||
@extend .row-list;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
& label {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&.full-height {
|
||||
height: 100vh;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.signin-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
// justify-content: center;
|
||||
// text-align: center;
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& label {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
& input {
|
||||
height: 2rem;
|
||||
text-align: center;
|
||||
height: 2em;
|
||||
// max-width: 80%;
|
||||
width: 70%;
|
||||
|
||||
#number {
|
||||
font-size: 2rem;
|
||||
max-width: 50vw;
|
||||
&#number {
|
||||
text-align: center;
|
||||
// font-size: 2rem;
|
||||
// width: 20%;
|
||||
width: 3ch;
|
||||
}
|
||||
}
|
||||
|
||||
&>button {
|
||||
margin-top: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1832,67 +1872,67 @@ li.choice {
|
|||
}
|
||||
|
||||
|
||||
.story {
|
||||
.cast {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
// .story {
|
||||
// .cast {
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// flex-wrap: wrap;
|
||||
// gap: 10px;
|
||||
// justify-content: center;
|
||||
// }
|
||||
|
||||
.time-period {
|
||||
user-select: text;
|
||||
// .time-period {
|
||||
// user-select: text;
|
||||
|
||||
.day {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
// .day {
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// flex-wrap: wrap;
|
||||
// align-items: center;
|
||||
|
||||
.executed {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
// .executed {
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// flex-wrap: wrap;
|
||||
// gap: 10px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.night {
|
||||
&>label {
|
||||
margin-left: 10vw;
|
||||
font-size: 2rem;
|
||||
font-weight: lighter;
|
||||
}
|
||||
// .night {
|
||||
// &>label {
|
||||
// margin-left: 10vw;
|
||||
// font-size: 2rem;
|
||||
// font-weight: lighter;
|
||||
// }
|
||||
|
||||
ul.changes,
|
||||
ul.choices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
// ul.changes,
|
||||
// ul.choices {
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// flex-wrap: nowrap;
|
||||
// gap: 10px;
|
||||
|
||||
&>li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
// &>li {
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// flex-wrap: wrap;
|
||||
// align-items: center;
|
||||
|
||||
gap: 10px;
|
||||
}
|
||||
// gap: 10px;
|
||||
// }
|
||||
|
||||
|
||||
& span {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// & span {
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// flex-wrap: wrap;
|
||||
// align-items: center;
|
||||
// gap: 10px;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
.attribute-span {
|
||||
display: flex;
|
||||
|
|
@ -2209,13 +2249,6 @@ li.choice {
|
|||
gap: 10px;
|
||||
}
|
||||
|
||||
.add-player {
|
||||
background-color: black;
|
||||
border: 1px solid white;
|
||||
padding: 20px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.joined {
|
||||
$joined_color: rgba(0, 255, 0, 0.7);
|
||||
$joined_border: color.change($joined_color, $alpha: 1);
|
||||
|
|
@ -2370,6 +2403,7 @@ li.choice {
|
|||
left: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
|
||||
.dialog-box {
|
||||
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 {
|
||||
|
|
@ -2648,3 +2698,110 @@ li.choice {
|
|||
flex-wrap: nowrap;
|
||||
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::{
|
||||
clients::client::connection::{Connection2, ConnectionError},
|
||||
components::{
|
||||
Button, CoverOfDarkness, Footer, Identity, Story,
|
||||
Button, CoverOfDarkness, Footer, Identity,
|
||||
client::{ClientNav, Signin},
|
||||
story::Story,
|
||||
},
|
||||
storage::StorageKey,
|
||||
};
|
||||
|
|
@ -39,6 +40,7 @@ use crate::WerewolfError;
|
|||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum ClientEvent2 {
|
||||
Signin,
|
||||
Disconnected,
|
||||
Connecting,
|
||||
ShowRole(RoleTitle),
|
||||
|
|
@ -54,7 +56,6 @@ pub enum ClientEvent2 {
|
|||
#[derive(Default, Clone, PartialEq)]
|
||||
pub struct ClientContext {
|
||||
pub error_cb: Callback<Option<WerewolfError>>,
|
||||
pub forced_identity: Option<Identification>,
|
||||
}
|
||||
|
||||
static LOST_FOCUS: AtomicI64 = AtomicI64::new(0);
|
||||
|
|
@ -73,6 +74,8 @@ pub(super) fn time_spent_unfocused() -> Option<TimeDelta> {
|
|||
|
||||
#[function_component]
|
||||
pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
||||
let ident_state = use_state(|| Option::<(PlayerId, PublicIdentity)>::None);
|
||||
|
||||
if gloo::utils::window().onfocus().is_none() {
|
||||
let on_focus = {
|
||||
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 ClientContext {
|
||||
error_cb,
|
||||
forced_identity,
|
||||
} = use_context::<ClientContext>().unwrap_or_default();
|
||||
let force = use_force_update();
|
||||
let ClientContext { error_cb } = use_context::<ClientContext>().unwrap_or_default();
|
||||
// let force = use_force_update();
|
||||
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_state.clone(), recv));
|
||||
|
||||
let ident = if let Some(Identification { player_id, public }) = forced_identity {
|
||||
(player_id, public)
|
||||
let on_signin = {
|
||||
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 {
|
||||
match PlayerId::load_from_storage()
|
||||
.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(_)) => {
|
||||
let on_signin = Callback::from(move |ident: PublicIdentity| {
|
||||
PlayerId::new().save_to_storage().expect("saving player id");
|
||||
ident.save_to_storage().expect("saving ident");
|
||||
force.force_update();
|
||||
});
|
||||
client_state.set(ClientEvent2::Signin);
|
||||
return html! {
|
||||
<Signin callback={on_signin}/>
|
||||
// <Signin callback={on_signin}/>
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("storage error: {err}");
|
||||
error_cb.emit(Some(err.into()));
|
||||
PlayerId::delete();
|
||||
PublicIdentity::delete();
|
||||
force.force_update();
|
||||
return html! {};
|
||||
// force.force_update();
|
||||
|
||||
// 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 {
|
||||
ClientEvent2::Signin => html! {
|
||||
<Signin callback={on_signin} />
|
||||
},
|
||||
ClientEvent2::GameInProgress => html! {
|
||||
<CoverOfDarkness message={"game in progress".to_string()}/>
|
||||
},
|
||||
|
|
@ -245,7 +270,7 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> 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)]
|
||||
pub struct Connection2 {
|
||||
state: UseStateSetter<ClientEvent2>,
|
||||
ident: UseStateHandle<(PlayerId, PublicIdentity)>,
|
||||
ident: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||
receiver: Rc<RefCell<UnboundedReceiver<ClientMessage>>>,
|
||||
active: Rc<RefCell<()>>,
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ pub struct Connection2 {
|
|||
impl Connection2 {
|
||||
pub fn new(
|
||||
state: UseStateSetter<ClientEvent2>,
|
||||
ident: UseStateHandle<(PlayerId, PublicIdentity)>,
|
||||
ident: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||
receiver: Rc<RefCell<UnboundedReceiver<ClientMessage>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -64,9 +64,19 @@ impl Connection2 {
|
|||
}
|
||||
}
|
||||
fn identification(&self) -> Identification {
|
||||
Identification {
|
||||
player_id: self.ident.0,
|
||||
public: self.ident.1.clone(),
|
||||
match self.ident.as_ref() {
|
||||
Some(ident) => Identification {
|
||||
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 {
|
||||
|
|
@ -108,7 +118,10 @@ impl Connection2 {
|
|||
yew::platform::spawn_local(async move {
|
||||
let active = conn.active.clone();
|
||||
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;
|
||||
core::mem::drop(active_borrow);
|
||||
});
|
||||
|
|
@ -274,9 +287,11 @@ impl Connection2 {
|
|||
return None;
|
||||
}
|
||||
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);
|
||||
self.ident.set((pid, ident));
|
||||
self.ident.set(Some((pid, ident)));
|
||||
return None;
|
||||
}
|
||||
ServerMessage::GameInProgress => ClientEvent2::GameInProgress,
|
||||
|
|
|
|||
|
|
@ -42,10 +42,11 @@ use yew::{html::Scope, prelude::*};
|
|||
use crate::{
|
||||
callback,
|
||||
components::{
|
||||
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Story, Victory,
|
||||
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Victory,
|
||||
action::{ActionResultView, Prompt},
|
||||
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup},
|
||||
settings::Settings,
|
||||
story::Story,
|
||||
},
|
||||
pages::RolePage,
|
||||
storage::StorageKey,
|
||||
|
|
|
|||
|
|
@ -26,3 +26,11 @@ const BASE_URL: &str = match option_env!("BASE_URL") {
|
|||
Some(base_url) => base_url,
|
||||
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)]
|
||||
pub struct ClientNavProps {
|
||||
pub identity: UseStateHandle<(PlayerId, PublicIdentity)>,
|
||||
pub identity: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||
pub message_callback: Callback<ClientMessage>,
|
||||
}
|
||||
|
||||
|
|
@ -38,18 +38,19 @@ pub fn ClientNav(
|
|||
message_callback,
|
||||
}: &ClientNavProps,
|
||||
) -> Html {
|
||||
const MUST_HAVE_IDENTITY: &str = "client nav must have identity";
|
||||
let pronouns = identity
|
||||
.1
|
||||
.pronouns
|
||||
.as_ref()
|
||||
.map(|pronouns| {
|
||||
.and_then(|identity| {
|
||||
identity.1.pronouns.as_ref().map(|pronouns| {
|
||||
html! {
|
||||
<div>{"("}{pronouns.as_str()}{")"}</div>
|
||||
<span>{"("}{pronouns.as_str()}{")"}</span>
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
html! {
|
||||
<div>{"(None)"}</div>
|
||||
<span class="faint">{"(None)"}</span>
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -62,10 +63,13 @@ pub fn ClientNav(
|
|||
|
||||
let submit_ident = identity.clone();
|
||||
let current_num = identity
|
||||
.1
|
||||
.number
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| String::from("???"));
|
||||
.as_ref()
|
||||
.and_then(|identity| identity.1.number.map(|v| html! {{v.to_string()}}))
|
||||
.unwrap_or_else(|| {
|
||||
html! {
|
||||
<span class="red">{"???"}</span>
|
||||
}
|
||||
});
|
||||
let open_set = number_open.setter();
|
||||
let on_submit = {
|
||||
let val = current_value.clone();
|
||||
|
|
@ -74,13 +78,16 @@ pub fn ClientNav(
|
|||
Some(num) => num,
|
||||
None => return,
|
||||
};
|
||||
let Some(submit_ident_ref) = submit_ident.as_ref() else {
|
||||
return;
|
||||
};
|
||||
message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Number(num)));
|
||||
let new_ident = PublicIdentity {
|
||||
name: submit_ident.1.name.clone(),
|
||||
pronouns: submit_ident.1.pronouns.clone(),
|
||||
name: submit_ident_ref.1.name.clone(),
|
||||
pronouns: submit_ident_ref.1.pronouns.clone(),
|
||||
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() {
|
||||
log::error!("saving public identity after change: {err}");
|
||||
}
|
||||
|
|
@ -114,6 +121,7 @@ pub fn ClientNav(
|
|||
let ident = identity.clone();
|
||||
let message_callback = message_callback.clone();
|
||||
Callback::from(move |value: String| -> Option<PublicIdentity> {
|
||||
let ident = ident.as_ref().expect(MUST_HAVE_IDENTITY);
|
||||
value.trim().is_empty().not().then(|| {
|
||||
let name = value.trim().to_string();
|
||||
message_callback
|
||||
|
|
@ -134,6 +142,12 @@ pub fn ClientNav(
|
|||
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! {
|
||||
<ClickableTextEdit
|
||||
value={name.clone()}
|
||||
|
|
@ -144,7 +158,7 @@ pub fn ClientNav(
|
|||
on_open={close_others}
|
||||
label={String::from("name")}
|
||||
>
|
||||
<div class="name">{identity.1.name.as_str()}</div>
|
||||
<div class="name">{name_str}</div>
|
||||
</ClickableTextEdit>
|
||||
}
|
||||
};
|
||||
|
|
@ -154,15 +168,18 @@ pub fn ClientNav(
|
|||
let on_submit = {
|
||||
let ident = identity.clone();
|
||||
let message_callback = message_callback.clone();
|
||||
let pronuns_state = pronuns_state.clone();
|
||||
Callback::from(move |value: String| -> Option<PublicIdentity> {
|
||||
let pronouns = value.trim().is_empty().not().then_some(value);
|
||||
message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Pronouns(
|
||||
pronouns.clone(),
|
||||
)));
|
||||
Some(PublicIdentity {
|
||||
pronouns,
|
||||
name: ident.1.name.clone(),
|
||||
number: ident.1.number,
|
||||
pronuns_state.set(String::new());
|
||||
ident.as_ref().map(|id| {
|
||||
let mut public = id.1.clone();
|
||||
public.pronouns = pronouns;
|
||||
ident.set(Some((id.0, public.clone())));
|
||||
public
|
||||
})
|
||||
})
|
||||
};
|
||||
|
|
@ -196,14 +213,10 @@ pub fn ClientNav(
|
|||
cb.emit(ClientMessage::Goodbye);
|
||||
let _ = gloo::utils::window().location().reload();
|
||||
};
|
||||
let host_click = Callback::from(|_| {
|
||||
if let Some(loc) = gloo::utils::document().location() {
|
||||
let _ = loc.replace("/host");
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
</>
|
||||
}
|
||||
|
|
@ -223,7 +236,7 @@ struct ClickableTextEditProps {
|
|||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
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 field_name: &'static str,
|
||||
pub state: UseStateHandle<bool>,
|
||||
|
|
@ -257,8 +270,10 @@ fn ClickableTextEdit(
|
|||
let submit = {
|
||||
let submit_ident = submit_ident.clone();
|
||||
move |_| {
|
||||
if let Some(new_ident) = message_callback.emit(value.trim().to_string()) {
|
||||
submit_ident.set((submit_ident.0, new_ident.clone()));
|
||||
if let Some(new_ident) = message_callback.emit(value.trim().to_string())
|
||||
&& 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() {
|
||||
log::error!("saving public identity after change: {err}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ use crate::components::Button;
|
|||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct SigninProps {
|
||||
pub callback: Callback<PublicIdentity>,
|
||||
#[prop_or(true)]
|
||||
pub full_height: bool,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -60,15 +62,34 @@ pub fn Signin(props: &SigninProps) -> Html {
|
|||
});
|
||||
|
||||
let on_change = crate::components::input_element_number_oninput(num_value);
|
||||
let full_height = props.full_height.then_some("full-height");
|
||||
html! {
|
||||
<div class="signin full-height">
|
||||
<div class="column-list">
|
||||
<div class={classes!("signin", full_height)}>
|
||||
<div class="signin-box">
|
||||
<div class="field">
|
||||
<label for="number">{"Seat Number"}</label>
|
||||
<input
|
||||
oninput={on_change}
|
||||
type="text"
|
||||
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"/>
|
||||
<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"/>
|
||||
<label for="number">{"Number"}</label>
|
||||
<input oninput={on_change} type="text" name="number" id="number"/>
|
||||
</div>
|
||||
<Button on_click={on_click}>{"Submit"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -109,18 +109,25 @@ pub fn ClickableNumberEdit(
|
|||
label,
|
||||
}: &ClickableNumberEditProps,
|
||||
) -> 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 label = label.is_empty().not().then_some(html! {
|
||||
<label>{label}</label>
|
||||
});
|
||||
// let label = label.is_empty().not().then_some(html! {
|
||||
// <label>{label}</label>
|
||||
// });
|
||||
|
||||
let options = 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>
|
||||
// <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>
|
||||
<NumberEdit
|
||||
value={value.clone()}
|
||||
on_submit={on_submit.clone()}
|
||||
field_name={field_name}
|
||||
label={label.clone()}
|
||||
>
|
||||
</NumberEdit>
|
||||
};
|
||||
html! {
|
||||
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
||||
|
|
@ -128,3 +135,36 @@ pub fn ClickableNumberEdit(
|
|||
</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 yew::prelude::*;
|
||||
|
||||
use crate::components::{Button, ClickableField, ClickableNumberEdit, Identity};
|
||||
use crate::components::{
|
||||
Button, ClickableField, ClickableNumberEdit, Identity, NumberEdit,
|
||||
modal::{Dialog, SubmenuDialog},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct LobbyPlayerProps {
|
||||
|
|
@ -35,7 +38,11 @@ pub enum LobbyPlayerAction {
|
|||
#[function_component]
|
||||
pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) -> Html {
|
||||
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 action_open = open.clone();
|
||||
let action = |action: LobbyPlayerAction| {
|
||||
|
|
@ -65,9 +72,22 @@ pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) ->
|
|||
on_action.emit((pid, LobbyPlayerAction::SetNumber(number)));
|
||||
open.set(false);
|
||||
});
|
||||
const NUMBER_DIALOG_ID: &str = "player-set-number-dialog";
|
||||
|
||||
html! {
|
||||
<>
|
||||
<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
|
||||
state={number_open}
|
||||
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! {
|
||||
// <SubmenuDialog
|
||||
// object={object}
|
||||
// >
|
||||
// {submenu}
|
||||
// </SubmenuDialog>
|
||||
<ClickableField
|
||||
state={open}
|
||||
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::{
|
||||
Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon,
|
||||
client::Signin,
|
||||
modal::Dialog,
|
||||
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 min_roles = settings.min_players_needed();
|
||||
let min_roles_class = (current_roles < min_roles).then_some("red");
|
||||
let player_count = players_in_lobby.len();
|
||||
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! {
|
||||
<div class="settings">
|
||||
|
|
@ -295,12 +298,16 @@ pub fn Settings(
|
|||
{clear_all_assignments}
|
||||
{clear_bad_assigned}
|
||||
</div>
|
||||
<ClickableField
|
||||
options={add_player_opts}
|
||||
state={add_player_open}
|
||||
<Dialog
|
||||
id={ADD_PLAYER_DIALOG_ID.to_string()}
|
||||
button={html!{"add player"}}
|
||||
close_button=false
|
||||
>
|
||||
{"add player"}
|
||||
</ClickableField>
|
||||
<div>
|
||||
<h3>{"manually add a player"}</h3>
|
||||
<Signin callback={add_player_dialog_cb} full_height=false/>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<div class="roles-add-list">
|
||||
{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 components {
|
||||
werewolves_macros::include_path!("werewolves/src/components");
|
||||
pub mod modal {
|
||||
werewolves_macros::include_path!("werewolves/src/components/modal");
|
||||
}
|
||||
pub mod attributes {
|
||||
werewolves_macros::include_path!("werewolves/src/components/attributes");
|
||||
}
|
||||
|
|
@ -34,6 +37,9 @@ mod components {
|
|||
pub mod settings {
|
||||
werewolves_macros::include_path!("werewolves/src/components/settings");
|
||||
}
|
||||
pub mod story {
|
||||
werewolves_macros::include_path!("werewolves/src/components/story");
|
||||
}
|
||||
}
|
||||
mod pages {
|
||||
werewolves_macros::include_path!("werewolves/src/pages");
|
||||
|
|
@ -81,13 +87,14 @@ fn main() {
|
|||
} else {
|
||||
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 {
|
||||
yew::Renderer::<ContextProvider<ClientContext>>::with_root_and_props(
|
||||
app_element,
|
||||
ContextProviderProps {
|
||||
context: ClientContext {
|
||||
error_cb: error_callback.clone(),
|
||||
forced_identity: None,
|
||||
},
|
||||
children: html! {
|
||||
<Client2 auto_join=false/>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ fn FalselyAppearsAs(
|
|||
<div class="bottom-bound">
|
||||
<h5>
|
||||
{"ROLES THAT FALSELY APPEAR AS "}
|
||||
<span class="yellow">{alignment_text}</span>
|
||||
<span class="yellow">{*alignment_text}</span>
|
||||
</h5>
|
||||
<div class="false-positives yellow">
|
||||
{false_positives}
|
||||
|
|
|
|||
Loading…
Reference in New Issue