overrides + arcanist fixes
This commit is contained in:
parent
c8e51f36e2
commit
5a452b220c
|
|
@ -7,6 +7,7 @@ werewolves/img/icons.svg
|
|||
license_headers.fish
|
||||
util/
|
||||
werewolves/Trunk-local.toml
|
||||
public/img/icons.svg
|
||||
|
||||
werewolves-old-client/
|
||||
werewolves-old-server/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="69.549118mm"
|
||||
height="56.160782mm"
|
||||
viewBox="0 0 69.549118 56.160782"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer4"
|
||||
transform="translate(-747.27464,-854.93728)"><g
|
||||
id="g114"><g
|
||||
id="g74-6-0"
|
||||
transform="matrix(0.49535939,0,0,0.49535939,742.24755,744.64487)"><path
|
||||
d="m 41.438814,253.85737 v -11.44064 c -0.10314,0.002 -0.206411,0.006 -0.309542,0.0114 -1.713539,0.0921 -3.402067,0.64565 -4.275191,1.61799 -1.746251,1.94469 -1.477015,6.95296 0.467672,8.69921 0.577742,0.51878 1.869333,0.91432 2.82205,1.08623 0.05301,0.04 0.6259,0.0432 1.295011,0.0258 z m 0,0 v -11.44064 c 0.10314,0.002 0.206411,0.006 0.309542,0.0114 1.713539,0.0921 3.402067,0.64565 4.275191,1.61799 1.746251,1.94469 1.477015,6.95296 -0.467672,8.69921 -0.577742,0.51878 -1.869333,0.91432 -2.82205,1.08623 -0.05301,0.04 -0.6259,0.0432 -1.295011,0.0258 z"
|
||||
style="fill:#0f07ff;stroke:#0f07ff;stroke-width:0.646547"
|
||||
id="path73-2-2" /><path
|
||||
d="m 41.437781,281.85256 v -26.00719 c -0.537817,0.003 -1.074156,0.0344 -1.599386,0.0879 -2.633998,0.53128 -5.422716,2.05847 -6.299357,4.75888 -1.013707,3.1868 -0.65843,6.59772 -0.366903,9.8702 0.3233,3.36773 0.971622,6.70664 1.07487,10.09344 0.63992,0.8794 1.985837,0.77429 2.985348,0.97565 1.39519,0.12946 2.800457,0.21075 4.205428,0.22117 z m 0.0021,0 v -26.00719 c 0.537817,0.003 1.074156,0.0344 1.599386,0.0879 2.633998,0.53128 5.422716,2.05847 6.299357,4.75888 1.013707,3.1868 0.65843,6.59772 0.366903,9.8702 -0.3233,3.36773 -0.971622,6.70664 -1.07487,10.09344 -0.63992,0.8794 -1.985837,0.77429 -2.985348,0.97565 -1.39519,0.12946 -2.800457,0.21075 -4.205428,0.22117 z"
|
||||
style="fill:#0f07ff;stroke:#0f07ff;stroke-width:0.646547"
|
||||
id="path74-6-3" /></g><g
|
||||
id="g74-6-0-0"
|
||||
transform="matrix(0.49535939,0,0,0.49535939,780.79656,744.64437)"><path
|
||||
d="m 41.438814,253.85737 v -11.44064 c -0.10314,0.002 -0.206411,0.006 -0.309542,0.0114 -1.713539,0.0921 -3.402067,0.64565 -4.275191,1.61799 -1.746251,1.94469 -1.477015,6.95296 0.467672,8.69921 0.577742,0.51878 1.869333,0.91432 2.82205,1.08623 0.05301,0.04 0.6259,0.0432 1.295011,0.0258 z m 0,0 v -11.44064 c 0.10314,0.002 0.206411,0.006 0.309542,0.0114 1.713539,0.0921 3.402067,0.64565 4.275191,1.61799 1.746251,1.94469 1.477015,6.95296 -0.467672,8.69921 -0.577742,0.51878 -1.869333,0.91432 -2.82205,1.08623 -0.05301,0.04 -0.6259,0.0432 -1.295011,0.0258 z"
|
||||
style="fill:#0f07ff;stroke:#0f07ff;stroke-width:0.646547"
|
||||
id="path73-2-2-3" /><path
|
||||
d="m 41.437781,281.85256 v -26.00719 c -0.537817,0.003 -1.074156,0.0344 -1.599386,0.0879 -2.633998,0.53128 -5.422716,2.05847 -6.299357,4.75888 -1.013707,3.1868 -0.65843,6.59772 -0.366903,9.8702 0.3233,3.36773 0.971622,6.70664 1.07487,10.09344 0.63992,0.8794 1.985837,0.77429 2.985348,0.97565 1.39519,0.12946 2.800457,0.21075 4.205428,0.22117 z m 0.0021,0 v -26.00719 c 0.537817,0.003 1.074156,0.0344 1.599386,0.0879 2.633998,0.53128 5.422716,2.05847 6.299357,4.75888 1.013707,3.1868 0.65843,6.59772 0.366903,9.8702 -0.3233,3.36773 -0.971622,6.70664 -1.07487,10.09344 -0.63992,0.8794 -1.985837,0.77429 -2.985348,0.97565 -1.39519,0.12946 -2.800457,0.21075 -4.205428,0.22117 z"
|
||||
style="fill:#0f07ff;stroke:#0f07ff;stroke-width:0.646547"
|
||||
id="path74-6-3-0" /></g><g
|
||||
id="g46-3"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="translate(89.910176,11.519392)"><path
|
||||
style="fill:none;fill-opacity:1;stroke:#fff800;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:0.595237"
|
||||
d="m 688.06805,869.55518 -15.10948,-20.06719 -15.30685,19.99488"
|
||||
id="path46-0-6" /><path
|
||||
style="fill:none;fill-opacity:1;stroke:#fff800;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:0.595237"
|
||||
d="m 672.95522,849.48086 -0.008,20.1008"
|
||||
id="path46-5" /></g><path
|
||||
style="fill:#fffc82;fill-opacity:1;stroke:#67653e;stroke-width:0.734271;stroke-opacity:1"
|
||||
id="path45-6"
|
||||
d="m 777.90756,881.06818 a 15.132905,8.016284 0 0 1 -7.56645,6.9423 15.132905,8.016284 0 0 1 -15.1329,0 15.132905,8.016284 0 0 1 -7.56646,-6.9423 h 15.13291 z" /><g
|
||||
id="g46-3-6"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="translate(128.45917,11.519442)"><path
|
||||
style="fill:none;fill-opacity:1;stroke:#fff800;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:0.595237"
|
||||
d="m 688.06805,869.55518 -15.10948,-20.06719 -15.30685,19.99488"
|
||||
id="path46-0-6-1" /><path
|
||||
style="fill:none;fill-opacity:1;stroke:#fff800;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:0.595237"
|
||||
d="m 672.95522,849.48086 -0.008,20.1008"
|
||||
id="path46-5-1" /></g><path
|
||||
style="fill:#fffc82;fill-opacity:1;stroke:#67653e;stroke-width:0.734271;stroke-opacity:1"
|
||||
id="path45-6-5"
|
||||
d="m 816.45657,881.06824 a 15.132905,8.016284 0 0 1 -7.56645,6.9423 15.132905,8.016284 0 0 1 -15.1329,0 15.132905,8.016284 0 0 1 -7.56646,-6.9423 h 15.13291 z" /><rect
|
||||
style="fill:#fffa65;fill-opacity:1;stroke:#67653e;stroke-width:1;stroke-opacity:1"
|
||||
id="rect31-9"
|
||||
width="59.770382"
|
||||
height="2.958041"
|
||||
x="752.13153"
|
||||
y="858.67627"
|
||||
rx="11.1125"
|
||||
ry="1.0782195" /><path
|
||||
d="m 781.92294,855.43738 c -1.08829,0 -1.96422,1.53814 -1.96422,3.44888 v 46.49773 c 0,0.0628 7.2e-4,0.12512 0.003,0.18707 h -10.46448 c -6.15632,0 -11.1125,1.12104 -11.1125,2.51354 0,1.3925 -0.0661,2.51355 -0.0661,2.51355 h 47.42657 v -2.51355 c 0,-1.3925 -4.95617,-2.51354 -11.1125,-2.51354 h -10.5606 c 0.002,-0.062 0.003,-0.12426 0.003,-0.18707 v -46.49773 c 0,-1.91074 -0.87593,-3.44888 -1.96422,-3.44888 z"
|
||||
style="fill:#fff965;fill-opacity:1;stroke:#67653e;stroke-opacity:1"
|
||||
id="path33-0" /></g></g></svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
5293
public/img/icons.svg
5293
public/img/icons.svg
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 866 KiB |
|
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="81.261642mm"
|
||||
height="66.812714mm"
|
||||
viewBox="0 0 81.261642 66.812714"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer4"
|
||||
transform="translate(-658.32806,-803.4284)"><g
|
||||
id="g111"><g
|
||||
id="g112"><g
|
||||
id="g106"
|
||||
transform="translate(-1.7773424,0.18708867)"><g
|
||||
id="g100"
|
||||
transform="translate(7.1414687,-15.751776)"><g
|
||||
id="g1-3"
|
||||
transform="matrix(0.44604857,0,0,0.44604857,607.75722,785.83144)"><path
|
||||
id="path148-7"
|
||||
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
|
||||
style="fill:#fff001;fill-opacity:1;stroke:#ff2424;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path150-4"
|
||||
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
|
||||
style="fill:#fff001;fill-opacity:1;stroke:#ff2424;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path151-5"
|
||||
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><g
|
||||
id="g46"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="translate(-4.4004781,0.4458306)"><path
|
||||
style="fill:none;fill-opacity:1;stroke:#fff800;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:0.595237"
|
||||
d="m 688.06805,869.55518 -15.10948,-20.06719 -15.30685,19.99488"
|
||||
id="path46-0" /><path
|
||||
style="fill:none;fill-opacity:1;stroke:#fff800;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:0.595237"
|
||||
d="m 672.95522,849.48086 -0.008,20.1008"
|
||||
id="path46" /></g><path
|
||||
style="fill:#fffc82;fill-opacity:1;stroke:#67653e;stroke-width:0.734271;stroke-opacity:1"
|
||||
id="path45"
|
||||
d="m 683.59689,869.99463 a 15.132905,8.016284 0 0 1 -7.56645,6.9423 15.132905,8.016284 0 0 1 -15.1329,0 15.132905,8.016284 0 0 1 -7.56646,-6.9423 h 15.13291 z" /></g><g
|
||||
id="g104"
|
||||
transform="translate(-5.4255714,1.122532)"><g
|
||||
id="g97"
|
||||
transform="rotate(-107.4873,716.6014,857.88295)"><path
|
||||
d="m 741.45011,864.47313 c -0.61965,0.46041 -4.08011,3.1101 -5.51427,5.86399 l 1.64993,3.62001 -2.43068,-1.72258 c -0.29115,0.61767 -1.3281,3.67079 -1.5476,4.52025 l 1.45896,3.01928 -1.86206,-1.31473 c -0.18852,0.80806 -0.54956,4.17462 -0.55848,5.07496 l 2.31516,2.45068 -2.25264,-0.65679 c 0.13308,2.85064 1.26,5.03306 2.5083,5.34423 0.15431,0.0384 0.31307,0.0535 0.47654,0.045 0.14493,0.0761 0.29668,0.12925 0.45291,0.15891 1.26393,0.23992 3.20053,-1.27052 4.51461,-3.8037 l -2.32328,-0.3465 3.1284,-1.25554 c 0.36875,-0.82142 1.45023,-4.03153 1.61725,-4.8443 l -2.23934,0.41316 2.58842,-2.12932 c 0.15619,-0.86335 0.49096,-4.07031 0.48509,-4.75312 l -2.92603,0.54524 3.01258,-2.59499 c -0.14977,-3.10134 -2.18373,-6.95671 -2.55377,-7.63423 z"
|
||||
style="fill:#ededed;stroke:#676767;stroke-width:0.507712;stroke-opacity:1"
|
||||
id="path85" /><path
|
||||
style="fill:none;stroke:#666666;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 739.51041,895.87916 c -0.77616,1.36482 -6.52512,-3.59608 0.85657,-25.75538"
|
||||
id="path60" /></g><g
|
||||
id="g46-5"
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="translate(56.871733,-35.227408)"><path
|
||||
style="fill:none;fill-opacity:1;stroke:#fff800;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:0.596078"
|
||||
d="m 688.06805,869.55518 -15.10948,-28.79844 -15.30685,28.72613"
|
||||
id="path46-0-4" /><path
|
||||
style="fill:none;fill-opacity:1;stroke:#fff800;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:0.596078"
|
||||
d="m 672.95522,840.74961 -0.008,28.83205"
|
||||
id="path46-7" /></g><path
|
||||
style="fill:#fffc82;fill-opacity:1;stroke:#67653e;stroke-width:0.734271;stroke-opacity:1"
|
||||
id="path45-15"
|
||||
d="m 744.86905,834.32141 a 15.132905,8.016284 0 0 1 -7.56645,6.94231 15.132905,8.016284 0 0 1 -15.13291,0 15.132905,8.016284 0 0 1 -7.56645,-6.94231 h 15.13291 z" /></g><rect
|
||||
style="fill:#fffa65;fill-opacity:1;stroke:#67653e;stroke-width:1;stroke-opacity:1"
|
||||
id="rect31"
|
||||
width="59.770382"
|
||||
height="2.958041"
|
||||
x="167.49957"
|
||||
y="1058.3093"
|
||||
rx="11.1125"
|
||||
ry="1.0782195"
|
||||
transform="rotate(-30)" /></g><path
|
||||
d="m 700.74059,814.58039 c -1.08829,0 -1.96422,1.53814 -1.96422,3.44888 V 864.527 c 0,0.0628 7.2e-4,0.12512 0.003,0.18707 h -10.46448 c -6.15632,0 -11.1125,1.12104 -11.1125,2.51354 0,1.3925 -0.0661,2.51355 -0.0661,2.51355 h 47.42657 v -2.51355 c 0,-1.3925 -4.95617,-2.51354 -11.1125,-2.51354 h -10.5606 c 0.002,-0.062 0.003,-0.12426 0.003,-0.18707 v -46.49773 c 0,-1.91074 -0.87593,-3.44888 -1.96422,-3.44888 z"
|
||||
style="fill:#fff965;fill-opacity:1;stroke:#67653e;stroke-opacity:1"
|
||||
id="path33" /></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 7.1 KiB |
|
|
@ -17,4 +17,5 @@
|
|||
|
||||
.icon-shrink {
|
||||
flex-shrink: 1;
|
||||
height: 1em;
|
||||
}
|
||||
|
|
|
|||
140
style/main.scss
140
style/main.scss
|
|
@ -185,6 +185,10 @@ nav.header {
|
|||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
font-size: 1.5em;
|
||||
|
||||
.username {
|
||||
|
|
@ -292,6 +296,23 @@ dialog::backdrop {
|
|||
}
|
||||
}
|
||||
|
||||
.host-in-game-wrapper {
|
||||
z-index: 1;
|
||||
background-color: black;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
&>div,
|
||||
&>nav {
|
||||
padding: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
#change-password,
|
||||
#update-profile {
|
||||
.pwless-notice {
|
||||
|
|
@ -999,6 +1020,10 @@ form {
|
|||
gap: 0.5ch;
|
||||
padding-bottom: 1ch;
|
||||
|
||||
max-height: 75vh;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: thin;
|
||||
|
||||
.character {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
|
@ -1028,3 +1053,118 @@ form {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overrides-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
|
||||
width: 80vw;
|
||||
margin-left: 10vw;
|
||||
margin-right: 10vw;
|
||||
|
||||
.override-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:not(:hover) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
gap: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
max-width: 80vw;
|
||||
|
||||
.overrides-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-overrides,
|
||||
.result-overrides {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 40vw;
|
||||
align-items: center;
|
||||
|
||||
.close {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
font-size: 2em;
|
||||
|
||||
.result-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;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-number {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1ch;
|
||||
|
||||
button {
|
||||
padding-left: 0.5ch;
|
||||
padding-right: 0.5ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@
|
|||
|
||||
font-size: 1.75em;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.subtext {
|
||||
font-size: 1.5rem;
|
||||
|
|
@ -71,6 +73,9 @@
|
|||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 2ch;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -123,17 +128,53 @@
|
|||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5ch;
|
||||
row-gap: 0px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.icon-fit {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bool-picker {
|
||||
width: calc(100% - 6ch);
|
||||
height: calc(100% - 6ch);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
padding: 3ch;
|
||||
gap: 3ch;
|
||||
|
||||
&>button {
|
||||
font-size: 3em;
|
||||
width: 30vw;
|
||||
flex-grow: 1;
|
||||
|
||||
background-color: color.change($red1, $alpha: 0.1);
|
||||
border: 1px solid color.change($red1, $alpha: 0.6);
|
||||
|
||||
&:hover {
|
||||
background-color: color.change($blue1, $alpha: 0.3);
|
||||
border: 1px solid $blue1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.target-picker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.allow-scroll {
|
||||
max-height: 70vh;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: thin;
|
||||
justify-content: unset;
|
||||
}
|
||||
|
||||
height: 100%;
|
||||
font-size: 2em;
|
||||
|
||||
|
|
@ -161,6 +202,12 @@
|
|||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
gap: 10%;
|
||||
|
||||
@media only screen and (min-width : 1200px) {
|
||||
&>img {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.two-column {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ decl_icon!(
|
|||
Mason: "/img/mason.svg",
|
||||
NotEqual: "/img/not-equal.svg",
|
||||
Equal: "/img/equal.svg",
|
||||
UnbalancedScales: "/img/unbalanced-scales.svg",
|
||||
BalancedScales: "/img/balanced-scales.svg",
|
||||
RedX: "/img/red-x.svg",
|
||||
Damned: "/img/damned.svg",
|
||||
Bloodlet: "/img/bloodlet.svg",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
use core::ops::{Add, AddAssign, Not, RangeInclusive, SubAssign};
|
||||
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn IncDecU8(
|
||||
value: RwSignal<u8>,
|
||||
#[prop(default = 0..=0xFFu8)] value_range: RangeInclusive<u8>,
|
||||
) -> impl IntoView {
|
||||
let dec_disabled = {
|
||||
let value_range = value_range.clone();
|
||||
move || !value_range.contains(&value.get().saturating_sub(1))
|
||||
};
|
||||
view! {
|
||||
<div class="inc-dec">
|
||||
<button
|
||||
on:click=move |_| value.set(value.get().saturating_sub(1))
|
||||
disabled=dec_disabled
|
||||
>
|
||||
"-"
|
||||
</button>
|
||||
<span class="value">{move || value.get()}</span>
|
||||
<button
|
||||
on:click=move |_| value.set(value.get().saturating_add(1))
|
||||
disabled=move || !value_range.contains(&value.get().saturating_add(1))
|
||||
>
|
||||
"+"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Increment: Copy + AddAssign<Self> {
|
||||
fn increment(self) -> Self;
|
||||
}
|
||||
|
||||
pub trait Decrement: Copy + SubAssign<Self> {
|
||||
fn decrement(self) -> Self;
|
||||
}
|
||||
|
||||
macro_rules! inc_dec_impl {
|
||||
($($n:ty),*) => {
|
||||
$(
|
||||
impl Increment for $n {
|
||||
fn increment(self) -> Self {
|
||||
self.saturating_add(1)
|
||||
}
|
||||
}
|
||||
impl Decrement for $n {
|
||||
fn decrement(self) -> Self {
|
||||
self.saturating_sub(1)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
inc_dec_impl!(
|
||||
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
|
||||
);
|
||||
|
||||
#[component]
|
||||
pub fn IncDec<V>(
|
||||
value: RwSignal<V>,
|
||||
#[prop(default = V::default()..=V::default().not())] value_range: RangeInclusive<V>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
V: Increment
|
||||
+ Decrement
|
||||
+ Eq
|
||||
+ Ord
|
||||
+ Not<Output = V>
|
||||
+ Default
|
||||
+ Send
|
||||
+ Sync
|
||||
+ ToString
|
||||
+ 'static,
|
||||
{
|
||||
let dec_disabled = {
|
||||
let value_range = value_range.clone();
|
||||
move || !value_range.contains(&value.get().decrement())
|
||||
};
|
||||
view! {
|
||||
<div class="inc-dec">
|
||||
<button on:click=move |_| value.set(value.get().decrement()) disabled=dec_disabled>
|
||||
"-"
|
||||
</button>
|
||||
<span class="value">{move || value.get().to_string()}</span>
|
||||
<button
|
||||
on:click=move |_| value.set(value.get().increment())
|
||||
disabled=move || !value_range.contains(&value.get().increment())
|
||||
>
|
||||
"+"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,17 @@ use leptos::{ev::MouseEvent, prelude::*};
|
|||
use leptos_router::hooks::use_url;
|
||||
use reactive_stores::Store;
|
||||
use uuid::Uuid;
|
||||
use werewolves_proto::{error::ServerError, game::GameId, token::TokenString};
|
||||
use werewolves_proto::{
|
||||
error::ServerError,
|
||||
game::GameId,
|
||||
message::host::{HostGameMessage, HostMessage, HostNightMessage},
|
||||
token::TokenString,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
components::LinkButton,
|
||||
pages::host::HostPage,
|
||||
storage::user::{AuthContext, AuthContextStoreFields},
|
||||
},
|
||||
db::AppState,
|
||||
|
|
@ -140,3 +146,46 @@ fn is_big_screen_path(path: &str) -> bool {
|
|||
&& Uuid::parse_str(parts[1]).is_ok()
|
||||
&& parts[2] == "big"
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn HostInGameNav(
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
night: ReadSignal<bool>,
|
||||
show_back_button_only: RwSignal<bool>,
|
||||
overrides_set: RwSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
let previous = move |_| reply.set(Some(HostMessage::InGame(HostGameMessage::PreviousState)));
|
||||
let skip = move |_| {
|
||||
reply.set(Some(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::SkipAction,
|
||||
))))
|
||||
};
|
||||
let back = move |_| {
|
||||
show_back_button_only.set(false);
|
||||
overrides_set.set(false);
|
||||
reply.set(Some(HostMessage::GetState));
|
||||
};
|
||||
let hide_if_not_back_button = move || show_back_button_only.get();
|
||||
let hide_if_not_night = move || show_back_button_only.get() || !night.get();
|
||||
view! {
|
||||
<nav class="header" hidden=hide_if_not_night>
|
||||
<button on:click=back hidden=move || !show_back_button_only.get()>
|
||||
"back"
|
||||
</button>
|
||||
<button on:click=previous hidden=hide_if_not_night>
|
||||
"previous"
|
||||
</button>
|
||||
<button
|
||||
on:click=move |_| {
|
||||
overrides_set.set(true);
|
||||
}
|
||||
hidden=move || show_back_button_only.get() || !night.get()
|
||||
>
|
||||
"overrides"
|
||||
</button>
|
||||
<button on:click=skip hidden=hide_if_not_night>
|
||||
"skip"
|
||||
</button>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
pub mod big;
|
||||
mod host;
|
||||
pub mod host;
|
||||
mod player;
|
||||
|
||||
use codee::binary::MsgpackSerdeCodec;
|
||||
|
|
@ -10,6 +10,7 @@ use leptos_use::{
|
|||
use_websocket_with_options,
|
||||
};
|
||||
use reactive_stores::Store;
|
||||
use werewolves_proto::message::host::ServerToHostMessage;
|
||||
use werewolves_proto::message::{ClientMessage, host::HostMessage};
|
||||
use werewolves_proto::message::{IntoClientResponse, WrappedServerMessage};
|
||||
|
||||
|
|
@ -91,7 +92,18 @@ pub fn GamePage(error: WriteSignal<Option<WolfError>>) -> impl IntoView {
|
|||
}
|
||||
match message.clone() {
|
||||
Some(IntoClientResponse::Host(host_msg)) => {
|
||||
log::debug!("got host message: {:?}", host_msg.title());
|
||||
match &host_msg {
|
||||
ServerToHostMessage::ActionPrompt(prompt, page) => {
|
||||
log::debug!(
|
||||
"got host message: ActionPrompt({page}, {:?})",
|
||||
prompt.title()
|
||||
)
|
||||
}
|
||||
ServerToHostMessage::ActionResult(_, result) => {
|
||||
log::debug!("got host message: ActionResult({:?})", result.title())
|
||||
}
|
||||
_ => log::debug!("got host message: {:?}", host_msg.title()),
|
||||
};
|
||||
host_message.set(Some(host_msg));
|
||||
}
|
||||
Some(IntoClientResponse::Player(player_msg)) => {
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ pub fn BigScreen() -> impl IntoView {
|
|||
<div class:big-screen-wrapper=move || {
|
||||
!matches!(page.get(), BigScreenPage::Setup)
|
||||
}>{content}</div>
|
||||
}.into_any()
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
werewolves_macros::include_path!("werewolves/src/app/pages/game/host");
|
||||
mod overrides {
|
||||
werewolves_macros::include_path!("werewolves/src/app/pages/game/host/overrides");
|
||||
}
|
||||
|
||||
use core::num::NonZeroU8;
|
||||
use core::{num::NonZeroU8, ops::Not};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use leptos::prelude::*;
|
||||
|
|
@ -16,13 +19,17 @@ use werewolves_proto::{
|
|||
|
||||
use crate::app::{
|
||||
Preferences,
|
||||
components::DialogModal,
|
||||
pages::night_actions::{RolePrompt, RoleResult},
|
||||
components::{DialogModal, HostInGameNav},
|
||||
pages::{
|
||||
game::host::overrides::Overrides,
|
||||
host::overrides::OverrideScreen,
|
||||
night_actions::{RolePrompt, RoleResult},
|
||||
},
|
||||
};
|
||||
use crate::{ConsoleLogError, app::error::WolfError};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
enum HostPage {
|
||||
pub enum HostPage {
|
||||
#[default]
|
||||
None,
|
||||
Settings,
|
||||
|
|
@ -56,6 +63,13 @@ pub fn HostGamePage(
|
|||
let players: RwSignal<Box<[PlayerState]>> = RwSignal::new(Box::new([]));
|
||||
let dialog_open = RwSignal::new(HashMap::new());
|
||||
let acks: RwSignal<Box<[RoleRevealCharacter]>> = RwSignal::new(Box::new([]));
|
||||
let night = RwSignal::new(false);
|
||||
let show_back_button_only = RwSignal::new(false);
|
||||
let on_overrides_screen = RwSignal::new(false);
|
||||
let overrides_screen: RwSignal<Option<OverrideScreen>> = RwSignal::new(None);
|
||||
let override_page_number = RwSignal::new(0usize);
|
||||
let prompt_page = RwSignal::new(0usize);
|
||||
let target_picker_count = RwSignal::new(16u8);
|
||||
|
||||
let open_categories = RwSignal::new(
|
||||
Category::ALL
|
||||
|
|
@ -115,6 +129,7 @@ pub fn HostGamePage(
|
|||
page.set(HostPage::RoleRevealAcks);
|
||||
}
|
||||
Srv2Host::ActionPrompt(prompt, prompt_page) => {
|
||||
override_page_number.set(prompt_page);
|
||||
page.set(HostPage::ActionPrompt {
|
||||
prompt,
|
||||
page: prompt_page,
|
||||
|
|
@ -146,7 +161,27 @@ pub fn HostGamePage(
|
|||
.is_some()
|
||||
.then_some(view! { <CancelGame reply=reply prefs=prefs /> })
|
||||
};
|
||||
let content = move || match page.get() {
|
||||
Effect::new(move || match page.get() {
|
||||
HostPage::None
|
||||
| HostPage::Settings
|
||||
| HostPage::RoleRevealAcks
|
||||
| HostPage::Daytime { .. } => night.set(false),
|
||||
HostPage::ActionPrompt { .. } | HostPage::ActionResult { .. } => night.set(true),
|
||||
});
|
||||
let content = move || {
|
||||
if on_overrides_screen.get() {
|
||||
show_back_button_only.set(true);
|
||||
return view! {
|
||||
<Overrides
|
||||
reply=reply
|
||||
screen=overrides_screen
|
||||
page=prompt_page
|
||||
target_picker_count=target_picker_count
|
||||
/>
|
||||
}
|
||||
.into_any();
|
||||
}
|
||||
match page.get() {
|
||||
HostPage::None => ().into_any(),
|
||||
HostPage::Settings => view! {
|
||||
<Settings
|
||||
|
|
@ -188,15 +223,40 @@ pub fn HostGamePage(
|
|||
characters,
|
||||
marked,
|
||||
reply,
|
||||
} => view! {
|
||||
<DaytimePlayerList day=day characters=characters marked=marked reply=reply/>
|
||||
} => {
|
||||
view! { <DaytimePlayerList day=day characters=characters marked=marked reply=reply /> }
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
.into_any(),
|
||||
};
|
||||
view! {
|
||||
|
||||
move || match page.get() {
|
||||
HostPage::Settings | HostPage::None => view! {
|
||||
{cancel}
|
||||
{content}
|
||||
}
|
||||
.into_any(),
|
||||
HostPage::RoleRevealAcks
|
||||
| HostPage::ActionPrompt { .. }
|
||||
| HostPage::ActionResult { .. }
|
||||
| HostPage::Daytime { .. } => {
|
||||
let cancel = matches!(&*page.read(), HostPage::Daytime { .. })
|
||||
.not()
|
||||
.then_some(cancel);
|
||||
view! {
|
||||
<div class="host-in-game-wrapper">
|
||||
<HostInGameNav
|
||||
reply=reply
|
||||
night=night.read_only()
|
||||
show_back_button_only=show_back_button_only
|
||||
overrides_set=on_overrides_screen
|
||||
/>
|
||||
<div>{cancel} {content}</div>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ pub fn DaytimePlayerList(
|
|||
.collect::<Box<[_]>>();
|
||||
kills.is_empty().not().then_some(view! {
|
||||
<div class="info-tidbit">
|
||||
<label>{"died last night"}</label>
|
||||
<span class="breakable">"died last night"</span>
|
||||
<div class="last-nights-kills">{kills.into_iter().collect_view()}</div>
|
||||
</div>
|
||||
})
|
||||
|
|
@ -127,10 +127,7 @@ pub fn DaytimePlayerList(
|
|||
<DialogModal button_content=button_text.clone()>
|
||||
<h3>{confirmation_text.clone()}</h3>
|
||||
<button on:click=move |_| {
|
||||
reply
|
||||
.set(
|
||||
Some(HostMessage::InGame(HostGameMessage::Day(HostDayMessage::Execute))),
|
||||
)
|
||||
reply.set(Some(HostMessage::InGame(HostGameMessage::Day(HostDayMessage::Execute))))
|
||||
}>{button_text.clone()}</button>
|
||||
</DialogModal>
|
||||
};
|
||||
|
|
@ -174,7 +171,12 @@ fn DaytimePlayer(
|
|||
let text = role.to_string().to_case(Case::Title);
|
||||
let align_class = role.wolf().then_some("red");
|
||||
view! {
|
||||
<button on:click=select class="character no-hover" class:dead=died_to.is_some() class:marked=marked>
|
||||
<button
|
||||
on:click=select
|
||||
class="character no-hover"
|
||||
class:dead=died_to.is_some()
|
||||
class:marked=marked
|
||||
>
|
||||
<div class="day-char">
|
||||
<span class="headline">
|
||||
<Icon source=icon r#type=IconType::Small />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,452 @@
|
|||
// Copyright (C) 2025-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::num::NonZeroU8;
|
||||
use leptos::prelude::*;
|
||||
use werewolves_proto::message::host::HostMessage;
|
||||
use werewolves_proto::{
|
||||
character::CharacterId,
|
||||
diedto::DiedToTitle,
|
||||
message::{
|
||||
CharacterIdentity,
|
||||
host::ServerToHostMessage,
|
||||
night::{ActionPrompt, ActionPromptTitle, ActionResult, ActionResultTitle, Visits},
|
||||
},
|
||||
role::{Alignment, AlignmentEq, Killer, Powerful, RoleTitle},
|
||||
};
|
||||
|
||||
use crate::app::class::AsClasses;
|
||||
use crate::app::components::IncDecU8;
|
||||
use crate::app::pages::game::host::overrides::{PromptScreenTest, ResultScreenTest};
|
||||
|
||||
#[component]
|
||||
pub fn Overrides(
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
screen: RwSignal<Option<OverrideScreen>>,
|
||||
page: RwSignal<usize>,
|
||||
target_picker_count: RwSignal<u8>,
|
||||
) -> impl IntoView {
|
||||
let internal_send: RwSignal<Option<ServerToHostMessage>> = RwSignal::new(None);
|
||||
let prompt: RwSignal<Option<ActionPrompt>> = RwSignal::new(None);
|
||||
Effect::new(move || {
|
||||
let Some(msg) = internal_send.get() else {
|
||||
return;
|
||||
};
|
||||
match &msg {
|
||||
ServerToHostMessage::ActionPrompt(prompt, p) => {
|
||||
page.set(*p);
|
||||
screen.set(Some(OverrideScreen::Prompt(prompt.clone())))
|
||||
}
|
||||
ServerToHostMessage::ActionResult(_, result) => {
|
||||
screen.set(Some(OverrideScreen::Result(result.clone())))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
reply.set(Some(HostMessage::Echo(msg)));
|
||||
});
|
||||
Effect::new(move || {
|
||||
if let Some(OverrideScreen::Prompt(p)) = screen.get() {
|
||||
prompt.set(Some(p));
|
||||
}
|
||||
});
|
||||
|
||||
let screen_opts = move || {
|
||||
screen.get().map(|screen| match screen {
|
||||
OverrideScreen::Prompt(p) => {
|
||||
view! { <PromptScreenTest prompt=prompt page=page send=internal_send.write_only() /> }
|
||||
.into_any()
|
||||
}
|
||||
OverrideScreen::Result(result) => {
|
||||
view! { <ResultScreenTest result=result.clone() send=internal_send.write_only() /> }
|
||||
.into_any()
|
||||
}
|
||||
OverrideScreen::TargetPicker(_) => {
|
||||
Effect::new(move || reply.set(Some(HostMessage::Echo(ServerToHostMessage::ActionPrompt(ActionPrompt::WolfPackKill { living_villagers: identities(target_picker_count.get() as _), marked: None }, usize::MAX)))));
|
||||
view! {
|
||||
<span>"targets"</span>
|
||||
<IncDecU8 value=target_picker_count value_range=1..=0xFFu8 />
|
||||
}.into_any()
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="overrides-screen">
|
||||
<TestScreenSelector screen=screen send=internal_send.write_only() />
|
||||
<button on:click=move |_| {
|
||||
screen.set(Some(OverrideScreen::TargetPicker(identities(16))))
|
||||
}>"target picker"</button>
|
||||
{screen_opts}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TestScreenSelector(
|
||||
screen: RwSignal<Option<OverrideScreen>>,
|
||||
send: WriteSignal<Option<ServerToHostMessage>>,
|
||||
) -> impl IntoView {
|
||||
let prompts = move || {
|
||||
ActionPromptTitle::ALL
|
||||
.into_iter()
|
||||
.map(|title| {
|
||||
let OverrideScreen::Prompt(prompt) = Into::<OverrideScreen>::into(title) else {
|
||||
unreachable!()
|
||||
};
|
||||
let picked_class = if let Some(OverrideScreen::Prompt(current)) = screen.get()
|
||||
&& current.title() == title
|
||||
{
|
||||
Some("selected")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let callback = move |_| {
|
||||
let OverrideScreen::Prompt(prompt) = Into::<OverrideScreen>::into(title) else {
|
||||
unreachable!()
|
||||
};
|
||||
screen.set(Some(OverrideScreen::Prompt(prompt.clone())));
|
||||
send.set(Some(ServerToHostMessage::ActionPrompt(prompt, 0)));
|
||||
};
|
||||
let class = prompt_class(&prompt);
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
on:click=callback
|
||||
class=[class, picked_class, Some("hover"), Some("box")].as_classes()
|
||||
>
|
||||
{format!("{title:?}")}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
};
|
||||
|
||||
let results = move || {
|
||||
ActionResultTitle::ALL
|
||||
.into_iter()
|
||||
.filter(|title| !matches!(title, ActionResultTitle::Continue))
|
||||
.map(|title| {
|
||||
let OverrideScreen::Result(result) = Into::<OverrideScreen>::into(title) else {
|
||||
unreachable!()
|
||||
};
|
||||
let picked_class = if let Some(OverrideScreen::Result(current)) = screen.get()
|
||||
&& current.title() == title
|
||||
{
|
||||
Some("selected")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let callback = move |_| {
|
||||
let OverrideScreen::Result(result) = Into::<OverrideScreen>::into(title) else {
|
||||
unreachable!()
|
||||
};
|
||||
screen.set(Some(OverrideScreen::Result(result.clone())));
|
||||
send.set(Some(ServerToHostMessage::ActionResult(None, result)));
|
||||
};
|
||||
let class = result_class(&result);
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
on:click=callback
|
||||
class=[picked_class, class, Some("hover"), Some("box")].as_classes()
|
||||
>
|
||||
{format!("{title:?}")}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
};
|
||||
view! {
|
||||
<div class="override-selector">
|
||||
<div class="category">
|
||||
<span>"prompts"</span>
|
||||
<ul class="overrides-screens">{prompts}</ul>
|
||||
</div>
|
||||
<div class="category">
|
||||
<span>"results"</span>
|
||||
<ul class="overrides-screens">{results}</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) 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()
|
||||
}
|
||||
|
||||
pub(super) fn identity() -> CharacterIdentity {
|
||||
identities(1).into_iter().next().unwrap()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum OverrideScreen {
|
||||
Prompt(ActionPrompt),
|
||||
Result(ActionResult),
|
||||
TargetPicker(Box<[CharacterIdentity]>),
|
||||
}
|
||||
|
||||
impl From<ActionResultTitle> for OverrideScreen {
|
||||
fn from(value: ActionResultTitle) -> Self {
|
||||
OverrideScreen::Result(match value {
|
||||
ActionResultTitle::SkippedByHost => ActionResult::SkippedByHost,
|
||||
ActionResultTitle::RoleBlocked => ActionResult::RoleBlocked,
|
||||
ActionResultTitle::Drunk => ActionResult::Drunk,
|
||||
ActionResultTitle::Seer => ActionResult::Seer(identity(), Alignment::Village),
|
||||
ActionResultTitle::PowerSeer => ActionResult::PowerSeer {
|
||||
target: identity(),
|
||||
powerful: Powerful::Powerful,
|
||||
},
|
||||
ActionResultTitle::Adjudicator => ActionResult::Adjudicator {
|
||||
target: identity(),
|
||||
killer: Killer::Killer,
|
||||
},
|
||||
ActionResultTitle::Arcanist => ActionResult::Arcanist(
|
||||
(identity(), identities(2).last().cloned().unwrap()),
|
||||
AlignmentEq::Same,
|
||||
),
|
||||
ActionResultTitle::GraveDigger => ActionResult::GraveDigger(identity(), None),
|
||||
ActionResultTitle::Mortician => {
|
||||
ActionResult::Mortician(identity(), DiedToTitle::Execution)
|
||||
}
|
||||
ActionResultTitle::Insomniac => ActionResult::Insomniac(Visits::new(identities(2))),
|
||||
ActionResultTitle::Empath => ActionResult::Empath {
|
||||
target: identity(),
|
||||
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 OverrideScreen {
|
||||
fn from(value: ActionPromptTitle) -> Self {
|
||||
Self::Prompt(match value {
|
||||
ActionPromptTitle::BeholderWakes => ActionPrompt::BeholderWakes {
|
||||
character_id: identity(),
|
||||
},
|
||||
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(),
|
||||
nights_til_starvation: 0,
|
||||
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::BeholderChooses => ActionPrompt::BeholderChooses {
|
||||
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::DamnedIntro => ActionPrompt::DamnedIntro {
|
||||
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
|
||||
| ActionResultTitle::SkippedByHost => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_class(prompt: &ActionPrompt) -> Option<&'static str> {
|
||||
match prompt {
|
||||
ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
| ActionPrompt::CoverOfDarkness => None,
|
||||
ActionPrompt::BeholderWakes { .. }
|
||||
| ActionPrompt::Seer { .. }
|
||||
| ActionPrompt::Arcanist { .. }
|
||||
| ActionPrompt::Gravedigger { .. }
|
||||
| ActionPrompt::Adjudicator { .. }
|
||||
| ActionPrompt::PowerSeer { .. }
|
||||
| ActionPrompt::Mortician { .. }
|
||||
| ActionPrompt::BeholderChooses { .. }
|
||||
| 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::DamnedIntro { .. } => Some("damned"),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
use convert_case::{Case, Casing};
|
||||
use gloo::dialogs::prompt;
|
||||
// Copyright (C) 2025-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 leptos::{
|
||||
ev::{Event, Targeted, WheelEvent},
|
||||
prelude::*,
|
||||
web_sys::HtmlSelectElement,
|
||||
};
|
||||
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
use werewolves_proto::{
|
||||
message::{host::ServerToHostMessage, night::ActionPrompt},
|
||||
role::{PreviousGuardianAction, RoleTitle},
|
||||
};
|
||||
|
||||
use crate::app::{
|
||||
components::IncDecU8,
|
||||
pages::{game::host::overrides, night_actions},
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn PromptScreenTest(
|
||||
prompt: RwSignal<Option<ActionPrompt>>,
|
||||
page: RwSignal<usize>,
|
||||
send: WriteSignal<Option<ServerToHostMessage>>,
|
||||
) -> impl IntoView {
|
||||
let options = move || {
|
||||
let prompt_signal = prompt;
|
||||
let Some(prompt) = prompt.get() else {
|
||||
return ().into_any();
|
||||
};
|
||||
match prompt {
|
||||
ActionPrompt::WolvesIntro { wolves } => {
|
||||
let new_count = RwSignal::new(wolves.len() as u8);
|
||||
|
||||
Effect::new(move || {
|
||||
let new_prompt = ActionPrompt::WolvesIntro {
|
||||
wolves: overrides::identities(new_count.get() as _)
|
||||
.into_iter()
|
||||
.zip(RoleTitle::ALL.into_iter().filter(|w| w.wolf()).cycle())
|
||||
.collect(),
|
||||
};
|
||||
send.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
});
|
||||
view! {
|
||||
<span>"wolf count"</span>
|
||||
<IncDecU8 value=new_count value_range=1..=0xFF />
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionPrompt::RoleChange { new_role, .. } => {
|
||||
let roles = RoleTitle::ALL
|
||||
.into_iter()
|
||||
.map(|role| {
|
||||
view! { <option selected=role == new_role>{role.to_string().to_case(Case::Title)}</option> }
|
||||
})
|
||||
.collect_view();
|
||||
let on_change_cb = move |t: Targeted<Event, HtmlSelectElement>| {
|
||||
let select = t.target();
|
||||
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.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let on_wheel = move |t: Targeted<WheelEvent, HtmlSelectElement>| {
|
||||
let target = t.target();
|
||||
let index = target.selected_index();
|
||||
let new_index = match t.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.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
}
|
||||
};
|
||||
view! {
|
||||
<div class="prompt-option">
|
||||
<span>{"new role"}</span>
|
||||
<select on:wheel:target=on_wheel on:change:target=on_change_cb>
|
||||
{roles}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
ActionPrompt::Hunter { current_target, .. } => {
|
||||
let toggle_target = current_target.is_none().then_some(super::identity());
|
||||
let button_text = if toggle_target.is_some() {
|
||||
"remove previous target"
|
||||
} else {
|
||||
"set previous target"
|
||||
};
|
||||
let on_toggle = move |_| {
|
||||
let new_prompt = ActionPrompt::Hunter {
|
||||
character_id: super::identity(),
|
||||
current_target: toggle_target.clone(),
|
||||
living_players: overrides::identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="prompt-options">
|
||||
<button on:click=on_toggle>{button_text}</button>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionPrompt::MapleWolf {
|
||||
nights_til_starvation,
|
||||
..
|
||||
} => {
|
||||
let nights_til_starvation = RwSignal::new(nights_til_starvation);
|
||||
Effect::new(move || {
|
||||
let new_prompt = ActionPrompt::MapleWolf {
|
||||
character_id: super::identity(),
|
||||
nights_til_starvation: nights_til_starvation.get(),
|
||||
living_players: overrides::identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="prompt-options">
|
||||
<span>"nights til starvation"</span>
|
||||
<IncDecU8 value=nights_til_starvation />
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionPrompt::Guardian { .. } => {
|
||||
let none_disabled = move || {
|
||||
matches!(
|
||||
prompt_signal.get(),
|
||||
Some(ActionPrompt::Guardian { previous: None, .. })
|
||||
)
|
||||
};
|
||||
let prev_none = move |_| {
|
||||
let new_prompt = ActionPrompt::Guardian {
|
||||
character_id: super::identity(),
|
||||
previous: None,
|
||||
living_players: super::identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
};
|
||||
|
||||
let p = prompt.clone();
|
||||
let prot_disabled = move || {
|
||||
matches!(
|
||||
p,
|
||||
ActionPrompt::Guardian {
|
||||
previous: Some(PreviousGuardianAction::Protect(_)),
|
||||
..
|
||||
}
|
||||
)
|
||||
};
|
||||
let prev_prot = 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.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
};
|
||||
let p = prompt.clone();
|
||||
let guard_disabled = move || {
|
||||
matches!(
|
||||
p,
|
||||
ActionPrompt::Guardian {
|
||||
previous: Some(PreviousGuardianAction::Guard(_)),
|
||||
..
|
||||
}
|
||||
)
|
||||
};
|
||||
let prev_guard = 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.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
};
|
||||
|
||||
view! {
|
||||
<span>"previous protect"</span>
|
||||
<div class="option-set">
|
||||
<button disabled=none_disabled on:click=prev_none>
|
||||
"none"
|
||||
</button>
|
||||
<button disabled=prot_disabled on:click=prev_prot>
|
||||
"protected"
|
||||
</button>
|
||||
<button disabled=guard_disabled on:click=prev_guard>
|
||||
"guarded"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionPrompt::MasonsWake { masons, .. } => {
|
||||
let mason_count = RwSignal::new(masons.len() as u8);
|
||||
Effect::new(move || {
|
||||
let new_prompt = ActionPrompt::MasonsWake {
|
||||
leader: super::identity(),
|
||||
masons: super::identities(mason_count.get() as _),
|
||||
};
|
||||
send.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
});
|
||||
view! {
|
||||
<span>"masons"</span>
|
||||
<IncDecU8 value=mason_count value_range=1..=0xFF />
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionPrompt::MasonLeaderRecruit { recruits_left, .. } => {
|
||||
let recruits_left = RwSignal::new(recruits_left.get());
|
||||
Effect::new(move || {
|
||||
let new_prompt = ActionPrompt::MasonLeaderRecruit {
|
||||
character_id: super::identity(),
|
||||
recruits_left: NonZeroU8::new(recruits_left.get()).unwrap(),
|
||||
potential_recruits: overrides::identities(20),
|
||||
marked: None,
|
||||
};
|
||||
send.set(Some(ServerToHostMessage::ActionPrompt(
|
||||
new_prompt.clone(),
|
||||
0,
|
||||
)));
|
||||
});
|
||||
view! { <IncDecU8 value=recruits_left value_range=1..=0xFF /> }.into_any()
|
||||
}
|
||||
|
||||
ActionPrompt::BeholderWakes { .. }
|
||||
| ActionPrompt::Protector { .. }
|
||||
| ActionPrompt::Arcanist { .. }
|
||||
| ActionPrompt::Gravedigger { .. }
|
||||
| ActionPrompt::Militia { .. }
|
||||
| ActionPrompt::Adjudicator { .. }
|
||||
| ActionPrompt::PowerSeer { .. }
|
||||
| ActionPrompt::Mortician { .. }
|
||||
| ActionPrompt::BeholderChooses { .. }
|
||||
| ActionPrompt::Empath { .. }
|
||||
| ActionPrompt::Vindicator { .. }
|
||||
| ActionPrompt::PyreMaster { .. }
|
||||
| ActionPrompt::WolfPackKill { .. }
|
||||
| ActionPrompt::AlphaWolf { .. }
|
||||
| ActionPrompt::DireWolf { .. }
|
||||
| ActionPrompt::LoneWolfKill { .. }
|
||||
| ActionPrompt::Bloodletter { .. }
|
||||
| ActionPrompt::Seer { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::DamnedIntro { .. }
|
||||
| ActionPrompt::Insomniac { .. }
|
||||
| ActionPrompt::Shapeshifter { .. }
|
||||
| ActionPrompt::CoverOfDarkness => ().into_any(),
|
||||
}
|
||||
};
|
||||
let prev_page_disabled = move || page.get() == 0;
|
||||
let prev_page = move |_| {
|
||||
if let Some(prompt) = prompt.get() {
|
||||
let new_page = page.get().saturating_sub(1);
|
||||
page.set(new_page);
|
||||
send.set(Some(ServerToHostMessage::ActionPrompt(prompt, new_page)));
|
||||
}
|
||||
};
|
||||
|
||||
let next_page = move |_| {
|
||||
if let Some(prompt) = prompt.get() {
|
||||
let new_page = page.get().saturating_add(1);
|
||||
page.set(new_page);
|
||||
send.set(Some(ServerToHostMessage::ActionPrompt(prompt, new_page)));
|
||||
}
|
||||
};
|
||||
let title = move || {
|
||||
prompt
|
||||
.get()
|
||||
.map(|p| p.title().to_string())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let is_at_max_page = move || {
|
||||
let Some(prompt) = prompt.get() else {
|
||||
return true;
|
||||
};
|
||||
night_actions::pages_for_prompt(prompt, None).len() <= page.get().saturating_add(1)
|
||||
};
|
||||
view! {
|
||||
<div class="prompt-overrides">
|
||||
<span>{title}</span>
|
||||
<div class="prompt-number">
|
||||
<span>"page:"</span>
|
||||
<button on:click=prev_page disabled=prev_page_disabled>
|
||||
"-"
|
||||
</button>
|
||||
<span>{move || page.get().saturating_add(1)}</span>
|
||||
<button on:click=next_page disabled=is_at_max_page>
|
||||
"+"
|
||||
</button>
|
||||
</div>
|
||||
<div class="prompt-options">{options}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
// Copyright (C) 2025-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 leptos::{
|
||||
ev::{Event, Targeted, WheelEvent},
|
||||
prelude::*,
|
||||
web_sys::HtmlSelectElement,
|
||||
};
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use werewolves_proto::{
|
||||
diedto::DiedToTitle,
|
||||
message::{
|
||||
host::ServerToHostMessage,
|
||||
night::{ActionResult, Visits},
|
||||
},
|
||||
role::{Alignment, RoleTitle},
|
||||
};
|
||||
|
||||
use crate::app::{components::IncDecU8, pages::game::host::overrides};
|
||||
|
||||
#[component]
|
||||
pub fn ResultScreenTest(
|
||||
result: ActionResult,
|
||||
send: WriteSignal<Option<ServerToHostMessage>>,
|
||||
) -> impl IntoView {
|
||||
let options = match result.clone() {
|
||||
ActionResult::BeholderSawNothing
|
||||
| ActionResult::BeholderSawEverything
|
||||
| ActionResult::GoBackToSleep
|
||||
| ActionResult::ShiftFailed
|
||||
| ActionResult::Continue
|
||||
| ActionResult::Drunk
|
||||
| ActionResult::RoleBlocked
|
||||
| ActionResult::SkippedByHost => ().into_any(),
|
||||
ActionResult::Seer(target, alignment) => {
|
||||
let all = Alignment::ALL
|
||||
.into_iter()
|
||||
.map(|align| {
|
||||
let target = target.clone();
|
||||
let on_click = move |_| {
|
||||
send.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Seer(target.clone(), align),
|
||||
)));
|
||||
};
|
||||
view! {
|
||||
<button on:click=on_click disabled=align == alignment>
|
||||
{align.to_string()}
|
||||
</button>
|
||||
}
|
||||
})
|
||||
.collect_view();
|
||||
view! {
|
||||
<div class="prompt-options">
|
||||
<div class="option-set">{all}</div>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionResult::PowerSeer { target, powerful } => {
|
||||
let on_toggle = move |_| {
|
||||
send.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::PowerSeer {
|
||||
target: target.clone(),
|
||||
powerful: !powerful,
|
||||
},
|
||||
)));
|
||||
};
|
||||
let text = if powerful.powerful() {
|
||||
"make not powerful"
|
||||
} else {
|
||||
"make powerful"
|
||||
};
|
||||
view! {
|
||||
<div class="prompt-options">
|
||||
<button on:click=on_toggle>{text}</button>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionResult::Adjudicator { target, killer } => {
|
||||
let on_toggle = move |_| {
|
||||
send.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Adjudicator {
|
||||
target: target.clone(),
|
||||
killer: !killer,
|
||||
},
|
||||
)));
|
||||
};
|
||||
let text = if killer.killer() {
|
||||
"make not killer"
|
||||
} else {
|
||||
"make killer"
|
||||
};
|
||||
view! {
|
||||
<div class="prompt-options">
|
||||
<button on:click=on_toggle>{text}</button>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionResult::Arcanist(targets, alignment_eq) => {
|
||||
let on_toggle = move |_| {
|
||||
send.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Arcanist(targets.clone(), !alignment_eq),
|
||||
)));
|
||||
};
|
||||
let text = if alignment_eq.same() {
|
||||
"make different"
|
||||
} else {
|
||||
"make same"
|
||||
};
|
||||
view! {
|
||||
<div class="prompt-options">
|
||||
<button on:click=on_toggle>{text}</button>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionResult::GraveDigger(target, 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"));
|
||||
view! { <option selected=*role == role_title>{text}</option> }
|
||||
})
|
||||
.collect_view();
|
||||
|
||||
let res_target = target.clone();
|
||||
let p = possibilities.clone();
|
||||
let on_change_cb = move |ev: Targeted<Event, HtmlSelectElement>| {
|
||||
let select = ev.target();
|
||||
let selected = select.selected_index();
|
||||
if selected == -1 {
|
||||
return;
|
||||
}
|
||||
if let Some(new_role) = p.get(selected as usize) {
|
||||
send.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::GraveDigger(res_target.clone(), *new_role),
|
||||
)));
|
||||
}
|
||||
};
|
||||
let res_target = target.clone();
|
||||
let on_wheel = move |ev: Targeted<WheelEvent, HtmlSelectElement>| {
|
||||
let target = ev.target();
|
||||
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.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::GraveDigger(res_target.clone(), *new_role),
|
||||
)));
|
||||
}
|
||||
};
|
||||
view! {
|
||||
<div class="result-option">
|
||||
<label>{"dug up"}</label>
|
||||
<select on:wheel:target=on_wheel on:change:target=on_change_cb>
|
||||
{roles}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionResult::Mortician(target, died_to_title) => {
|
||||
let roles = DiedToTitle::ALL
|
||||
.into_iter()
|
||||
.map(|died_to| {
|
||||
view! { <option selected=died_to == died_to_title>{died_to.to_string()}</option> }
|
||||
})
|
||||
.collect_view();
|
||||
let res_target = target.clone();
|
||||
let on_change_cb = move |ev: Targeted<Event, HtmlSelectElement>| {
|
||||
let select = ev.target();
|
||||
let selected = select.selected_index();
|
||||
if selected == -1 {
|
||||
return;
|
||||
}
|
||||
if let Some(died_to) = DiedToTitle::ALL.into_iter().nth(selected as _) {
|
||||
send.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Mortician(res_target.clone(), died_to),
|
||||
)));
|
||||
}
|
||||
};
|
||||
let res_target = target.clone();
|
||||
let on_wheel = move |ev: Targeted<WheelEvent, HtmlSelectElement>| {
|
||||
let target = ev.target();
|
||||
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.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Mortician(res_target.clone(), died_to),
|
||||
)));
|
||||
}
|
||||
};
|
||||
view! {
|
||||
<div class="prompt-option">
|
||||
<label>{"died to"}</label>
|
||||
<select on:wheel:target=on_wheel on:change:target=on_change_cb>
|
||||
{roles}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionResult::Insomniac(visits) => {
|
||||
let visits = RwSignal::new(visits.len() as u8);
|
||||
Effect::new(move || {
|
||||
send.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Insomniac(Visits::new(overrides::identities(visits.get() as _))),
|
||||
)))
|
||||
});
|
||||
view! {
|
||||
<div class="result-number">
|
||||
<span>"visits"</span>
|
||||
<IncDecU8 value=visits value_range=1..=0xFF />
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
ActionResult::Empath { target, scapegoat } => {
|
||||
let on_toggle = move |_| {
|
||||
send.set(Some(ServerToHostMessage::ActionResult(
|
||||
None,
|
||||
ActionResult::Empath {
|
||||
scapegoat: !scapegoat,
|
||||
target: target.clone(),
|
||||
},
|
||||
)));
|
||||
};
|
||||
let text = if scapegoat {
|
||||
"make not scapegoat"
|
||||
} else {
|
||||
"make scapegoat"
|
||||
};
|
||||
view! {
|
||||
<div class="prompt-options">
|
||||
<button on:click=on_toggle>{text}</button>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
};
|
||||
view! {
|
||||
<div class="result-overrides">
|
||||
<span>{result.title().to_string()}</span>
|
||||
<div class="result-options">{options}</div>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn HostLobby() -> impl IntoView {}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
use convert_case::{Case, Casing};
|
||||
use leptos::prelude::*;
|
||||
use werewolves_proto::{
|
||||
character::CharacterId,
|
||||
|
|
@ -6,12 +7,12 @@ use werewolves_proto::{
|
|||
host::HostNightMessage,
|
||||
night::{ActionPrompt, ActionResponse},
|
||||
},
|
||||
role::PreviousGuardianAction,
|
||||
role::{PreviousGuardianAction, RoleTitle},
|
||||
};
|
||||
|
||||
use crate::app::{
|
||||
class::AsClasses,
|
||||
components::{Cover, IdentityInline},
|
||||
components::{Cover, IdentityInline, Sample, TutorialBox},
|
||||
error::WolfError,
|
||||
pages::night_actions::{
|
||||
DamnedIntroPage1, DamnedIntroPage2, RoleChange, WolfPackKill, WolvesIntro,
|
||||
|
|
@ -22,7 +23,7 @@ use crate::app::{
|
|||
GuardianPagePreviousProtect2, HunterPage1, InsomniacPage1, LoneWolfPage1,
|
||||
MapleWolfPage1, MasonRecruitPage1, MasonRecruitPage2, MasonsWake, MilitiaPage1,
|
||||
MorticianPage1, PowerSeerPage1, ProtectorPage1, PyremasterPage1, SeerPage1,
|
||||
VindicatorPage1,
|
||||
ShapeshifterPage1, VindicatorPage1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -31,27 +32,23 @@ pub trait RolePage {
|
|||
fn role_pages(&self, big_screen: bool) -> ViewFn;
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RolePrompt(
|
||||
pub fn target_picker_tutorials(prompt: &ActionPrompt) -> Vec<AnyView> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
match prompt {
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pages_for_prompt(
|
||||
prompt: ActionPrompt,
|
||||
page: usize,
|
||||
#[prop(optional)] reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
||||
#[prop(optional)] error: Option<WriteSignal<Option<WolfError>>>,
|
||||
) -> impl IntoView {
|
||||
reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
||||
) -> Vec<AnyView> {
|
||||
let ident = move |character_id: CharacterIdentity| {
|
||||
reply.map(|_| {
|
||||
view! { <IdentityInline ident=character_id.into() /> }
|
||||
})
|
||||
};
|
||||
let interactive = prompt.interactive();
|
||||
let targets = prompt.targets().map(|t| t.to_vec().into_boxed_slice());
|
||||
let marked = prompt
|
||||
.marked()
|
||||
.map(|t| [t.0].into_iter().chain(t.1))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Box<_>>();
|
||||
let mut pages: Vec<AnyView> = match prompt {
|
||||
match prompt {
|
||||
ActionPrompt::CoverOfDarkness => vec![match reply {
|
||||
Some(reply) => view! { <Cover reply=reply /> }.into_any(),
|
||||
None => view! { <Cover /> }.into_any(),
|
||||
|
|
@ -180,8 +177,8 @@ pub fn RolePrompt(
|
|||
ActionPrompt::PyreMaster { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <PyremasterPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::Shapeshifter { .. } => {
|
||||
vec![]
|
||||
ActionPrompt::Shapeshifter { character_id } => {
|
||||
vec![view! {{ident(character_id)} <ShapeshifterPage1 />}.into_any()]
|
||||
}
|
||||
ActionPrompt::AlphaWolf { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <AlphaWolfPage1 /> }.into_any()]
|
||||
|
|
@ -224,7 +221,27 @@ pub fn RolePrompt(
|
|||
ActionPrompt::WolvesIntro { wolves } => {
|
||||
vec![view! { <WolvesIntro wolves=wolves /> }.into_any()]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RolePrompt(
|
||||
prompt: ActionPrompt,
|
||||
page: usize,
|
||||
#[prop(optional)] reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
||||
#[prop(optional)] error: Option<WriteSignal<Option<WolfError>>>,
|
||||
) -> impl IntoView {
|
||||
let interactive = prompt.interactive();
|
||||
let targets = prompt.targets().map(|t| t.to_vec().into_boxed_slice());
|
||||
let marked = prompt
|
||||
.marked()
|
||||
.map(|t| [t.0].into_iter().chain(t.1))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Box<_>>();
|
||||
let tutorials = target_picker_tutorials(&prompt);
|
||||
let shapeshifter = matches!(prompt, ActionPrompt::Shapeshifter { .. });
|
||||
let mut pages: Vec<AnyView> = pages_for_prompt(prompt, reply);
|
||||
// isn't it great that AnyView isn't Clone????
|
||||
if let Some(page) = pages.get(page).is_some().then(|| {
|
||||
let p = pages.swap_remove(page);
|
||||
|
|
@ -249,6 +266,23 @@ pub fn RolePrompt(
|
|||
}) {
|
||||
return page.into_any();
|
||||
}
|
||||
// shapeshifter gets a yes/no
|
||||
if shapeshifter {
|
||||
let decision = RwSignal::new(None);
|
||||
if let Some(reply) = reply {
|
||||
Effect::new(move || match decision.get() {
|
||||
Some(true) => reply.set(Some(HostNightMessage::ActionResponse(
|
||||
ActionResponse::Shapeshift,
|
||||
))),
|
||||
Some(false) => reply.set(Some(HostNightMessage::ActionResponse(
|
||||
ActionResponse::Continue,
|
||||
))),
|
||||
None => {}
|
||||
});
|
||||
}
|
||||
|
||||
return view! { <BooleanPicker decision=decision.write_only() /> }.into_any();
|
||||
}
|
||||
let target_picker = match targets {
|
||||
Some(targets) => {
|
||||
let pick = RwSignal::new(None);
|
||||
|
|
@ -288,8 +322,11 @@ pub fn RolePrompt(
|
|||
</button>
|
||||
}
|
||||
});
|
||||
let tutorials = (continue_btn.is_some() && !tutorials.is_empty())
|
||||
.then(|| tutorials.into_iter().collect_view());
|
||||
view! {
|
||||
{target_picker}
|
||||
{tutorials}
|
||||
{continue_btn}
|
||||
}
|
||||
.into_any()
|
||||
|
|
@ -323,5 +360,19 @@ pub fn TargetPicker(
|
|||
})
|
||||
.collect_view();
|
||||
|
||||
view! { <div class="target-picker">{targets}</div> }
|
||||
view! { <div class="target-picker" class:allow-scroll=pick.is_some()>{targets}</div> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BooleanPicker(decision: WriteSignal<Option<bool>>) -> impl IntoView {
|
||||
view! {
|
||||
<div class="bool-picker">
|
||||
<button class="no-hover" on:click=move |_| decision.set(Some(true))>
|
||||
"yes"
|
||||
</button>
|
||||
<button class="no-hover" on:click=move |_| decision.set(Some(false))>
|
||||
"no"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ use werewolves_proto::message::{
|
|||
use crate::app::{
|
||||
components::Cover,
|
||||
pages::night_actions::{
|
||||
DrunkPage, RoleblockPage,
|
||||
DrunkPage, RoleblockPage, ShiftFailed,
|
||||
role::{
|
||||
AdjudicatorResult, ArcanistResult, EmpathResult, GravediggerResultPage,
|
||||
InsomniacResult, MorticianResultPage, PowerSeerResult, SeerResult,
|
||||
AdjudicatorResult, ArcanistResult, BeholderSawEverything, BeholderSawNothing,
|
||||
EmpathResult, GravediggerResultPage, InsomniacResult, MorticianResultPage,
|
||||
PowerSeerResult, SeerResult,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -23,57 +24,41 @@ pub fn RoleResult(
|
|||
#[prop(optional)] reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
||||
) -> impl IntoView {
|
||||
let body = match result {
|
||||
ActionResult::RoleBlocked => view!{
|
||||
<RoleblockPage />
|
||||
}.into_any(),
|
||||
ActionResult::Drunk => view! {
|
||||
<DrunkPage />
|
||||
}.into_any(),
|
||||
ActionResult::Seer(target, alignment) => view!{
|
||||
<SeerResult target=target.into_public() alignment=alignment/>
|
||||
}.into_any(),
|
||||
ActionResult::PowerSeer { target, powerful } => view!{
|
||||
<PowerSeerResult target=target.into_public() powerful=powerful/>
|
||||
}.into_any(),
|
||||
ActionResult::Adjudicator { target, killer } => view!{
|
||||
<AdjudicatorResult target=target.into_public() killer=killer/>
|
||||
}.into_any(),
|
||||
ActionResult::RoleBlocked => view! { <RoleblockPage /> }.into_any(),
|
||||
ActionResult::Drunk => view! { <DrunkPage /> }.into_any(),
|
||||
ActionResult::Seer(target, alignment) => view! { <SeerResult target=target.into_public() alignment=alignment /> }.into_any(),
|
||||
ActionResult::PowerSeer { target, powerful } => view! { <PowerSeerResult target=target.into_public() powerful=powerful /> }.into_any(),
|
||||
ActionResult::Adjudicator { target, killer } => view! { <AdjudicatorResult target=target.into_public() killer=killer /> }.into_any(),
|
||||
ActionResult::Arcanist((target1, target2), alignment_eq) => view! {
|
||||
<ArcanistResult
|
||||
value=alignment_eq
|
||||
targets=(target1.into_public(), target2.into_public())
|
||||
/>
|
||||
}.into_any(),
|
||||
ActionResult::GraveDigger(target, role) => view! {
|
||||
<GravediggerResultPage target=target.into_public() role=role/>
|
||||
}.into_any(),
|
||||
ActionResult::Mortician(target, died_to) => view!{
|
||||
<MorticianResultPage target=target.into_public() died_to=died_to />
|
||||
}.into_any(),
|
||||
ActionResult::Insomniac(visits) => view!{
|
||||
<InsomniacResult visits=visits/>
|
||||
}.into_any(),
|
||||
ActionResult::Empath { target, scapegoat } => view! {
|
||||
<EmpathResult target=target.into_public() scapegoat=scapegoat />
|
||||
}.into_any(),
|
||||
ActionResult::BeholderSawNothing => todo!(),
|
||||
ActionResult::BeholderSawEverything => todo!(),
|
||||
ActionResult::GraveDigger(target, role) => view! { <GravediggerResultPage target=target.into_public() role=role /> }.into_any(),
|
||||
ActionResult::Mortician(target, died_to) => view! { <MorticianResultPage target=target.into_public() died_to=died_to /> }.into_any(),
|
||||
ActionResult::Insomniac(visits) => view! { <InsomniacResult visits=visits /> }.into_any(),
|
||||
ActionResult::Empath { target, scapegoat } => view! { <EmpathResult target=target.into_public() scapegoat=scapegoat /> }.into_any(),
|
||||
ActionResult::BeholderSawNothing => view! { <BeholderSawNothing /> }.into_any(),
|
||||
ActionResult::BeholderSawEverything => view! { <BeholderSawEverything /> }.into_any(),
|
||||
ActionResult::GoBackToSleep => return match reply {
|
||||
Some(reply) => view! { <Cover message="go to sleep" reply=reply reply_to_send=HostNightMessage::Next /> }
|
||||
.into_any(),
|
||||
None => view! { <Cover message="go to sleep" /> }.into_any(),
|
||||
},
|
||||
ActionResult::ShiftFailed => todo!(),
|
||||
ActionResult::ShiftFailed => view!{<ShiftFailed />}.into_any(),
|
||||
ActionResult::SkippedByHost |
|
||||
ActionResult::Continue => {
|
||||
Effect::new(move || {
|
||||
let Some(reply) = reply else {
|
||||
return;
|
||||
};
|
||||
// NOTE: in the old code this was a GetState and the return was
|
||||
// a blank <Cover />
|
||||
reply.set(Some(HostNightMessage::Next));
|
||||
});
|
||||
().into_any()
|
||||
}
|
||||
ActionResult::SkippedByHost => todo!(),
|
||||
};
|
||||
let next_btn = reply.map(|reply| {
|
||||
view! {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ pub fn AdjudicatorPage1() -> impl IntoView {
|
|||
<div class="role-page">
|
||||
<span class="defensive box title">"ADJUDICATOR"</span>
|
||||
<div class="information box defensive faint">
|
||||
<h4>"PICK A PLAYER"</h4>
|
||||
<span>"PICK A PLAYER"</span>
|
||||
<Icon source=IconSource::Killer r#type=IconType::Fit />
|
||||
<h4 class="yellow">"YOU WILL CHECK IF THEY APPEAR AS A KILLER"</h4>
|
||||
<span class="yellow">"YOU WILL CHECK IF THEY APPEAR AS A KILLER"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ pub fn AdjudicatorResult(killer: Killer, target: PublicIdentity) -> impl IntoVie
|
|||
<div class="information box defensive faint">
|
||||
<IdentityInline ident=target.clone() />
|
||||
{icon}
|
||||
<h3 class="yellow">{text}</h3>
|
||||
<span class="yellow">{text}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,12 +39,14 @@ pub fn ArcanistResult(
|
|||
targets: (PublicIdentity, PublicIdentity),
|
||||
) -> impl IntoView {
|
||||
let text = match value {
|
||||
AlignmentEq::Same => "ARE THE SAME",
|
||||
AlignmentEq::Different => "ARE DIFFERENT",
|
||||
AlignmentEq::Same => view! {"ARE THE SAME"}.into_any(),
|
||||
AlignmentEq::Different => {
|
||||
view! {"ARE "<span class="wolves underline">"DIFFERENT"</span>}.into_any()
|
||||
}
|
||||
};
|
||||
let icons = match value {
|
||||
AlignmentEq::Same => view! { <Icon source=IconSource::Equal /> },
|
||||
AlignmentEq::Different => view! { <Icon source=IconSource::NotEqual /> },
|
||||
AlignmentEq::Same => IconSource::BalancedScales,
|
||||
AlignmentEq::Different => IconSource::UnbalancedScales,
|
||||
};
|
||||
view! {
|
||||
<div class="role-page">
|
||||
|
|
@ -55,7 +57,7 @@ pub fn ArcanistResult(
|
|||
<span class="and">"AND"</span>
|
||||
<IdentityInline ident=targets.1 />
|
||||
</div>
|
||||
<div class="icons">{icons}</div>
|
||||
<Icon source=icons />
|
||||
<span class="yellow">{text}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ pub fn BeholderSawNothing() -> impl IntoView {
|
|||
<div class="role-page">
|
||||
<span class="intel box title">"BEHOLDER"</span>
|
||||
<div class="information box intel faint">
|
||||
<h1>"YOUR TARGET HAS DIED"</h1>
|
||||
<span>"YOUR TARGET HAS DIED"</span>
|
||||
<Icon source=IconSource::RedX />
|
||||
<h1>"BUT SAW NOTHING"</h1>
|
||||
<span>"BUT SAW NOTHING"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
use crate::app::components::{Icon, IconSource, IconType};
|
||||
|
||||
#[component]
|
||||
pub fn BloodletterPage1() -> impl IntoView {
|
||||
|
|
@ -24,9 +24,13 @@ pub fn BloodletterPage1() -> impl IntoView {
|
|||
<div class="information box wolves faint">
|
||||
<span>"PICK A PLAYER"</span>
|
||||
<span class="inline-icons">
|
||||
"THEY'LL APPEAR AS A WOLF " <Icon source=IconSource::Wolves /> "KILLER"
|
||||
<Icon source=IconSource::Killer /> "AND POWERFUL"
|
||||
<Icon source=IconSource::Powerful /> "IN CHECKS FOR 2 NIGHTS"
|
||||
"THEY'LL APPEAR AS A" <span class="wolves underline">"WOLF"</span>
|
||||
<Icon source=IconSource::Wolves r#type=IconType::Shrink />
|
||||
<span class="wolves underline">"KILLER"</span>
|
||||
<Icon source=IconSource::Killer r#type=IconType::Shrink /> "AND"
|
||||
<span class="wolves underline">"POWERFUL"</span>
|
||||
<Icon source=IconSource::Powerful r#type=IconType::Shrink />
|
||||
"IN CHECKS FOR 2 NIGHTS"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
use leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn ShapeshifterPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="wolves box title">"SHAPESHIFTER"</span>
|
||||
<div class="information box wolves faint">
|
||||
<span>
|
||||
"WOULD YOU LIKE TO USE YOUR " <span class="yellow">{"ONCE PER GAME"}</span>
|
||||
" SHAPESHIFT ABILITY?"
|
||||
</span>
|
||||
<span class="breakable">
|
||||
<span class="yellow breakable">"YOU WILL DIE"</span>
|
||||
", BUT THE TARGET OF THE WOLFPACK KILL SHALL INSTEAD BECOME A WOLF"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ShiftFailed() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="wolves box title">"SHIFT FAILED"</span>
|
||||
<div class="information wolves box faint">
|
||||
"YOUR SHIFT HAS FAILED" <Icon source=IconSource::RedX />
|
||||
"YOU RETAIN YOUR SHAPESHIFT ABILITY"
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue