screen test screen + minor ui fixups
This commit is contained in:
parent
6be2263f40
commit
7a50c3320a
|
|
@ -556,7 +556,7 @@ pub enum ActionResponse {
|
|||
ContinueToResult,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
|
||||
pub enum ActionResult {
|
||||
RoleBlocked,
|
||||
Drunk,
|
||||
|
|
@ -609,7 +609,7 @@ impl ActionResult {
|
|||
pub struct Visits(Box<[CharacterIdentity]>);
|
||||
|
||||
impl Visits {
|
||||
pub(crate) const fn new(visits: Box<[CharacterIdentity]>) -> Self {
|
||||
pub const fn new(visits: Box<[CharacterIdentity]>) -> Self {
|
||||
Self(visits)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
use core::{fmt::Display, num::NonZeroU8, ops::Not};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::{ChecksAs, Titles};
|
||||
use werewolves_macros::{All, ChecksAs, Titles};
|
||||
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
|
|
@ -455,7 +455,7 @@ impl RoleTitle {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, All)]
|
||||
pub enum Alignment {
|
||||
Village,
|
||||
Wolves,
|
||||
|
|
|
|||
|
|
@ -106,10 +106,6 @@ impl Host {
|
|||
let slice: &[u8] = &bytes;
|
||||
ciborium::from_reader(slice)?
|
||||
};
|
||||
if let HostMessage::Echo(echo) = &msg {
|
||||
self.send_message(echo).await.log_warn();
|
||||
return Ok(());
|
||||
}
|
||||
log::debug!(
|
||||
"{} {}",
|
||||
"[host::incoming::message]".bold(),
|
||||
|
|
|
|||
|
|
@ -2,95 +2,27 @@
|
|||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="98.388664mm"
|
||||
height="98.388702mm"
|
||||
viewBox="0 0 98.388664 98.388702"
|
||||
width="98.38868mm"
|
||||
height="98.388695mm"
|
||||
viewBox="0 0 98.38868 98.388695"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="icons.svg"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5"
|
||||
inkscape:cx="1224"
|
||||
inkscape:cy="1639"
|
||||
inkscape:window-width="1918"
|
||||
inkscape:window-height="1042"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="17"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer4"><inkscape:grid
|
||||
id="grid1"
|
||||
units="mm"
|
||||
originx="-298.47592"
|
||||
originy="-475.7392"
|
||||
spacingx="0.26458333"
|
||||
spacingy="0.26458334"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
enabled="true"
|
||||
visible="false" /><inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="98.388664"
|
||||
height="98.388702"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" /></sodipodi:namedview><defs
|
||||
id="defs1"><inkscape:path-effect
|
||||
effect="mirror_symmetry"
|
||||
start_point="373.14932,480.05752"
|
||||
end_point="373.14932,569.65152"
|
||||
center_point="373.14932,524.85452"
|
||||
id="path-effect66"
|
||||
is_visible="true"
|
||||
lpeversion="1.2"
|
||||
lpesatellites=""
|
||||
mode="free"
|
||||
discard_orig_path="false"
|
||||
fuse_paths="true"
|
||||
oposite_fuse="false"
|
||||
split_items="false"
|
||||
split_open="false"
|
||||
link_styles="false" /></defs><g
|
||||
inkscape:groupmode="layer"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer4"
|
||||
inkscape:label="Layer 4"
|
||||
transform="translate(-298.47593,-475.73921)"><path
|
||||
d="m 282.57053,402.28177 -9.24492,22.95571 7.37371,2.96984 13.68651,5.51181 6.76496,2.7249 7.55613,-18.76268 -17.83044,6.70088 z m 18.58026,34.16226 -2.96985,7.37371 -5.5118,13.6865 -2.7249,6.76496 18.76268,7.55613 -6.70089,-17.83044 22.10046,-8.30595 z m -11.20655,27.82517 -7.37371,-2.96984 -13.68651,-5.51181 -6.76496,-2.72438 -7.55612,18.76216 17.83043,-6.70088 8.30595,22.10046 z m -27.82518,-11.20603 2.96985,-7.37423 5.51181,-13.6865 2.72489,-6.76496 -18.76267,-7.55613 6.70088,17.83043 -22.10098,8.30596 z"
|
||||
style="fill:#3c34ff;stroke:#0f07ff;stroke-width:0.483509"
|
||||
id="path55" /><g
|
||||
transform="translate(-239.73841,-338.68501)"><g
|
||||
id="g93"
|
||||
transform="rotate(45,343.56148,515.63613)"><path
|
||||
transform="rotate(45,479.63174,376.20661)"><path
|
||||
id="path65"
|
||||
style="fill:#643e00;fill-opacity:1;stroke:#000000;stroke-width:2.454;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 335.22656,480.05664 v 29.23242 h 10.47461 v -6.27148 h 8.22461 v 6.27148 h 12.50781 v 60.36328 h 6.71485 6.71679 v -60.36328 h 12.50782 v -6.27148 h 8.22461 v 6.27148 h 10.47461 v -29.23242 l -10.47461,0.002 v 6.26953 h -8.22461 v -6.27148 h -19.22461 -19.22266 v 6.27148 h -8.22461 v -6.26953 z"
|
||||
sodipodi:nodetypes="ccccccccccccccc"
|
||||
transform="translate(-20.108333,10.054166)"
|
||||
inkscape:original-d="m 373.14932,480.05752 v 89.59401 h -6.71536 v -60.36272 h -12.50879 l 1.6e-4,-6.27098 -8.22395,1.7e-4 v 6.271 l -10.4743,-1.9e-4 v -29.23129 l 10.4743,4.8e-4 -5e-5,6.27083 8.224,1.7e-4 -1.6e-4,-6.27148 z"
|
||||
inkscape:path-effect="#path-effect66" /><g
|
||||
transform="translate(-20.108333,10.054166)" /><g
|
||||
id="g91"><path
|
||||
id="rect82"
|
||||
style="fill:#1a1a1a;fill-opacity:1;stroke:#000000;stroke-width:2.454;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 325.593,496.383 v 16.689 h 8.224 v -16.689 z"
|
||||
sodipodi:nodetypes="ccccc" /><path
|
||||
d="m 325.593,496.383 v 16.689 h 8.224 v -16.689 z" /><path
|
||||
id="rect82-3"
|
||||
style="fill:#1a1a1a;fill-opacity:1;stroke:#000000;stroke-width:2.454;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 372.267,496.383 v 16.689 h 8.224 v -16.689 z"
|
||||
sodipodi:nodetypes="ccccc" /></g></g></g></svg>
|
||||
d="m 372.267,496.383 v 16.689 h 8.224 v -16.689 z" /></g></g></g></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
|
@ -3,114 +3,26 @@
|
|||
|
||||
<svg
|
||||
width="48.950741mm"
|
||||
height="46.091217mm"
|
||||
viewBox="0 0 48.950741 46.091217"
|
||||
height="46.091213mm"
|
||||
viewBox="0 0 48.950741 46.091213"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="icons.svg"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.50000001"
|
||||
inkscape:cx="1243"
|
||||
inkscape:cy="22.999999"
|
||||
inkscape:window-width="1918"
|
||||
inkscape:window-height="1042"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="17"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer4"><inkscape:grid
|
||||
id="grid1"
|
||||
units="mm"
|
||||
originx="-111.78421"
|
||||
originy="-150.91972"
|
||||
spacingx="0.26458333"
|
||||
spacingy="0.26458334"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
enabled="true"
|
||||
visible="false" /><inkscape:page
|
||||
x="0"
|
||||
y="-1.003508e-12"
|
||||
width="48.950741"
|
||||
height="46.091213"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" /></sodipodi:namedview><defs
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Layer 4"
|
||||
transform="translate(-111.78421,-150.91973)"><path
|
||||
id="path145"
|
||||
style="fill:#878787;fill-opacity:0.8;stroke:#4c4c4c;stroke-width:0.861;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 183.52738,191.20695 c -2.41435,0.0449 -4.82586,0.45785 -7.10241,1.26865 -8.69357,2.98468 -16.70982,4.81246 -15.17168,20.63595 -0.32878,4.70686 -0.39306,9.48738 0.31884,14.16245 0.38831,2.68165 1.91493,5.49905 4.44572,6.62182 2.23089,0.44558 4.92605,-0.0675 6.56291,1.96318 4.44235,3.40984 1.74827,10.34937 5.14025,9.86452 2.132,-0.30475 0.68242,-3.69585 1.30638,-4.01164 0.96346,-0.48761 0.90617,-0.30265 1.20872,0.90588 0.16213,1.91232 -0.26173,3.48859 1.40611,3.3104 2.75286,-0.29411 1.43353,-2.40746 1.6397,-4.79144 0.37937,-0.0842 0.43999,-0.0618 0.44803,-0.0568 h 5.2e-4 l 5.1e-4,5.2e-4 5.2e-4,-5.2e-4 h 5.2e-4 c 0.008,-0.005 0.0686,-0.0274 0.44803,0.0568 0.20617,2.38398 -1.11317,4.49733 1.63969,4.79144 1.66784,0.17819 1.24399,-1.39808 1.40612,-3.3104 0.30255,-1.20853 0.24525,-1.39349 1.20871,-0.90588 0.62396,0.31579 -0.82562,3.70689 1.30638,4.01164 3.39198,0.48485 0.69791,-6.45468 5.14026,-9.86452 1.63686,-2.03072 4.33201,-1.5176 6.5629,-1.96318 2.53079,-1.12277 4.05742,-3.94017 4.44573,-6.62182 0.7119,-4.67507 0.64762,-9.45559 0.31884,-14.16245 1.53814,-15.82349 -6.47811,-17.65127 -15.17168,-20.63595 -2.27655,-0.8108 -4.68806,-1.22375 -7.10241,-1.26865 -0.0675,0 -0.13528,3e-5 -0.20361,5.2e-4 -0.0683,-4.9e-4 -0.1361,-5.2e-4 -0.2036,-5.2e-4 z m -10.36216,20.92224 c 3.5177,-0.0177 5.10805,2.49566 5.4715,3.52846 2.31327,5.55922 -1.86498,7.28273 -5.86993,7.12825 -3.49992,0.49438 -6.29206,0.25354 -6.71276,-5.06481 -0.05,-1.66043 1.54504,-4.80832 5.48338,-5.41775 0.58325,-0.11752 1.12528,-0.17163 1.62781,-0.17415 z m 21.13153,0 c 0.50252,0.003 1.04404,0.0566 1.62729,0.17415 3.93834,0.60943 5.53395,3.75732 5.4839,5.41775 -0.4207,5.31835 -3.21336,5.55919 -6.71328,5.06481 -4.00495,0.15448 -8.18268,-1.56903 -5.86941,-7.12825 0.36344,-1.0328 1.9538,-3.54611 5.4715,-3.52846 z m -10.56577,14.70866 c 1.07599,0.10793 1.42136,1.92498 2.91817,3.88142 1.38429,2.50152 -0.8238,3.48208 -2.04897,2.41535 -0.78178,-0.62609 -0.86916,-0.65369 -0.8692,-0.65371 -4e-5,2e-5 -0.0874,0.0276 -0.86919,0.65371 -1.22517,1.06673 -3.43326,0.0862 -2.04897,-2.41535 1.49681,-1.95644 1.84217,-3.77349 2.91816,-3.88142 z"
|
||||
inkscape:export-filename="../../src/werewolves/werewolves/img/skull.svg"
|
||||
inkscape:export-xdpi="900.08"
|
||||
inkscape:export-ydpi="900.08" /><g
|
||||
transform="translate(-111.78421,-150.91971)"><g
|
||||
id="g1"><path
|
||||
id="path148"
|
||||
style="fill:#ff2424;fill-opacity:1;stroke:#ff2424;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 98.470476,189.15435 c -5.126341,-0.0936 -11.08233,2.61633 -12.495878,8.56846 -1.651423,-6.7938 -9.212596,-9.32255 -14.669906,-8.28166 -5.503903,1.04977 -9.783107,8.15614 -9.656258,13.75782 0.288519,12.74111 22.205993,30.02582 24.425899,31.74431 2.21079,-1.7302 24.034727,-19.12846 24.255887,-31.87092 0.0972,-5.60227 -4.21923,-12.68858 -9.72861,-13.70924 -0.68284,-0.1265 -1.3988,-0.1954 -2.131134,-0.20877 z"
|
||||
transform="translate(50.270833,-38.1)" /><path
|
||||
sodipodi:type="star"
|
||||
style="fill:#fff001;fill-opacity:1;stroke:#ff2424;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path150"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="4"
|
||||
sodipodi:cx="79.886864"
|
||||
sodipodi:cy="208.97804"
|
||||
sodipodi:r1="5.2418227"
|
||||
sodipodi:r2="2.8830025"
|
||||
sodipodi:arg1="0.607802"
|
||||
sodipodi:arg2="1.3932002"
|
||||
inkscape:rounded="0.5"
|
||||
inkscape:randomized="0"
|
||||
d="m 84.189903,211.97146 c -1.084141,1.55845 -1.92512,-0.49115 -3.793716,-0.15576 -1.868596,0.33539 -1.944289,2.54952 -3.502742,1.46538 -1.558453,-1.08414 0.491151,-1.92512 0.155762,-3.79371 -0.335389,-1.8686 -2.549524,-1.94429 -1.465383,-3.50275 1.084141,-1.55845 1.925121,0.49116 3.793717,0.15577 1.868596,-0.33539 1.944289,-2.54953 3.502742,-1.46539 1.558452,1.08414 -0.491152,1.92512 -0.155763,3.79372 0.335389,1.8686 2.549524,1.94429 1.465383,3.50274 z"
|
||||
transform="rotate(28.628814,162.35113,272.55182)" /><path
|
||||
sodipodi:type="star"
|
||||
style="fill:#fff001;fill-opacity:1;stroke:#ff2424;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path151"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="4"
|
||||
sodipodi:cx="68.848625"
|
||||
sodipodi:cy="200.93324"
|
||||
sodipodi:r1="6.7481718"
|
||||
sodipodi:r2="3.3373971"
|
||||
sodipodi:arg1="0.80500349"
|
||||
sodipodi:arg2="1.5925996"
|
||||
inkscape:rounded="0.5"
|
||||
inkscape:randomized="0"
|
||||
d="m 73.525842,205.79755 c -1.802133,1.72521 -2.255654,-1.4788 -4.749977,-1.5277 -2.4873,-0.0488 -3.071191,3.13767 -4.791545,1.34061 -1.725212,-1.80213 1.478793,-2.25565 1.527701,-4.74998 0.04877,-2.4873 -3.137671,-3.07119 -1.340613,-4.79154 1.802133,-1.72521 2.255655,1.47879 4.749978,1.5277 2.487299,0.0488 3.07119,-3.13767 4.791545,-1.34061 1.725212,1.80213 -1.478794,2.25565 -1.527702,4.74997 -0.04877,2.4873 3.137671,3.07119 1.340613,4.79155 z"
|
||||
transform="translate(50.270833,-38.1)" /></g><path
|
||||
sodipodi:type="star"
|
||||
style="fill:#7f6500;fill-opacity:1;stroke:#daaf00;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path155-0"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="6"
|
||||
sodipodi:cx="99.020317"
|
||||
sodipodi:cy="191.42604"
|
||||
sodipodi:r1="7.1881213"
|
||||
sodipodi:r2="4.5083508"
|
||||
sodipodi:arg1="1.906205"
|
||||
sodipodi:arg2="1.8672678"
|
||||
inkscape:rounded="0.5"
|
||||
inkscape:randomized="0"
|
||||
d="m 96.65431,198.21361 c -1.309919,0.3028 1.927067,-1.45786 1.048904,-2.4759 -2.111403,-2.44772 -3.538875,-0.60338 -5.744108,-2.96691 -0.917188,-0.98302 2.22608,0.93996 2.668647,-0.32957 1.064081,-3.05239 -1.2469,-3.36644 -0.30264,-6.458 0.392731,-1.28582 0.299014,2.39782 1.619743,2.14633 3.175484,-0.60467 2.291975,-2.76307 5.441464,-3.49109 1.30992,-0.30279 -1.927062,1.45787 -1.0489,2.4759 2.1114,2.44772 3.53888,0.60338 5.74411,2.96691 0.91719,0.98302 -2.22608,-0.93996 -2.66865,0.32957 -1.06408,3.05239 1.2469,3.36645 0.30264,6.458 -0.39273,1.28582 -0.29901,-2.39782 -1.61974,-2.14633 -3.175485,0.60467 -2.291977,2.76307 -5.44147,3.49109 z"
|
||||
transform="translate(6.9985536,-38.801292)" /></g></svg>
|
||||
transform="translate(50.270833,-38.1)" /></g></g></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
|
@ -1194,6 +1194,11 @@ input {
|
|||
}
|
||||
|
||||
.village {
|
||||
--faction-color: $village_color;
|
||||
--faction-border: $village_border;
|
||||
--faction-color-faint: $village_color_faint;
|
||||
--faction-border-faint: $village_border_faint;
|
||||
|
||||
background-color: $village_color;
|
||||
border: 1px solid $village_border;
|
||||
|
||||
|
|
@ -1213,6 +1218,11 @@ input {
|
|||
}
|
||||
|
||||
.wolves {
|
||||
--faction-color: $wolves_color;
|
||||
--faction-border: $wolves_border;
|
||||
--faction-color-faint: $wolves_color_faint;
|
||||
--faction-border-faint: $wolves_border_faint;
|
||||
|
||||
background-color: $wolves_color;
|
||||
border: 1px solid $wolves_border;
|
||||
|
||||
|
|
@ -1232,6 +1242,11 @@ input {
|
|||
}
|
||||
|
||||
.intel {
|
||||
--faction-color: $intel_color;
|
||||
--faction-border: $intel_border;
|
||||
--faction-color-faint: $intel_color_faint;
|
||||
--faction-border-faint: $intel_border_faint;
|
||||
|
||||
background-color: $intel_color;
|
||||
border: 1px solid $intel_border;
|
||||
|
||||
|
|
@ -1251,6 +1266,11 @@ input {
|
|||
}
|
||||
|
||||
.defensive {
|
||||
--faction-color: $defensive_color;
|
||||
--faction-border: $defensive_border;
|
||||
--faction-color-faint: $defensive_color_faint;
|
||||
--faction-border-faint: $defensive_border_faint;
|
||||
|
||||
background-color: $defensive_color;
|
||||
border: 1px solid $defensive_border;
|
||||
|
||||
|
|
@ -1270,6 +1290,11 @@ input {
|
|||
}
|
||||
|
||||
.offensive {
|
||||
--faction-color: $offensive_color;
|
||||
--faction-border: $offensive_border;
|
||||
--faction-color-faint: $offensive_color_faint;
|
||||
--faction-border-faint: $offensive_border_faint;
|
||||
|
||||
background-color: $offensive_color;
|
||||
border: 1px solid $offensive_border;
|
||||
|
||||
|
|
@ -1289,6 +1314,11 @@ input {
|
|||
}
|
||||
|
||||
.starts-as-villager {
|
||||
--faction-color: $starts_as_villager_color;
|
||||
--faction-border: $starts_as_villager_border;
|
||||
--faction-color-faint: $starts_as_villager_color_faint;
|
||||
--faction-border-faint: $starts_as_villager_border_faint;
|
||||
|
||||
background-color: $starts_as_villager_color;
|
||||
border: 1px solid $starts_as_villager_border;
|
||||
|
||||
|
|
@ -1308,6 +1338,11 @@ input {
|
|||
}
|
||||
|
||||
.traitor {
|
||||
--faction-color: $traitor_color;
|
||||
--faction-border: $traitor_border;
|
||||
--faction-color-faint: $traitor_color_faint;
|
||||
--faction-border-faint: $traitor_border_faint;
|
||||
|
||||
background-color: $traitor_color;
|
||||
border: 1px solid $traitor_border;
|
||||
|
||||
|
|
@ -1919,9 +1954,41 @@ li.choice {
|
|||
&.masons,
|
||||
&.large {
|
||||
font-size: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.info-player-boxes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
overflow-y: hidden;
|
||||
|
||||
.identity {
|
||||
// flex-grow: 1;
|
||||
font-size: auto;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 5px 10px 5px 10px;
|
||||
|
||||
.number {
|
||||
color: rgba(255, 255, 0, 0.7);
|
||||
font-size: 2em;
|
||||
padding-right: 1cm;
|
||||
}
|
||||
|
||||
.pronouns {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
flex-wrap: wrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 2fr;
|
||||
|
|
@ -2241,3 +2308,107 @@ li.choice {
|
|||
.dimmed {
|
||||
filter: opacity(70%);
|
||||
}
|
||||
|
||||
.test-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
width: 80vw;
|
||||
margin-left: 10vw;
|
||||
margin-right: 10vw;
|
||||
|
||||
.test-screen-selector {
|
||||
.category {
|
||||
.selected {
|
||||
background-color: rgba(0, 255, 0, 0.7);
|
||||
border: 1px solid rgba(0, 255, 0, 1.0);
|
||||
color: black;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
button {
|
||||
color: white;
|
||||
}
|
||||
|
||||
gap: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
max-width: 80vw;
|
||||
|
||||
.test-screens {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.prompt-test,
|
||||
.result-test {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 40vw;
|
||||
align-items: center;
|
||||
|
||||
.close {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
font-size: 2em;
|
||||
|
||||
.result-number,
|
||||
.prompt-number {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.result-options,
|
||||
.prompt-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
|
||||
.option-set {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.result-option,
|
||||
.prompt-option {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
border: 1px solid white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HostEvent {
|
||||
SetErrorCallback(Callback<Option<WerewolfError>>),
|
||||
SetBigScreenState(bool),
|
||||
|
|
@ -488,11 +489,13 @@ impl Component for Host {
|
|||
}));
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<>
|
||||
<Button on_click={on_error_click}>{"error"}</Button>
|
||||
{screen}
|
||||
<Button on_click={client_click}>{"client"}</Button>
|
||||
<a href="/host/test"><Button on_click={|_|()}>{"test screens"}</Button></a>
|
||||
<Button on_click={story_on_click}>{"story"}</Button>
|
||||
</>
|
||||
}
|
||||
|
|
@ -520,6 +523,7 @@ impl Component for Host {
|
|||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
log::debug!("update: {msg:?}, current: {:?}", self.state);
|
||||
match msg {
|
||||
HostEvent::QrMode(mode) => {
|
||||
self.qr_mode = mode;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ pub mod host {
|
|||
pub mod story_test;
|
||||
pub use host::*;
|
||||
}
|
||||
pub mod test_remote;
|
||||
// mod socket;
|
||||
|
||||
const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
// 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 futures::{SinkExt, StreamExt, TryStreamExt, lock::Mutex};
|
||||
use gloo::net::websocket::{Message, futures::WebSocket};
|
||||
use werewolves_proto::message::host::{HostLobbyMessage, HostMessage, ServerToHostMessage};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::test_util::TestScreens;
|
||||
|
||||
fn url() -> String {
|
||||
format!(
|
||||
"{}host",
|
||||
option_env!("LOCAL")
|
||||
.map(|_| crate::clients::DEBUG_URL)
|
||||
.unwrap_or(crate::clients::LIVE_URL)
|
||||
)
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn TestClientRemote() -> Html {
|
||||
gloo::utils::document().set_title("werewolves — remote test");
|
||||
let send = use_memo((), |_| Mutex::new(WebSocket::open(url().as_str()).unwrap()));
|
||||
let send_cb = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |msg: ServerToHostMessage| {
|
||||
let send = send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
send.lock()
|
||||
.await
|
||||
.send(gloo::net::websocket::Message::Bytes({
|
||||
let mut v = Vec::new();
|
||||
ciborium::into_writer(&HostMessage::Echo(msg.clone()), &mut v).unwrap();
|
||||
v
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
loop {
|
||||
match send.lock().await.try_next().await {
|
||||
Ok(Some(Message::Bytes(b))) => {
|
||||
let recv: ServerToHostMessage =
|
||||
ciborium::from_reader(b.as_slice()).unwrap();
|
||||
if recv == msg {
|
||||
log::debug!("recv'd echo");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok(_) => panic!("got text message"),
|
||||
Err(err) => panic!("{err}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<TestScreens send={send_cb} />
|
||||
}
|
||||
}
|
||||
|
|
@ -72,14 +72,18 @@ pub fn CharacterCard(CharacterCardProps { faint, char, dead }: &CharacterCardPro
|
|||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CharacterTargetCardProps {
|
||||
pub ident: CharacterIdentity,
|
||||
#[prop_or_default]
|
||||
pub classes: Classes,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn CharacterTargetCard(CharacterTargetCardProps { ident }: &CharacterTargetCardProps) -> Html {
|
||||
pub fn CharacterTargetCard(
|
||||
CharacterTargetCardProps { ident, classes }: &CharacterTargetCardProps,
|
||||
) -> Html {
|
||||
let name = ident.name.clone();
|
||||
|
||||
html! {
|
||||
<span class={classes!("character-span")}>
|
||||
<span class={classes!("character-span", classes.clone())}>
|
||||
<span class={classes!("number")}><b>{ident.number.get()}</b></span>
|
||||
<span class="name">{name}</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,17 @@
|
|||
// 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 werewolves_proto::game::GameOver;
|
||||
use yew::prelude::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ mod assets;
|
|||
mod class;
|
||||
mod clients;
|
||||
mod storage;
|
||||
mod test_util;
|
||||
mod components {
|
||||
werewolves_macros::include_path!("werewolves/src/components");
|
||||
pub mod attributes {
|
||||
|
|
@ -45,9 +46,13 @@ const BUILD_ID_LONG: &str = werewolves_macros::build_id_long!();
|
|||
const BUILD_DIRTY: bool = werewolves_macros::build_dirty!();
|
||||
const BUILD_TIME: &str = werewolves_macros::build_time!();
|
||||
|
||||
use crate::clients::{
|
||||
client::{Client2, ClientContext},
|
||||
host::{Host, HostEvent},
|
||||
use crate::{
|
||||
clients::{
|
||||
client::{Client2, ClientContext},
|
||||
host::{Host, HostEvent},
|
||||
test_remote::TestClientRemote,
|
||||
},
|
||||
test_util::TestScreens,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
|
|
@ -65,7 +70,9 @@ fn main() {
|
|||
let error_callback =
|
||||
Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err));
|
||||
|
||||
if path.starts_with("/host") {
|
||||
if path.starts_with("/host/test") {
|
||||
yew::Renderer::<TestClientRemote>::with_root(app_element).render();
|
||||
} else if path.starts_with("/host") {
|
||||
let host = yew::Renderer::<Host>::with_root(app_element).render();
|
||||
if path.starts_with("/host/big") {
|
||||
host.send_message(HostEvent::SetBigScreenState(true));
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ pub fn BeholderSawNothing() -> Html {
|
|||
<h1 class="intel">{"BEHOLDER"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h1>{"YOUR TARGET HAS DIED"}</h1>
|
||||
<h1><Icon source={IconSource::RedX} icon_type={IconType::Informational}/></h1>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::RedX}/>
|
||||
</div>
|
||||
<h1>{"BUT SAW NOTHING"}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ pub fn GuardianPagePreviousProtect1(GuardianPageProps { previous }: &GuardianPag
|
|||
<h1 class="defensive">{"GUARDIAN"}</h1>
|
||||
<div class="information defensive faint">
|
||||
<h2>{"LAST TIME YOU PROTECTED"}</h2>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::ShieldAndSword} />
|
||||
</div>
|
||||
<div class="info-player-list">
|
||||
<CharacterTargetCard ident={previous.clone()} />
|
||||
</div>
|
||||
|
|
@ -74,6 +77,9 @@ pub fn GuardianPagePreviousProtect2(GuardianPageProps { previous }: &GuardianPag
|
|||
<h1 class="defensive">{"GUARDIAN"}</h1>
|
||||
<div class="information defensive faint">
|
||||
<h2>{"LAST TIME YOU PROTECTED"}</h2>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::ShieldAndSword} />
|
||||
</div>
|
||||
<div class="info-player-list">
|
||||
<CharacterTargetCard ident={previous.clone()} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
use werewolves_proto::message::night::Visits;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{CharacterTargetCard, Icon, IconSource};
|
||||
use crate::components::{CharacterTargetCard, Icon, IconSource, Identity};
|
||||
|
||||
#[function_component]
|
||||
pub fn InsomniacPage1() -> Html {
|
||||
|
|
@ -46,7 +46,10 @@ pub fn InsomniacResult(InsomniacResultProps { visits }: &InsomniacResultProps) -
|
|||
.iter()
|
||||
.map(|visitor| {
|
||||
html! {
|
||||
<CharacterTargetCard ident={visitor.clone()}/>
|
||||
<div class="identity intel">
|
||||
<span class="number">{visitor.number.get()}</span>
|
||||
<span class="name">{visitor.name.clone()}</span>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
|
@ -55,7 +58,7 @@ pub fn InsomniacResult(InsomniacResultProps { visits }: &InsomniacResultProps) -
|
|||
<h1 class="intel">{"INSOMNIAC"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"YOU WERE VISITED IN THE NIGHT BY:"}</h2>
|
||||
<div class="info-player-list large">
|
||||
<div class="info-player-boxes">
|
||||
{visitors}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ pub fn MorticianPage1() -> Html {
|
|||
<span class="yellow">{"DEAD"}</span>
|
||||
{" PLAYER"}
|
||||
</h2>
|
||||
<div class="icons">
|
||||
<Icon source={IconSource::Mortician} icon_type={IconType::Informational}/>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Mortician}/>
|
||||
</div>
|
||||
<h3 class="yellow">
|
||||
{"YOU WILL LEARN THE CAUSE "}
|
||||
|
|
@ -70,12 +70,9 @@ pub fn MorticianResultPage(
|
|||
<h1 class="intel">{"MORTICIAN"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"YOUR TARGET DIED TO"}</h2>
|
||||
<h4>
|
||||
<Icon
|
||||
source={icon}
|
||||
icon_type={IconType::Informational}
|
||||
/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={icon}/>
|
||||
</div>
|
||||
<h3 class="yellow">{text}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,449 @@
|
|||
// 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/>.
|
||||
mod prompt;
|
||||
mod result;
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
use werewolves_proto::{
|
||||
character::CharacterId,
|
||||
diedto::DiedToTitle,
|
||||
message::{
|
||||
CharacterIdentity,
|
||||
host::{HostMessage, ServerToHostMessage},
|
||||
night::{
|
||||
ActionPrompt, ActionPromptTitle, ActionResult, ActionResultTitle, ActionType, Visits,
|
||||
},
|
||||
},
|
||||
role::{Alignment, AlignmentEq, Killer, Powerful, RoleTitle},
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::{
|
||||
components::Button,
|
||||
test_util::{prompt::PromptScreenTest, result::ResultScreenTest},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct TestScreensProps {
|
||||
pub send: Callback<ServerToHostMessage>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn TestScreens(TestScreensProps { send }: &TestScreensProps) -> Html {
|
||||
let screen: UseStateHandle<Option<TestScreen>> = use_state(|| None);
|
||||
let page = use_state(|| 0usize);
|
||||
|
||||
let back = {
|
||||
let screen = screen.setter();
|
||||
Callback::from(move |_| screen.set(None))
|
||||
};
|
||||
|
||||
let send = {
|
||||
let send = send.clone();
|
||||
let page = page.clone();
|
||||
let screen = screen.clone();
|
||||
Callback::from(move |msg: ServerToHostMessage| {
|
||||
match &msg {
|
||||
ServerToHostMessage::ActionPrompt(prompt, new_page) => {
|
||||
if *page != *new_page {
|
||||
page.set(*new_page);
|
||||
}
|
||||
if let Some(TestScreen::Prompt(current)) = &*screen
|
||||
&& current != prompt
|
||||
{
|
||||
screen.set(Some(TestScreen::Prompt(prompt.clone())));
|
||||
}
|
||||
}
|
||||
ServerToHostMessage::ActionResult(_, result) => {
|
||||
screen.set(Some(TestScreen::Result(result.clone())));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
send.emit(msg);
|
||||
})
|
||||
};
|
||||
|
||||
let screen_settings = screen.as_ref().map(|screen| match screen {
|
||||
TestScreen::Prompt(p) => html! {
|
||||
<PromptScreenTest
|
||||
prompt={p.clone()}
|
||||
page={*page}
|
||||
back={back}
|
||||
send={send.clone()}
|
||||
/>
|
||||
},
|
||||
TestScreen::Result(result) => html! {
|
||||
<ResultScreenTest
|
||||
result={result.clone()}
|
||||
send={send.clone()}
|
||||
back={back}
|
||||
/>
|
||||
},
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class="test-screen">
|
||||
<TestScreenSelector screen={screen.clone()} send={send.clone()}/>
|
||||
{screen_settings}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct TestScreenSelectorProps {
|
||||
pub screen: UseStateHandle<Option<TestScreen>>,
|
||||
pub send: Callback<ServerToHostMessage>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn TestScreenSelector(
|
||||
TestScreenSelectorProps { screen, send }: &TestScreenSelectorProps,
|
||||
) -> Html {
|
||||
let prompts = ActionPromptTitle::ALL
|
||||
.into_iter()
|
||||
.map(|title| {
|
||||
let TestScreen::Prompt(prompt) = Into::<TestScreen>::into(title) else {
|
||||
unreachable!()
|
||||
};
|
||||
let picked_class = if let Some(TestScreen::Prompt(current)) = &**screen
|
||||
&& current.title() == title
|
||||
{
|
||||
Some("selected")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let callback = {
|
||||
let screen = screen.clone();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let TestScreen::Prompt(prompt) = Into::<TestScreen>::into(title) else {
|
||||
unreachable!()
|
||||
};
|
||||
screen.set(Some(TestScreen::Prompt(prompt.clone())));
|
||||
send.emit(ServerToHostMessage::ActionPrompt(prompt, 0));
|
||||
})
|
||||
};
|
||||
let class = prompt_class(&prompt);
|
||||
html! {
|
||||
<li>
|
||||
<Button on_click={callback} classes={classes!(class, picked_class)}>
|
||||
{format!("{title:?}")}
|
||||
</Button>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
||||
let results = ActionResultTitle::ALL
|
||||
.into_iter()
|
||||
.filter(|title| !matches!(title, ActionResultTitle::Continue))
|
||||
.map(|title| {
|
||||
let TestScreen::Result(result) = Into::<TestScreen>::into(title) else {
|
||||
unreachable!()
|
||||
};
|
||||
let picked_class = if let Some(TestScreen::Result(current)) = &**screen
|
||||
&& current.title() == title
|
||||
{
|
||||
Some("selected")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let callback = {
|
||||
let screen = screen.clone();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let TestScreen::Result(result) = Into::<TestScreen>::into(title) else {
|
||||
unreachable!()
|
||||
};
|
||||
screen.set(Some(TestScreen::Result(result.clone())));
|
||||
send.emit(ServerToHostMessage::ActionResult(None, result));
|
||||
})
|
||||
};
|
||||
let class = result_class(&result);
|
||||
html! {
|
||||
<li>
|
||||
<Button on_click={callback} classes={classes!(picked_class, class)}>
|
||||
{format!("{title:?}")}
|
||||
</Button>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
html! {
|
||||
<div class="test-screen-selector">
|
||||
<div class="category">
|
||||
<label>{"prompts"}</label>
|
||||
<ul class="test-screens">
|
||||
{prompts}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="category">
|
||||
<label>{"results"}</label>
|
||||
<ul class="test-screens">
|
||||
{results}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn identities(num: usize) -> Box<[CharacterIdentity]> {
|
||||
(1..=num)
|
||||
.map(|num| {
|
||||
CharacterIdentity::new(
|
||||
CharacterId::from_u128(num as _),
|
||||
format!("Player {num}"),
|
||||
Some("they/them".into()),
|
||||
NonZeroU8::new(num as _).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn identity() -> CharacterIdentity {
|
||||
identities(1).into_iter().next().unwrap()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TestScreen {
|
||||
Prompt(ActionPrompt),
|
||||
Result(ActionResult),
|
||||
}
|
||||
|
||||
impl From<ActionResultTitle> for TestScreen {
|
||||
fn from(value: ActionResultTitle) -> Self {
|
||||
TestScreen::Result(match value {
|
||||
ActionResultTitle::RoleBlocked => ActionResult::RoleBlocked,
|
||||
ActionResultTitle::Drunk => ActionResult::Drunk,
|
||||
ActionResultTitle::Seer => ActionResult::Seer(Alignment::Village),
|
||||
ActionResultTitle::PowerSeer => ActionResult::PowerSeer {
|
||||
powerful: Powerful::Powerful,
|
||||
},
|
||||
ActionResultTitle::Adjudicator => ActionResult::Adjudicator {
|
||||
killer: Killer::Killer,
|
||||
},
|
||||
ActionResultTitle::Arcanist => ActionResult::Arcanist(AlignmentEq::Same),
|
||||
ActionResultTitle::GraveDigger => ActionResult::GraveDigger(None),
|
||||
ActionResultTitle::Mortician => ActionResult::Mortician(DiedToTitle::Execution),
|
||||
ActionResultTitle::Insomniac => ActionResult::Insomniac(Visits::new(identities(2))),
|
||||
ActionResultTitle::Empath => ActionResult::Empath { scapegoat: true },
|
||||
ActionResultTitle::BeholderSawNothing => ActionResult::BeholderSawNothing,
|
||||
ActionResultTitle::BeholderSawEverything => ActionResult::BeholderSawEverything,
|
||||
ActionResultTitle::GoBackToSleep => ActionResult::GoBackToSleep,
|
||||
ActionResultTitle::ShiftFailed => ActionResult::ShiftFailed,
|
||||
ActionResultTitle::Continue => ActionResult::Continue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ActionPromptTitle> for TestScreen {
|
||||
fn from(value: ActionPromptTitle) -> Self {
|
||||
Self::Prompt(match value {
|
||||
ActionPromptTitle::CoverOfDarkness => ActionPrompt::CoverOfDarkness,
|
||||
ActionPromptTitle::WolvesIntro => ActionPrompt::WolvesIntro {
|
||||
wolves: identities(5)
|
||||
.into_iter()
|
||||
.zip([
|
||||
RoleTitle::Werewolf,
|
||||
RoleTitle::AlphaWolf,
|
||||
RoleTitle::DireWolf,
|
||||
RoleTitle::LoneWolf,
|
||||
RoleTitle::Bloodletter,
|
||||
])
|
||||
.collect(),
|
||||
},
|
||||
ActionPromptTitle::RoleChange => ActionPrompt::RoleChange {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
new_role: RoleTitle::Adjudicator,
|
||||
},
|
||||
ActionPromptTitle::ElderReveal => ActionPrompt::ElderReveal {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
},
|
||||
ActionPromptTitle::Seer => ActionPrompt::Seer {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Protector => ActionPrompt::Protector {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
targets: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Arcanist => ActionPrompt::Arcanist {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: (None, None),
|
||||
},
|
||||
ActionPromptTitle::Gravedigger => ActionPrompt::Gravedigger {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
dead_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Hunter => ActionPrompt::Hunter {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
current_target: None,
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Militia => ActionPrompt::Militia {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::MapleWolf => ActionPrompt::MapleWolf {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
kill_or_die: false,
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Guardian => ActionPrompt::Guardian {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
previous: None,
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Adjudicator => ActionPrompt::Adjudicator {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::PowerSeer => ActionPrompt::PowerSeer {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Mortician => ActionPrompt::Mortician {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
dead_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Beholder => ActionPrompt::Beholder {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::MasonsWake => ActionPrompt::MasonsWake {
|
||||
leader: identities(1).into_iter().next().unwrap(),
|
||||
masons: identities(3),
|
||||
},
|
||||
ActionPromptTitle::MasonLeaderRecruit => ActionPrompt::MasonLeaderRecruit {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
recruits_left: NonZeroU8::new(3).unwrap(),
|
||||
potential_recruits: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Empath => ActionPrompt::Empath {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Vindicator => ActionPrompt::Vindicator {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::PyreMaster => ActionPrompt::PyreMaster {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::WolfPackKill => ActionPrompt::WolfPackKill {
|
||||
living_villagers: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Shapeshifter => ActionPrompt::Shapeshifter {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
},
|
||||
ActionPromptTitle::AlphaWolf => ActionPrompt::AlphaWolf {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_villagers: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::DireWolf => ActionPrompt::DireWolf {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::LoneWolfKill => ActionPrompt::LoneWolfKill {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::Insomniac => ActionPrompt::Insomniac {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
},
|
||||
ActionPromptTitle::Bloodletter => ActionPrompt::Bloodletter {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
},
|
||||
ActionPromptTitle::TraitorIntro => ActionPrompt::TraitorIntro {
|
||||
character_id: identities(1).into_iter().next().unwrap(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn result_class(result: &ActionResult) -> Option<&'static str> {
|
||||
match result.title() {
|
||||
ActionResultTitle::Drunk | ActionResultTitle::RoleBlocked => Some("drunk"),
|
||||
ActionResultTitle::PowerSeer
|
||||
| ActionResultTitle::Adjudicator
|
||||
| ActionResultTitle::Arcanist
|
||||
| ActionResultTitle::GraveDigger
|
||||
| ActionResultTitle::Mortician
|
||||
| ActionResultTitle::Insomniac
|
||||
| ActionResultTitle::Empath
|
||||
| ActionResultTitle::BeholderSawNothing
|
||||
| ActionResultTitle::BeholderSawEverything
|
||||
| ActionResultTitle::Seer => Some("intel"),
|
||||
ActionResultTitle::ShiftFailed => Some("wolves"),
|
||||
ActionResultTitle::GoBackToSleep | ActionResultTitle::Continue => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_class(prompt: &ActionPrompt) -> Option<&'static str> {
|
||||
match prompt {
|
||||
ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
| ActionPrompt::CoverOfDarkness => None,
|
||||
ActionPrompt::Seer { .. }
|
||||
| ActionPrompt::Arcanist { .. }
|
||||
| ActionPrompt::Gravedigger { .. }
|
||||
| ActionPrompt::Adjudicator { .. }
|
||||
| ActionPrompt::PowerSeer { .. }
|
||||
| ActionPrompt::Mortician { .. }
|
||||
| ActionPrompt::Beholder { .. }
|
||||
| ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::MasonLeaderRecruit { .. }
|
||||
| ActionPrompt::Empath { .. } => Some("intel"),
|
||||
ActionPrompt::Protector { .. }
|
||||
| ActionPrompt::Guardian { .. }
|
||||
| ActionPrompt::Vindicator { .. } => Some("defensive"),
|
||||
ActionPrompt::Hunter { .. }
|
||||
| ActionPrompt::Militia { .. }
|
||||
| ActionPrompt::MapleWolf { .. }
|
||||
| ActionPrompt::PyreMaster { .. } => Some("offensive"),
|
||||
ActionPrompt::WolvesIntro { .. }
|
||||
| ActionPrompt::WolfPackKill { .. }
|
||||
| ActionPrompt::Shapeshifter { .. }
|
||||
| ActionPrompt::AlphaWolf { .. }
|
||||
| ActionPrompt::DireWolf { .. }
|
||||
| ActionPrompt::LoneWolfKill { .. }
|
||||
| ActionPrompt::Insomniac { .. }
|
||||
| ActionPrompt::Bloodletter { .. } => Some("wolves"),
|
||||
ActionPrompt::TraitorIntro { .. } => Some("traitor"),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,453 @@
|
|||
// 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 yew::prelude::*;
|
||||
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
use web_sys::HtmlSelectElement;
|
||||
use werewolves_proto::{
|
||||
message::{
|
||||
host::{HostGameMessage, HostMessage, HostNightMessage, ServerToHostMessage},
|
||||
night::ActionPrompt,
|
||||
},
|
||||
player::RoleChange,
|
||||
role::{PreviousGuardianAction, RoleTitle},
|
||||
};
|
||||
|
||||
use crate::{components::Button, pages::RolePage, test_util::identities};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct PromptScreenTestProps {
|
||||
pub prompt: ActionPrompt,
|
||||
pub page: usize,
|
||||
pub send: Callback<ServerToHostMessage>,
|
||||
pub back: Callback<()>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn PromptScreenTest(
|
||||
PromptScreenTestProps {
|
||||
prompt,
|
||||
page,
|
||||
send,
|
||||
back,
|
||||
}: &PromptScreenTestProps,
|
||||
) -> Html {
|
||||
let options = match prompt {
|
||||
ActionPrompt::WolvesIntro { wolves } => {
|
||||
let dec_disabled = wolves
|
||||
.is_empty()
|
||||
.then_some(String::from("already have zero wolves"));
|
||||
let dec = {
|
||||
let current = wolves.len();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::WolvesIntro {
|
||||
wolves: super::identities(current - 1)
|
||||
.into_iter()
|
||||
.zip(RoleTitle::ALL.into_iter().filter(|w| w.wolf()).cycle())
|
||||
.collect(),
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
let inc_disabled =
|
||||
(wolves.len() + 1 > 0xFF).then_some(String::from("already at max wolves"));
|
||||
let inc = {
|
||||
let current = wolves.len();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::WolvesIntro {
|
||||
wolves: super::identities(current + 1)
|
||||
.into_iter()
|
||||
.zip(RoleTitle::ALL.into_iter().filter(|w| w.wolf()).cycle())
|
||||
.collect(),
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-number">
|
||||
<Button
|
||||
on_click={dec}
|
||||
disabled_reason={dec_disabled}
|
||||
>
|
||||
{"decrease"}
|
||||
</Button>
|
||||
<label>{wolves.len()}{" wolves"}</label>
|
||||
<Button
|
||||
on_click={inc}
|
||||
disabled_reason={inc_disabled}
|
||||
>
|
||||
{"increase"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionPrompt::RoleChange { new_role, .. } => {
|
||||
let roles = RoleTitle::ALL
|
||||
.into_iter()
|
||||
.map(|role| {
|
||||
html! {
|
||||
<option selected={role == *new_role}>{role.to_string()}</option>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let on_change_cb = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |ev: Event| {
|
||||
if let Some(select) = ev.target_dyn_into::<HtmlSelectElement>() {
|
||||
let selected = select.selected_index();
|
||||
if selected == -1 {
|
||||
return;
|
||||
}
|
||||
if let Some(new_role) = RoleTitle::ALL.into_iter().nth(selected as _) {
|
||||
let new_prompt = ActionPrompt::RoleChange {
|
||||
character_id: super::identity(),
|
||||
new_role,
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let on_wheel = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |ev: WheelEvent| {
|
||||
let Some(target) = ev.target_dyn_into::<HtmlSelectElement>() else {
|
||||
return;
|
||||
};
|
||||
let index = target.selected_index();
|
||||
let new_index = match ev.delta_y().total_cmp(&0.0) {
|
||||
core::cmp::Ordering::Equal => return,
|
||||
core::cmp::Ordering::Less => {
|
||||
if index != 0 {
|
||||
index - 1
|
||||
} else {
|
||||
(target.children().length() - 1) as i32
|
||||
}
|
||||
}
|
||||
core::cmp::Ordering::Greater => {
|
||||
if index + 1 < target.children().length() as i32 {
|
||||
index + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
};
|
||||
target.set_selected_index(new_index);
|
||||
if let Some(new_role) = RoleTitle::ALL.into_iter().nth(new_index as _) {
|
||||
let new_prompt = ActionPrompt::RoleChange {
|
||||
character_id: super::identity(),
|
||||
new_role,
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
}
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-option">
|
||||
<label>{"new role"}</label>
|
||||
<select onwheel={on_wheel} onchange={on_change_cb}>
|
||||
{roles}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
ActionPrompt::Hunter { current_target, .. } => {
|
||||
let toggle_target = current_target.is_none().then_some(super::identity());
|
||||
let on_toggle = {
|
||||
let toggle_target = toggle_target.clone();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::Hunter {
|
||||
character_id: super::identity(),
|
||||
current_target: toggle_target.clone(),
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
|
||||
let button_text = if toggle_target.is_some() {
|
||||
"remove previous target"
|
||||
} else {
|
||||
"set previous target"
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-options">
|
||||
<Button on_click={on_toggle}>{button_text}</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionPrompt::MapleWolf { kill_or_die, .. } => {
|
||||
let toggle = !*kill_or_die;
|
||||
let on_toggle = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::MapleWolf {
|
||||
character_id: super::identity(),
|
||||
kill_or_die: toggle,
|
||||
living_players: identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
|
||||
let button_text = if toggle {
|
||||
"turn starving"
|
||||
} else {
|
||||
"feed the poor boy"
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-options">
|
||||
<Button on_click={on_toggle}>{button_text}</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionPrompt::Guardian { previous, .. } => {
|
||||
let none_disabled = previous.is_none().then_some(String::from("already set"));
|
||||
let prev_none = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::Guardian {
|
||||
character_id: super::identity(),
|
||||
previous: None,
|
||||
living_players: super::identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
|
||||
let prot_disabled = previous.as_ref().and_then(|prev| match prev {
|
||||
PreviousGuardianAction::Protect(_) => Some(String::from("already set")),
|
||||
PreviousGuardianAction::Guard(_) => None,
|
||||
});
|
||||
let prev_prot = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::Guardian {
|
||||
character_id: super::identity(),
|
||||
previous: Some(PreviousGuardianAction::Protect(
|
||||
super::identities(2).into_iter().nth(1).unwrap(),
|
||||
)),
|
||||
living_players: super::identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
let guard_disabled = previous.as_ref().and_then(|prev| match prev {
|
||||
PreviousGuardianAction::Guard(_) => Some(String::from("already set")),
|
||||
PreviousGuardianAction::Protect(_) => None,
|
||||
});
|
||||
let prev_guard = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::Guardian {
|
||||
character_id: super::identity(),
|
||||
previous: Some(PreviousGuardianAction::Guard(
|
||||
super::identities(2).into_iter().nth(1).unwrap(),
|
||||
)),
|
||||
living_players: super::identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<>
|
||||
<label>{"previous protect"}</label>
|
||||
<div class="option-set">
|
||||
<Button disabled_reason={none_disabled} on_click={prev_none}>{"none"}</Button>
|
||||
<Button disabled_reason={prot_disabled} on_click={prev_prot}>{"protected"}</Button>
|
||||
<Button disabled_reason={guard_disabled} on_click={prev_guard}>{"guarded"}</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
ActionPrompt::MasonsWake { masons, .. } => {
|
||||
let dec_disabled =
|
||||
(masons.len() == 1).then_some(String::from("already at min recruits"));
|
||||
let dec = {
|
||||
let current = masons.len();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::MasonsWake {
|
||||
leader: super::identity(),
|
||||
masons: super::identities(current.saturating_sub(1)),
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
let inc_disabled =
|
||||
(masons.len() > 0xFF).then_some(String::from("already at max wolves"));
|
||||
let inc = {
|
||||
let current = masons.len();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::MasonsWake {
|
||||
leader: super::identity(),
|
||||
masons: super::identities(current.saturating_add(1)),
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-number">
|
||||
<Button
|
||||
on_click={dec}
|
||||
disabled_reason={dec_disabled}
|
||||
>
|
||||
{"decrease"}
|
||||
</Button>
|
||||
<label>{masons.len()}{" masons"}</label>
|
||||
<Button
|
||||
on_click={inc}
|
||||
disabled_reason={inc_disabled}
|
||||
>
|
||||
{"increase"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionPrompt::MasonLeaderRecruit { recruits_left, .. } => {
|
||||
let dec_disabled =
|
||||
(recruits_left.get() == 1).then_some(String::from("already at min recruits"));
|
||||
let dec = {
|
||||
let current = recruits_left.get();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::MasonLeaderRecruit {
|
||||
character_id: super::identity(),
|
||||
recruits_left: NonZeroU8::new(current.saturating_sub(1)).unwrap(),
|
||||
potential_recruits: identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
let inc_disabled =
|
||||
(recruits_left.get() == 0xFF).then_some(String::from("already at max wolves"));
|
||||
let inc = {
|
||||
let current = recruits_left.get();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
let new_prompt = ActionPrompt::MasonLeaderRecruit {
|
||||
character_id: super::identity(),
|
||||
recruits_left: NonZeroU8::new(current.saturating_add(1)).unwrap(),
|
||||
potential_recruits: identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-number">
|
||||
<Button
|
||||
on_click={dec}
|
||||
disabled_reason={dec_disabled}
|
||||
>
|
||||
{"decrease"}
|
||||
</Button>
|
||||
<label>{recruits_left.get()}{" recruits"}</label>
|
||||
<Button
|
||||
on_click={inc}
|
||||
disabled_reason={inc_disabled}
|
||||
>
|
||||
{"increase"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
ActionPrompt::Protector { .. }
|
||||
| ActionPrompt::Arcanist { .. }
|
||||
| ActionPrompt::Gravedigger { .. }
|
||||
| ActionPrompt::Militia { .. }
|
||||
| ActionPrompt::Adjudicator { .. }
|
||||
| ActionPrompt::PowerSeer { .. }
|
||||
| ActionPrompt::Mortician { .. }
|
||||
| ActionPrompt::Beholder { .. }
|
||||
| ActionPrompt::Empath { .. }
|
||||
| ActionPrompt::Vindicator { .. }
|
||||
| ActionPrompt::PyreMaster { .. }
|
||||
| ActionPrompt::WolfPackKill { .. }
|
||||
| ActionPrompt::AlphaWolf { .. }
|
||||
| ActionPrompt::DireWolf { .. }
|
||||
| ActionPrompt::LoneWolfKill { .. }
|
||||
| ActionPrompt::Bloodletter { .. }
|
||||
| ActionPrompt::Seer { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::TraitorIntro { .. }
|
||||
| ActionPrompt::Insomniac { .. }
|
||||
| ActionPrompt::Shapeshifter { .. }
|
||||
| ActionPrompt::CoverOfDarkness => html! {},
|
||||
};
|
||||
let prev_page_disabled = (*page == 0).then_some(String::from("on page zero"));
|
||||
let prev_page = {
|
||||
let send = send.clone();
|
||||
let prompt = prompt.clone();
|
||||
let page = *page;
|
||||
Callback::from(move |_| {
|
||||
send.emit(ServerToHostMessage::ActionPrompt(
|
||||
prompt.clone(),
|
||||
page.saturating_sub(1),
|
||||
));
|
||||
})
|
||||
};
|
||||
let next_page_disabled = (*page + 1 >= prompt.role_pages(true).len())
|
||||
.then_some(String::from("already at last page"));
|
||||
let next_page = {
|
||||
let send = send.clone();
|
||||
let prompt = prompt.clone();
|
||||
let page = *page;
|
||||
Callback::from(move |_| {
|
||||
send.emit(ServerToHostMessage::ActionPrompt(
|
||||
prompt.clone(),
|
||||
page.saturating_add(1),
|
||||
));
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-test">
|
||||
<Button on_click={back.clone()} classes={classes!("close")}>{"close"}</Button>
|
||||
<label>{prompt.title().to_string()}</label>
|
||||
<div class="prompt-number">
|
||||
<Button
|
||||
on_click={prev_page}
|
||||
disabled_reason={prev_page_disabled}
|
||||
>
|
||||
{"previous page"}
|
||||
</Button>
|
||||
<span>{page.saturating_add(1)}{"/"}{prompt.role_pages(true).len().max(1)}</span>
|
||||
<Button
|
||||
on_click={next_page}
|
||||
disabled_reason={next_page_disabled}
|
||||
>
|
||||
{"next page"}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="prompt-options">
|
||||
{options}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
// 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 yew::prelude::*;
|
||||
|
||||
use core::num::NonZeroU8;
|
||||
use std::rc::Rc;
|
||||
|
||||
use web_sys::HtmlSelectElement;
|
||||
use werewolves_proto::{
|
||||
diedto::DiedToTitle,
|
||||
message::{
|
||||
host::{HostGameMessage, HostMessage, HostNightMessage, ServerToHostMessage},
|
||||
night::{ActionPrompt, ActionResult, ActionResultTitle, Visits},
|
||||
},
|
||||
player::RoleChange,
|
||||
role::{Alignment, PreviousGuardianAction, RoleTitle},
|
||||
};
|
||||
|
||||
use crate::{components::Button, pages::RolePage, test_util::identities};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct ResultScreenTestProps {
|
||||
pub result: ActionResult,
|
||||
pub send: Callback<ServerToHostMessage>,
|
||||
pub back: Callback<()>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ResultScreenTest(
|
||||
ResultScreenTestProps { result, send, back }: &ResultScreenTestProps,
|
||||
) -> Html {
|
||||
let options = match result {
|
||||
ActionResult::BeholderSawNothing
|
||||
| ActionResult::BeholderSawEverything
|
||||
| ActionResult::GoBackToSleep
|
||||
| ActionResult::ShiftFailed
|
||||
| ActionResult::Continue
|
||||
| ActionResult::Drunk
|
||||
| ActionResult::RoleBlocked => html! {},
|
||||
ActionResult::Seer(alignment) => {
|
||||
let all = Alignment::ALL
|
||||
.into_iter()
|
||||
.map(|align| {
|
||||
let on_click = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Seer(align),
|
||||
));
|
||||
})
|
||||
};
|
||||
let disabled = (align == *alignment).then_some(String::from("already selected")) ;
|
||||
html! {
|
||||
<Button on_click={on_click} disabled_reason={disabled}>{align.to_string()}</Button>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
html! {
|
||||
<div class="prompt-options">
|
||||
<div class="option-set">
|
||||
{all}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionResult::PowerSeer { powerful } => {
|
||||
let on_toggle = {
|
||||
let set = !*powerful;
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::PowerSeer { powerful: set },
|
||||
));
|
||||
})
|
||||
};
|
||||
let text = if powerful.powerful() {
|
||||
"make not powerful"
|
||||
} else {
|
||||
"make powerful"
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-options">
|
||||
<Button on_click={on_toggle}>{text}</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionResult::Adjudicator { killer } => {
|
||||
let on_toggle = {
|
||||
let set = !*killer;
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Adjudicator { killer: set },
|
||||
));
|
||||
})
|
||||
};
|
||||
let text = if killer.killer() {
|
||||
"make not killer"
|
||||
} else {
|
||||
"make killer"
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-options">
|
||||
<Button on_click={on_toggle}>{text}</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionResult::Arcanist(alignment_eq) => {
|
||||
let on_toggle = {
|
||||
let set = !*alignment_eq;
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Arcanist(set),
|
||||
));
|
||||
})
|
||||
};
|
||||
let text = if alignment_eq.same() {
|
||||
"make different"
|
||||
} else {
|
||||
"make same"
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-options">
|
||||
<Button on_click={on_toggle}>{text}</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionResult::GraveDigger(role_title) => {
|
||||
let possibilities = [None]
|
||||
.into_iter()
|
||||
.chain(RoleTitle::ALL.into_iter().map(Some))
|
||||
.collect::<Rc<[_]>>();
|
||||
let roles = possibilities
|
||||
.iter()
|
||||
.map(|role| {
|
||||
let text = role
|
||||
.as_ref()
|
||||
.map(|r| r.to_string())
|
||||
.unwrap_or(String::from("empty grave"));
|
||||
html! {
|
||||
<option selected={role == role_title}>{text}</option>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let on_change_cb = {
|
||||
let send = send.clone();
|
||||
let possibilities = possibilities.clone();
|
||||
Callback::from(move |ev: Event| {
|
||||
if let Some(select) = ev.target_dyn_into::<HtmlSelectElement>() {
|
||||
let selected = select.selected_index();
|
||||
if selected == -1 {
|
||||
return;
|
||||
}
|
||||
if let Some(new_role) = possibilities.get(selected as usize) {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::GraveDigger(*new_role),
|
||||
));
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let on_wheel = {
|
||||
let send = send.clone();
|
||||
let possibilities = possibilities.clone();
|
||||
Callback::from(move |ev: WheelEvent| {
|
||||
let Some(target) = ev.target_dyn_into::<HtmlSelectElement>() else {
|
||||
return;
|
||||
};
|
||||
let index = target.selected_index();
|
||||
let new_index = match ev.delta_y().total_cmp(&0.0) {
|
||||
core::cmp::Ordering::Equal => return,
|
||||
core::cmp::Ordering::Less => {
|
||||
if index != 0 {
|
||||
index - 1
|
||||
} else {
|
||||
(target.children().length() - 1) as i32
|
||||
}
|
||||
}
|
||||
core::cmp::Ordering::Greater => {
|
||||
if index + 1 < target.children().length() as i32 {
|
||||
index + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
};
|
||||
target.set_selected_index(new_index);
|
||||
if let Some(new_role) = possibilities.get(new_index as usize) {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::GraveDigger(*new_role),
|
||||
));
|
||||
}
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<div class="result-option">
|
||||
<label>{"dug up"}</label>
|
||||
<select onwheel={on_wheel} onchange={on_change_cb}>
|
||||
{roles}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionResult::Mortician(died_to_title) => {
|
||||
let roles = DiedToTitle::ALL
|
||||
.into_iter()
|
||||
.map(|died_to| {
|
||||
html! {
|
||||
<option selected={died_to == *died_to_title}>{died_to.to_string()}</option>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let on_change_cb = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |ev: Event| {
|
||||
if let Some(select) = ev.target_dyn_into::<HtmlSelectElement>() {
|
||||
let selected = select.selected_index();
|
||||
if selected == -1 {
|
||||
return;
|
||||
}
|
||||
if let Some(died_to) = DiedToTitle::ALL.into_iter().nth(selected as _) {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Mortician(died_to),
|
||||
));
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let on_wheel = {
|
||||
let send = send.clone();
|
||||
Callback::from(move |ev: WheelEvent| {
|
||||
let Some(target) = ev.target_dyn_into::<HtmlSelectElement>() else {
|
||||
return;
|
||||
};
|
||||
let index = target.selected_index();
|
||||
let new_index = match ev.delta_y().total_cmp(&0.0) {
|
||||
core::cmp::Ordering::Equal => return,
|
||||
core::cmp::Ordering::Less => {
|
||||
if index != 0 {
|
||||
index - 1
|
||||
} else {
|
||||
(target.children().length() - 1) as i32
|
||||
}
|
||||
}
|
||||
core::cmp::Ordering::Greater => {
|
||||
if index + 1 < target.children().length() as i32 {
|
||||
index + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
};
|
||||
target.set_selected_index(new_index);
|
||||
if let Some(died_to) = DiedToTitle::ALL.into_iter().nth(new_index as _) {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Mortician(died_to),
|
||||
));
|
||||
}
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-option">
|
||||
<label>{"died to"}</label>
|
||||
<select onwheel={on_wheel} onchange={on_change_cb}>
|
||||
{roles}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionResult::Insomniac(visits) => {
|
||||
let dec_disabled =
|
||||
(visits.len() <= 1).then_some(String::from("already have minimum visits"));
|
||||
let dec = {
|
||||
let current = visits.len();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Insomniac(Visits::new(super::identities(
|
||||
current.saturating_sub(1).max(1),
|
||||
))),
|
||||
));
|
||||
})
|
||||
};
|
||||
let inc_disabled =
|
||||
(visits.len() + 1 > 0xFF).then_some(String::from("already at max visits"));
|
||||
let inc = {
|
||||
let current = visits.len();
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Insomniac(Visits::new(super::identities(
|
||||
current.saturating_add(1).max(1),
|
||||
))),
|
||||
));
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<div class="result-number">
|
||||
<Button
|
||||
on_click={dec}
|
||||
disabled_reason={dec_disabled}
|
||||
>
|
||||
{"decrease"}
|
||||
</Button>
|
||||
<label>{visits.len()}{" visits"}</label>
|
||||
<Button
|
||||
on_click={inc}
|
||||
disabled_reason={inc_disabled}
|
||||
>
|
||||
{"increase"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionResult::Empath { scapegoat } => {
|
||||
let on_toggle = {
|
||||
let set = !*scapegoat;
|
||||
let send = send.clone();
|
||||
Callback::from(move |_| {
|
||||
send.emit(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Empath { scapegoat: set },
|
||||
));
|
||||
})
|
||||
};
|
||||
let text = if *scapegoat {
|
||||
"make not scapegoat"
|
||||
} else {
|
||||
"make scapegoat"
|
||||
};
|
||||
html! {
|
||||
<div class="prompt-options">
|
||||
<Button on_click={on_toggle}>{text}</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
};
|
||||
html! {
|
||||
<div class="result-test">
|
||||
<Button on_click={back.clone()} classes={classes!("close")}>{"close"}</Button>
|
||||
<label>{result.title().to_string()}</label>
|
||||
|
||||
<div class="result-options">
|
||||
{options}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue