Compare commits
18 Commits
main
...
leptos-por
| Author | SHA1 | Date |
|---|---|---|
|
|
567e023a73 | |
|
|
5a452b220c | |
|
|
c8e51f36e2 | |
|
|
80859f58d0 | |
|
|
d24ed6d86f | |
|
|
9bba472917 | |
|
|
d3d2819e12 | |
|
|
f280f35305 | |
|
|
14e8f369ea | |
|
|
852973eddf | |
|
|
06294d872e | |
|
|
6b3cce5e37 | |
|
|
6850da7555 | |
|
|
0fd0f8f5c9 | |
|
|
ab1cb29952 | |
|
|
6fb105ca82 | |
|
|
4f04a8597d | |
|
|
314e113a46 |
|
|
@ -7,3 +7,7 @@ werewolves/img/icons.svg
|
||||||
license_headers.fish
|
license_headers.fish
|
||||||
util/
|
util/
|
||||||
werewolves/Trunk-local.toml
|
werewolves/Trunk-local.toml
|
||||||
|
public/img/icons.svg
|
||||||
|
|
||||||
|
werewolves-old-client/
|
||||||
|
werewolves-old-server/
|
||||||
|
|
|
||||||
132
Cargo.toml
|
|
@ -1,8 +1,136 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = [
|
members = [
|
||||||
"werewolves",
|
# "werewolves-old-client",
|
||||||
"werewolves-macros",
|
"werewolves-macros",
|
||||||
"werewolves-proto",
|
"werewolves-proto",
|
||||||
"werewolves-server",
|
# "werewolves-server",
|
||||||
|
"werewolves",
|
||||||
|
# "api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[workspace.metadata.leptos]]
|
||||||
|
watch-additional-files = ["werewolves", "api", "style", "public"]
|
||||||
|
|
||||||
|
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||||
|
output-name = "werewolves"
|
||||||
|
|
||||||
|
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||||
|
site-root = "target/site"
|
||||||
|
|
||||||
|
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||||
|
# Defaults to pkg
|
||||||
|
site-pkg-dir = "pkg"
|
||||||
|
|
||||||
|
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||||
|
style-file = "style/main.scss"
|
||||||
|
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||||
|
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||||
|
#
|
||||||
|
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||||
|
assets-dir = "public"
|
||||||
|
|
||||||
|
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||||
|
site-addr = "127.0.0.1:3000"
|
||||||
|
# site-addr = "192.168.1.3:3000"
|
||||||
|
|
||||||
|
# The port to use for automatic reload monitoring
|
||||||
|
reload-port = 3001
|
||||||
|
|
||||||
|
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||||
|
# [Windows] for non-WSL use "npx.cmd playwright test"
|
||||||
|
# This binary name can be checked in Powershell with Get-Command npx
|
||||||
|
end2end-cmd = "npx playwright test"
|
||||||
|
end2end-dir = "end2end"
|
||||||
|
|
||||||
|
# The browserlist query used for optimizing the CSS.
|
||||||
|
browserquery = "defaults"
|
||||||
|
|
||||||
|
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||||
|
env = "DEV"
|
||||||
|
|
||||||
|
# The features to use when compiling the bin target
|
||||||
|
#
|
||||||
|
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||||
|
bin-features = ["ssr"]
|
||||||
|
|
||||||
|
# If the --no-default-features flag should be used when compiling the bin target
|
||||||
|
#
|
||||||
|
# Optional. Defaults to false.
|
||||||
|
bin-default-features = false
|
||||||
|
|
||||||
|
# The features to use when compiling the lib target
|
||||||
|
#
|
||||||
|
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||||
|
lib-features = ["hydrate"]
|
||||||
|
|
||||||
|
# If the --no-default-features flag should be used when compiling the lib target
|
||||||
|
#
|
||||||
|
# Optional. Defaults to false.
|
||||||
|
lib-default-features = false
|
||||||
|
|
||||||
|
# The profile to use for the lib target when compiling for release
|
||||||
|
#
|
||||||
|
# Optional. Defaults to "release".
|
||||||
|
lib-profile-release = "wasm-release"
|
||||||
|
name = "werewolves"
|
||||||
|
bin-package = "werewolves"
|
||||||
|
lib-package = "werewolves"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
axum = "0.8.1"
|
||||||
|
axum-extra = { version = "0.12", features = ["typed-header"] }
|
||||||
|
cfg-if = "1.0.0"
|
||||||
|
console_error_panic_hook = "0.1.7"
|
||||||
|
console_log = "1.0.0"
|
||||||
|
http = "1.3.1"
|
||||||
|
log = "0.4.27"
|
||||||
|
simple_logger = "5.0.0"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
wasm-bindgen = "0.2.106"
|
||||||
|
leptos-use = { version = "0.18" }
|
||||||
|
# leptos-use = { path = "../repos/leptos-use" }
|
||||||
|
werewolves-macros = { path = "werewolves-macros" }
|
||||||
|
werewolves-proto = { path = "werewolves-proto" }
|
||||||
|
serde_json = { version = "1" }
|
||||||
|
futures = { version = "*" }
|
||||||
|
codee = { version = "0.3", features = ["msgpack_serde"] }
|
||||||
|
bytes = { version = "1.10" }
|
||||||
|
convert_case = { version = "0.11" }
|
||||||
|
fast_qr = { version = "0.13", features = ["svg"] }
|
||||||
|
anyhow = { version = "1" }
|
||||||
|
uuid = { version = "1.18" }
|
||||||
|
sqlx = { version = "0.8", features = [
|
||||||
|
"runtime-tokio",
|
||||||
|
"postgres",
|
||||||
|
"derive",
|
||||||
|
"macros",
|
||||||
|
"uuid",
|
||||||
|
"chrono",
|
||||||
|
] }
|
||||||
|
argon2 = { version = "0.5" }
|
||||||
|
async-trait = { version = "0.1" }
|
||||||
|
chrono = { version = "0.4" }
|
||||||
|
leptos = { version = "0.8.2" }
|
||||||
|
leptos_axum = { version = "0.8.2" }
|
||||||
|
leptos_meta = { version = "0.8.2" }
|
||||||
|
leptos_router = { version = "0.8.2" }
|
||||||
|
rand = { version = "*" }
|
||||||
|
serde = { version = "1.0.228" }
|
||||||
|
tokio = { version = "1.45.0", features = ["full"] }
|
||||||
|
tower = { version = "0.5.2", features = ["full"] }
|
||||||
|
tower-http = { version = "0.6.4", features = ["full"] }
|
||||||
|
ciborium = { version = "0.2" }
|
||||||
|
pretty_assertions = { version = "1.4" }
|
||||||
|
colored = { version = "3.1" }
|
||||||
|
pretty_env_logger = { version = "0.5" }
|
||||||
|
sorted-vec = { version = "0.8" }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 0
|
||||||
|
debug = "full"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
drop table if exists users cascade;
|
||||||
|
create table users (
|
||||||
|
id uuid not null default gen_random_uuid() primary key,
|
||||||
|
username text not null,
|
||||||
|
display_name text,
|
||||||
|
pronouns text,
|
||||||
|
password_hash text not null,
|
||||||
|
dummy boolean not null default false,
|
||||||
|
|
||||||
|
created_at timestamp with time zone not null,
|
||||||
|
updated_at timestamp with time zone not null,
|
||||||
|
|
||||||
|
check (created_at <= updated_at)
|
||||||
|
);
|
||||||
|
drop index if exists users_username_idx;
|
||||||
|
create index users_username_idx on users (username);
|
||||||
|
drop index if exists users_username_unique;
|
||||||
|
create unique index users_username_unique on users (lower(username));
|
||||||
|
|
||||||
|
drop table if exists login_tokens cascade;
|
||||||
|
create table login_tokens (
|
||||||
|
token text not null primary key,
|
||||||
|
user_id uuid not null references users(id) on delete cascade,
|
||||||
|
created_at timestamp with time zone not null,
|
||||||
|
expires_at timestamp with time zone not null,
|
||||||
|
|
||||||
|
check (created_at < expires_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
drop type if exists game_status cascade;
|
||||||
|
create type game_status as enum (
|
||||||
|
'Lobby',
|
||||||
|
'RoleReveal',
|
||||||
|
'Started',
|
||||||
|
'GameOver',
|
||||||
|
'Cancelled'
|
||||||
|
);
|
||||||
|
|
||||||
|
drop table if exists games cascade;
|
||||||
|
create table games (
|
||||||
|
id uuid not null primary key,
|
||||||
|
host uuid not null references users(id) on delete cascade,
|
||||||
|
created_at timestamp with time zone not null default now(),
|
||||||
|
game_state jsonb not null,
|
||||||
|
game_status game_status not null default 'Lobby'
|
||||||
|
);
|
||||||
|
|
||||||
|
drop table if exists game_characters cascade;
|
||||||
|
create table game_characters (
|
||||||
|
character_id uuid not null primary key,
|
||||||
|
game_id uuid not null references games(id) on delete cascade,
|
||||||
|
player_id uuid not null references users(id) on delete cascade,
|
||||||
|
number integer
|
||||||
|
);
|
||||||
|
|
||||||
|
drop index if exists game_characters_player_id_game_id_unique;
|
||||||
|
create unique index game_characters_player_id_game_id_unique on game_characters (player_id, game_id);
|
||||||
|
|
||||||
|
|
||||||
|
drop table if exists dead_chat cascade;
|
||||||
|
create table dead_chat (
|
||||||
|
message_id uuid not null primary key,
|
||||||
|
game_id uuid not null references games(id) on delete cascade,
|
||||||
|
created_at timestamp with time zone not null,
|
||||||
|
message jsonb not null
|
||||||
|
);
|
||||||
|
|
||||||
|
drop index if exists dead_chat_created_at;
|
||||||
|
create index dead_chat_created_at on dead_chat(created_at);
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 979 B After Width: | Height: | Size: 979 B |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 776 B After Width: | Height: | Size: 776 B |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 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 |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
|
@ -0,0 +1,48 @@
|
||||||
|
.big-screen-wrapper {
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
font-size: 3em;
|
||||||
|
|
||||||
|
.target-picker {
|
||||||
|
font-size: 1.25em;
|
||||||
|
|
||||||
|
.target {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-reveal {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
// font-size: 2em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5ch;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.player {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: color.change($red1, $alpha: 0.1);
|
||||||
|
border: 1px solid color.change($red1, $alpha: 0.6);
|
||||||
|
|
||||||
|
&.ready {
|
||||||
|
background-color: color.change($blue1, $alpha: 0.3);
|
||||||
|
border: 1px solid $blue1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,409 @@
|
||||||
|
|
||||||
|
.village {
|
||||||
|
--faction-color: $village_color;
|
||||||
|
--faction-border: $village_border;
|
||||||
|
--faction-color-faint: $village_color_faint;
|
||||||
|
--faction-border-faint: $village_border_faint;
|
||||||
|
|
||||||
|
&.box {
|
||||||
|
background-color: $village_color;
|
||||||
|
border: 1px solid $village_border;
|
||||||
|
|
||||||
|
.selected:not(.faint) {
|
||||||
|
color: white;
|
||||||
|
background-color: $village_border;
|
||||||
|
}
|
||||||
|
.selected.faint {
|
||||||
|
color: white;
|
||||||
|
background-color: $village_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover:not(.selected):hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $village_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint:not(.selected) {
|
||||||
|
border: 1px solid $village_border_faint;
|
||||||
|
background-color: $village_color_faint;
|
||||||
|
|
||||||
|
&.hover:hover {
|
||||||
|
background-color: $village_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.underline {
|
||||||
|
text-decoration: $village_color underline;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
text-decoration: $village_color_faint underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-color {
|
||||||
|
color: $village_border;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
color: $village_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolves {
|
||||||
|
--faction-color: $wolves_color;
|
||||||
|
--faction-border: $wolves_border;
|
||||||
|
--faction-color-faint: $wolves_color_faint;
|
||||||
|
--faction-border-faint: $wolves_border_faint;
|
||||||
|
|
||||||
|
&.box {
|
||||||
|
background-color: $wolves_color;
|
||||||
|
border: 1px solid $wolves_border;
|
||||||
|
|
||||||
|
.selected:not(.faint) {
|
||||||
|
color: white;
|
||||||
|
background-color: $wolves_border;
|
||||||
|
}
|
||||||
|
.selected.faint {
|
||||||
|
color: white;
|
||||||
|
background-color: $wolves_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover:not(.selected):hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $wolves_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint:not(.selected) {
|
||||||
|
border: 1px solid $wolves_border_faint;
|
||||||
|
background-color: $wolves_color_faint;
|
||||||
|
|
||||||
|
&.hover:hover {
|
||||||
|
background-color: $wolves_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.underline {
|
||||||
|
text-decoration: $wolves_color underline;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
text-decoration: $wolves_color_faint underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-color {
|
||||||
|
color: $wolves_border;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
color: $wolves_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.offensive {
|
||||||
|
--faction-color: $offensive_color;
|
||||||
|
--faction-border: $offensive_border;
|
||||||
|
--faction-color-faint: $offensive_color_faint;
|
||||||
|
--faction-border-faint: $offensive_border_faint;
|
||||||
|
|
||||||
|
&.box {
|
||||||
|
background-color: $offensive_color;
|
||||||
|
border: 1px solid $offensive_border;
|
||||||
|
|
||||||
|
.selected:not(.faint) {
|
||||||
|
color: white;
|
||||||
|
background-color: $offensive_border;
|
||||||
|
}
|
||||||
|
.selected.faint {
|
||||||
|
color: white;
|
||||||
|
background-color: $offensive_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover:not(.selected):hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $offensive_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint:not(.selected) {
|
||||||
|
border: 1px solid $offensive_border_faint;
|
||||||
|
background-color: $offensive_color_faint;
|
||||||
|
|
||||||
|
&.hover:hover {
|
||||||
|
background-color: $offensive_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.underline {
|
||||||
|
text-decoration: $offensive_color underline;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
text-decoration: $offensive_color_faint underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-color {
|
||||||
|
color: $offensive_border;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
color: $offensive_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.defensive {
|
||||||
|
--faction-color: $defensive_color;
|
||||||
|
--faction-border: $defensive_border;
|
||||||
|
--faction-color-faint: $defensive_color_faint;
|
||||||
|
--faction-border-faint: $defensive_border_faint;
|
||||||
|
|
||||||
|
&.box {
|
||||||
|
background-color: $defensive_color;
|
||||||
|
border: 1px solid $defensive_border;
|
||||||
|
|
||||||
|
.selected:not(.faint) {
|
||||||
|
color: white;
|
||||||
|
background-color: $defensive_border;
|
||||||
|
}
|
||||||
|
.selected.faint {
|
||||||
|
color: white;
|
||||||
|
background-color: $defensive_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover:not(.selected):hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $defensive_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint:not(.selected) {
|
||||||
|
border: 1px solid $defensive_border_faint;
|
||||||
|
background-color: $defensive_color_faint;
|
||||||
|
|
||||||
|
&.hover:hover {
|
||||||
|
background-color: $defensive_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.underline {
|
||||||
|
text-decoration: $defensive_color underline;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
text-decoration: $defensive_color_faint underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-color {
|
||||||
|
color: $defensive_border;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
color: $defensive_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intel {
|
||||||
|
--faction-color: $intel_color;
|
||||||
|
--faction-border: $intel_border;
|
||||||
|
--faction-color-faint: $intel_color_faint;
|
||||||
|
--faction-border-faint: $intel_border_faint;
|
||||||
|
|
||||||
|
&.box {
|
||||||
|
background-color: $intel_color;
|
||||||
|
border: 1px solid $intel_border;
|
||||||
|
|
||||||
|
.selected:not(.faint) {
|
||||||
|
color: white;
|
||||||
|
background-color: $intel_border;
|
||||||
|
}
|
||||||
|
.selected.faint {
|
||||||
|
color: white;
|
||||||
|
background-color: $intel_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover:not(.selected):hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $intel_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint:not(.selected) {
|
||||||
|
border: 1px solid $intel_border_faint;
|
||||||
|
background-color: $intel_color_faint;
|
||||||
|
|
||||||
|
&.hover:hover {
|
||||||
|
background-color: $intel_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.underline {
|
||||||
|
text-decoration: $intel_color underline;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
text-decoration: $intel_color_faint underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-color {
|
||||||
|
color: $intel_border;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
color: $intel_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
&.box {
|
||||||
|
background-color: $starts_as_villager_color;
|
||||||
|
border: 1px solid $starts_as_villager_border;
|
||||||
|
|
||||||
|
.selected:not(.faint) {
|
||||||
|
color: white;
|
||||||
|
background-color: $starts_as_villager_border;
|
||||||
|
}
|
||||||
|
.selected.faint {
|
||||||
|
color: white;
|
||||||
|
background-color: $starts_as_villager_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover:not(.selected):hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $starts_as_villager_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint:not(.selected) {
|
||||||
|
border: 1px solid $starts_as_villager_border_faint;
|
||||||
|
background-color: $starts_as_villager_color_faint;
|
||||||
|
|
||||||
|
&.hover:hover {
|
||||||
|
background-color: $starts_as_villager_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.underline {
|
||||||
|
text-decoration: $starts_as_villager_color underline;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
text-decoration: $starts_as_villager_color_faint underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-color {
|
||||||
|
color: $starts_as_villager_border;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
color: $starts_as_villager_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.damned {
|
||||||
|
--faction-color: $damned_color;
|
||||||
|
--faction-border: $damned_border;
|
||||||
|
--faction-color-faint: $damned_color_faint;
|
||||||
|
--faction-border-faint: $damned_border_faint;
|
||||||
|
|
||||||
|
&.box {
|
||||||
|
background-color: $damned_color;
|
||||||
|
border: 1px solid $damned_border;
|
||||||
|
|
||||||
|
.selected:not(.faint) {
|
||||||
|
color: white;
|
||||||
|
background-color: $damned_border;
|
||||||
|
}
|
||||||
|
.selected.faint {
|
||||||
|
color: white;
|
||||||
|
background-color: $damned_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover:not(.selected):hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $damned_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint:not(.selected) {
|
||||||
|
border: 1px solid $damned_border_faint;
|
||||||
|
background-color: $damned_color_faint;
|
||||||
|
|
||||||
|
&.hover:hover {
|
||||||
|
background-color: $damned_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.underline {
|
||||||
|
text-decoration: $damned_color underline;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
text-decoration: $damned_color_faint underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-color {
|
||||||
|
color: $damned_border;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
color: $damned_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drunk {
|
||||||
|
--faction-color: $drunk_color;
|
||||||
|
--faction-border: $drunk_border;
|
||||||
|
--faction-color-faint: $drunk_color_faint;
|
||||||
|
--faction-border-faint: $drunk_border_faint;
|
||||||
|
|
||||||
|
&.box {
|
||||||
|
background-color: $drunk_color;
|
||||||
|
border: 1px solid $drunk_border;
|
||||||
|
|
||||||
|
.selected:not(.faint) {
|
||||||
|
color: white;
|
||||||
|
background-color: $drunk_border;
|
||||||
|
}
|
||||||
|
.selected.faint {
|
||||||
|
color: white;
|
||||||
|
background-color: $drunk_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover:not(.selected):hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $drunk_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint:not(.selected) {
|
||||||
|
border: 1px solid $drunk_border_faint;
|
||||||
|
background-color: $drunk_color_faint;
|
||||||
|
|
||||||
|
&.hover:hover {
|
||||||
|
background-color: $drunk_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.underline {
|
||||||
|
text-decoration: $drunk_color underline;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
text-decoration: $drunk_color_faint underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-color {
|
||||||
|
color: $drunk_border;
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
color: $drunk_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
.icon-fit {
|
||||||
|
height: 1em;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
padding: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: contrast(120%) brightness(120%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-shrink {
|
||||||
|
flex-shrink: 1;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
.cover-of-darkness {
|
||||||
|
background-color: black;
|
||||||
|
font-size: 3em;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
text-wrap: wrap;
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 3ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
width: fit-content;
|
||||||
|
text-align: center;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolves-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.information {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5ch;
|
||||||
|
|
||||||
|
font-size: 1.75em;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.subtext {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arcanist-targets {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1ch;
|
||||||
|
font-size: 0.7em;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.and {
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 50%;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-page {
|
||||||
|
padding: 1vh 1vw 1vh 1vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 1ch;
|
||||||
|
height: 98%;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 2ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.character {
|
||||||
|
padding: 1ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow {
|
||||||
|
color: $wakes_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolves-list {
|
||||||
|
padding: 1ch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
.character {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 37vw;
|
||||||
|
|
||||||
|
font-size: 1.5em;
|
||||||
|
|
||||||
|
.role {
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.continue-button {
|
||||||
|
font-size: 2.25em;
|
||||||
|
padding: 0.3ch;
|
||||||
|
margin: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.breakable {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-icons {
|
||||||
|
display: flex;
|
||||||
|
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;
|
||||||
|
|
||||||
|
.target {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
background-color: color.change($red1, $alpha: 0.1);
|
||||||
|
border: 1px solid color.change($red1, $alpha: 0.6);
|
||||||
|
|
||||||
|
&.marked {
|
||||||
|
background-color: color.change($blue1, $alpha: 0.3);
|
||||||
|
border: 1px solid $blue1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seer-icons,
|
||||||
|
.arcanist-icons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
gap: 10%;
|
||||||
|
|
||||||
|
@media only screen and (min-width : 1200px) {
|
||||||
|
&>img {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 2fr;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seer-check {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.false-positives {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.5em;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bound {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
.setup-screen {
|
||||||
|
.inactive {
|
||||||
|
filter: brightness(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
margin: 2%;
|
||||||
|
font-size: 1.5em;
|
||||||
|
|
||||||
|
.setup {
|
||||||
|
display: grid;
|
||||||
|
grid: auto-flow / 1fr 1fr 1fr;
|
||||||
|
gap: 5vw;
|
||||||
|
row-gap: 2ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-category {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 0.25ch;
|
||||||
|
text-align: left;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding: 0.1ch;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: black 1px 1px;
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 0.25ch;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
padding: 0 0.5ch 0 0.5ch;
|
||||||
|
|
||||||
|
&.invisible {
|
||||||
|
opacity: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.attributes {
|
||||||
|
margin-left: 10px;
|
||||||
|
align-self: flex-end;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-shadow: black 1px 1px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
filter: saturate(40%);
|
||||||
|
padding: 0.25ch 0 0.25ch 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wakes {
|
||||||
|
border: 2px solid $wakes_color;
|
||||||
|
box-shadow: 0 0 3px $wakes_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
z-index: 100;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: 5vw;
|
||||||
|
width: 90vw;
|
||||||
|
height: 90vh;
|
||||||
|
gap: 1cm;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 70%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
font-size: 5vw;
|
||||||
|
border: 1px solid $village_border;
|
||||||
|
background-color: color.change($village_color, $alpha: 0.3);
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&>* {
|
||||||
|
margin-top: 0.5cm;
|
||||||
|
margin-bottom: 0.5cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,6 @@ use proc_macro2::Span;
|
||||||
use quote::{ToTokens, quote};
|
use quote::{ToTokens, quote};
|
||||||
use syn::{parse::Parse, parse_macro_input};
|
use syn::{parse::Parse, parse_macro_input};
|
||||||
|
|
||||||
use crate::ref_and_mut::RefAndMut;
|
|
||||||
|
|
||||||
mod all;
|
mod all;
|
||||||
mod checks;
|
mod checks;
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,38 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = { version = "2" }
|
bytes = { version = "1.10.1", features = ["serde"] }
|
||||||
log = { version = "0.4" }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { version = "1.0" }
|
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
thiserror = { workspace = true }
|
||||||
uuid = { version = "1.17", features = ["v4", "serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
rand = { version = "0.9", features = ["std_rng"] }
|
sqlx = { workspace = true, optional = true }
|
||||||
werewolves-macros = { path = "../werewolves-macros" }
|
axum = { workspace = true, optional = true, features = ["macros"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
axum-extra = { workspace = true, optional = true, features = ["typed-header"] }
|
||||||
|
argon2 = { workspace = true, optional = true }
|
||||||
|
ciborium = { workspace = true }
|
||||||
|
async-trait = { workspace = true, optional = true }
|
||||||
|
serde_json = { workspace = true, optional = true }
|
||||||
|
futures = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
log.workspace = true
|
||||||
|
leptos.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
werewolves-macros.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { version = "1" }
|
colored.workspace = true
|
||||||
pretty_env_logger = { version = "0.5" }
|
pretty_assertions.workspace = true
|
||||||
colored = { version = "3.0" }
|
pretty_env_logger.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
ssr = [
|
||||||
|
"dep:sqlx",
|
||||||
|
"dep:axum",
|
||||||
|
"dep:axum-extra",
|
||||||
|
"dep:argon2",
|
||||||
|
"dep:async-trait",
|
||||||
|
"dep:serde_json",
|
||||||
|
"dep:futures",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
use axum::{
|
||||||
|
body::Bytes,
|
||||||
|
extract::{FromRequest, Request, rejection::BytesRejection},
|
||||||
|
http::{HeaderMap, HeaderValue, StatusCode, header},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use axum_extra::headers::Mime;
|
||||||
|
use bytes::{BufMut, BytesMut};
|
||||||
|
use core::fmt::Display;
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
const CBOR_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("application/cbor");
|
||||||
|
const PLAIN_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("text/plain");
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub struct Cbor<T>(pub T);
|
||||||
|
|
||||||
|
impl<T> Cbor<T> {
|
||||||
|
pub const fn new(t: T) -> Self {
|
||||||
|
Self(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, S> FromRequest<S> for Cbor<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = CborRejection;
|
||||||
|
|
||||||
|
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
if !cbor_content_type(req.headers()) {
|
||||||
|
return Err(CborRejection::MissingCborContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = Bytes::from_request(req, state).await?;
|
||||||
|
Ok(Self(ciborium::from_reader::<T, _>(&*bytes)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IntoResponse for Cbor<T>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
// Extracted into separate fn so it's only compiled once for all T.
|
||||||
|
fn make_response(buf: BytesMut, ser_result: Result<(), CborRejection>) -> Response {
|
||||||
|
match ser_result {
|
||||||
|
Ok(()) => {
|
||||||
|
([(header::CONTENT_TYPE, CBOR_CONTENT_TYPE)], buf.freeze()).into_response()
|
||||||
|
}
|
||||||
|
Err(err) => err.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a small initial capacity of 128 bytes like serde_json::to_vec
|
||||||
|
// https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189
|
||||||
|
let mut buf = BytesMut::with_capacity(128).writer();
|
||||||
|
let res = ciborium::into_writer(&self.0, &mut buf)
|
||||||
|
.map_err(|err| CborRejection::SerdeRejection(err.to_string()));
|
||||||
|
make_response(buf.into_inner(), res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CborRejection {
|
||||||
|
MissingCborContentType,
|
||||||
|
BytesRejection(BytesRejection),
|
||||||
|
DeserializeRejection(String),
|
||||||
|
SerdeRejection(String),
|
||||||
|
}
|
||||||
|
impl<T: Display> From<ciborium::de::Error<T>> for CborRejection {
|
||||||
|
fn from(value: ciborium::de::Error<T>) -> Self {
|
||||||
|
Self::SerdeRejection(match value {
|
||||||
|
ciborium::de::Error::Io(err) => format!("i/o: {err}"),
|
||||||
|
ciborium::de::Error::Syntax(offset) => format!("syntax error at {offset}"),
|
||||||
|
ciborium::de::Error::Semantic(offset, err) => format!(
|
||||||
|
"semantic parse: {err}{}",
|
||||||
|
offset
|
||||||
|
.map(|offset| format!(" at {offset}"))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
ciborium::de::Error::RecursionLimitExceeded => {
|
||||||
|
String::from("the input caused serde to recurse too much")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BytesRejection> for CborRejection {
|
||||||
|
fn from(value: BytesRejection) -> Self {
|
||||||
|
Self::BytesRejection(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for CborRejection {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
CborRejection::MissingCborContentType => (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
[(header::CONTENT_TYPE, PLAIN_CONTENT_TYPE)],
|
||||||
|
String::from("missing cbor content type"),
|
||||||
|
),
|
||||||
|
CborRejection::BytesRejection(err) => (
|
||||||
|
err.status(),
|
||||||
|
[(header::CONTENT_TYPE, PLAIN_CONTENT_TYPE)],
|
||||||
|
format!("bytes rejection: {}", err.body_text()),
|
||||||
|
),
|
||||||
|
CborRejection::SerdeRejection(err) => (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
[(header::CONTENT_TYPE, PLAIN_CONTENT_TYPE)],
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
CborRejection::DeserializeRejection(err) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
[(header::CONTENT_TYPE, PLAIN_CONTENT_TYPE)],
|
||||||
|
err,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cbor_content_type(headers: &HeaderMap) -> bool {
|
||||||
|
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(content_type) = content_type.to_str() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(mime) = content_type.parse::<Mime>() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
mime.type_() == "application"
|
||||||
|
&& (mime.subtype() == "cbor" || mime.suffix().is_some_and(|name| name == "cbor"))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
use bytes::Bytes;
|
||||||
|
use leptos::{
|
||||||
|
server::codee::{Decoder, Encoder},
|
||||||
|
server_fn::{
|
||||||
|
ContentType, Decodes, Encodes, Format, FormatType,
|
||||||
|
codec::{Post, Put},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
pub type CborPost = Post<CborEncoding>;
|
||||||
|
pub type CborPut = Put<CborEncoding>;
|
||||||
|
|
||||||
|
/// Serializes and deserializes JSON with [`serde_json`].
|
||||||
|
pub struct CborEncoding;
|
||||||
|
|
||||||
|
impl<T> Decoder<T> for CborEncoding
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
type Error = ciborium::de::Error<std::io::Error>;
|
||||||
|
|
||||||
|
type Encoded = [u8];
|
||||||
|
|
||||||
|
fn decode(val: &Self::Encoded) -> Result<T, Self::Error> {
|
||||||
|
ciborium::from_reader::<T, _>(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Encoder<T> for CborEncoding
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
type Error = ciborium::ser::Error<std::io::Error>;
|
||||||
|
|
||||||
|
type Encoded = Vec<u8>;
|
||||||
|
|
||||||
|
fn encode(val: &T) -> Result<Self::Encoded, Self::Error> {
|
||||||
|
let mut encoded = vec![];
|
||||||
|
ciborium::into_writer(val, &mut encoded)?;
|
||||||
|
Ok(encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentType for CborEncoding {
|
||||||
|
const CONTENT_TYPE: &'static str = "application/cbor";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatType for CborEncoding {
|
||||||
|
const FORMAT_TYPE: Format = Format::Binary;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Encodes<T> for CborEncoding
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
type Error = ciborium::ser::Error<std::io::Error>;
|
||||||
|
|
||||||
|
fn encode(output: &T) -> Result<Bytes, Self::Error> {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
ciborium::into_writer(output, &mut bytes)?;
|
||||||
|
Ok(Bytes::from_owner(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Decodes<T> for CborEncoding
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
type Error = ciborium::de::Error<std::io::Error>;
|
||||||
|
|
||||||
|
fn decode(bytes: Bytes) -> Result<T, Self::Error> {
|
||||||
|
ciborium::from_reader::<T, _>(&*bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{
|
use core::{
|
||||||
fmt::Display,
|
|
||||||
num::NonZeroU8,
|
num::NonZeroU8,
|
||||||
ops::{Deref, Not},
|
ops::{Deref, Not},
|
||||||
};
|
};
|
||||||
|
|
@ -26,10 +25,11 @@ use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameTime, Village, night::changes::NightChange},
|
game::{GameTime, Village, night::changes::NightChange},
|
||||||
|
id_impl,
|
||||||
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
||||||
player::{PlayerId, RoleChange},
|
player::{PlayerId, RoleChange},
|
||||||
role::{
|
role::{
|
||||||
Alignment, HunterMut, HunterRef, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT,
|
Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT,
|
||||||
Powerful, PreviousGuardianAction, Role, RoleTitle,
|
Powerful, PreviousGuardianAction, Role, RoleTitle,
|
||||||
},
|
},
|
||||||
team::Team,
|
team::Team,
|
||||||
|
|
@ -37,23 +37,7 @@ use crate::{
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
id_impl!(CharacterId);
|
||||||
pub struct CharacterId(uuid::Uuid);
|
|
||||||
|
|
||||||
impl CharacterId {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(uuid::Uuid::new_v4())
|
|
||||||
}
|
|
||||||
pub const fn from_u128(v: u128) -> Self {
|
|
||||||
Self(uuid::Uuid::from_u128(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for CharacterId {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Character {
|
pub struct Character {
|
||||||
|
|
@ -66,7 +50,11 @@ pub struct Character {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Character {
|
impl Character {
|
||||||
pub fn new(
|
pub fn new(ident: Identification, role: Role, auras: Vec<Aura>) -> Option<Self> {
|
||||||
|
Self::new_with_character_id(ident, role, auras, CharacterId::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_with_character_id(
|
||||||
Identification {
|
Identification {
|
||||||
player_id,
|
player_id,
|
||||||
public:
|
public:
|
||||||
|
|
@ -78,6 +66,7 @@ impl Character {
|
||||||
}: Identification,
|
}: Identification,
|
||||||
role: Role,
|
role: Role,
|
||||||
auras: Vec<Aura>,
|
auras: Vec<Aura>,
|
||||||
|
character_id: CharacterId,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
Some(Self {
|
Some(Self {
|
||||||
role,
|
role,
|
||||||
|
|
@ -86,7 +75,7 @@ impl Character {
|
||||||
auras: Auras::new(auras),
|
auras: Auras::new(auras),
|
||||||
role_changes: Vec::new(),
|
role_changes: Vec::new(),
|
||||||
identity: CharacterIdentity {
|
identity: CharacterIdentity {
|
||||||
character_id: CharacterId::new(),
|
character_id,
|
||||||
name,
|
name,
|
||||||
pronouns,
|
pronouns,
|
||||||
number: number?,
|
number: number?,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ use werewolves_macros::Titles;
|
||||||
|
|
||||||
use crate::{character::CharacterId, game::GameTime};
|
use crate::{character::CharacterId, game::GameTime};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Titles)]
|
||||||
pub enum DiedTo {
|
pub enum DiedTo {
|
||||||
Execution {
|
Execution {
|
||||||
day: NonZeroU8,
|
day: NonZeroU8,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,13 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{game::GameTime, message::PublicIdentity, player::PlayerId, role::RoleTitle};
|
use crate::{
|
||||||
|
game::{GameId, GameTime},
|
||||||
|
message::PublicIdentity,
|
||||||
|
player::PlayerId,
|
||||||
|
role::RoleTitle,
|
||||||
|
};
|
||||||
|
use leptos::prelude::ServerFnErrorErr;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
||||||
pub enum GameError {
|
pub enum GameError {
|
||||||
|
|
@ -45,6 +51,10 @@ pub enum GameError {
|
||||||
HostChannelClosed,
|
HostChannelClosed,
|
||||||
#[error("too many players: there's {got} players but only {need} roles")]
|
#[error("too many players: there's {got} players but only {need} roles")]
|
||||||
TooManyPlayers { got: u8, need: u8 },
|
TooManyPlayers { got: u8, need: u8 },
|
||||||
|
#[error(
|
||||||
|
"too few players: there's {got} players and this setup would require {need} roles to not be at instant parity"
|
||||||
|
)]
|
||||||
|
TooFewPlayers { got: u32, need: u32 },
|
||||||
#[error("it's already daytime")]
|
#[error("it's already daytime")]
|
||||||
AlreadyDaytime,
|
AlreadyDaytime,
|
||||||
#[error("it's not the end of the night yet")]
|
#[error("it's not the end of the night yet")]
|
||||||
|
|
@ -109,4 +119,165 @@ pub enum GameError {
|
||||||
NoCurrentPromptForAura,
|
NoCurrentPromptForAura,
|
||||||
#[error("you're not dead")]
|
#[error("you're not dead")]
|
||||||
NotDead,
|
NotDead,
|
||||||
|
#[error("invalid character id assignment for player ID {for_player}")]
|
||||||
|
InvalidCharacterIdAssignment { for_player: PlayerId },
|
||||||
|
#[error("already joined")]
|
||||||
|
AlreadyJoined,
|
||||||
|
#[error("cannot join own game")]
|
||||||
|
CannotJoinOwnGame,
|
||||||
|
#[error("cannot leave a started game")]
|
||||||
|
CannotLeaveOnceStarted,
|
||||||
|
#[error("cannot join a started game")]
|
||||||
|
CannotJoinStartedGame,
|
||||||
|
#[error("game already started")]
|
||||||
|
GameAlreadyStarted,
|
||||||
|
#[error("you're already in another game: {0}")]
|
||||||
|
AlreadyInAnotherGame(GameId),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
||||||
|
pub enum ServerError {
|
||||||
|
#[error("internal server error")]
|
||||||
|
InternalServerError,
|
||||||
|
#[error("not found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("user already exists")]
|
||||||
|
UserAlreadyExists,
|
||||||
|
#[error("invalid credentials")]
|
||||||
|
InvalidCredentials,
|
||||||
|
#[error("token expired")]
|
||||||
|
ExpiredToken,
|
||||||
|
#[error("connection error")]
|
||||||
|
ConnectionError,
|
||||||
|
#[error("invalid request: {0}")]
|
||||||
|
InvalidRequest(String),
|
||||||
|
#[error("you're already in an active game: {0}")]
|
||||||
|
AlreadyInActiveGame(GameId),
|
||||||
|
#[error("not your game")]
|
||||||
|
NotYourGame,
|
||||||
|
#[error("this game is already over")]
|
||||||
|
GameAlreadyOver,
|
||||||
|
#[error("{0}")]
|
||||||
|
GameError(#[from] GameError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl leptos::prelude::FromServerFnError for ServerError {
|
||||||
|
type Encoder = leptos::server_fn::codec::JsonEncoding;
|
||||||
|
|
||||||
|
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
|
||||||
|
match value {
|
||||||
|
ServerFnErrorErr::ServerError(err) => {
|
||||||
|
log::error!("server error: {err}; truncating to ServerError::InternalServerError");
|
||||||
|
ServerError::InternalServerError
|
||||||
|
}
|
||||||
|
ServerFnErrorErr::MiddlewareError(err) => {
|
||||||
|
log::error!(
|
||||||
|
"middleware error: {err}; truncating to ServerError::InternalServerError"
|
||||||
|
);
|
||||||
|
ServerError::InternalServerError
|
||||||
|
}
|
||||||
|
ServerFnErrorErr::Request(err) => {
|
||||||
|
const CONN_ERR: &str = "TypeError: NetworkError when attempting to fetch resource.";
|
||||||
|
if err == CONN_ERR {
|
||||||
|
Self::ConnectionError
|
||||||
|
} else {
|
||||||
|
Self::InvalidRequest(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err => {
|
||||||
|
let t = match &err {
|
||||||
|
ServerFnErrorErr::Registration(_) => "Registration",
|
||||||
|
ServerFnErrorErr::UnsupportedRequestMethod(_) => "UnsupportedRequestMethod",
|
||||||
|
ServerFnErrorErr::Request(_) => "Request",
|
||||||
|
ServerFnErrorErr::ServerError(_) => "ServerError",
|
||||||
|
ServerFnErrorErr::MiddlewareError(_) => "MiddlewareError",
|
||||||
|
ServerFnErrorErr::Deserialization(_) => "Deserialization",
|
||||||
|
ServerFnErrorErr::Serialization(_) => "Serialization",
|
||||||
|
ServerFnErrorErr::Args(_) => "Args",
|
||||||
|
ServerFnErrorErr::MissingArg(_) => "MissingArg",
|
||||||
|
ServerFnErrorErr::Response(_) => "Response",
|
||||||
|
};
|
||||||
|
Self::InvalidRequest(format!("[{t}]: {err}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DatabaseError> for ServerError {
|
||||||
|
fn from(err: DatabaseError) -> Self {
|
||||||
|
match err {
|
||||||
|
DatabaseError::NotFound => ServerError::NotFound,
|
||||||
|
DatabaseError::UserAlreadyExists => ServerError::UserAlreadyExists,
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
_ => {
|
||||||
|
log::error!(
|
||||||
|
"converting database error into ServerError::InternalServerError: {err}"
|
||||||
|
);
|
||||||
|
ServerError::InternalServerError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Error)]
|
||||||
|
pub enum DatabaseError {
|
||||||
|
#[error("user already exists")]
|
||||||
|
UserAlreadyExists,
|
||||||
|
#[error("password hashing error: {0}")]
|
||||||
|
PasswordHashError(String),
|
||||||
|
#[error("sqlx error: {0}")]
|
||||||
|
SqlxError(String),
|
||||||
|
#[error("not found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("(de)serialization error: {0}")]
|
||||||
|
Serialization(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<leptos::serde_json::Error> for DatabaseError {
|
||||||
|
fn from(value: leptos::serde_json::Error) -> Self {
|
||||||
|
Self::Serialization(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl axum::response::IntoResponse for ServerError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
use axum::{Json, http::StatusCode};
|
||||||
|
|
||||||
|
(
|
||||||
|
match self {
|
||||||
|
ServerError::NotYourGame
|
||||||
|
| ServerError::GameAlreadyOver
|
||||||
|
| ServerError::AlreadyInActiveGame(_)
|
||||||
|
| ServerError::GameError(_)
|
||||||
|
| ServerError::InvalidCredentials
|
||||||
|
| ServerError::InvalidRequest(_)
|
||||||
|
| ServerError::UserAlreadyExists => StatusCode::BAD_REQUEST,
|
||||||
|
ServerError::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
ServerError::ConnectionError | ServerError::InternalServerError => {
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
}
|
||||||
|
ServerError::ExpiredToken => StatusCode::UNAUTHORIZED,
|
||||||
|
},
|
||||||
|
Json(self),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl From<sqlx::Error> for DatabaseError {
|
||||||
|
fn from(err: sqlx::Error) -> Self {
|
||||||
|
match err {
|
||||||
|
sqlx::Error::RowNotFound => Self::NotFound,
|
||||||
|
_ => Self::SqlxError(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl From<argon2::password_hash::Error> for DatabaseError {
|
||||||
|
fn from(err: argon2::password_hash::Error) -> Self {
|
||||||
|
Self::PasswordHashError(err.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,10 @@ use core::{
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use rand::{Rng, seq::SliceRandom};
|
use rand::{Rng, seq::SliceRandom};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{
|
||||||
|
Deserialize, Serialize,
|
||||||
|
de::{Expected, Unexpected},
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -37,6 +40,7 @@ use crate::{
|
||||||
night::{Night, ServerAction},
|
night::{Night, ServerAction},
|
||||||
story::{DayDetail, GameActions, GameStory, NightDetails},
|
story::{DayDetail, GameActions, GameStory, NightDetails},
|
||||||
},
|
},
|
||||||
|
id_impl,
|
||||||
message::{
|
message::{
|
||||||
CharacterState, ClientDeadChat, Identification, ServerToClientMessage,
|
CharacterState, ClientDeadChat, Identification, ServerToClientMessage,
|
||||||
dead::{DeadChatContent, DeadChatMessage},
|
dead::{DeadChatContent, DeadChatMessage},
|
||||||
|
|
@ -53,6 +57,8 @@ pub use {
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
|
id_impl!(GameId);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
started: DateTime<Utc>,
|
started: DateTime<Utc>,
|
||||||
|
|
@ -61,6 +67,19 @@ pub struct Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
|
pub fn new_with_assigned_character_ids(
|
||||||
|
players: &[(Identification, CharacterId)],
|
||||||
|
settings: GameSettings,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let village = Village::new_with_assigned_character_ids(players, settings)?;
|
||||||
|
Ok(Self {
|
||||||
|
started: Utc::now(),
|
||||||
|
history: GameStory::new(village.clone()),
|
||||||
|
state: GameState::Night {
|
||||||
|
night: Night::new(village)?,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
||||||
let village = Village::new(players, settings)?;
|
let village = Village::new(players, settings)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
@ -194,9 +213,12 @@ impl Game {
|
||||||
self.process(HostGameMessage::GetState)
|
self.process(HostGameMessage::GetState)
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
GameState::Day { village: _, marked },
|
GameState::Day { village, marked },
|
||||||
HostGameMessage::Day(HostDayMessage::MarkForExecution(target)),
|
HostGameMessage::Day(HostDayMessage::MarkForExecution(target)),
|
||||||
) => {
|
) => {
|
||||||
|
if village.character_by_id(target)?.died_to().is_some() {
|
||||||
|
return Err(GameError::CharacterAlreadyDead);
|
||||||
|
}
|
||||||
match marked
|
match marked
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
|
@ -466,12 +488,70 @@ pub enum Maybe {
|
||||||
Maybe,
|
Maybe,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
|
||||||
pub enum GameTime {
|
pub enum GameTime {
|
||||||
Day { number: NonZeroU8 },
|
Day { number: NonZeroU8 },
|
||||||
Night { number: u8 },
|
Night { number: u8 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serialize for GameTime {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
GameTime::Day { number } => format!("day_{number}"),
|
||||||
|
GameTime::Night { number } => format!("night_{number}"),
|
||||||
|
}
|
||||||
|
.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for GameTime {
|
||||||
|
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
enum ExpectedGameTime {
|
||||||
|
Format,
|
||||||
|
NumberU8,
|
||||||
|
NonZeroU8,
|
||||||
|
}
|
||||||
|
impl Expected for ExpectedGameTime {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str(match self {
|
||||||
|
Self::Format => "expected day_{num_nz} or night_{num}, where {num_nz} is a nonzero u8, and {num} is a u8",
|
||||||
|
Self::NumberU8 => "expected a u8 after day_ or night_",
|
||||||
|
Self::NonZeroU8 => "expected a non-zero day number",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let s = crate::limited::ClampedString::<5, 9>::deserialize(deserializer)?;
|
||||||
|
let (day_or_night, number) = s.split_once('_').ok_or(serde::de::Error::invalid_value(
|
||||||
|
Unexpected::Str(s.as_str()),
|
||||||
|
&ExpectedGameTime::Format,
|
||||||
|
))?;
|
||||||
|
let parsed_number = number.parse::<u8>().map_err(|_| {
|
||||||
|
serde::de::Error::invalid_value(Unexpected::Str(number), &ExpectedGameTime::NumberU8)
|
||||||
|
})?;
|
||||||
|
match day_or_night {
|
||||||
|
"day" => NonZeroU8::new(parsed_number)
|
||||||
|
.map(|number| GameTime::Day { number })
|
||||||
|
.ok_or(serde::de::Error::invalid_value(
|
||||||
|
Unexpected::Str(number),
|
||||||
|
&ExpectedGameTime::NonZeroU8,
|
||||||
|
)),
|
||||||
|
"night" => Ok(GameTime::Night {
|
||||||
|
number: parsed_number,
|
||||||
|
}),
|
||||||
|
_ => Err(serde::de::Error::invalid_value(
|
||||||
|
Unexpected::Str(day_or_night),
|
||||||
|
&ExpectedGameTime::Format,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialOrd for GameTime {
|
impl PartialOrd for GameTime {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,12 @@ use crate::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
GameTime, Village,
|
GameTime, Village,
|
||||||
kill::{self, KillOutcome},
|
|
||||||
night::changes::{ChangesLookup, NightChange},
|
night::changes::{ChangesLookup, NightChange},
|
||||||
},
|
},
|
||||||
message::{
|
message::{
|
||||||
dead::{DeadChat, DeadChatMessage},
|
dead::DeadChat,
|
||||||
night::{
|
night::{
|
||||||
ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, ActionType, Visits,
|
ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
role::RoleTitle,
|
role::RoleTitle,
|
||||||
|
|
@ -829,7 +828,7 @@ impl Night {
|
||||||
NightChange::Protection {
|
NightChange::Protection {
|
||||||
target,
|
target,
|
||||||
protection: _,
|
protection: _,
|
||||||
} => target == kill_target,
|
} => target == kill_target || target == *source,
|
||||||
_ => false,
|
_ => false,
|
||||||
}) {
|
}) {
|
||||||
// there is protection, so the kill doesn't happen -> no shapeshift
|
// there is protection, so the kill doesn't happen -> no shapeshift
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::{num::NonZeroU8, ops::Not};
|
use core::ops::Not;
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,14 @@
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
aura::{Aura, AuraTitle},
|
aura::Aura,
|
||||||
bag::DrunkRoll,
|
bag::DrunkRoll,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::night::{
|
game::night::{
|
||||||
ActionComplete, CurrentResult, Night, NightState, ResponseOutcome, changes::NightChange,
|
ActionComplete, CurrentResult, Night, NightState, ResponseOutcome, changes::NightChange,
|
||||||
},
|
},
|
||||||
message::{
|
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
CharacterIdentity,
|
|
||||||
night::{ActionPrompt, ActionResponse, ActionResult, ActionType},
|
|
||||||
},
|
|
||||||
player::Protection,
|
player::Protection,
|
||||||
role::{
|
role::{
|
||||||
Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleBlock, RoleTitle,
|
Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleBlock, RoleTitle,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,12 @@ use super::Result;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{character::Character, error::GameError, message::Identification, role::RoleTitle};
|
use crate::{
|
||||||
|
character::{Character, CharacterId},
|
||||||
|
error::GameError,
|
||||||
|
message::Identification,
|
||||||
|
role::RoleTitle,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct GameSettings {
|
pub struct GameSettings {
|
||||||
|
|
@ -115,8 +120,12 @@ impl GameSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assign(&self, players: &[Identification]) -> Result<Box<[Character]>> {
|
pub fn assign_with_set_character_ids(
|
||||||
self.check_with_player_list(players)?;
|
&self,
|
||||||
|
players: &[(Identification, CharacterId)],
|
||||||
|
) -> Result<Box<[Character]>> {
|
||||||
|
let idents_only = players.iter().map(|i| i.0.clone()).collect::<Box<_>>();
|
||||||
|
self.check_with_player_list(&idents_only)?;
|
||||||
|
|
||||||
let roles_in_game = self
|
let roles_in_game = self
|
||||||
.roles
|
.roles
|
||||||
|
|
@ -131,7 +140,7 @@ impl GameSettings {
|
||||||
s.assign_to.as_ref().map(|assign_to| {
|
s.assign_to.as_ref().map(|assign_to| {
|
||||||
players
|
players
|
||||||
.iter()
|
.iter()
|
||||||
.find(|pid| pid.player_id == *assign_to)
|
.find(|(pid, _)| pid.player_id == *assign_to)
|
||||||
.ok_or(GameError::AssignedPlayerMissing(*assign_to))
|
.ok_or(GameError::AssignedPlayerMissing(*assign_to))
|
||||||
.map(|id| (id, s))
|
.map(|id| (id, s))
|
||||||
})
|
})
|
||||||
|
|
@ -140,10 +149,10 @@ impl GameSettings {
|
||||||
|
|
||||||
let mut random_assign_players = players
|
let mut random_assign_players = players
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|p| {
|
.filter(|(p, _)| {
|
||||||
!with_assigned_roles
|
!with_assigned_roles
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(r, _)| r.player_id == p.player_id)
|
.any(|((r, _), _)| r.player_id == p.player_id)
|
||||||
})
|
})
|
||||||
.collect::<Box<[_]>>();
|
.collect::<Box<[_]>>();
|
||||||
|
|
||||||
|
|
@ -156,12 +165,29 @@ impl GameSettings {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(self.roles.iter().filter(|s| s.assign_to.is_none())),
|
.zip(self.roles.iter().filter(|s| s.assign_to.is_none())),
|
||||||
)
|
)
|
||||||
.map(|(id, slot)| slot.clone().into_character(id.clone(), &roles_in_game))
|
.map(|((ident, char_id), slot)| {
|
||||||
|
slot.clone()
|
||||||
|
.into_character_with_id(ident.clone(), &roles_in_game, *char_id)
|
||||||
|
})
|
||||||
.collect::<Result<Box<[_]>>>()
|
.collect::<Result<Box<[_]>>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn assign(&self, players: &[Identification]) -> Result<Box<[Character]>> {
|
||||||
|
let with_cids = players
|
||||||
|
.iter()
|
||||||
|
.map(|ident| (ident.clone(), CharacterId::new()))
|
||||||
|
.collect::<Box<_>>();
|
||||||
|
self.assign_with_set_character_ids(&with_cids)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn check_with_player_list(&self, players: &[Identification]) -> Result<()> {
|
pub fn check_with_player_list(&self, players: &[Identification]) -> Result<()> {
|
||||||
self.check()?;
|
self.check()?;
|
||||||
|
if self.min_players_needed() > players.len() {
|
||||||
|
return Err(GameError::TooFewPlayers {
|
||||||
|
got: players.len() as _,
|
||||||
|
need: self.min_players_needed() as _,
|
||||||
|
});
|
||||||
|
}
|
||||||
let (p_len, r_len) = (players.len(), self.roles.len());
|
let (p_len, r_len) = (players.len(), self.roles.len());
|
||||||
if p_len > r_len {
|
if p_len > r_len {
|
||||||
return Err(GameError::TooManyPlayers {
|
return Err(GameError::TooManyPlayers {
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,9 @@ use werewolves_macros::{All, ChecksAs, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
aura::AuraTitle,
|
aura::AuraTitle,
|
||||||
character::Character,
|
character::{Character, CharacterId},
|
||||||
error::GameError,
|
error::GameError,
|
||||||
|
id_impl,
|
||||||
message::Identification,
|
message::Identification,
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
role::{Role, RoleTitle},
|
role::{Role, RoleTitle},
|
||||||
|
|
@ -426,14 +427,7 @@ impl From<RoleTitle> for SetupRole {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
id_impl!(SlotId);
|
||||||
pub struct SlotId(Uuid);
|
|
||||||
|
|
||||||
impl SlotId {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(Uuid::new_v4())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct SetupSlot {
|
pub struct SetupSlot {
|
||||||
|
|
@ -470,17 +464,22 @@ impl SetupSlot {
|
||||||
)
|
)
|
||||||
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Category {
|
pub fn into_character_with_id(
|
||||||
pub const fn class(&self) -> &'static str {
|
self,
|
||||||
match self {
|
ident: Identification,
|
||||||
Category::Wolves => "wolves",
|
roles_in_game: &[RoleTitle],
|
||||||
Category::Villager => "village",
|
id: CharacterId,
|
||||||
Category::Intel => "intel",
|
) -> Result<Character, GameError> {
|
||||||
Category::Defensive => "defensive",
|
Character::new_with_character_id(
|
||||||
Category::Offensive => "offensive",
|
ident.clone(),
|
||||||
Category::StartsAsVillager => "starts-as-villager",
|
self.role.into_role(roles_in_game)?,
|
||||||
}
|
self.auras
|
||||||
|
.into_iter()
|
||||||
|
.map(|aura| aura.into_aura())
|
||||||
|
.collect(),
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,27 @@ pub struct Village {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Village {
|
impl Village {
|
||||||
|
pub fn new_with_assigned_character_ids(
|
||||||
|
players: &[(Identification, CharacterId)],
|
||||||
|
settings: GameSettings,
|
||||||
|
) -> Result<Self> {
|
||||||
|
if settings.min_players_needed() > players.len() {
|
||||||
|
return Err(GameError::TooManyRoles {
|
||||||
|
players: players.len() as u8,
|
||||||
|
roles: settings.min_players_needed() as u8,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut characters = settings.assign_with_set_character_ids(players)?;
|
||||||
|
assert_eq!(characters.len(), players.len());
|
||||||
|
characters.sort_by_key(|l| l.number());
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
settings,
|
||||||
|
characters,
|
||||||
|
time: GameTime::Night { number: 0 },
|
||||||
|
dead_chat: DeadChat::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
||||||
if settings.min_players_needed() > players.len() {
|
if settings.min_players_needed() > players.len() {
|
||||||
return Err(GameError::TooManyRoles {
|
return Err(GameError::TooManyRoles {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
use core::{num::NonZeroU8, ops::Not};
|
use core::{num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
aura::{Aura, AuraTitle},
|
aura::Aura,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
use crate::{
|
||||||
|
game::{Game, GameSettings, story::GameStory},
|
||||||
|
player::PlayerId,
|
||||||
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub use crate::game::GameId;
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ssr", derive(::sqlx::FromRow))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GameRecord {
|
||||||
|
pub id: GameId,
|
||||||
|
pub host: PlayerId,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub game_state: GameRecordState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum GameRecordState {
|
||||||
|
Lobby(GameSettings),
|
||||||
|
RoleReveal(Game),
|
||||||
|
Started(Game),
|
||||||
|
GameOver(GameStory),
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ mod time;
|
||||||
use crate::{
|
use crate::{
|
||||||
character::{Character, CharacterId},
|
character::{Character, CharacterId},
|
||||||
diedto::DiedToTitle,
|
diedto::DiedToTitle,
|
||||||
error::GameError,
|
error::{GameError, ServerError},
|
||||||
game::{Game, GameOver, GameSettings, OrRandom, SetupRole, SetupSlot},
|
game::{Game, GameOver, GameSettings, OrRandom, SetupRole, SetupSlot},
|
||||||
message::{
|
message::{
|
||||||
CharacterState, Identification, PublicIdentity,
|
CharacterState, Identification, PublicIdentity,
|
||||||
|
|
@ -803,7 +803,8 @@ fn wolfpack_kill_all_targets_valid() {
|
||||||
|
|
||||||
for (idx, target) in living_villagers.into_iter().enumerate() {
|
for (idx, target) in living_villagers.into_iter().enumerate() {
|
||||||
let mut attempt = game.clone();
|
let mut attempt = game.clone();
|
||||||
if let ServerToHostMessage::Error(GameError::InvalidTarget) = attempt
|
if let ServerToHostMessage::Error(ServerError::GameError(GameError::InvalidTarget)) =
|
||||||
|
attempt
|
||||||
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
ActionResponse::MarkTarget(target.character_id),
|
ActionResponse::MarkTarget(target.character_id),
|
||||||
)))
|
)))
|
||||||
|
|
@ -1119,7 +1120,7 @@ fn big_game_test_based_on_story_test() {
|
||||||
);
|
);
|
||||||
|
|
||||||
game.execute().title().vindicator();
|
game.execute().title().vindicator();
|
||||||
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().wolf_pack_kill();
|
game.next().title().wolf_pack_kill();
|
||||||
|
|
@ -1127,11 +1128,16 @@ fn big_game_test_based_on_story_test() {
|
||||||
game.r#continue().r#continue();
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
game.next().title().shapeshifter();
|
game.next().title().shapeshifter();
|
||||||
|
assert_eq!(
|
||||||
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
ActionResponse::Shapeshift,
|
ActionResponse::Shapeshift,
|
||||||
)))
|
)))
|
||||||
.expect("shapeshift");
|
.expect("shapeshift"),
|
||||||
// game.r#continue().r#continue();
|
ServerToHostMessage::ActionResult(
|
||||||
|
Some(game.character_by_player_id(shapeshifter).identity()),
|
||||||
|
ActionResult::Continue
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.next(),
|
game.next(),
|
||||||
|
|
|
||||||
|
|
@ -444,3 +444,52 @@ fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() {
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shapeshifter_protected_when_shifting_prevents_shift() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let shapeshifter = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let protector = player_ids.next().unwrap();
|
||||||
|
let hunter = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Protector, protector);
|
||||||
|
settings.add_and_assign(SetupRole::Hunter, hunter);
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().protector();
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(game.character_by_player_id(hunter).character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
match game
|
||||||
|
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::Shapeshift,
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
ServerToHostMessage::ActionResult(_, res) => assert_eq!(res, ActionResult::ShiftFailed),
|
||||||
|
other => panic!("expected action result, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,121 @@
|
||||||
#![allow(clippy::new_without_default)]
|
#![allow(clippy::new_without_default)]
|
||||||
pub mod aura;
|
pub mod aura;
|
||||||
pub mod bag;
|
pub mod bag;
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod cbor;
|
||||||
|
pub mod cbor_leptos;
|
||||||
pub mod character;
|
pub mod character;
|
||||||
pub mod diedto;
|
pub mod diedto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
|
pub mod game_record;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod game_test;
|
mod game_test;
|
||||||
|
pub mod limited;
|
||||||
|
mod log;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod nonzero;
|
pub mod nonzero;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod role;
|
pub mod role;
|
||||||
pub mod team;
|
pub mod team;
|
||||||
|
pub mod token;
|
||||||
|
pub use log::*;
|
||||||
|
|
||||||
|
pub type ServerResult<T> = core::result::Result<T, error::ServerError>;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! id_impl {
|
||||||
|
($name:ident) => {
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, Hash, ::serde::Serialize, ::serde::Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct $name(uuid::Uuid);
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl sqlx::TypeInfo for $name {
|
||||||
|
fn is_null(&self) -> bool {
|
||||||
|
self.0 == uuid::Uuid::nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl sqlx::Type<sqlx::Postgres> for $name {
|
||||||
|
fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
|
||||||
|
<uuid::Uuid as sqlx::Type<sqlx::Postgres>>::type_info()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for $name {
|
||||||
|
fn encode_by_ref(
|
||||||
|
&self,
|
||||||
|
buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'q>,
|
||||||
|
) -> core::result::Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
|
||||||
|
self.0.encode_by_ref(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl<'r> sqlx::Decode<'r, sqlx::Postgres> for $name {
|
||||||
|
fn decode(
|
||||||
|
value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
|
||||||
|
) -> core::result::Result<Self, sqlx::error::BoxDynError> {
|
||||||
|
Ok(Self(uuid::Uuid::decode(value)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<uuid::Uuid> for $name {
|
||||||
|
fn from(value: uuid::Uuid) -> Self {
|
||||||
|
Self::from_uuid(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$name> for uuid::Uuid {
|
||||||
|
fn from(value: $name) -> Self {
|
||||||
|
value.into_uuid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for $name {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(uuid::Uuid::new_v4())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_uuid(uuid: uuid::Uuid) -> Self {
|
||||||
|
Self(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn into_uuid(self) -> uuid::Uuid {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_u128(u: u128) -> Self {
|
||||||
|
Self(::uuid::Uuid::from_u128(u))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for $name {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
core::fmt::Display::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::str::FromStr for $name {
|
||||||
|
type Err = ::uuid::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
uuid::Uuid::parse_str(s).map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
use core::{
|
||||||
|
fmt::Display,
|
||||||
|
ops::{Deref, RangeInclusive},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct FixedLenString<const LEN: usize>(String);
|
||||||
|
|
||||||
|
impl<const LEN: usize> Display for FixedLenString<LEN> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const LEN: usize> FixedLenString<LEN> {
|
||||||
|
pub fn new(s: String) -> Option<Self> {
|
||||||
|
(s.chars().take(LEN + 1).count() == LEN).then_some(Self(s))
|
||||||
|
}
|
||||||
|
pub unsafe fn new_unchecked(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const LEN: usize> Deref for FixedLenString<LEN> {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, const LEN: usize> Deserialize<'de> for FixedLenString<LEN> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct ExpectedLen(usize);
|
||||||
|
impl serde::de::Expected for ExpectedLen {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "a string exactly {} characters long", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<String as Deserialize>::deserialize(deserializer).and_then(|s| {
|
||||||
|
let char_count = s.chars().take(LEN.saturating_add(1)).count();
|
||||||
|
if char_count != LEN {
|
||||||
|
Err(serde::de::Error::invalid_length(
|
||||||
|
char_count,
|
||||||
|
&ExpectedLen(LEN),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(Self(s))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const LEN: usize> Serialize for FixedLenString<LEN> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(self.0.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ClampedString<const MIN: usize, const MAX: usize>(String);
|
||||||
|
|
||||||
|
impl<const MIN: usize, const MAX: usize> ClampedString<MIN, MAX> {
|
||||||
|
pub const MIN_LEN: usize = MIN;
|
||||||
|
pub const MAX_LEN: usize = MAX;
|
||||||
|
|
||||||
|
pub fn new(s: String) -> Result<Self, RangeInclusive<usize>> {
|
||||||
|
let str_len = s.chars().take(MAX.saturating_add(1)).count();
|
||||||
|
(str_len >= MIN && str_len <= MAX)
|
||||||
|
.then_some(Self(s))
|
||||||
|
.ok_or(MIN..=MAX)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn new_unchecked(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> String {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const MIN: usize, const MAX: usize> Display for ClampedString<MIN, MAX> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const MIN: usize, const MAX: usize> Deref for ClampedString<MIN, MAX> {
|
||||||
|
type Target = String;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, const MIN: usize, const MAX: usize> Deserialize<'de> for ClampedString<MIN, MAX> {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct ExpectedLen(usize, usize);
|
||||||
|
impl serde::de::Expected for ExpectedLen {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"a string between {} and {} characters long",
|
||||||
|
self.0, self.1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<String as Deserialize>::deserialize(deserializer).and_then(|s| {
|
||||||
|
let char_count = s.chars().take(MAX.saturating_add(1)).count();
|
||||||
|
if char_count < MIN || char_count > MAX {
|
||||||
|
Err(serde::de::Error::invalid_length(
|
||||||
|
char_count,
|
||||||
|
&ExpectedLen(MIN, MAX),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(Self(s))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const MIN: usize, const MAX: usize> Serialize for ClampedString<MIN, MAX> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(self.0.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
pub trait LogError {
|
||||||
|
fn log(self, loc: CodePath, level: log::Level);
|
||||||
|
fn log_warn(self, loc: CodePath);
|
||||||
|
fn log_err(self, loc: CodePath);
|
||||||
|
fn log_debug(self, loc: CodePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CodePath {
|
||||||
|
pub module_path: &'static str,
|
||||||
|
pub loc: &'static std::panic::Location<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! loc {
|
||||||
|
() => {
|
||||||
|
(::werewolves_proto::CodePath {
|
||||||
|
module_path: log::__private_api::module_path!(),
|
||||||
|
loc: log::__private_api::loc(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> LogError for Result<T, E>
|
||||||
|
where
|
||||||
|
E: core::fmt::Display,
|
||||||
|
{
|
||||||
|
fn log(self, loc: CodePath, lvl: log::Level) {
|
||||||
|
let Err(err) = self else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if lvl <= log::STATIC_MAX_LEVEL && lvl <= log::max_level() {
|
||||||
|
log::__private_api::log(
|
||||||
|
log::__log_logger!(__log_global_logger),
|
||||||
|
log::__private_api::format_args!("{err}"),
|
||||||
|
lvl,
|
||||||
|
&(loc.module_path, loc.module_path, loc.loc),
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_warn(self, loc: CodePath) {
|
||||||
|
if self.is_err() {
|
||||||
|
self.log(loc, log::Level::Warn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_err(self, loc: CodePath) {
|
||||||
|
if self.is_err() {
|
||||||
|
self.log(loc, log::Level::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_debug(self, loc: CodePath) {
|
||||||
|
if self.is_err() {
|
||||||
|
self.log(loc, log::Level::Debug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,18 +17,20 @@ pub mod host;
|
||||||
mod ident;
|
mod ident;
|
||||||
pub mod night;
|
pub mod night;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
message::host::{HostMessage, ServerToHostMessage},
|
||||||
|
token::TokenString,
|
||||||
|
};
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
pub use ident::*;
|
pub use ident::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use werewolves_macros::Titles;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId, error::GameError, game::story::GameStory,
|
||||||
error::GameError,
|
message::dead::DeadChatMessage, role::RoleTitle,
|
||||||
game::{GameOver, story::GameStory},
|
|
||||||
message::dead::DeadChatMessage,
|
|
||||||
role::RoleTitle,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
|
@ -62,20 +64,19 @@ pub struct DayCharacter {
|
||||||
pub alive: bool,
|
pub alive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
|
||||||
pub enum ServerToClientMessage {
|
pub enum ServerToClientMessage {
|
||||||
|
GameCancelled,
|
||||||
Disconnect,
|
Disconnect,
|
||||||
LobbyInfo {
|
LobbyInfo {
|
||||||
joined: bool,
|
joined: bool,
|
||||||
players: Box<[PublicIdentity]>,
|
players: Box<[PublicIdentity]>,
|
||||||
|
current_number: Option<NonZeroU8>,
|
||||||
},
|
},
|
||||||
GameInProgress,
|
GameInProgress,
|
||||||
GameStart {
|
GameStart {
|
||||||
role: RoleTitle,
|
role: RoleTitle,
|
||||||
},
|
},
|
||||||
InvalidMessageForGameState,
|
|
||||||
NoSuchTarget,
|
|
||||||
GameOver(GameOver),
|
|
||||||
Story(GameStory),
|
Story(GameStory),
|
||||||
Update(PlayerUpdate),
|
Update(PlayerUpdate),
|
||||||
DeadChat(Box<[DeadChatMessage]>),
|
DeadChat(Box<[DeadChatMessage]>),
|
||||||
|
|
@ -85,7 +86,38 @@ pub enum ServerToClientMessage {
|
||||||
Error(GameError),
|
Error(GameError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum PlayerUpdate {
|
pub enum PlayerUpdate {
|
||||||
Number(NonZeroU8),
|
Number(NonZeroU8),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum WrappedServerMessage {
|
||||||
|
Authentication(TokenString),
|
||||||
|
HostMessage(HostMessage),
|
||||||
|
ClientMessage(ClientMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TokenString> for WrappedServerMessage {
|
||||||
|
fn from(value: TokenString) -> Self {
|
||||||
|
Self::Authentication(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HostMessage> for WrappedServerMessage {
|
||||||
|
fn from(value: HostMessage) -> Self {
|
||||||
|
Self::HostMessage(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ClientMessage> for WrappedServerMessage {
|
||||||
|
fn from(value: ClientMessage) -> Self {
|
||||||
|
Self::ClientMessage(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum IntoClientResponse {
|
||||||
|
Player(ServerToClientMessage),
|
||||||
|
Host(ServerToHostMessage),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use core::hash::Hash;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
|
|
@ -197,14 +198,32 @@ impl DeadChat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct DeadChatMessage {
|
pub struct DeadChatMessage {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub timestamp: DateTime<Utc>,
|
pub timestamp: DateTime<Utc>,
|
||||||
pub message: DeadChatContent,
|
pub message: DeadChatContent,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
impl Hash for DeadChatMessage {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for DeadChatMessage {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.id.cmp(&other.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for DeadChatMessage {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum DeadChatContent {
|
pub enum DeadChatContent {
|
||||||
PlayerMessage {
|
PlayerMessage {
|
||||||
from: CharacterIdentity,
|
from: CharacterIdentity,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
error::GameError,
|
error::{GameError, ServerError},
|
||||||
game::{GameOver, GameSettings, story::GameStory},
|
game::{GameOver, GameSettings, story::GameStory},
|
||||||
message::{
|
message::{
|
||||||
CharacterIdentity,
|
CharacterIdentity,
|
||||||
|
|
@ -37,8 +37,11 @@ pub enum HostMessage {
|
||||||
Lobby(HostLobbyMessage),
|
Lobby(HostLobbyMessage),
|
||||||
InGame(HostGameMessage),
|
InGame(HostGameMessage),
|
||||||
ForceRoleAckFor(CharacterId),
|
ForceRoleAckFor(CharacterId),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
ForceAllRoleAcks,
|
||||||
PostGame(PostGameMessage),
|
PostGame(PostGameMessage),
|
||||||
Echo(ServerToHostMessage),
|
Echo(ServerToHostMessage),
|
||||||
|
CancelGame,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
|
@ -92,6 +95,7 @@ pub enum HostLobbyMessage {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, werewolves_macros::Titles)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, werewolves_macros::Titles)]
|
||||||
pub enum ServerToHostMessage {
|
pub enum ServerToHostMessage {
|
||||||
|
GameCancelled,
|
||||||
Disconnect,
|
Disconnect,
|
||||||
Daytime {
|
Daytime {
|
||||||
characters: Box<[CharacterState]>,
|
characters: Box<[CharacterState]>,
|
||||||
|
|
@ -105,9 +109,9 @@ pub enum ServerToHostMessage {
|
||||||
Lobby {
|
Lobby {
|
||||||
players: Box<[PlayerState]>,
|
players: Box<[PlayerState]>,
|
||||||
settings: GameSettings,
|
settings: GameSettings,
|
||||||
|
qr_mode: bool,
|
||||||
},
|
},
|
||||||
QrMode(bool),
|
Error(ServerError),
|
||||||
Error(GameError),
|
|
||||||
GameOver(GameOver),
|
GameOver(GameOver),
|
||||||
WaitingForRoleRevealAcks {
|
WaitingForRoleRevealAcks {
|
||||||
ackd: Box<[CharacterIdentity]>,
|
ackd: Box<[CharacterIdentity]>,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ pub struct PublicIdentity {
|
||||||
pub number: Option<NonZeroU8>,
|
pub number: Option<NonZeroU8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct CharacterIdentity {
|
pub struct CharacterIdentity {
|
||||||
pub character_id: CharacterId,
|
pub character_id: CharacterId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@ impl ActionPrompt {
|
||||||
| ActionPrompt::Insomniac { .. } => true,
|
| ActionPrompt::Insomniac { .. } => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub(crate) const fn marked(&self) -> Option<(CharacterId, Option<CharacterId>)> {
|
pub const fn marked(&self) -> Option<(CharacterId, Option<CharacterId>)> {
|
||||||
match self {
|
match self {
|
||||||
ActionPrompt::Seer { marked, .. }
|
ActionPrompt::Seer { marked, .. }
|
||||||
| ActionPrompt::Protector { marked, .. }
|
| ActionPrompt::Protector { marked, .. }
|
||||||
|
|
@ -581,6 +581,46 @@ impl ActionPrompt {
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn targets(&self) -> Option<&[CharacterIdentity]> {
|
||||||
|
match self {
|
||||||
|
ActionPrompt::Seer { living_players: targets,.. }
|
||||||
|
| ActionPrompt::Protector { targets,.. }
|
||||||
|
| ActionPrompt::Arcanist { living_players: targets,.. }
|
||||||
|
| ActionPrompt::Gravedigger { dead_players: targets,.. }
|
||||||
|
| ActionPrompt::Hunter { living_players: targets,.. }
|
||||||
|
| ActionPrompt::Militia { living_players: targets,.. }
|
||||||
|
| ActionPrompt::MapleWolf { living_players: targets,.. }
|
||||||
|
| ActionPrompt::Guardian { living_players: targets,.. }
|
||||||
|
| ActionPrompt::Adjudicator { living_players: targets,.. }
|
||||||
|
| ActionPrompt::PowerSeer { living_players: targets,.. }
|
||||||
|
| ActionPrompt::Mortician { dead_players: targets,.. }
|
||||||
|
| ActionPrompt::BeholderChooses { living_players: targets,.. }
|
||||||
|
| ActionPrompt::MasonLeaderRecruit { potential_recruits: targets,.. }
|
||||||
|
| ActionPrompt::Empath { living_players: targets,.. }
|
||||||
|
| ActionPrompt::Vindicator { living_players: targets,.. }
|
||||||
|
| ActionPrompt::PyreMaster { living_players: targets,.. }
|
||||||
|
| ActionPrompt::WolfPackKill { living_villagers: targets,.. }
|
||||||
|
| ActionPrompt::AlphaWolf { living_villagers: targets,.. }
|
||||||
|
| ActionPrompt::DireWolf { living_players: targets,.. }
|
||||||
|
| ActionPrompt::LoneWolfKill { living_players: targets, .. }
|
||||||
|
| ActionPrompt::Bloodletter {
|
||||||
|
living_players: targets,
|
||||||
|
..
|
||||||
|
} => Some(&**targets),
|
||||||
|
|
||||||
|
ActionPrompt::WolvesIntro { .. }
|
||||||
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
| ActionPrompt::Shapeshifter { .. }
|
||||||
|
| ActionPrompt::ElderReveal { .. }
|
||||||
|
| ActionPrompt::Insomniac { .. }
|
||||||
|
| ActionPrompt::CoverOfDarkness
|
||||||
|
| ActionPrompt::MasonsWake { .. }
|
||||||
|
| ActionPrompt::BeholderWakes { .. }
|
||||||
|
| ActionPrompt::DamnedIntro { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for ActionPrompt {
|
impl PartialOrd for ActionPrompt {
|
||||||
|
|
|
||||||
|
|
@ -12,32 +12,20 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::fmt::Display;
|
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
|
id_impl,
|
||||||
|
limited::ClampedString,
|
||||||
|
message::PublicIdentity,
|
||||||
role::{Role, RoleTitle},
|
role::{Role, RoleTitle},
|
||||||
|
token::TokenString,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
id_impl!(PlayerId);
|
||||||
pub struct PlayerId(uuid::Uuid);
|
|
||||||
|
|
||||||
impl PlayerId {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(uuid::Uuid::new_v4())
|
|
||||||
}
|
|
||||||
pub const fn from_u128(v: u128) -> Self {
|
|
||||||
Self(uuid::Uuid::from_u128(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for PlayerId {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
|
|
@ -73,3 +61,48 @@ pub struct RoleChange {
|
||||||
pub new_role: RoleTitle,
|
pub new_role: RoleTitle,
|
||||||
pub changed_on_night: u8,
|
pub changed_on_night: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type Username = ClampedString<1, 0x40>;
|
||||||
|
pub type Password = ClampedString<6, 0x100>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct UserLogin {
|
||||||
|
pub username: Username,
|
||||||
|
pub password: Password,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct ChangePassword {
|
||||||
|
pub current: Password,
|
||||||
|
pub new: Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct DeleteUserRequest {
|
||||||
|
pub password: Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ssr", derive(::sqlx::FromRow))]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Session {
|
||||||
|
pub username: String,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub pronouns: Option<String>,
|
||||||
|
|
||||||
|
pub user_created_at: DateTime<Utc>,
|
||||||
|
pub user_updated_at: DateTime<Utc>,
|
||||||
|
pub token_created_at: DateTime<Utc>,
|
||||||
|
pub token_expires_at: DateTime<Utc>,
|
||||||
|
pub token: TokenString,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProfileUpdate {
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub pronouns: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct PlayerIdentity {
|
||||||
|
pub player_id: PlayerId,
|
||||||
|
pub character_id: CharacterId,
|
||||||
|
pub public: PublicIdentity,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use werewolves_macros::{All, ChecksAs, RefAndMut, Titles};
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{GameTime, Village},
|
game::{Category, GameTime, Village},
|
||||||
message::CharacterIdentity,
|
message::CharacterIdentity,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -122,50 +122,59 @@ pub enum Role {
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks(Powerful::NotPowerful)]
|
#[checks(Powerful::NotPowerful)]
|
||||||
|
#[checks(Category::Villager)]
|
||||||
Villager,
|
Villager,
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Category::Villager)]
|
||||||
Scapegoat { redeemed: bool },
|
Scapegoat { redeemed: bool },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
#[checks("doesnt_wake_if_died_tonight")]
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
Seer,
|
Seer,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
#[checks("doesnt_wake_if_died_tonight")]
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
Arcanist,
|
Arcanist,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
#[checks("doesnt_wake_if_died_tonight")]
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
Adjudicator,
|
Adjudicator,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
#[checks("doesnt_wake_if_died_tonight")]
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
PowerSeer,
|
PowerSeer,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
#[checks("doesnt_wake_if_died_tonight")]
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
Mortician,
|
Mortician,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
#[checks("doesnt_wake_if_died_tonight")]
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
Beholder,
|
Beholder,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
MasonLeader {
|
MasonLeader {
|
||||||
recruits_available: u8,
|
recruits_available: u8,
|
||||||
recruits: Box<[CharacterId]>,
|
recruits: Box<[CharacterId]>,
|
||||||
|
|
@ -173,58 +182,69 @@ pub enum Role {
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
Empath { cursed: bool },
|
Empath { cursed: bool },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
Vindicator,
|
Vindicator,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
Diseased,
|
Diseased,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
BlackKnight { attacked: Option<DiedTo> },
|
BlackKnight { attacked: Option<DiedTo> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
Weightlifter,
|
Weightlifter,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
PyreMaster { villagers_killed: u8 },
|
PyreMaster { villagers_killed: u8 },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
#[checks("doesnt_wake_if_died_tonight")]
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
Gravedigger,
|
Gravedigger,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
#[checks]
|
#[checks]
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
Hunter { target: Option<CharacterId> },
|
Hunter { target: Option<CharacterId> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
Militia { targeted: Option<CharacterId> },
|
Militia { targeted: Option<CharacterId> },
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
MapleWolf { last_kill_on_night: u8 },
|
MapleWolf { last_kill_on_night: u8 },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
Guardian {
|
Guardian {
|
||||||
last_protected: Option<PreviousGuardianAction>,
|
last_protected: Option<PreviousGuardianAction>,
|
||||||
},
|
},
|
||||||
|
|
@ -232,14 +252,17 @@ pub enum Role {
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
Protector { last_protected: Option<CharacterId> },
|
Protector { last_protected: Option<CharacterId> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
|
#[checks(Category::StartsAsVillager)]
|
||||||
Apprentice(RoleTitle),
|
Apprentice(RoleTitle),
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
|
#[checks(Category::StartsAsVillager)]
|
||||||
Elder {
|
Elder {
|
||||||
knows_on_night: NonZeroU8,
|
knows_on_night: NonZeroU8,
|
||||||
woken_for_reveal: bool,
|
woken_for_reveal: bool,
|
||||||
|
|
@ -249,6 +272,7 @@ pub enum Role {
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks(Killer::NotKiller)]
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("doesnt_wake_if_died_tonight")]
|
#[checks("doesnt_wake_if_died_tonight")]
|
||||||
|
#[checks(Category::Intel)]
|
||||||
Insomniac,
|
Insomniac,
|
||||||
|
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
|
|
@ -256,33 +280,39 @@ pub enum Role {
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
#[checks("killing_wolf")]
|
#[checks("killing_wolf")]
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
Werewolf,
|
Werewolf,
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
#[checks("killing_wolf")]
|
#[checks("killing_wolf")]
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
AlphaWolf { killed: Option<CharacterId> },
|
AlphaWolf { killed: Option<CharacterId> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
DireWolf { last_blocked: Option<CharacterId> },
|
DireWolf { last_blocked: Option<CharacterId> },
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
#[checks("killing_wolf")]
|
#[checks("killing_wolf")]
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
Shapeshifter { shifted_into: Option<CharacterId> },
|
Shapeshifter { shifted_into: Option<CharacterId> },
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
LoneWolf,
|
LoneWolf,
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks(Killer::Killer)]
|
#[checks(Killer::Killer)]
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
Bloodletter,
|
Bloodletter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright (C) 2026 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use crate::{limited::FixedLenString, player::Username};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub const TOKEN_LEN: usize = 0x20;
|
||||||
|
|
||||||
|
pub type TokenString = FixedLenString<TOKEN_LEN>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Token {
|
||||||
|
pub token: TokenString,
|
||||||
|
pub username: Username,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub pronouns: Option<String>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub expires_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Token {
|
||||||
|
pub fn login_token(&self) -> TokenLogin {
|
||||||
|
TokenLogin(self.token.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct TokenLogin(pub FixedLenString<TOKEN_LEN>);
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl axum_extra::headers::authorization::Credentials for TokenLogin {
|
||||||
|
const SCHEME: &'static str = "Bearer";
|
||||||
|
|
||||||
|
fn decode(value: &axum::http::HeaderValue) -> Option<Self> {
|
||||||
|
value
|
||||||
|
.to_str()
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| FixedLenString::new(v.strip_prefix("Bearer ").unwrap_or(v).to_string()))
|
||||||
|
.map(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(&self) -> axum::http::HeaderValue {
|
||||||
|
axum::http::HeaderValue::from_str(self.0.as_str()).expect("bearer token encode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[target.wasm32-unknown-unknown]
|
|
||||||
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
|
|
||||||
|
|
@ -3,45 +3,83 @@ name = "werewolves"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
web-sys = { version = "0.3", features = [
|
leptos = { workspace = true }
|
||||||
"HtmlTableCellElement",
|
leptos_router = { workspace = true }
|
||||||
"Event",
|
axum = { workspace = true, optional = true }
|
||||||
"EventTarget",
|
console_error_panic_hook = { version = "0.1", optional = true }
|
||||||
"HtmlImageElement",
|
leptos_axum = { workspace = true, optional = true }
|
||||||
"HtmlDivElement",
|
leptos_meta = { workspace = true }
|
||||||
"HtmlSelectElement",
|
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||||
"HtmlDialogElement",
|
wasm-bindgen = { workspace = true, optional = true }
|
||||||
"DomRect",
|
getrandom = { version = "=0.3.4", optional = true }
|
||||||
"WheelEvent",
|
colored = { workspace = true, optional = true }
|
||||||
] }
|
pretty_env_logger = { workspace = true, optional = true }
|
||||||
wasm-bindgen = { version = "=0.2.100" }
|
sqlx = { workspace = true, optional = true }
|
||||||
log = "0.4"
|
tower-http = { workspace = true, optional = true }
|
||||||
rand = { version = "0.9", features = ["small_rng"] }
|
mime-sniffer = { version = "0.1", optional = true }
|
||||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
futures = { version = "0.3", optional = true }
|
||||||
uuid = { version = "*", features = ["js"] }
|
wasm-logger = { version = "0.2" }
|
||||||
yew = { version = "0.22", features = ["csr"] }
|
gloo = { version = "0.11" }
|
||||||
yew-router = "0.19"
|
leptos-use = { workspace = true }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { version = "1.0", optional = true }
|
rand = { workspace = true }
|
||||||
gloo = "0.11"
|
reactive_stores = { version = "0.3" }
|
||||||
wasm-logger = "0.2"
|
axum-extra = { workspace = true, optional = true }
|
||||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
anyhow = { workspace = true, optional = true }
|
||||||
once_cell = "1"
|
bytes = { workspace = true, optional = true }
|
||||||
chrono = { version = "0.4" }
|
fast_qr = { workspace = true, optional = true }
|
||||||
werewolves-macros = { path = "../werewolves-macros" }
|
argon2 = { workspace = true, optional = true }
|
||||||
werewolves-proto = { path = "../werewolves-proto" }
|
log.workspace = true
|
||||||
futures = "0.3"
|
uuid.workspace = true
|
||||||
wasm-bindgen-futures = "0.4"
|
chrono.workspace = true
|
||||||
thiserror = { version = "2" }
|
werewolves-macros.workspace = true
|
||||||
convert_case = { version = "0.10" }
|
werewolves-proto.workspace = true
|
||||||
ciborium = { version = "0.2", optional = true }
|
codee.workspace = true
|
||||||
chrono-humanize = { version = "0.2.3", features = ["wasmbind"] }
|
convert_case.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
sorted-vec.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cbor"]
|
hydrate = [
|
||||||
# default = ["json"]
|
"leptos/hydrate",
|
||||||
cbor = ["dep:ciborium"]
|
"dep:console_error_panic_hook",
|
||||||
json = ["dep:serde_json"]
|
"dep:wasm-bindgen",
|
||||||
|
"uuid/js",
|
||||||
|
"dep:getrandom",
|
||||||
|
"getrandom/wasm_js",
|
||||||
|
]
|
||||||
|
ssr = [
|
||||||
|
"dep:axum",
|
||||||
|
"dep:tokio",
|
||||||
|
"dep:argon2",
|
||||||
|
"dep:leptos_axum",
|
||||||
|
"dep:colored",
|
||||||
|
"dep:pretty_env_logger",
|
||||||
|
"dep:sqlx",
|
||||||
|
"dep:tower-http",
|
||||||
|
"dep:mime-sniffer",
|
||||||
|
"dep:futures",
|
||||||
|
"dep:axum-extra",
|
||||||
|
"dep:anyhow",
|
||||||
|
"dep:bytes",
|
||||||
|
"dep:fast_qr",
|
||||||
|
|
||||||
|
"leptos/ssr",
|
||||||
|
"leptos_meta/ssr",
|
||||||
|
"leptos_router/ssr",
|
||||||
|
"leptos-use/ssr",
|
||||||
|
"leptos-use/axum",
|
||||||
|
"werewolves-proto/ssr",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Defines a size-optimized profile for the WASM bundle in release mode
|
||||||
|
[profile.wasm-release]
|
||||||
|
inherits = "release"
|
||||||
|
opt-level = 'z'
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
[build]
|
|
||||||
target = "index.html" # The index HTML file to drive the bundling process.
|
|
||||||
html_output = "index.html" # The name of the output HTML file.
|
|
||||||
release = true # Build in release mode.
|
|
||||||
dist = "dist" # The output dir for all final assets.
|
|
||||||
public_url = "/" # The public URL from which assets are to be served.
|
|
||||||
filehash = true # Whether to include hash values in the output file names.
|
|
||||||
inject_scripts = true # Whether to inject scripts (and module preloads) into the finalized output.
|
|
||||||
offline = false # Run without network access
|
|
||||||
frozen = false # Require Cargo.lock and cache are up to date
|
|
||||||
locked = false # Require Cargo.lock is up to date
|
|
||||||
# minify = "on_release" # Control minification: can be one of: never, on_release, always
|
|
||||||
minify = "always" # Control minification: can be one of: never, on_release, always
|
|
||||||
no_sri = false # Allow disabling sub-resource integrity (SRI)
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
use std::{fs::File, io::Write};
|
||||||
|
|
||||||
|
const STYLESHEET_PATH: &str = "../style/faction.scss";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo::rerun-if-changed=../style/main.scss");
|
||||||
|
|
||||||
|
let mut sheet_file = File::create(STYLESHEET_PATH).unwrap();
|
||||||
|
let mut out = String::new();
|
||||||
|
for faction in [
|
||||||
|
"village",
|
||||||
|
"wolves",
|
||||||
|
"offensive",
|
||||||
|
"defensive",
|
||||||
|
"intel",
|
||||||
|
"starts-as-villager",
|
||||||
|
"damned",
|
||||||
|
"drunk",
|
||||||
|
] {
|
||||||
|
let name = faction.replace("-", "_");
|
||||||
|
out += format!(
|
||||||
|
r#"
|
||||||
|
.{faction} {{
|
||||||
|
--faction-color: ${name}_color;
|
||||||
|
--faction-border: ${name}_border;
|
||||||
|
--faction-color-faint: ${name}_color_faint;
|
||||||
|
--faction-border-faint: ${name}_border_faint;
|
||||||
|
|
||||||
|
&.box {{
|
||||||
|
background-color: ${name}_color;
|
||||||
|
border: 1px solid ${name}_border;
|
||||||
|
|
||||||
|
.selected:not(.faint) {{
|
||||||
|
color: white;
|
||||||
|
background-color: ${name}_border;
|
||||||
|
}}
|
||||||
|
.selected.faint {{
|
||||||
|
color: white;
|
||||||
|
background-color: ${name}_border_faint;
|
||||||
|
}}
|
||||||
|
|
||||||
|
&.hover:not(.selected):hover {{
|
||||||
|
color: white;
|
||||||
|
background-color: ${name}_border;
|
||||||
|
}}
|
||||||
|
|
||||||
|
&.faint:not(.selected) {{
|
||||||
|
border: 1px solid ${name}_border_faint;
|
||||||
|
background-color: ${name}_color_faint;
|
||||||
|
|
||||||
|
&.hover:hover {{
|
||||||
|
background-color: ${name}_border_faint;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
&.underline {{
|
||||||
|
text-decoration: ${name}_color underline;
|
||||||
|
|
||||||
|
&.faint {{
|
||||||
|
text-decoration: ${name}_color_faint underline;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
&.text-color {{
|
||||||
|
color: ${name}_border;
|
||||||
|
|
||||||
|
&.faint {{
|
||||||
|
color: ${name}_border_faint;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.as_str();
|
||||||
|
}
|
||||||
|
sheet_file.write_all(out.as_bytes()).unwrap();
|
||||||
|
}
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>werewolves</title>
|
|
||||||
<link rel="icon" href="/img/wolf.svg" />
|
|
||||||
<link rel="stylesheet" href="/assets/fonts/liberation-serif.css" />
|
|
||||||
<link data-trunk rel="sass" href="index.scss" />
|
|
||||||
<link data-trunk rel="copy-dir" href="img">
|
|
||||||
<link data-trunk rel="copy-dir" href="assets">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<app></app>
|
|
||||||
<error></error>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
pub mod pages {
|
||||||
|
werewolves_macros::include_path!("werewolves/src/app/pages");
|
||||||
|
|
||||||
|
pub mod night_actions {
|
||||||
|
werewolves_macros::include_path!("werewolves/src/app/pages/night_actions");
|
||||||
|
|
||||||
|
pub mod role {
|
||||||
|
werewolves_macros::include_path!("werewolves/src/app/pages/night_actions/role");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod components {
|
||||||
|
werewolves_macros::include_path!("werewolves/src/app/components");
|
||||||
|
pub mod input {
|
||||||
|
werewolves_macros::include_path!("werewolves/src/app/components/input");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod class;
|
||||||
|
pub mod error;
|
||||||
|
pub mod storage;
|
||||||
|
|
||||||
|
use codee::string::JsonSerdeCodec;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_meta::{Link, MetaTags, Stylesheet, Title, provide_meta_context};
|
||||||
|
use leptos_router::{
|
||||||
|
components::{ProtectedRoute, Route, Router, Routes},
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
use leptos_use::storage::use_local_storage;
|
||||||
|
use reactive_stores::Store;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{
|
||||||
|
components::{ErrorBox, Nav},
|
||||||
|
error::WolfError,
|
||||||
|
pages::{GamePage, Main, NotFound, Signin, Signup, UserSettings, big::BigScreen},
|
||||||
|
storage::{
|
||||||
|
Stored,
|
||||||
|
user::{AuthContext, AuthContextStoreFields},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state::{InitOrUpdateStore, SessionState},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Link rel="icon" href="/favicon.svg" type_="text/xml+svg" />
|
||||||
|
<AutoReload options=options.clone() />
|
||||||
|
<HydrationScripts options />
|
||||||
|
<MetaTags />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<App />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
|
||||||
|
pub struct Preferences {
|
||||||
|
pub tutorials_enabled: bool,
|
||||||
|
pub show_cancel_game: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Preferences {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
tutorials_enabled: true,
|
||||||
|
show_cancel_game: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stored for Preferences {
|
||||||
|
const STORAGE_KEY: &str = "preferences";
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn App() -> impl IntoView {
|
||||||
|
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||||
|
provide_meta_context();
|
||||||
|
let auth_store = Store::new(AuthContext::new());
|
||||||
|
provide_context(auth_store);
|
||||||
|
let session_store = Store::new(SessionState::new());
|
||||||
|
provide_context(session_store);
|
||||||
|
Effect::new(move || auth_store.init_or_update());
|
||||||
|
Effect::new(move || session_store.init_or_update());
|
||||||
|
|
||||||
|
let (pref_read, pref_write, _) =
|
||||||
|
use_local_storage::<Preferences, JsonSerdeCodec>(Preferences::STORAGE_KEY);
|
||||||
|
provide_context((pref_read, pref_write));
|
||||||
|
|
||||||
|
let is_logged_in = move || {
|
||||||
|
auth_store
|
||||||
|
.initialized()
|
||||||
|
.get()
|
||||||
|
.then_some(auth_store.session().get().is_some())
|
||||||
|
};
|
||||||
|
let not_logged_in = move || Some(auth_store.session().get().is_none());
|
||||||
|
let error: RwSignal<Option<WolfError>> = RwSignal::new(None);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Stylesheet id="leptos" href="/pkg/werewolves.css" />
|
||||||
|
// sets the document title
|
||||||
|
<Title text="werewolves" />
|
||||||
|
|
||||||
|
// content for this welcome page
|
||||||
|
<Router>
|
||||||
|
<main>
|
||||||
|
<Nav />
|
||||||
|
<ErrorBox msg=error />
|
||||||
|
<Routes fallback=NotFound>
|
||||||
|
<Route
|
||||||
|
path=path!("/")
|
||||||
|
view=move || view! { <Main error=error.write_only() /> }
|
||||||
|
/>
|
||||||
|
<ProtectedRoute
|
||||||
|
path=path!("/signin")
|
||||||
|
view=move || view! { <Signin error=error.write_only() /> }
|
||||||
|
condition=not_logged_in
|
||||||
|
redirect_path=|| "/"
|
||||||
|
/>
|
||||||
|
<ProtectedRoute
|
||||||
|
path=path!("/signup")
|
||||||
|
view=move || view! { <Signup error=error.write_only() /> }
|
||||||
|
condition=not_logged_in
|
||||||
|
redirect_path=|| "/"
|
||||||
|
/>
|
||||||
|
<ProtectedRoute
|
||||||
|
path=path!("/user/settings")
|
||||||
|
view=move || view! { <UserSettings error=error.write_only() /> }
|
||||||
|
condition=is_logged_in
|
||||||
|
redirect_path=|| "/"
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path=path!("/games/:id")
|
||||||
|
view=move || view! { <GamePage error=error.write_only() /> }
|
||||||
|
/>
|
||||||
|
<Route path=path!("/games/:id/big") view=BigScreen />
|
||||||
|
</Routes>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</Router>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
// Copyright (C) 2026 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use core::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use werewolves_proto::{
|
||||||
|
aura::AuraTitle, character::Character, game::Category, role::RoleTitle, team::Team,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait PartialClass {
|
||||||
|
fn partial_class(&self) -> Option<&'static str>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialClass for AuraTitle {
|
||||||
|
fn partial_class(&self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
AuraTitle::Damned => Some("damned"),
|
||||||
|
AuraTitle::Drunk => Some("drunk"),
|
||||||
|
AuraTitle::Insane
|
||||||
|
| AuraTitle::Bloodlet
|
||||||
|
| AuraTitle::Scapegoat
|
||||||
|
| AuraTitle::RedeemableScapegoat
|
||||||
|
| AuraTitle::VindictiveScapegoat
|
||||||
|
| AuraTitle::SpitefulScapegoat
|
||||||
|
| AuraTitle::InevitableScapegoat => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Class {
|
||||||
|
fn class(&self) -> &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Class for Character {
|
||||||
|
fn class(&self) -> &'static str {
|
||||||
|
if let Team::AnyEvil = self.team() {
|
||||||
|
return "damned";
|
||||||
|
}
|
||||||
|
|
||||||
|
self.role_title().category().class()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Class for RoleTitle {
|
||||||
|
fn class(&self) -> &'static str {
|
||||||
|
self.category().class()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Class for Category {
|
||||||
|
fn class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Category::Wolves => "wolves",
|
||||||
|
Category::Villager => "village",
|
||||||
|
Category::Intel => "intel",
|
||||||
|
Category::Defensive => "defensive",
|
||||||
|
Category::Offensive => "offensive",
|
||||||
|
Category::StartsAsVillager => "starts-as-villager",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct Classes(Vec<String>);
|
||||||
|
impl Classes {
|
||||||
|
pub fn push_opt(&mut self, opt: Option<String>) {
|
||||||
|
if let Some(val) = opt {
|
||||||
|
self.0.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Deref for Classes {
|
||||||
|
type Target = Vec<String>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DerefMut for Classes {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<I> From<I> for Classes
|
||||||
|
where
|
||||||
|
I: Into<Vec<String>>,
|
||||||
|
{
|
||||||
|
fn from(value: I) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl core::fmt::Display for Classes {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(self.0.join(" ").as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AsClasses {
|
||||||
|
fn as_classes(&self) -> Classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsClasses for [&str] {
|
||||||
|
fn as_classes(&self) -> Classes {
|
||||||
|
Classes(self.iter().map(|s| s.to_string()).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> AsClasses for [Option<T>]
|
||||||
|
where
|
||||||
|
T: ToString,
|
||||||
|
{
|
||||||
|
fn as_classes(&self) -> Classes {
|
||||||
|
Classes(
|
||||||
|
self.iter()
|
||||||
|
.filter_map(|c| c.as_ref().map(|c| c.to_string()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl leptos::tachys::html::class::IntoClass for Classes {
|
||||||
|
type AsyncOutput = Self;
|
||||||
|
type State = (leptos::tachys::renderer::types::Element, Self);
|
||||||
|
type Cloneable = Self;
|
||||||
|
type CloneableOwned = Self;
|
||||||
|
|
||||||
|
fn html_len(&self) -> usize {
|
||||||
|
let len = self.0.len();
|
||||||
|
self.0.iter().map(|c| c.len()).sum::<usize>()
|
||||||
|
+ if len == 2 {
|
||||||
|
1
|
||||||
|
} else if len > 2 {
|
||||||
|
len - 1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_html(self, class: &mut String) {
|
||||||
|
class.push_str(self.0.join(" ").as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_overwrite(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &leptos::tachys::renderer::types::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
leptos::tachys::renderer::Rndr::set_attribute(el, "class", self.to_string().as_str());
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &leptos::tachys::renderer::types::Element) -> Self::State {
|
||||||
|
leptos::tachys::renderer::Rndr::set_attribute(el, "class", self.to_string().as_str());
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
let (el, prev) = state;
|
||||||
|
if self != *prev {
|
||||||
|
leptos::tachys::renderer::Rndr::set_attribute(el, "class", self.to_string().as_str());
|
||||||
|
}
|
||||||
|
*prev = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_cloneable(self) -> Self::Cloneable {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_cloneable_owned(self) -> Self::CloneableOwned {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dry_resolve(&mut self) {}
|
||||||
|
|
||||||
|
async fn resolve(self) -> Self::AsyncOutput {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(state: &mut Self::State) {
|
||||||
|
let (el, _prev) = state;
|
||||||
|
leptos::tachys::renderer::Rndr::remove_attribute(el, "class");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use werewolves_proto::message::{host::HostNightMessage, night::ActionResponse};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Cover(
|
||||||
|
#[prop(optional)] message: &'static str,
|
||||||
|
#[prop(optional)] reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
||||||
|
#[prop(default=HostNightMessage::ActionResponse(ActionResponse::Continue))]
|
||||||
|
reply_to_send: HostNightMessage,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let message = if message.is_empty() {
|
||||||
|
"go to sleep"
|
||||||
|
} else {
|
||||||
|
message
|
||||||
|
};
|
||||||
|
let next = move || {
|
||||||
|
reply.map(|reply| {
|
||||||
|
let reply_to_send = reply_to_send.clone();
|
||||||
|
view! { <button on:click=move |_| { reply.set(Some(reply_to_send.clone())) }>"continue"</button> }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
move || {
|
||||||
|
view! {
|
||||||
|
<div class="cover-of-darkness">
|
||||||
|
<p>{message}</p>
|
||||||
|
{next.clone()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn DebugMarker() -> impl IntoView {
|
||||||
|
option_env!("LOCAL_DEBUG").map(|_| {
|
||||||
|
view! { <div class="debug-marker">"DEBUG"</div> }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
use leptos::{html::Div, prelude::*};
|
||||||
|
use leptos_use::{
|
||||||
|
UseDraggableOptions, UseDraggableReturn, core::Position, use_draggable_with_options,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::error::WolfError;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ErrorBox(msg: RwSignal<Option<WolfError>>) -> impl IntoView {
|
||||||
|
let el = NodeRef::<Div>::new();
|
||||||
|
|
||||||
|
// `style` is a helper string "left: {x}px; top: {y}px;"
|
||||||
|
let UseDraggableReturn { style, .. } = use_draggable_with_options(
|
||||||
|
el,
|
||||||
|
UseDraggableOptions::default().initial_value(Position { x: 40.0, y: 40.0 }),
|
||||||
|
);
|
||||||
|
let content = move || {
|
||||||
|
msg.get().map(|err| {
|
||||||
|
view! {
|
||||||
|
<div class="error_container" hidden=move || msg.get().is_none()>
|
||||||
|
<div node_ref=el style=move || style.get() class="error">
|
||||||
|
<h5>"error"</h5>
|
||||||
|
<p>{err.to_string()}</p>
|
||||||
|
<button on:click=move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
msg.set(None);
|
||||||
|
}>"close"</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
view! { {content} }
|
||||||
|
}
|
||||||
|
|
@ -12,12 +12,14 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use leptos::prelude::*;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
aura::AuraTitle,
|
aura::AuraTitle,
|
||||||
diedto::DiedToTitle,
|
diedto::DiedToTitle,
|
||||||
role::{Alignment, Killer, Powerful, RoleTitle},
|
role::{Alignment, Killer, Powerful, RoleTitle},
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
|
||||||
|
use crate::app::class::{Class, Classes, PartialClass};
|
||||||
|
|
||||||
macro_rules! decl_icon {
|
macro_rules! decl_icon {
|
||||||
($($name:ident: $path:literal,)*) => {
|
($($name:ident: $path:literal,)*) => {
|
||||||
|
|
@ -76,6 +78,8 @@ decl_icon!(
|
||||||
Mason: "/img/mason.svg",
|
Mason: "/img/mason.svg",
|
||||||
NotEqual: "/img/not-equal.svg",
|
NotEqual: "/img/not-equal.svg",
|
||||||
Equal: "/img/equal.svg",
|
Equal: "/img/equal.svg",
|
||||||
|
UnbalancedScales: "/img/unbalanced-scales.svg",
|
||||||
|
BalancedScales: "/img/balanced-scales.svg",
|
||||||
RedX: "/img/red-x.svg",
|
RedX: "/img/red-x.svg",
|
||||||
Damned: "/img/damned.svg",
|
Damned: "/img/damned.svg",
|
||||||
Bloodlet: "/img/bloodlet.svg",
|
Bloodlet: "/img/bloodlet.svg",
|
||||||
|
|
@ -83,8 +87,8 @@ decl_icon!(
|
||||||
Insane: "/img/insane.svg",
|
Insane: "/img/insane.svg",
|
||||||
);
|
);
|
||||||
|
|
||||||
impl IconSource {
|
impl PartialClass for IconSource {
|
||||||
pub const fn class(&self) -> Option<&'static str> {
|
fn partial_class(&self) -> Option<&'static str> {
|
||||||
match self {
|
match self {
|
||||||
IconSource::Killer => Some("killer"),
|
IconSource::Killer => Some("killer"),
|
||||||
IconSource::Powerful => Some("powerful"),
|
IconSource::Powerful => Some("powerful"),
|
||||||
|
|
@ -95,53 +99,31 @@ impl IconSource {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
pub enum IconType {
|
pub enum IconType {
|
||||||
List,
|
|
||||||
Small,
|
Small,
|
||||||
Fit,
|
|
||||||
Icon15Pct,
|
|
||||||
Informational,
|
|
||||||
#[default]
|
#[default]
|
||||||
RoleCheck,
|
Fit,
|
||||||
|
Shrink,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IconType {
|
impl Class for IconType {
|
||||||
pub const fn class(&self) -> &'static str {
|
fn class(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
IconType::Icon15Pct => "icon-15pct",
|
|
||||||
IconType::Fit => "icon-fit",
|
IconType::Fit => "icon-fit",
|
||||||
IconType::List => "icon-in-list",
|
|
||||||
IconType::Small => "icon",
|
IconType::Small => "icon",
|
||||||
IconType::Informational => "icon-info",
|
IconType::Shrink => "icon-shrink",
|
||||||
IconType::RoleCheck => "check-icon",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[component]
|
||||||
pub struct IconProps {
|
|
||||||
pub source: IconSource,
|
|
||||||
#[prop_or_default]
|
|
||||||
pub inactive: bool,
|
|
||||||
#[prop_or_default]
|
|
||||||
pub icon_type: IconType,
|
|
||||||
#[prop_or_default]
|
|
||||||
pub classes: Classes,
|
|
||||||
}
|
|
||||||
#[function_component]
|
|
||||||
pub fn Icon(
|
pub fn Icon(
|
||||||
IconProps {
|
source: IconSource,
|
||||||
source,
|
#[prop(optional)] r#type: IconType,
|
||||||
inactive,
|
#[prop(optional)] mut classes: Classes,
|
||||||
icon_type,
|
) -> impl IntoView {
|
||||||
classes,
|
classes.push_opt(source.partial_class().map(ToString::to_string));
|
||||||
}: &IconProps,
|
classes.push(r#type.class().to_string());
|
||||||
) -> Html {
|
view! { <img src=source.source() class=classes.to_string() /> }
|
||||||
html! {
|
|
||||||
<img
|
|
||||||
src={source.source()}
|
|
||||||
class={classes!(source.class(), icon_type.class(), inactive.then_some("inactive"), classes.clone())}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PartialAssociatedIcon {
|
pub trait PartialAssociatedIcon {
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use werewolves_proto::message::{Identification, PublicIdentity};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn IdentityInline(ident: PublicIdentity) -> impl IntoView {
|
||||||
|
let number = ident
|
||||||
|
.number
|
||||||
|
.as_ref()
|
||||||
|
.map(|num| view! { <span class="number">{num.get()}</span> }.into_any())
|
||||||
|
.unwrap_or_else(|| view! { <span class="number red">"?"</span> }.into_any());
|
||||||
|
let pronouns = move || {
|
||||||
|
ident.pronouns.as_ref().map(|p| {
|
||||||
|
view! { <span class="pronouns">"("{p.clone()}")"</span> }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<span class="identity">
|
||||||
|
{number} <span class="name">{move || ident.name.clone()}</span> {pronouns}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn IdentificationInline(ident: Identification) -> impl IntoView {
|
||||||
|
if !ident.public.name.trim().is_empty() {
|
||||||
|
return view! { <IdentityInline ident=ident.public /> }.into_any();
|
||||||
|
}
|
||||||
|
view! {
|
||||||
|
<span class="identity">
|
||||||
|
<span class="player-id">{ident.player_id.to_string()}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use leptos::{
|
||||||
|
ev::{Event, MouseEvent, SubmitEvent, Targeted},
|
||||||
|
prelude::*,
|
||||||
|
web_sys::HtmlInputElement,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::error::WolfError;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ChangePlayerNumber(
|
||||||
|
submitted_number: WriteSignal<Option<NonZeroU8>>,
|
||||||
|
error: WriteSignal<Option<WolfError>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let number: RwSignal<Option<NonZeroU8>> = RwSignal::new(None);
|
||||||
|
let update = move |e: Targeted<Event, HtmlInputElement>| {
|
||||||
|
e.prevent_default();
|
||||||
|
let value = e.target().value();
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
number.set(None);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current = number.get_untracked();
|
||||||
|
let default = current.map(|c| c.get().to_string()).unwrap_or_default();
|
||||||
|
let value_u8 = match e.target().value().trim().parse::<u8>() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("{err}");
|
||||||
|
e.target().set_value(default.as_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(nz) = NonZeroU8::new(value_u8) {
|
||||||
|
number.set(Some(nz));
|
||||||
|
} else {
|
||||||
|
e.target().set_value(default.as_str());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let submit = move |ev: SubmitEvent| {
|
||||||
|
ev.prevent_default();
|
||||||
|
log::warn!("called submit with number: {:?}", number.get());
|
||||||
|
let Some(num) = number.get() else {
|
||||||
|
error.set(Some(WolfError::NoSeatNumber));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
submitted_number.set(Some(num));
|
||||||
|
};
|
||||||
|
let submit_click = move |ev: MouseEvent| {
|
||||||
|
ev.prevent_default();
|
||||||
|
log::warn!("called submit with number: {:?}", number.get());
|
||||||
|
let Some(num) = number.get() else {
|
||||||
|
error.set(Some(WolfError::NoSeatNumber));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
submitted_number.set(Some(num));
|
||||||
|
};
|
||||||
|
move || {
|
||||||
|
view! {
|
||||||
|
<form class="number-update" on:submit=submit>
|
||||||
|
<label for="player-number">"change seat number"</label>
|
||||||
|
<input
|
||||||
|
id="player-number"
|
||||||
|
type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
on:input:target=update
|
||||||
|
value=move || { number.get().map(|n| n.get().to_string()).unwrap_or_default() }
|
||||||
|
/>
|
||||||
|
<input value="submit" type="submit" on:click=submit_click />
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
use core::fmt::Display;
|
||||||
|
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum InputType {
|
||||||
|
#[default]
|
||||||
|
Text,
|
||||||
|
Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for InputType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
InputType::Text => f.write_str("text"),
|
||||||
|
InputType::Password => f.write_str("password"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TextInput(
|
||||||
|
#[prop(optional)] label: Option<String>,
|
||||||
|
value: RwSignal<String>,
|
||||||
|
#[prop(optional)] autocomplete: bool,
|
||||||
|
#[prop(optional)] r#type: InputType,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
let label = label.map(|label| {
|
||||||
|
view! { <label for=id.clone()>{label}</label> }
|
||||||
|
});
|
||||||
|
let initial_value = move || value.read().to_string();
|
||||||
|
view! {
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
type=r#type.to_string()
|
||||||
|
id=id
|
||||||
|
value=initial_value
|
||||||
|
on:input:target=move |ev| value.set(ev.target().value())
|
||||||
|
autocomplete=autocomplete
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||