Compare commits
No commits in common. "f3f4c43e81b821cb8eb074ccced9377838e24a98" and "01c61c143e3b90483b8e6a07c84d8c035917f10d" have entirely different histories.
f3f4c43e81
...
01c61c143e
|
|
@ -26,6 +26,12 @@ 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"
|
||||
|
|
@ -154,6 +160,12 @@ 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"
|
||||
|
|
@ -243,18 +255,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.9.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
|
@ -495,23 +507,74 @@ 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",
|
||||
"gloo-dialogs",
|
||||
"gloo-events",
|
||||
"gloo-file",
|
||||
"gloo-history",
|
||||
"gloo-net",
|
||||
"gloo-render",
|
||||
"gloo-storage",
|
||||
"gloo-timers",
|
||||
"gloo-utils",
|
||||
"gloo-worker",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -520,13 +583,23 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261"
|
||||
dependencies = [
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.2.0",
|
||||
"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"
|
||||
|
|
@ -537,6 +610,16 @@ 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"
|
||||
|
|
@ -547,6 +630,18 @@ 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"
|
||||
|
|
@ -554,12 +649,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"gloo-events",
|
||||
"gloo-events 0.2.0",
|
||||
"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"
|
||||
|
|
@ -567,16 +678,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"gloo-events",
|
||||
"gloo-utils",
|
||||
"gloo-events 0.2.0",
|
||||
"gloo-utils 0.2.0",
|
||||
"serde",
|
||||
"serde-wasm-bindgen",
|
||||
"serde-wasm-bindgen 0.6.5",
|
||||
"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"
|
||||
|
|
@ -586,7 +739,7 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.2.0",
|
||||
"http 0.2.12",
|
||||
"js-sys",
|
||||
"pin-project",
|
||||
|
|
@ -598,6 +751,16 @@ 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"
|
||||
|
|
@ -610,11 +773,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gloo-storage"
|
||||
version = "0.3.0"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
|
||||
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
|
||||
dependencies = [
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.1.7",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -623,6 +786,31 @@ dependencies = [
|
|||
"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",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"wasm-bindgen",
|
||||
"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"
|
||||
|
|
@ -635,6 +823,19 @@ 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"
|
||||
|
|
@ -648,6 +849,42 @@ 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"
|
||||
|
|
@ -656,7 +893,7 @@ checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d"
|
|||
dependencies = [
|
||||
"bincode",
|
||||
"futures",
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.2.0",
|
||||
"gloo-worker-macros",
|
||||
"js-sys",
|
||||
"pinned",
|
||||
|
|
@ -953,9 +1190,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "implicit-clone"
|
||||
version = "0.6.0"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1689b939ee35e3a075b0834b5672efd43aec8a6e81a1c6002b76a5ca2f211ae0"
|
||||
checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84"
|
||||
dependencies = [
|
||||
"implicit-clone-derive",
|
||||
"indexmap",
|
||||
|
|
@ -1274,6 +1511,23 @@ 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"
|
||||
|
|
@ -1402,6 +1656,17 @@ 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"
|
||||
|
|
@ -1671,23 +1936,6 @@ 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"
|
||||
|
|
@ -1961,10 +2209,10 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"chrono",
|
||||
"ciborium",
|
||||
"convert_case 0.10.0",
|
||||
"convert_case 0.8.0",
|
||||
"futures",
|
||||
"getrandom 0.3.4",
|
||||
"gloo",
|
||||
"gloo 0.11.0",
|
||||
"instant",
|
||||
"log",
|
||||
"once_cell",
|
||||
|
|
@ -2289,22 +2537,22 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
|||
|
||||
[[package]]
|
||||
name = "yew"
|
||||
version = "0.22.0"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3346273ed61b636f5d84e6c696d40f380045b5565b36c5c47f8fc634b8bf5be6"
|
||||
checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"futures",
|
||||
"gloo",
|
||||
"gloo 0.10.0",
|
||||
"implicit-clone",
|
||||
"indexmap",
|
||||
"js-sys",
|
||||
"prokio",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"slab",
|
||||
"thiserror 2.0.17",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokise",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
|
|
@ -2314,26 +2562,26 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "yew-macro"
|
||||
version = "0.22.0"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479e94d645dde3749e81d488c1d32987509dd3b8c31650fcf6e3af1f370e913b"
|
||||
checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2"
|
||||
dependencies = [
|
||||
"boolinator",
|
||||
"once_cell",
|
||||
"prettyplease",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yew-router"
|
||||
version = "0.19.0"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "415cb628900ddf1eaf55ebd04163adf1ea80d3f5a9832a876554f9c0fdd4c282"
|
||||
checksum = "4ca1d5052c96e6762b4d6209a8aded597758d442e6c479995faf0c7b5538e0c6"
|
||||
dependencies = [
|
||||
"gloo",
|
||||
"gloo 0.10.0",
|
||||
"js-sys",
|
||||
"route-recognizer",
|
||||
"serde",
|
||||
|
|
@ -2348,9 +2596,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "yew-router-macro"
|
||||
version = "0.19.0"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e87a3ce33434ab66a700edbaf2cc8a417d9b89f00a6fd8216fd6ac83b0e7b1c"
|
||||
checksum = "42bfd190a07ca8cfde7cd4c52b3ac463803dc07323db8c34daa697e86365978c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ use crate::{
|
|||
Alignment, HunterMut, HunterRef, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT,
|
||||
Powerful, PreviousGuardianAction, Role, RoleTitle,
|
||||
},
|
||||
team::Team,
|
||||
};
|
||||
|
||||
type Result<T> = core::result::Result<T, GameError>;
|
||||
|
|
@ -243,16 +242,6 @@ impl Character {
|
|||
!self.is_wolf()
|
||||
}
|
||||
|
||||
pub fn team(&self) -> Team {
|
||||
if let Alignment::Traitor = self.alignment() {
|
||||
return Team::AnyEvil;
|
||||
}
|
||||
if self.is_wolf() {
|
||||
return Team::Wolves;
|
||||
}
|
||||
Team::Village
|
||||
}
|
||||
|
||||
pub const fn known_elder(&self) -> bool {
|
||||
matches!(
|
||||
self.role,
|
||||
|
|
@ -611,15 +600,8 @@ impl Character {
|
|||
})
|
||||
}
|
||||
Role::MasonLeader { .. } => {
|
||||
log::debug!(
|
||||
"night_action_prompts got to MasonLeader;
|
||||
mason leader alive: {}; night: {night}; current prompt titles: {}",
|
||||
self.alive(),
|
||||
prompts
|
||||
.iter()
|
||||
.map(|p| p.title().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
log::error!(
|
||||
"night_action_prompts got to MasonLeader, should be handled before the living check"
|
||||
);
|
||||
}
|
||||
Role::Empath { cursed: false } => prompts.push(ActionPrompt::Empath {
|
||||
|
|
|
|||
|
|
@ -78,30 +78,6 @@ pub enum DiedTo {
|
|||
}
|
||||
|
||||
impl DiedTo {
|
||||
pub const fn day(&self) -> Option<NonZeroU8> {
|
||||
match self {
|
||||
DiedTo::Execution { day } => Some(*day),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub const fn night(&self) -> Option<u8> {
|
||||
Some(match self {
|
||||
DiedTo::Execution { .. } => return None,
|
||||
|
||||
DiedTo::MapleWolf { night, .. }
|
||||
| DiedTo::MapleWolfStarved { night }
|
||||
| DiedTo::Militia { night, .. }
|
||||
| DiedTo::Wolfpack { night, .. }
|
||||
| DiedTo::AlphaWolf { night, .. }
|
||||
| DiedTo::Shapeshift { night, .. }
|
||||
| DiedTo::Hunter { night, .. }
|
||||
| DiedTo::GuardianProtecting { night, .. }
|
||||
| DiedTo::PyreMasterLynchMob { night, .. }
|
||||
| DiedTo::PyreMaster { night, .. } => night.get(),
|
||||
|
||||
DiedTo::LoneWolf { night, .. } | DiedTo::MasonLeaderRecruitFail { night, .. } => *night,
|
||||
})
|
||||
}
|
||||
pub fn next_night(&self) -> Option<DiedTo> {
|
||||
let mut next = self.clone();
|
||||
match &mut next {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ pub mod story;
|
|||
mod village;
|
||||
|
||||
use core::{
|
||||
cmp::Ordering,
|
||||
fmt::{Debug, Display},
|
||||
num::NonZeroU8,
|
||||
ops::{Deref, Range, RangeBounds},
|
||||
|
|
@ -192,7 +191,6 @@ impl Game {
|
|||
GameActions::NightDetails(NightDetails::new(
|
||||
&night.used_actions(),
|
||||
recorded_changes,
|
||||
self.village(),
|
||||
)),
|
||||
)?;
|
||||
self.state = GameState::Day {
|
||||
|
|
@ -381,35 +379,6 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -45,17 +45,13 @@ pub struct NightDetails {
|
|||
}
|
||||
|
||||
impl NightDetails {
|
||||
pub fn new(
|
||||
choices: &[(ActionPrompt, ActionResult)],
|
||||
changes: Box<[NightChange]>,
|
||||
village: &Village,
|
||||
) -> Self {
|
||||
pub fn new(choices: &[(ActionPrompt, ActionResult)], changes: Box<[NightChange]>) -> Self {
|
||||
Self {
|
||||
changes,
|
||||
choices: choices
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(|(prompt, result)| NightChoice::new(prompt, result, village))
|
||||
.filter_map(|(prompt, result)| NightChoice::new(prompt, result))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
|
@ -68,9 +64,9 @@ pub struct NightChoice {
|
|||
}
|
||||
|
||||
impl NightChoice {
|
||||
pub fn new(prompt: ActionPrompt, result: ActionResult, village: &Village) -> Option<Self> {
|
||||
pub fn new(prompt: ActionPrompt, result: ActionResult) -> Option<Self> {
|
||||
Some(Self {
|
||||
prompt: StoryActionPrompt::new(prompt, village)?,
|
||||
prompt: StoryActionPrompt::new(prompt)?,
|
||||
result: StoryActionResult::new(result),
|
||||
})
|
||||
}
|
||||
|
|
@ -189,7 +185,6 @@ pub enum StoryActionPrompt {
|
|||
chosen: CharacterId,
|
||||
},
|
||||
WolfPackKill {
|
||||
killing_wolf: CharacterId,
|
||||
chosen: CharacterId,
|
||||
},
|
||||
Shapeshifter {
|
||||
|
|
@ -214,17 +209,12 @@ pub enum StoryActionPrompt {
|
|||
character_id: CharacterId,
|
||||
chosen: CharacterId,
|
||||
},
|
||||
BeholderWakes {
|
||||
character_id: CharacterId,
|
||||
},
|
||||
}
|
||||
|
||||
impl StoryActionPrompt {
|
||||
pub fn new(prompt: ActionPrompt, village: &Village) -> Option<Self> {
|
||||
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
||||
Some(match prompt {
|
||||
ActionPrompt::BeholderWakes { character_id } => Self::BeholderWakes {
|
||||
character_id: character_id.character_id,
|
||||
},
|
||||
ActionPrompt::BeholderWakes { .. } => return None, // TODO: rework story anyway
|
||||
ActionPrompt::Bloodletter {
|
||||
character_id,
|
||||
marked: Some(marked),
|
||||
|
|
@ -375,10 +365,7 @@ impl StoryActionPrompt {
|
|||
ActionPrompt::WolfPackKill {
|
||||
marked: Some(marked),
|
||||
..
|
||||
} => Self::WolfPackKill {
|
||||
chosen: marked,
|
||||
killing_wolf: village.killing_wolf().map(|c| c.character_id())?,
|
||||
},
|
||||
} => Self::WolfPackKill { chosen: marked },
|
||||
ActionPrompt::Shapeshifter { character_id } => Self::Shapeshifter {
|
||||
character_id: character_id.character_id,
|
||||
},
|
||||
|
|
@ -438,39 +425,6 @@ impl StoryActionPrompt {
|
|||
| ActionPrompt::CoverOfDarkness => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn character_id(&self) -> Option<CharacterId> {
|
||||
match self {
|
||||
StoryActionPrompt::MasonsWake { .. } => None,
|
||||
StoryActionPrompt::WolfPackKill {
|
||||
killing_wolf: character_id,
|
||||
..
|
||||
}
|
||||
| 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,16 +13,14 @@ 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.22", features = ["csr"] }
|
||||
yew-router = "0.19"
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
yew-router = "0.18"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
gloo = "0.11"
|
||||
|
|
@ -35,7 +33,7 @@ werewolves-proto = { path = "../werewolves-proto" }
|
|||
futures = "0.3"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
thiserror = { version = "2" }
|
||||
convert_case = { version = "0.10" }
|
||||
convert_case = { version = "0.8" }
|
||||
ciborium = { version = "0.2", optional = true }
|
||||
|
||||
[features]
|
||||
|
|
|
|||
|
|
@ -208,8 +208,8 @@ nav.host-nav {
|
|||
block-size: max-content;
|
||||
|
||||
&>button {
|
||||
width: 160px;
|
||||
height: 75px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid $disconnected_color;
|
||||
background-color: color.change($disconnected_color, $alpha: 0.15);
|
||||
color: $disconnected_color;
|
||||
|
|
@ -784,34 +784,6 @@ clients {
|
|||
display: flex;
|
||||
flex-basis: content;
|
||||
}
|
||||
|
||||
.story {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width : 799px) {
|
||||
.story-characters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 3px;
|
||||
row-gap: 5px;
|
||||
justify-content: space-between;
|
||||
overflow-x: scroll;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width : 800px) {
|
||||
.story-characters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px;
|
||||
row-gap: 5px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width : 1900px) {
|
||||
|
|
@ -1005,30 +977,17 @@ error {
|
|||
|
||||
}
|
||||
|
||||
// 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);
|
||||
input {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
|
||||
&:focus {
|
||||
outline: 1px solid white;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
|
@ -1349,7 +1308,7 @@ select {
|
|||
background-color: $village_color;
|
||||
border: 1px solid $village_border;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: $village_border;
|
||||
}
|
||||
|
|
@ -1358,7 +1317,7 @@ select {
|
|||
border: 1px solid $village_border_faint;
|
||||
background-color: $village_color_faint;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
background-color: $village_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1373,7 +1332,7 @@ select {
|
|||
background-color: $wolves_color;
|
||||
border: 1px solid $wolves_border;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: $wolves_border;
|
||||
}
|
||||
|
|
@ -1382,7 +1341,7 @@ select {
|
|||
border: 1px solid $wolves_border_faint;
|
||||
background-color: $wolves_color_faint;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
background-color: $wolves_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1397,7 +1356,7 @@ select {
|
|||
background-color: $intel_color;
|
||||
border: 1px solid $intel_border;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: $intel_border;
|
||||
}
|
||||
|
|
@ -1406,7 +1365,7 @@ select {
|
|||
border: 1px solid $intel_border_faint;
|
||||
background-color: $intel_color_faint;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
background-color: $intel_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1421,7 +1380,7 @@ select {
|
|||
background-color: $defensive_color;
|
||||
border: 1px solid $defensive_border;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: $defensive_border;
|
||||
}
|
||||
|
|
@ -1430,7 +1389,7 @@ select {
|
|||
border: 1px solid $defensive_border_faint;
|
||||
background-color: $defensive_color_faint;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
background-color: $defensive_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1445,7 +1404,7 @@ select {
|
|||
background-color: $offensive_color;
|
||||
border: 1px solid $offensive_border;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: $offensive_border;
|
||||
}
|
||||
|
|
@ -1454,7 +1413,7 @@ select {
|
|||
border: 1px solid $offensive_border_faint;
|
||||
background-color: $offensive_color_faint;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
background-color: $offensive_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1469,7 +1428,7 @@ select {
|
|||
background-color: $starts_as_villager_color;
|
||||
border: 1px solid $starts_as_villager_border;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: $starts_as_villager_border;
|
||||
}
|
||||
|
|
@ -1478,7 +1437,7 @@ select {
|
|||
border: 1px solid $starts_as_villager_border_faint;
|
||||
background-color: $starts_as_villager_color_faint;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
background-color: $starts_as_villager_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1493,7 +1452,7 @@ select {
|
|||
background-color: $traitor_color;
|
||||
border: 1px solid $traitor_border;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: $traitor_border;
|
||||
}
|
||||
|
|
@ -1502,7 +1461,7 @@ select {
|
|||
border: 1px solid $traitor_border_faint;
|
||||
background-color: $traitor_color_faint;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
background-color: $traitor_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1512,7 +1471,7 @@ select {
|
|||
background-color: $drunk_color;
|
||||
border: 1px solid $drunk_border;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: $drunk_border;
|
||||
}
|
||||
|
|
@ -1521,7 +1480,7 @@ select {
|
|||
border: 1px solid $drunk_border_faint;
|
||||
background-color: $drunk_color_faint;
|
||||
|
||||
&.hover:hover {
|
||||
&:hover {
|
||||
background-color: $drunk_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1571,10 +1530,6 @@ select {
|
|||
}
|
||||
|
||||
.setup-screen {
|
||||
.inactive {
|
||||
filter: brightness(0%);
|
||||
}
|
||||
|
||||
margin-top: 2%;
|
||||
font-size: 1.5vw;
|
||||
|
||||
|
|
@ -1725,6 +1680,11 @@ li.choice {
|
|||
}
|
||||
}
|
||||
|
||||
.inactive {
|
||||
// filter: grayscale(100%) brightness(30%);
|
||||
filter: brightness(0%);
|
||||
}
|
||||
|
||||
.qrcode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -1839,52 +1799,25 @@ li.choice {
|
|||
}
|
||||
|
||||
.signin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
@extend .row-list;
|
||||
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;
|
||||
& input {
|
||||
height: 2rem;
|
||||
text-align: center;
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& label {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
& input {
|
||||
height: 2em;
|
||||
// max-width: 80%;
|
||||
width: 70%;
|
||||
|
||||
&#number {
|
||||
text-align: center;
|
||||
// font-size: 2rem;
|
||||
// width: 20%;
|
||||
width: 3ch;
|
||||
}
|
||||
}
|
||||
|
||||
&>button {
|
||||
margin-top: 7px;
|
||||
#number {
|
||||
font-size: 2rem;
|
||||
max-width: 50vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1899,132 +1832,116 @@ 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;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
align-items: baseline;
|
||||
align-content: baseline;
|
||||
justify-content: baseline;
|
||||
justify-items: baseline;
|
||||
gap: 1ch;
|
||||
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 5px;
|
||||
padding-right: 10px;
|
||||
|
||||
.inactive {
|
||||
filter: grayscale(100%);
|
||||
border: none;
|
||||
&:has(.killer) {
|
||||
border: 1px solid rgba(212, 85, 0, 0.5);
|
||||
}
|
||||
|
||||
// img {
|
||||
// vertical-align: sub;
|
||||
// }
|
||||
&:has(.powerful) {
|
||||
border: 1px solid rgba(0, 173, 193, 0.5);
|
||||
}
|
||||
|
||||
&:has(.inactive) {
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
&.execution {
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.alignment-eq,
|
||||
.roleblock-span {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1ch;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.highlight-span {
|
||||
height: max-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
word-wrap: normal;
|
||||
|
||||
.name {
|
||||
color: white;
|
||||
.alignment-eq {
|
||||
img {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.dead {
|
||||
text-decoration: line-through;
|
||||
font-style: italic;
|
||||
}
|
||||
border: 1px solid $intel_border;
|
||||
background-color: color.change($intel_color, $alpha: 0.1);
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
|
||||
&:hover::after {
|
||||
content: attr(role);
|
||||
overflow-y: hidden;
|
||||
position: absolute;
|
||||
margin-top: 60px;
|
||||
color: white;
|
||||
background-color: black;
|
||||
border: 1px solid white;
|
||||
padding: 3px;
|
||||
z-index: 4;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.character-span {
|
||||
|
|
@ -2033,12 +1950,7 @@ li.choice {
|
|||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 1ch;
|
||||
|
||||
.dead {
|
||||
text-decoration: line-through;
|
||||
font-style: italic;
|
||||
}
|
||||
gap: 5px;
|
||||
|
||||
.number {
|
||||
color: rgba(255, 255, 0, 0.7);
|
||||
|
|
@ -2297,6 +2209,13 @@ 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);
|
||||
|
|
@ -2451,7 +2370,6 @@ li.choice {
|
|||
left: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
|
||||
.dialog-box {
|
||||
border: 1px solid white;
|
||||
|
|
@ -2477,22 +2395,6 @@ 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 {
|
||||
|
|
@ -2746,221 +2648,3 @@ dialog::backdrop {
|
|||
flex-wrap: nowrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
// gap: 3px;
|
||||
margin: 0;
|
||||
align-self: flex-start;
|
||||
width: 100%;
|
||||
|
||||
.tab-button:not(.selected) {
|
||||
flex-grow: 1;
|
||||
// color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
color: white;
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
text-shadow: 2px 2px black;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.story {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
// width: 100vw;
|
||||
justify-content: space-evenly;
|
||||
row-gap: 5px;
|
||||
margin: 5vh 5vw 0px 5vw;
|
||||
|
||||
.character-headline {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.icon-spacer {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
padding: 0.2em 1em 0.2em 1em;
|
||||
min-width: 5cm;
|
||||
|
||||
.identity {
|
||||
text-align: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
// min-height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.no-content {
|
||||
filter: grayscale(60%);
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
cursor: default;
|
||||
|
||||
// backdrop-filter: brightness(20%);
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.sub-headline {
|
||||
display: none;
|
||||
// margin-left: 0.5cm;
|
||||
// font-size: 1.5em;
|
||||
// font-style: italic;
|
||||
}
|
||||
|
||||
.action,
|
||||
.change {
|
||||
font-size: 1.25em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1ch;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.character-details {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
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: 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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wolves-highlight {
|
||||
color: $wolves_color;
|
||||
|
||||
.number {
|
||||
color: $wolves_border;
|
||||
}
|
||||
}
|
||||
|
||||
.village-highlight {
|
||||
color: $village_color;
|
||||
|
||||
.number {
|
||||
color: $village_border;
|
||||
}
|
||||
}
|
||||
|
||||
.intel-highlight {
|
||||
color: $intel_color;
|
||||
|
||||
.number {
|
||||
color: $intel_border;
|
||||
}
|
||||
}
|
||||
|
||||
.defensive-highlight {
|
||||
color: $defensive_color;
|
||||
|
||||
.number {
|
||||
color: $defensive_border;
|
||||
}
|
||||
}
|
||||
|
||||
.offensive-highlight {
|
||||
color: $offensive_color;
|
||||
|
||||
.number {
|
||||
color: $offensive_border;
|
||||
}
|
||||
}
|
||||
|
||||
.starts-as-villager-highlight {
|
||||
color: $starts_as_villager_color;
|
||||
|
||||
.number {
|
||||
color: $starts_as_villager_border;
|
||||
}
|
||||
}
|
||||
|
||||
.traitor-highlight {
|
||||
color: $traitor_color;
|
||||
|
||||
.number {
|
||||
color: $traitor_color;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2026 Emilis Bliūdžius
|
||||
// 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
|
||||
|
|
@ -12,22 +12,6 @@
|
|||
//
|
||||
// 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 werewolves_proto::{character::Character, game::SetupRole, team::Team};
|
||||
|
||||
pub trait Class {
|
||||
fn class(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
impl Class for Character {
|
||||
fn class(&self) -> Option<&'static str> {
|
||||
if let Team::AnyEvil = self.team() {
|
||||
return Some("traitor");
|
||||
}
|
||||
Some(
|
||||
Into::<SetupRole>::into(self.role_title())
|
||||
.category()
|
||||
.class(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,8 @@ use yew::prelude::*;
|
|||
use crate::{
|
||||
clients::client::connection::{Connection2, ConnectionError},
|
||||
components::{
|
||||
Button, CoverOfDarkness, Footer, Identity,
|
||||
Button, CoverOfDarkness, Footer, Identity, Story,
|
||||
client::{ClientNav, Signin},
|
||||
story::Story,
|
||||
},
|
||||
storage::StorageKey,
|
||||
};
|
||||
|
|
@ -40,7 +39,6 @@ use crate::WerewolfError;
|
|||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum ClientEvent2 {
|
||||
Signin,
|
||||
Disconnected,
|
||||
Connecting,
|
||||
ShowRole(RoleTitle),
|
||||
|
|
@ -56,6 +54,7 @@ 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);
|
||||
|
|
@ -74,8 +73,6 @@ 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 || {
|
||||
|
|
@ -96,67 +93,45 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
|||
}
|
||||
|
||||
let client_state = use_state(|| ClientEvent2::Connecting);
|
||||
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 ClientContext {
|
||||
error_cb,
|
||||
forced_identity,
|
||||
} = use_context::<ClientContext>().unwrap_or_default();
|
||||
let force = use_force_update();
|
||||
|
||||
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()
|
||||
let ident = if let Some(Identification { player_id, public }) = forced_identity {
|
||||
(player_id, public)
|
||||
} else {
|
||||
match PlayerId::load_from_storage()
|
||||
.and_then(|pid| PublicIdentity::load_from_storage().map(|ident| (pid, ident)))
|
||||
{
|
||||
Ok((pid, ident)) => {
|
||||
ident_state.set(Some((pid, ident.clone())));
|
||||
(pid, ident)
|
||||
}
|
||||
Ok((pid, ident)) => (pid, ident),
|
||||
Err(StorageError::KeyNotFound(_)) => {
|
||||
client_state.set(ClientEvent2::Signin);
|
||||
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();
|
||||
});
|
||||
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();
|
||||
|
||||
// client_state.set(ClientEvent2::Connecting);
|
||||
client_state.set(ClientEvent2::Signin);
|
||||
return html! {
|
||||
// <Signin callback={on_signin} />
|
||||
};
|
||||
force.force_update();
|
||||
return html! {};
|
||||
}
|
||||
}
|
||||
};
|
||||
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()}/>
|
||||
},
|
||||
|
|
@ -270,7 +245,7 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
|||
}
|
||||
};
|
||||
html! {
|
||||
<ClientNav identity={ident_state.clone()} message_callback={client_nav_msg_cb} />
|
||||
<ClientNav identity={ident.clone()} message_callback={client_nav_msg_cb} />
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ fn url() -> String {
|
|||
#[derive(Clone)]
|
||||
pub struct Connection2 {
|
||||
state: UseStateSetter<ClientEvent2>,
|
||||
ident: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||
ident: UseStateHandle<(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<Option<(PlayerId, PublicIdentity)>>,
|
||||
ident: UseStateHandle<(PlayerId, PublicIdentity)>,
|
||||
receiver: Rc<RefCell<UnboundedReceiver<ClientMessage>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -64,19 +64,9 @@ impl Connection2 {
|
|||
}
|
||||
}
|
||||
fn identification(&self) -> Identification {
|
||||
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,
|
||||
},
|
||||
},
|
||||
Identification {
|
||||
player_id: self.ident.0,
|
||||
public: self.ident.1.clone(),
|
||||
}
|
||||
}
|
||||
async fn connect_ws() -> WebSocket {
|
||||
|
|
@ -118,10 +108,7 @@ impl Connection2 {
|
|||
yew::platform::spawn_local(async move {
|
||||
let active = conn.active.clone();
|
||||
conn.active = Rc::new(RefCell::new(()));
|
||||
let Ok(active_borrow) = active.try_borrow_mut() else {
|
||||
log::warn!("active connection already borrowed; exiting");
|
||||
return;
|
||||
};
|
||||
let active_borrow = active.borrow_mut();
|
||||
conn.run().await;
|
||||
core::mem::drop(active_borrow);
|
||||
});
|
||||
|
|
@ -287,11 +274,9 @@ impl Connection2 {
|
|||
return None;
|
||||
}
|
||||
ServerMessage::Update(PlayerUpdate::Number(new_num)) => {
|
||||
let Some((pid, mut ident)) = (*self.ident).clone() else {
|
||||
return None;
|
||||
};
|
||||
let (pid, mut ident) = (*self.ident).clone();
|
||||
ident.number = Some(new_num);
|
||||
self.ident.set(Some((pid, ident)));
|
||||
self.ident.set((pid, ident));
|
||||
return None;
|
||||
}
|
||||
ServerMessage::GameInProgress => ClientEvent2::GameInProgress,
|
||||
|
|
|
|||
|
|
@ -42,11 +42,10 @@ use yew::{html::Scope, prelude::*};
|
|||
use crate::{
|
||||
callback,
|
||||
components::{
|
||||
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Victory,
|
||||
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Story, Victory,
|
||||
action::{ActionResultView, Prompt},
|
||||
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup},
|
||||
settings::Settings,
|
||||
story::Story,
|
||||
},
|
||||
pages::RolePage,
|
||||
storage::StorageKey,
|
||||
|
|
|
|||
|
|
@ -26,11 +26,3 @@ 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()}/>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2026 Emilis Bliūdžius
|
||||
// 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
|
||||
|
|
@ -29,13 +29,15 @@ pub struct AlignmentSpanProps {
|
|||
#[function_component]
|
||||
pub fn AlignmentSpan(AlignmentSpanProps { alignment }: &AlignmentSpanProps) -> Html {
|
||||
let class = match alignment {
|
||||
role::Alignment::Village => "village-highlight",
|
||||
role::Alignment::Wolves => "wolves-highlight",
|
||||
role::Alignment::Traitor => "traitor-highlight",
|
||||
role::Alignment::Village => "village",
|
||||
role::Alignment::Wolves => "wolves",
|
||||
role::Alignment::Traitor => "traitor",
|
||||
};
|
||||
html! {
|
||||
<span class={classes!("attribute-span", class)}>
|
||||
<Icon source={alignment.icon()} icon_type={IconType::Fit}/>
|
||||
<span class={classes!("attribute-span", "faint", class)}>
|
||||
<div>
|
||||
<Icon source={alignment.icon()} icon_type={IconType::Small}/>
|
||||
</div>
|
||||
{alignment.to_string()}
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,20 @@ pub fn AlignmentComparisonSpan(
|
|||
match comparison {
|
||||
AlignmentEq::Same => html! {
|
||||
<span class="alignment-eq">
|
||||
<Icon source={IconSource::Equal} icon_type={IconType::Fit}/>
|
||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||
{"the same"}
|
||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||
</span>
|
||||
},
|
||||
AlignmentEq::Different => html! {
|
||||
<span class="alignment-eq">
|
||||
<Icon source={IconSource::NotEqual} icon_type={IconType::Fit}/>
|
||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||
{"different"}
|
||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||
</span>
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,29 @@ pub struct DiedToSpanProps {
|
|||
|
||||
#[function_component]
|
||||
pub fn DiedToSpan(DiedToSpanProps { died_to }: &DiedToSpanProps) -> Html {
|
||||
let class = match died_to {
|
||||
DiedToTitle::Execution => "execution",
|
||||
DiedToTitle::MapleWolfStarved | DiedToTitle::MapleWolf => {
|
||||
SetupRoleTitle::MapleWolf.category().class()
|
||||
}
|
||||
DiedToTitle::Militia => SetupRoleTitle::Militia.category().class(),
|
||||
DiedToTitle::LoneWolf
|
||||
| DiedToTitle::AlphaWolf
|
||||
| DiedToTitle::Shapeshift
|
||||
| DiedToTitle::Wolfpack => SetupRoleTitle::Werewolf.category().class(),
|
||||
DiedToTitle::Hunter => SetupRoleTitle::Hunter.category().class(),
|
||||
DiedToTitle::GuardianProtecting => SetupRoleTitle::Guardian.category().class(),
|
||||
DiedToTitle::PyreMasterLynchMob | DiedToTitle::PyreMaster => {
|
||||
SetupRoleTitle::PyreMaster.category().class()
|
||||
}
|
||||
DiedToTitle::MasonLeaderRecruitFail => SetupRoleTitle::MasonLeader.category().class(),
|
||||
};
|
||||
let icon = died_to.icon().unwrap_or(IconSource::Skull);
|
||||
html! {
|
||||
<span class={classes!("attribute-span",)}>
|
||||
<Icon source={icon} icon_type={IconType::Fit}/>
|
||||
<span class={classes!("attribute-span", "faint", class)}>
|
||||
<div>
|
||||
<Icon source={icon} icon_type={IconType::Small}/>
|
||||
</div>
|
||||
{died_to.to_string().to_case(Case::Title)}
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,10 @@ pub fn KillerSpan(KillerSpanProps { killer }: &KillerSpanProps) -> Html {
|
|||
Killer::NotKiller => "inactive",
|
||||
};
|
||||
html! {
|
||||
<span class={classes!("attribute-span")}>
|
||||
<Icon
|
||||
source={killer.icon()}
|
||||
icon_type={IconType::Fit}
|
||||
classes={classes!(class)}
|
||||
/>
|
||||
<span class={classes!("attribute-span", "faint")}>
|
||||
<div class={classes!(class)}>
|
||||
<Icon source={killer.icon()} icon_type={IconType::Small}/>
|
||||
</div>
|
||||
{killer.to_string()}
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,10 @@ pub fn PowerfulSpan(PowerfulSpanProps { powerful }: &PowerfulSpanProps) -> Html
|
|||
Powerful::NotPowerful => "inactive",
|
||||
};
|
||||
html! {
|
||||
<span class={classes!("attribute-span")}>
|
||||
<Icon
|
||||
source={powerful.icon()}
|
||||
icon_type={IconType::Fit}
|
||||
classes={classes!(class)}
|
||||
/>
|
||||
<span class={classes!("attribute-span", "faint")}>
|
||||
<div class={classes!(class)}>
|
||||
<Icon source={powerful.icon()} icon_type={IconType::Small}/>
|
||||
</div>
|
||||
{powerful.to_string()}
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use core::ops::Not;
|
||||
|
||||
// Copyright (C) 2025 Emilis Bliūdžius
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -15,17 +13,10 @@ use core::ops::Not;
|
|||
// 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 convert_case::{Case, Casing};
|
||||
use werewolves_proto::{
|
||||
character::Character,
|
||||
game::{GameTime, SetupRole},
|
||||
message::CharacterIdentity,
|
||||
};
|
||||
use werewolves_proto::{character::Character, game::SetupRole, message::CharacterIdentity};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::{
|
||||
class::Class,
|
||||
components::{Icon, IconSource, IconType, PartialAssociatedIcon},
|
||||
};
|
||||
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CharacterCardProps {
|
||||
|
|
@ -98,47 +89,3 @@ pub fn CharacterTargetCard(
|
|||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CharacterHighlightProps {
|
||||
pub char: Character,
|
||||
pub time: GameTime,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn CharacterHighlight(
|
||||
CharacterHighlightProps { char, time }: &CharacterHighlightProps,
|
||||
) -> Html {
|
||||
let class = char.class().map(|c| format!("{c}-highlight"));
|
||||
let icon = char
|
||||
.role_title()
|
||||
.icon()
|
||||
.map(|source| {
|
||||
html! {
|
||||
<Icon source={source} icon_type={IconType::Fit}/>
|
||||
}
|
||||
})
|
||||
.unwrap_or(html! {
|
||||
<div class="icon-spacer"/>
|
||||
});
|
||||
let dead = char.died_to().and_then(|died_to| {
|
||||
let night = died_to.night();
|
||||
let day = died_to.day();
|
||||
match (time, day, night) {
|
||||
(GameTime::Day { number }, Some(day), None) => number.get() > day.get(),
|
||||
(GameTime::Night { number }, None, Some(night)) => *number > night,
|
||||
(GameTime::Day { number }, None, Some(night)) => number.get() > night,
|
||||
(GameTime::Night { number }, Some(day), None) => *number >= day.get(),
|
||||
(_, None, None) | (_, Some(_), Some(_)) => true,
|
||||
}
|
||||
.then_some("dead")
|
||||
});
|
||||
let role_text = char.role_title().to_string().to_case(Case::Title);
|
||||
html! {
|
||||
<span class={classes!("highlight-span", class)} role={role_text}>
|
||||
{icon}
|
||||
<span class={classes!("number")}><b>{char.number().get()}</b></span>
|
||||
<span class={classes!("name", dead)}>{char.name()}</span>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use crate::{
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct ClientNavProps {
|
||||
pub identity: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||
pub identity: UseStateHandle<(PlayerId, PublicIdentity)>,
|
||||
pub message_callback: Callback<ClientMessage>,
|
||||
}
|
||||
|
||||
|
|
@ -38,19 +38,18 @@ pub fn ClientNav(
|
|||
message_callback,
|
||||
}: &ClientNavProps,
|
||||
) -> Html {
|
||||
const MUST_HAVE_IDENTITY: &str = "client nav must have identity";
|
||||
let pronouns = identity
|
||||
.1
|
||||
.pronouns
|
||||
.as_ref()
|
||||
.and_then(|identity| {
|
||||
identity.1.pronouns.as_ref().map(|pronouns| {
|
||||
html! {
|
||||
<span>{"("}{pronouns.as_str()}{")"}</span>
|
||||
}
|
||||
})
|
||||
.map(|pronouns| {
|
||||
html! {
|
||||
<div>{"("}{pronouns.as_str()}{")"}</div>
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
html! {
|
||||
<span class="faint">{"(None)"}</span>
|
||||
<div>{"(None)"}</div>
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -63,13 +62,10 @@ pub fn ClientNav(
|
|||
|
||||
let submit_ident = identity.clone();
|
||||
let current_num = identity
|
||||
.as_ref()
|
||||
.and_then(|identity| identity.1.number.map(|v| html! {{v.to_string()}}))
|
||||
.unwrap_or_else(|| {
|
||||
html! {
|
||||
<span class="red">{"???"}</span>
|
||||
}
|
||||
});
|
||||
.1
|
||||
.number
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| String::from("???"));
|
||||
let open_set = number_open.setter();
|
||||
let on_submit = {
|
||||
let val = current_value.clone();
|
||||
|
|
@ -78,16 +74,13 @@ 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_ref.1.name.clone(),
|
||||
pronouns: submit_ident_ref.1.pronouns.clone(),
|
||||
name: submit_ident.1.name.clone(),
|
||||
pronouns: submit_ident.1.pronouns.clone(),
|
||||
number: Some(num),
|
||||
};
|
||||
submit_ident.set(Some((submit_ident_ref.0, new_ident.clone())));
|
||||
submit_ident.set((submit_ident.0, new_ident.clone()));
|
||||
if let Err(err) = new_ident.save_to_storage() {
|
||||
log::error!("saving public identity after change: {err}");
|
||||
}
|
||||
|
|
@ -121,7 +114,6 @@ 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
|
||||
|
|
@ -142,12 +134,6 @@ 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()}
|
||||
|
|
@ -158,7 +144,7 @@ pub fn ClientNav(
|
|||
on_open={close_others}
|
||||
label={String::from("name")}
|
||||
>
|
||||
<div class="name">{name_str}</div>
|
||||
<div class="name">{identity.1.name.as_str()}</div>
|
||||
</ClickableTextEdit>
|
||||
}
|
||||
};
|
||||
|
|
@ -168,18 +154,15 @@ 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(),
|
||||
)));
|
||||
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
|
||||
Some(PublicIdentity {
|
||||
pronouns,
|
||||
name: ident.1.name.clone(),
|
||||
number: ident.1.number,
|
||||
})
|
||||
})
|
||||
};
|
||||
|
|
@ -213,10 +196,14 @@ 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! {
|
||||
<>
|
||||
<a href="/host"><Button on_click={Callback::noop()}>{"host"}</Button></a>
|
||||
<Button on_click={host_click}>{"host"}</Button>
|
||||
<Button on_click={forgor}>{"forgor 💀"}</Button>
|
||||
</>
|
||||
}
|
||||
|
|
@ -236,7 +223,7 @@ struct ClickableTextEditProps {
|
|||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
pub value: UseStateHandle<String>,
|
||||
pub submit_ident: UseStateHandle<Option<(PlayerId, PublicIdentity)>>,
|
||||
pub submit_ident: UseStateHandle<(PlayerId, PublicIdentity)>,
|
||||
pub on_submit: Callback<String, Option<PublicIdentity>>,
|
||||
pub field_name: &'static str,
|
||||
pub state: UseStateHandle<bool>,
|
||||
|
|
@ -270,10 +257,8 @@ fn ClickableTextEdit(
|
|||
let submit = {
|
||||
let submit_ident = submit_ident.clone();
|
||||
move |_| {
|
||||
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 Some(new_ident) = message_callback.emit(value.trim().to_string()) {
|
||||
submit_ident.set((submit_ident.0, new_ident.clone()));
|
||||
if let Err(err) = new_ident.save_to_storage() {
|
||||
log::error!("saving public identity after change: {err}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ use crate::components::Button;
|
|||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct SigninProps {
|
||||
pub callback: Callback<PublicIdentity>,
|
||||
#[prop_or(true)]
|
||||
pub full_height: bool,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -62,34 +60,15 @@ 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={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"
|
||||
// autocomplete="name nickname username"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="pronouns">{"Pronouns"}</label>
|
||||
<input oninput={pronouns_on_input} name="pronouns" id="pronouns" type="text"/>
|
||||
</div>
|
||||
<div class="signin full-height">
|
||||
<div class="column-list">
|
||||
<label for="name">{"Name"}</label>
|
||||
<input oninput={name_on_input} name="name" id="name" type="text"/>
|
||||
<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"/>
|
||||
<Button on_click={on_click}>{"Submit"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -109,25 +109,18 @@ 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>
|
||||
<NumberEdit
|
||||
value={value.clone()}
|
||||
on_submit={on_submit.clone()}
|
||||
field_name={field_name}
|
||||
label={label.clone()}
|
||||
>
|
||||
</NumberEdit>
|
||||
<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>
|
||||
};
|
||||
html! {
|
||||
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
||||
|
|
@ -135,36 +128,3 @@ 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,10 +17,7 @@ use core::num::NonZeroU8;
|
|||
use werewolves_proto::{message::PlayerState, player::PlayerId};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{
|
||||
Button, ClickableField, ClickableNumberEdit, Identity, NumberEdit,
|
||||
modal::{Dialog, SubmenuDialog},
|
||||
};
|
||||
use crate::components::{Button, ClickableField, ClickableNumberEdit, Identity};
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct LobbyPlayerProps {
|
||||
|
|
@ -38,11 +35,7 @@ pub enum LobbyPlayerAction {
|
|||
#[function_component]
|
||||
pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) -> Html {
|
||||
let open = use_state(|| false);
|
||||
let class = if player.connected {
|
||||
"connected"
|
||||
} else {
|
||||
"disconnected"
|
||||
};
|
||||
let class = player.connected.then_some("connected");
|
||||
let pid = player.identification.player_id;
|
||||
let action_open = open.clone();
|
||||
let action = |action: LobbyPlayerAction| {
|
||||
|
|
@ -72,22 +65,9 @@ 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}
|
||||
|
|
@ -99,17 +79,8 @@ 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}
|
||||
|
|
|
|||
|
|
@ -1,143 +0,0 @@
|
|||
// 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>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
// 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}
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// 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 convert_case::{Case, Casing};
|
||||
use werewolves_proto::{game::SetupRole, role::RoleTitle};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct RoleSpanProps {
|
||||
pub role: RoleTitle,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn RoleSpan(RoleSpanProps { role }: &RoleSpanProps) -> Html {
|
||||
let class = Into::<SetupRole>::into(*role).category().class();
|
||||
let icon = role
|
||||
.icon()
|
||||
.map(|icon| {
|
||||
html! {
|
||||
<Icon source={icon} icon_type={IconType::Small}/>
|
||||
}
|
||||
})
|
||||
.unwrap_or(html! {<div class="icon-spacer"/>});
|
||||
let role_name = role.to_string().to_case(Case::Title);
|
||||
html! {
|
||||
<span class={classes!("role-span", class, "faint")}>
|
||||
{icon}
|
||||
<span class="role-name">{role_name}</span>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct RoleblockProps {
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Roleblock(RoleblockProps { children }: &RoleblockProps) -> Html {
|
||||
let content = if *children == html! {} {
|
||||
html! {<span>{"Drunk"}</span>}
|
||||
} else {
|
||||
children.clone()
|
||||
};
|
||||
html! {
|
||||
<span class={classes!("roleblock-span")}>
|
||||
<Icon source={IconSource::Roleblock} icon_type={IconType::Fit}/>
|
||||
{content}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ use crate::{
|
|||
components::{
|
||||
Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon,
|
||||
client::Signin,
|
||||
modal::Dialog,
|
||||
settings::{AddRoleCategory, SettingSlotAction, SettingsSlot},
|
||||
},
|
||||
};
|
||||
|
|
@ -275,19 +274,17 @@ 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">
|
||||
|
|
@ -298,16 +295,12 @@ pub fn Settings(
|
|||
{clear_all_assignments}
|
||||
{clear_bad_assigned}
|
||||
</div>
|
||||
<Dialog
|
||||
id={ADD_PLAYER_DIALOG_ID.to_string()}
|
||||
button={html!{"add player"}}
|
||||
close_button=false
|
||||
<ClickableField
|
||||
options={add_player_opts}
|
||||
state={add_player_open}
|
||||
>
|
||||
<div>
|
||||
<h3>{"manually add a player"}</h3>
|
||||
<Signin callback={add_player_dialog_cb} full_height=false/>
|
||||
</div>
|
||||
</Dialog>
|
||||
{"add player"}
|
||||
</ClickableField>
|
||||
|
||||
<div class="roles-add-list">
|
||||
{add_roles_buttons}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,613 @@
|
|||
// 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()
|
||||
}
|
||||
|
|
@ -1,710 +0,0 @@
|
|||
// 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 core::ops::Not;
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use werewolves_proto::{
|
||||
aura::AuraTitle,
|
||||
character::{Character, CharacterId},
|
||||
diedto::DiedTo,
|
||||
game::{
|
||||
GameTime, SetupRole,
|
||||
night::changes::NightChange,
|
||||
story::{NightChoice, StoryActionPrompt, StoryActionResult},
|
||||
},
|
||||
player::Protection,
|
||||
role::{RoleBlock, RoleTitle},
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::{
|
||||
class::Class,
|
||||
components::{
|
||||
AuraSpan, Button, CharacterHighlight, Icon, IconSource, IconType, Identity, IdentitySpan,
|
||||
PartialAssociatedIcon, RoleSpan, Roleblock,
|
||||
attributes::{
|
||||
AlignmentComparisonSpan, AlignmentSpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CharacterStoryProps {
|
||||
pub all_characters: Rc<[Character]>,
|
||||
pub character: Character,
|
||||
pub actions: Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn CharacterStory(
|
||||
CharacterStoryProps {
|
||||
all_characters,
|
||||
character,
|
||||
actions,
|
||||
}: &CharacterStoryProps,
|
||||
) -> Html {
|
||||
let class = character.class();
|
||||
let mut by_time = actions
|
||||
.iter()
|
||||
.filter(|(_, changes, choices)| !(changes.is_empty() && choices.is_empty()))
|
||||
.cloned()
|
||||
.collect::<Box<[_]>>();
|
||||
by_time.sort_by_key(|s| s.0);
|
||||
let open_time = use_state(|| by_time.iter().next().map(|(t, _, _)| *t));
|
||||
if let Some(current_open_time) = open_time.as_ref()
|
||||
&& let Some(last) = by_time.last().map(|c| c.0)
|
||||
{
|
||||
if last < *current_open_time || !by_time.iter().any(|(t, _, _)| t == current_open_time) {
|
||||
open_time.set(Some(last));
|
||||
} else if let Some(first) = by_time.first().map(|c| c.0)
|
||||
&& first > *current_open_time
|
||||
{
|
||||
open_time.set(Some(first));
|
||||
}
|
||||
}
|
||||
let tabs = by_time
|
||||
.into_iter()
|
||||
.map(|(time, _, _)| {
|
||||
let time_text = match time {
|
||||
GameTime::Day { number } => format!("day {number}"),
|
||||
GameTime::Night { number } => format!("night {number}"),
|
||||
};
|
||||
let on_click = {
|
||||
let open_time = open_time.setter();
|
||||
Callback::from(move |_| open_time.set(Some(time)))
|
||||
};
|
||||
let selected = open_time
|
||||
.as_ref()
|
||||
.map(|open| *open == time)
|
||||
.unwrap_or_default();
|
||||
let faint = selected.not().then_some("faint");
|
||||
let selected = selected.then_some("selected");
|
||||
html! {
|
||||
<button
|
||||
onclick={on_click}
|
||||
class={classes!("tab-button", class, faint, selected, "hover")}
|
||||
>
|
||||
{time_text}
|
||||
</button>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
||||
let story_content = open_time
|
||||
.as_ref()
|
||||
.and_then(|time| actions.iter().find(|(t, _, _)| t == time))
|
||||
.map(|(time, changes, choices)| {
|
||||
let choices = choices
|
||||
.iter()
|
||||
.map(|c| {
|
||||
html! {
|
||||
<Choice
|
||||
all_characters={all_characters.clone()}
|
||||
character={character.clone()}
|
||||
choice={c.clone()}
|
||||
all_choices_that_night={choices.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let changes = changes
|
||||
.iter()
|
||||
.map(|c| {
|
||||
html! {
|
||||
<Change all_characters={all_characters.clone()} change={c.clone()} time={*time}/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let changes = (changes == html! {}).not().then_some(html! {
|
||||
<>
|
||||
<span class="sub-headline">{"changes"}</span>
|
||||
<div>
|
||||
{changes}
|
||||
</div>
|
||||
</>
|
||||
});
|
||||
let choices = (choices == html! {}).not().then_some(html! {
|
||||
<>
|
||||
<span class="sub-headline">{"choices"}</span>
|
||||
<div>
|
||||
{choices}
|
||||
</div>
|
||||
</>
|
||||
});
|
||||
html! {
|
||||
<>
|
||||
{choices}
|
||||
{changes}
|
||||
</>
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<div class="character-story">
|
||||
<div class="tabs">
|
||||
{tabs}
|
||||
</div>
|
||||
<div class="story-content">
|
||||
{story_content}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
struct ChoiceProps {
|
||||
all_characters: Rc<[Character]>,
|
||||
character: Character,
|
||||
choice: NightChoice,
|
||||
all_choices_that_night: Box<[NightChoice]>,
|
||||
time: GameTime,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Choice(
|
||||
ChoiceProps {
|
||||
all_characters,
|
||||
character,
|
||||
choice,
|
||||
all_choices_that_night,
|
||||
time,
|
||||
}: &ChoiceProps,
|
||||
) -> Html {
|
||||
let generate_prompt = |chosen: CharacterId, wording: &'static str| -> Option<Html> {
|
||||
all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == chosen)
|
||||
.map(|chosen| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{wording}</span>
|
||||
<CharacterHighlight
|
||||
char={chosen.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
})
|
||||
};
|
||||
let prompt = match &choice.prompt {
|
||||
StoryActionPrompt::Guardian {
|
||||
chosen, guarding, ..
|
||||
} => generate_prompt(*chosen, if *guarding { "guarded" } else { "protected" }),
|
||||
StoryActionPrompt::Seer { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||
StoryActionPrompt::Protector { chosen, .. } => generate_prompt(*chosen, "protected"),
|
||||
StoryActionPrompt::Arcanist {
|
||||
chosen: (target1, target2),
|
||||
..
|
||||
} => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *target1)
|
||||
.and_then(|t1| {
|
||||
all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *target2)
|
||||
.map(|t2| (t1, t2))
|
||||
})
|
||||
.map(|(t1, t2)| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"checked"}</span>
|
||||
<CharacterHighlight
|
||||
char={t1.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"and"}</span>
|
||||
<CharacterHighlight
|
||||
char={t2.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
StoryActionPrompt::Gravedigger { chosen, .. } => generate_prompt(*chosen, "dug"),
|
||||
StoryActionPrompt::Hunter { chosen, .. } => generate_prompt(*chosen, "set a trap for"),
|
||||
StoryActionPrompt::Militia { chosen, .. } => generate_prompt(*chosen, "shot"),
|
||||
StoryActionPrompt::MapleWolf { chosen, .. } => generate_prompt(*chosen, "ate"),
|
||||
StoryActionPrompt::Adjudicator { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||
StoryActionPrompt::PowerSeer { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||
StoryActionPrompt::Mortician { chosen, .. } => generate_prompt(*chosen, "examined"),
|
||||
StoryActionPrompt::Beholder { chosen, .. } => generate_prompt(*chosen, "beheld"),
|
||||
StoryActionPrompt::MasonLeaderRecruit { chosen, .. } => {
|
||||
generate_prompt(*chosen, "attempted to recruit")
|
||||
}
|
||||
StoryActionPrompt::WolfPackKill { chosen, .. } => {
|
||||
generate_prompt(*chosen, "took the wolfpack kill and attacked")
|
||||
}
|
||||
StoryActionPrompt::MasonsWake { leader, .. } => all_characters
|
||||
.iter()
|
||||
.find(|l| l.character_id() == *leader)
|
||||
.map(|leader| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"woke with the masons of"}</span>
|
||||
<CharacterHighlight
|
||||
char={leader.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
StoryActionPrompt::Empath { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||
StoryActionPrompt::Vindicator { chosen, .. } => generate_prompt(*chosen, "protected"),
|
||||
StoryActionPrompt::PyreMaster { chosen, .. } => generate_prompt(*chosen, "burned"),
|
||||
StoryActionPrompt::Shapeshifter { .. } => all_choices_that_night
|
||||
.iter()
|
||||
.find_map(|c| match c.prompt {
|
||||
StoryActionPrompt::WolfPackKill { chosen, .. } => Some(chosen),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|target| all_characters.iter().find(|c| c.character_id() == target))
|
||||
.map(|target| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"chose to shift into"}</span>
|
||||
<CharacterHighlight
|
||||
char={target.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
StoryActionPrompt::Insomniac { .. } => Some(html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"woke in the night due to visits from: "}</span>
|
||||
</>
|
||||
}),
|
||||
StoryActionPrompt::AlphaWolf { chosen, .. } => generate_prompt(*chosen, "killed"),
|
||||
StoryActionPrompt::DireWolf { chosen, .. } => {
|
||||
generate_prompt(*chosen, "roleblocked visitors to")
|
||||
}
|
||||
StoryActionPrompt::LoneWolfKill { chosen, .. } => generate_prompt(*chosen, "killed"),
|
||||
StoryActionPrompt::Bloodletter { chosen, .. } => generate_prompt(*chosen, "bloodlet"),
|
||||
StoryActionPrompt::BeholderWakes { character_id } => {
|
||||
generate_prompt(*character_id, "woke again to see")
|
||||
}
|
||||
};
|
||||
if prompt.is_none() {
|
||||
return html! {};
|
||||
}
|
||||
let result = choice.result.as_ref().map(|result| match result {
|
||||
StoryActionResult::RoleBlocked => html! {
|
||||
<>
|
||||
<span>{"but was"}</span>
|
||||
<Roleblock>{"Roleblocked"}</Roleblock>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Seer(alignment) => html! {
|
||||
<>
|
||||
<span>{"and saw"}</span>
|
||||
<AlignmentSpan alignment={*alignment}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::PowerSeer { powerful } => html! {
|
||||
<>
|
||||
<span>{"and saw"}</span>
|
||||
<PowerfulSpan powerful={*powerful}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Adjudicator { killer } => html! {
|
||||
<>
|
||||
<span>{"and saw"}</span>
|
||||
<KillerSpan killer={*killer}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Arcanist(alignment_eq) => html! {
|
||||
<>
|
||||
<span>{"and saw them as"}</span>
|
||||
<AlignmentComparisonSpan comparison={*alignment_eq}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::GraveDigger(None) => html! {
|
||||
<span>{"but found an empty grave"}</span>
|
||||
},
|
||||
StoryActionResult::GraveDigger(Some(role_title)) => html! {
|
||||
<>
|
||||
<span>{"as"}</span>
|
||||
<RoleSpan role={*role_title}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Mortician(died_to_title) => html! {
|
||||
<>
|
||||
<span>{"and found"}</span>
|
||||
<DiedToSpan died_to={*died_to_title}/>
|
||||
<span>{"to be the cause of death"}</span>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Insomniac { visits } => {
|
||||
let mut visit_chars = vec![];
|
||||
for visit in visits {
|
||||
match all_characters.iter().find(|c| c.character_id() == *visit) {
|
||||
Some(char) => visit_chars.push(char),
|
||||
None => {
|
||||
log::warn!("visit chars missing {visit}");
|
||||
return html! {};
|
||||
}
|
||||
}
|
||||
}
|
||||
visit_chars
|
||||
.into_iter()
|
||||
.map(|visitor| {
|
||||
html! {
|
||||
<CharacterHighlight
|
||||
char={visitor.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>()
|
||||
}
|
||||
StoryActionResult::Empath { scapegoat: true } => {
|
||||
html! {<span>{"and found their scapegoat"}</span>}
|
||||
}
|
||||
StoryActionResult::Empath { scapegoat: false } => {
|
||||
html! {<span>{"who was not a scapegoat"}</span>}
|
||||
}
|
||||
StoryActionResult::BeholderSawNothing => html! {
|
||||
<>
|
||||
{"and saw"}
|
||||
<em>{"nothing"}</em>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::BeholderSawEverything => html! {
|
||||
<>
|
||||
{"and saw"}
|
||||
<em>{"everything"}</em>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::ShiftFailed => html! {
|
||||
{"however, their shift failed"}
|
||||
},
|
||||
StoryActionResult::Drunk => html! {
|
||||
<>
|
||||
<span>{"but got"}</span>
|
||||
<AuraSpan aura={AuraTitle::Drunk} />
|
||||
</>
|
||||
},
|
||||
});
|
||||
html! {
|
||||
<div class="action">
|
||||
{prompt}
|
||||
{result}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
struct ChangeProps {
|
||||
all_characters: Rc<[Character]>,
|
||||
change: NightChange,
|
||||
time: GameTime,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn Change(
|
||||
ChangeProps {
|
||||
all_characters,
|
||||
change,
|
||||
time,
|
||||
}: &ChangeProps,
|
||||
) -> Html {
|
||||
match change {
|
||||
NightChange::RoleChange(_, role_title) => Some(html! {
|
||||
<>
|
||||
<span>{"role changed to"}</span>
|
||||
<RoleSpan role={*role_title}/>
|
||||
</>
|
||||
}),
|
||||
NightChange::Kill { died_to, .. } => Some(html! {
|
||||
<>
|
||||
<span>{"died to"}</span>
|
||||
<DiedToSpan died_to={died_to.title()}/>
|
||||
</>
|
||||
}),
|
||||
NightChange::RoleBlock {
|
||||
source,
|
||||
block_type: RoleBlock::Direwolf,
|
||||
..
|
||||
} => all_characters
|
||||
.iter()
|
||||
.find(|s| s.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"had visitors role blocked by"}</span>
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::Shapeshift { source, .. } => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
{"shapeshifted by"}
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
{"and became a werewolf"}
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::ElderReveal { .. } => Some(html! {
|
||||
<>
|
||||
<span>{"learned they are the"}</span>
|
||||
<RoleSpan role={RoleTitle::Elder}/>
|
||||
</>
|
||||
}),
|
||||
NightChange::MasonRecruit {
|
||||
mason_leader,
|
||||
recruiting,
|
||||
} => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *mason_leader)
|
||||
.and_then(|mason| {
|
||||
all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *recruiting)
|
||||
.map(|recruiting| (mason, recruiting))
|
||||
})
|
||||
.map(|(mason, recruiting)| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={mason.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"recruited"}</span>
|
||||
<CharacterHighlight
|
||||
char={recruiting.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"into the masons"}</span>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::ApplyAura { source, aura, .. } => {
|
||||
let from = all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"from"}</span>
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
});
|
||||
Some(html! {
|
||||
<>
|
||||
<span>{"received the"}</span>
|
||||
<AuraSpan aura={aura.title()}/>
|
||||
<span>{"aura"}</span>
|
||||
{from}
|
||||
</>
|
||||
})
|
||||
}
|
||||
NightChange::LostAura { aura, .. } => Some(html! {
|
||||
<>
|
||||
<span>{"lost the"}</span>
|
||||
<AuraSpan aura={aura.title()}/>
|
||||
<span>{"aura"}</span>
|
||||
</>
|
||||
}),
|
||||
NightChange::EmpathFoundScapegoat { scapegoat, .. } => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *scapegoat)
|
||||
.map(|scapegoat| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"took on the scapegoat's curse from"}</span>
|
||||
<CharacterHighlight
|
||||
char={scapegoat.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::HunterTarget { source, .. } => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
{"had the hunter's mark placed on them by"}
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::Protection { target, protection } => {
|
||||
let prot = match protection {
|
||||
Protection::Guardian {
|
||||
source,
|
||||
guarding: true,
|
||||
} => all_characters
|
||||
.iter()
|
||||
.find(|s| s.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"was guarded by"}</span>
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
Protection::Guardian {
|
||||
source,
|
||||
guarding: false,
|
||||
}
|
||||
| Protection::Protector { source }
|
||||
| Protection::Vindicator { source } => all_characters
|
||||
.iter()
|
||||
.find(|s| s.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"was protected by"}</span>
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
};
|
||||
all_characters
|
||||
.iter()
|
||||
.find(|t| t.character_id() == *target)
|
||||
.and_then(|target| prot.map(|prot| (target, prot)))
|
||||
.map(|(target, prot)| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={target.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
{prot}
|
||||
</>
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.map(|change| {
|
||||
html! {
|
||||
<div class="change">
|
||||
{change}
|
||||
</div>
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CharacterStoryButtonProps {
|
||||
pub character: Character,
|
||||
pub choices_by_time: Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||
pub on_click: Callback<(
|
||||
Character,
|
||||
Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||
)>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn CharacterStoryButton(
|
||||
CharacterStoryButtonProps {
|
||||
character,
|
||||
choices_by_time,
|
||||
on_click,
|
||||
}: &CharacterStoryButtonProps,
|
||||
) -> Html {
|
||||
let clickable = !choices_by_time
|
||||
.iter()
|
||||
.all(|(_, c1, c2)| c1.is_empty() && c2.is_empty());
|
||||
let role_class = character.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 = if clickable {
|
||||
let cb = on_click.clone();
|
||||
let char = character.clone();
|
||||
let act = choices_by_time.clone();
|
||||
Callback::from(move |_| cb.emit((char.clone(), act.clone())))
|
||||
} else {
|
||||
Callback::noop()
|
||||
};
|
||||
let inactive = clickable.not().then_some("no-content");
|
||||
let hover = clickable.then_some("hover");
|
||||
html! {
|
||||
<div class={classes!("character-headline", role_class, "faint", inactive, hover)} onclick={on_click}>
|
||||
{icon}
|
||||
<Identity ident={character.identity().into_public()}/>
|
||||
{dead_icon}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
// 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
|
|
@ -1,147 +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,
|
||||
},
|
||||
story::{CharacterStory, CharacterStoryButton, 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 selected_char = use_state::<
|
||||
Option<(
|
||||
Character,
|
||||
Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||
)>,
|
||||
_,
|
||||
>(|| actions_by_character.first().cloned());
|
||||
let on_char_pick = {
|
||||
let selected_char = selected_char.setter();
|
||||
Callback::from(move |(char, actions)| selected_char.set(Some((char, actions))))
|
||||
};
|
||||
let chars = actions_by_character
|
||||
.into_iter()
|
||||
.map(|(char, actions)| {
|
||||
html! {
|
||||
<CharacterStoryButton character={char} choices_by_time={actions} on_click={on_char_pick.clone()} />
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
||||
let all_characters = village.characters().into_iter().collect::<Rc<_>>();
|
||||
let actions = selected_char.as_ref().map(|(char, actions)| {
|
||||
let class = Into::<SetupRole>::into(char.role_title())
|
||||
.category()
|
||||
.class();
|
||||
|
||||
html! {
|
||||
<>
|
||||
<CharacterCard char={char.clone()} dead={char.died_to().is_some()} faint=true/>
|
||||
<div class={classes!("character-details", class, "faint")}>
|
||||
<CharacterStory
|
||||
character={char.clone()}
|
||||
actions={actions.clone()}
|
||||
all_characters={all_characters}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class="story">
|
||||
<div class="story-characters">
|
||||
{chars}
|
||||
</div>
|
||||
<div class="actions">
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -19,9 +19,6 @@ 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");
|
||||
}
|
||||
|
|
@ -37,9 +34,6 @@ 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");
|
||||
|
|
@ -87,14 +81,13 @@ 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