Compare commits
No commits in common. "leptos-port" and "main" have entirely different histories.
leptos-por
...
main
|
|
@ -7,7 +7,3 @@ werewolves/img/icons.svg
|
|||
license_headers.fish
|
||||
util/
|
||||
werewolves/Trunk-local.toml
|
||||
public/img/icons.svg
|
||||
|
||||
werewolves-old-client/
|
||||
werewolves-old-server/
|
||||
|
|
|
|||
132
Cargo.toml
|
|
@ -1,136 +1,8 @@
|
|||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
# "werewolves-old-client",
|
||||
"werewolves",
|
||||
"werewolves-macros",
|
||||
"werewolves-proto",
|
||||
# "werewolves-server",
|
||||
"werewolves",
|
||||
# "api",
|
||||
"werewolves-server",
|
||||
]
|
||||
|
||||
[[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
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
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 |
|
|
@ -1,67 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 6.3 KiB |
|
|
@ -1,81 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 7.1 KiB |
|
|
@ -1,48 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,409 +0,0 @@
|
|||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
1175
style/main.scss
243
style/night.scss
|
|
@ -1,243 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
107
style/setup.scss
|
|
@ -1,107 +0,0 @@
|
|||
.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,6 +24,7 @@ use proc_macro2::Span;
|
|||
use quote::{ToTokens, quote};
|
||||
use syn::{parse::Parse, parse_macro_input};
|
||||
|
||||
use crate::ref_and_mut::RefAndMut;
|
||||
|
||||
mod all;
|
||||
mod checks;
|
||||
|
|
|
|||
|
|
@ -4,38 +4,16 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bytes = { version = "1.10.1", features = ["serde"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||
thiserror = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
sqlx = { workspace = true, optional = true }
|
||||
axum = { workspace = true, optional = true, features = ["macros"] }
|
||||
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
|
||||
thiserror = { version = "2" }
|
||||
log = { version = "0.4" }
|
||||
serde_json = { version = "1.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "1.17", features = ["v4", "serde"] }
|
||||
rand = { version = "0.9", features = ["std_rng"] }
|
||||
werewolves-macros = { path = "../werewolves-macros" }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
colored.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
pretty_env_logger.workspace = true
|
||||
|
||||
[features]
|
||||
ssr = [
|
||||
"dep:sqlx",
|
||||
"dep:axum",
|
||||
"dep:axum-extra",
|
||||
"dep:argon2",
|
||||
"dep:async-trait",
|
||||
"dep:serde_json",
|
||||
"dep:futures",
|
||||
]
|
||||
pretty_assertions = { version = "1" }
|
||||
pretty_env_logger = { version = "0.5" }
|
||||
colored = { version = "3.0" }
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
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"))
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
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,6 +13,7 @@
|
|||
// 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::{
|
||||
fmt::Display,
|
||||
num::NonZeroU8,
|
||||
ops::{Deref, Not},
|
||||
};
|
||||
|
|
@ -25,11 +26,10 @@ use crate::{
|
|||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{GameTime, Village, night::changes::NightChange},
|
||||
id_impl,
|
||||
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
||||
player::{PlayerId, RoleChange},
|
||||
role::{
|
||||
Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT,
|
||||
Alignment, HunterMut, HunterRef, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT,
|
||||
Powerful, PreviousGuardianAction, Role, RoleTitle,
|
||||
},
|
||||
team::Team,
|
||||
|
|
@ -37,7 +37,23 @@ use crate::{
|
|||
|
||||
type Result<T> = core::result::Result<T, GameError>;
|
||||
|
||||
id_impl!(CharacterId);
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
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)]
|
||||
pub struct Character {
|
||||
|
|
@ -50,11 +66,7 @@ pub struct Character {
|
|||
}
|
||||
|
||||
impl Character {
|
||||
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(
|
||||
pub fn new(
|
||||
Identification {
|
||||
player_id,
|
||||
public:
|
||||
|
|
@ -66,7 +78,6 @@ impl Character {
|
|||
}: Identification,
|
||||
role: Role,
|
||||
auras: Vec<Aura>,
|
||||
character_id: CharacterId,
|
||||
) -> Option<Self> {
|
||||
Some(Self {
|
||||
role,
|
||||
|
|
@ -75,7 +86,7 @@ impl Character {
|
|||
auras: Auras::new(auras),
|
||||
role_changes: Vec::new(),
|
||||
identity: CharacterIdentity {
|
||||
character_id,
|
||||
character_id: CharacterId::new(),
|
||||
name,
|
||||
pronouns,
|
||||
number: number?,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use werewolves_macros::Titles;
|
|||
|
||||
use crate::{character::CharacterId, game::GameTime};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Titles)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
|
||||
pub enum DiedTo {
|
||||
Execution {
|
||||
day: NonZeroU8,
|
||||
|
|
|
|||
|
|
@ -15,13 +15,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
game::{GameId, GameTime},
|
||||
message::PublicIdentity,
|
||||
player::PlayerId,
|
||||
role::RoleTitle,
|
||||
};
|
||||
use leptos::prelude::ServerFnErrorErr;
|
||||
use crate::{game::GameTime, message::PublicIdentity, player::PlayerId, role::RoleTitle};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
||||
pub enum GameError {
|
||||
|
|
@ -51,10 +45,6 @@ pub enum GameError {
|
|||
HostChannelClosed,
|
||||
#[error("too many players: there's {got} players but only {need} roles")]
|
||||
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")]
|
||||
AlreadyDaytime,
|
||||
#[error("it's not the end of the night yet")]
|
||||
|
|
@ -119,165 +109,4 @@ pub enum GameError {
|
|||
NoCurrentPromptForAura,
|
||||
#[error("you're not dead")]
|
||||
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,10 +27,7 @@ use core::{
|
|||
|
||||
use chrono::{DateTime, Utc};
|
||||
use rand::{Rng, seq::SliceRandom};
|
||||
use serde::{
|
||||
Deserialize, Serialize,
|
||||
de::{Expected, Unexpected},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
|
|
@ -40,7 +37,6 @@ use crate::{
|
|||
night::{Night, ServerAction},
|
||||
story::{DayDetail, GameActions, GameStory, NightDetails},
|
||||
},
|
||||
id_impl,
|
||||
message::{
|
||||
CharacterState, ClientDeadChat, Identification, ServerToClientMessage,
|
||||
dead::{DeadChatContent, DeadChatMessage},
|
||||
|
|
@ -57,8 +53,6 @@ pub use {
|
|||
|
||||
type Result<T> = core::result::Result<T, GameError>;
|
||||
|
||||
id_impl!(GameId);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Game {
|
||||
started: DateTime<Utc>,
|
||||
|
|
@ -67,19 +61,6 @@ pub struct 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> {
|
||||
let village = Village::new(players, settings)?;
|
||||
Ok(Self {
|
||||
|
|
@ -213,12 +194,9 @@ impl Game {
|
|||
self.process(HostGameMessage::GetState)
|
||||
}
|
||||
(
|
||||
GameState::Day { village, marked },
|
||||
GameState::Day { village: _, marked },
|
||||
HostGameMessage::Day(HostDayMessage::MarkForExecution(target)),
|
||||
) => {
|
||||
if village.character_by_id(target)?.died_to().is_some() {
|
||||
return Err(GameError::CharacterAlreadyDead);
|
||||
}
|
||||
match marked
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
|
@ -488,70 +466,12 @@ pub enum Maybe {
|
|||
Maybe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)]
|
||||
pub enum GameTime {
|
||||
Day { number: NonZeroU8 },
|
||||
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 {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
|
|
|
|||
|
|
@ -29,12 +29,13 @@ use crate::{
|
|||
error::GameError,
|
||||
game::{
|
||||
GameTime, Village,
|
||||
kill::{self, KillOutcome},
|
||||
night::changes::{ChangesLookup, NightChange},
|
||||
},
|
||||
message::{
|
||||
dead::DeadChat,
|
||||
dead::{DeadChat, DeadChatMessage},
|
||||
night::{
|
||||
ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits,
|
||||
ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, ActionType, Visits,
|
||||
},
|
||||
},
|
||||
role::RoleTitle,
|
||||
|
|
@ -828,7 +829,7 @@ impl Night {
|
|||
NightChange::Protection {
|
||||
target,
|
||||
protection: _,
|
||||
} => target == kill_target || target == *source,
|
||||
} => target == kill_target,
|
||||
_ => false,
|
||||
}) {
|
||||
// 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
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use core::ops::Not;
|
||||
use core::{num::NonZeroU8, ops::Not};
|
||||
|
||||
use super::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
|
|||
|
|
@ -15,14 +15,17 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use crate::{
|
||||
aura::Aura,
|
||||
aura::{Aura, AuraTitle},
|
||||
bag::DrunkRoll,
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::night::{
|
||||
ActionComplete, CurrentResult, Night, NightState, ResponseOutcome, changes::NightChange,
|
||||
},
|
||||
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
message::{
|
||||
CharacterIdentity,
|
||||
night::{ActionPrompt, ActionResponse, ActionResult, ActionType},
|
||||
},
|
||||
player::Protection,
|
||||
role::{
|
||||
Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleBlock, RoleTitle,
|
||||
|
|
|
|||
|
|
@ -23,12 +23,7 @@ use super::Result;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
character::{Character, CharacterId},
|
||||
error::GameError,
|
||||
message::Identification,
|
||||
role::RoleTitle,
|
||||
};
|
||||
use crate::{character::Character, error::GameError, message::Identification, role::RoleTitle};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GameSettings {
|
||||
|
|
@ -120,12 +115,8 @@ impl GameSettings {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn assign_with_set_character_ids(
|
||||
&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)?;
|
||||
pub fn assign(&self, players: &[Identification]) -> Result<Box<[Character]>> {
|
||||
self.check_with_player_list(players)?;
|
||||
|
||||
let roles_in_game = self
|
||||
.roles
|
||||
|
|
@ -140,7 +131,7 @@ impl GameSettings {
|
|||
s.assign_to.as_ref().map(|assign_to| {
|
||||
players
|
||||
.iter()
|
||||
.find(|(pid, _)| pid.player_id == *assign_to)
|
||||
.find(|pid| pid.player_id == *assign_to)
|
||||
.ok_or(GameError::AssignedPlayerMissing(*assign_to))
|
||||
.map(|id| (id, s))
|
||||
})
|
||||
|
|
@ -149,10 +140,10 @@ impl GameSettings {
|
|||
|
||||
let mut random_assign_players = players
|
||||
.iter()
|
||||
.filter(|(p, _)| {
|
||||
.filter(|p| {
|
||||
!with_assigned_roles
|
||||
.iter()
|
||||
.any(|((r, _), _)| r.player_id == p.player_id)
|
||||
.any(|(r, _)| r.player_id == p.player_id)
|
||||
})
|
||||
.collect::<Box<[_]>>();
|
||||
|
||||
|
|
@ -165,29 +156,12 @@ impl GameSettings {
|
|||
.into_iter()
|
||||
.zip(self.roles.iter().filter(|s| s.assign_to.is_none())),
|
||||
)
|
||||
.map(|((ident, char_id), slot)| {
|
||||
slot.clone()
|
||||
.into_character_with_id(ident.clone(), &roles_in_game, *char_id)
|
||||
})
|
||||
.map(|(id, slot)| slot.clone().into_character(id.clone(), &roles_in_game))
|
||||
.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<()> {
|
||||
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());
|
||||
if p_len > r_len {
|
||||
return Err(GameError::TooManyPlayers {
|
||||
|
|
|
|||
|
|
@ -24,9 +24,8 @@ use werewolves_macros::{All, ChecksAs, Titles};
|
|||
|
||||
use crate::{
|
||||
aura::AuraTitle,
|
||||
character::{Character, CharacterId},
|
||||
character::Character,
|
||||
error::GameError,
|
||||
id_impl,
|
||||
message::Identification,
|
||||
player::PlayerId,
|
||||
role::{Role, RoleTitle},
|
||||
|
|
@ -427,7 +426,14 @@ impl From<RoleTitle> for SetupRole {
|
|||
}
|
||||
}
|
||||
|
||||
id_impl!(SlotId);
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SlotId(Uuid);
|
||||
|
||||
impl SlotId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetupSlot {
|
||||
|
|
@ -464,22 +470,17 @@ impl SetupSlot {
|
|||
)
|
||||
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_character_with_id(
|
||||
self,
|
||||
ident: Identification,
|
||||
roles_in_game: &[RoleTitle],
|
||||
id: CharacterId,
|
||||
) -> Result<Character, GameError> {
|
||||
Character::new_with_character_id(
|
||||
ident.clone(),
|
||||
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()))
|
||||
impl Category {
|
||||
pub const 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,27 +43,6 @@ pub struct 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> {
|
||||
if settings.min_players_needed() > players.len() {
|
||||
return Err(GameError::TooManyRoles {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
use core::{num::NonZeroU8, ops::Not};
|
||||
|
||||
use crate::{
|
||||
aura::Aura,
|
||||
aura::{Aura, AuraTitle},
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
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::{
|
||||
character::{Character, CharacterId},
|
||||
diedto::DiedToTitle,
|
||||
error::{GameError, ServerError},
|
||||
error::GameError,
|
||||
game::{Game, GameOver, GameSettings, OrRandom, SetupRole, SetupSlot},
|
||||
message::{
|
||||
CharacterState, Identification, PublicIdentity,
|
||||
|
|
@ -803,8 +803,7 @@ fn wolfpack_kill_all_targets_valid() {
|
|||
|
||||
for (idx, target) in living_villagers.into_iter().enumerate() {
|
||||
let mut attempt = game.clone();
|
||||
if let ServerToHostMessage::Error(ServerError::GameError(GameError::InvalidTarget)) =
|
||||
attempt
|
||||
if let ServerToHostMessage::Error(GameError::InvalidTarget) = attempt
|
||||
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||
ActionResponse::MarkTarget(target.character_id),
|
||||
)))
|
||||
|
|
@ -1120,7 +1119,7 @@ fn big_game_test_based_on_story_test() {
|
|||
);
|
||||
|
||||
game.execute().title().vindicator();
|
||||
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().wolf_pack_kill();
|
||||
|
|
@ -1128,16 +1127,11 @@ fn big_game_test_based_on_story_test() {
|
|||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().shapeshifter();
|
||||
assert_eq!(
|
||||
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||
ActionResponse::Shapeshift,
|
||||
)))
|
||||
.expect("shapeshift"),
|
||||
ServerToHostMessage::ActionResult(
|
||||
Some(game.character_by_player_id(shapeshifter).identity()),
|
||||
ActionResult::Continue
|
||||
)
|
||||
);
|
||||
.expect("shapeshift");
|
||||
// game.r#continue().r#continue();
|
||||
|
||||
assert_eq!(
|
||||
game.next(),
|
||||
|
|
|
|||
|
|
@ -444,52 +444,3 @@ fn shapeshift_removes_village_prompt_but_previous_can_bring_it_back() {
|
|||
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,121 +15,14 @@
|
|||
#![allow(clippy::new_without_default)]
|
||||
pub mod aura;
|
||||
pub mod bag;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod cbor;
|
||||
pub mod cbor_leptos;
|
||||
pub mod character;
|
||||
pub mod diedto;
|
||||
pub mod error;
|
||||
pub mod game;
|
||||
pub mod game_record;
|
||||
#[cfg(test)]
|
||||
mod game_test;
|
||||
pub mod limited;
|
||||
mod log;
|
||||
pub mod message;
|
||||
pub mod nonzero;
|
||||
pub mod player;
|
||||
pub mod role;
|
||||
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)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
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,20 +17,18 @@ pub mod host;
|
|||
mod ident;
|
||||
pub mod night;
|
||||
|
||||
use crate::{
|
||||
message::host::{HostMessage, ServerToHostMessage},
|
||||
token::TokenString,
|
||||
};
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
pub use ident::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::Titles;
|
||||
|
||||
use crate::{
|
||||
character::CharacterId, error::GameError, game::story::GameStory,
|
||||
message::dead::DeadChatMessage, role::RoleTitle,
|
||||
character::CharacterId,
|
||||
error::GameError,
|
||||
game::{GameOver, story::GameStory},
|
||||
message::dead::DeadChatMessage,
|
||||
role::RoleTitle,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
|
|
@ -64,19 +62,20 @@ pub struct DayCharacter {
|
|||
pub alive: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServerToClientMessage {
|
||||
GameCancelled,
|
||||
Disconnect,
|
||||
LobbyInfo {
|
||||
joined: bool,
|
||||
players: Box<[PublicIdentity]>,
|
||||
current_number: Option<NonZeroU8>,
|
||||
},
|
||||
GameInProgress,
|
||||
GameStart {
|
||||
role: RoleTitle,
|
||||
},
|
||||
InvalidMessageForGameState,
|
||||
NoSuchTarget,
|
||||
GameOver(GameOver),
|
||||
Story(GameStory),
|
||||
Update(PlayerUpdate),
|
||||
DeadChat(Box<[DeadChatMessage]>),
|
||||
|
|
@ -86,38 +85,7 @@ pub enum ServerToClientMessage {
|
|||
Error(GameError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum PlayerUpdate {
|
||||
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,7 +13,6 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use core::hash::Hash;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
|
|
@ -198,32 +197,14 @@ impl DeadChat {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DeadChatMessage {
|
||||
pub id: Uuid,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub message: DeadChatContent,
|
||||
}
|
||||
|
||||
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)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum DeadChatContent {
|
||||
PlayerMessage {
|
||||
from: CharacterIdentity,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
error::{GameError, ServerError},
|
||||
error::GameError,
|
||||
game::{GameOver, GameSettings, story::GameStory},
|
||||
message::{
|
||||
CharacterIdentity,
|
||||
|
|
@ -37,11 +37,8 @@ pub enum HostMessage {
|
|||
Lobby(HostLobbyMessage),
|
||||
InGame(HostGameMessage),
|
||||
ForceRoleAckFor(CharacterId),
|
||||
#[cfg(debug_assertions)]
|
||||
ForceAllRoleAcks,
|
||||
PostGame(PostGameMessage),
|
||||
Echo(ServerToHostMessage),
|
||||
CancelGame,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -95,7 +92,6 @@ pub enum HostLobbyMessage {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, werewolves_macros::Titles)]
|
||||
pub enum ServerToHostMessage {
|
||||
GameCancelled,
|
||||
Disconnect,
|
||||
Daytime {
|
||||
characters: Box<[CharacterState]>,
|
||||
|
|
@ -109,9 +105,9 @@ pub enum ServerToHostMessage {
|
|||
Lobby {
|
||||
players: Box<[PlayerState]>,
|
||||
settings: GameSettings,
|
||||
qr_mode: bool,
|
||||
},
|
||||
Error(ServerError),
|
||||
QrMode(bool),
|
||||
Error(GameError),
|
||||
GameOver(GameOver),
|
||||
WaitingForRoleRevealAcks {
|
||||
ackd: Box<[CharacterIdentity]>,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ pub struct PublicIdentity {
|
|||
pub number: Option<NonZeroU8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CharacterIdentity {
|
||||
pub character_id: CharacterId,
|
||||
pub name: String,
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ impl ActionPrompt {
|
|||
| ActionPrompt::Insomniac { .. } => true,
|
||||
}
|
||||
}
|
||||
pub const fn marked(&self) -> Option<(CharacterId, Option<CharacterId>)> {
|
||||
pub(crate) const fn marked(&self) -> Option<(CharacterId, Option<CharacterId>)> {
|
||||
match self {
|
||||
ActionPrompt::Seer { marked, .. }
|
||||
| ActionPrompt::Protector { marked, .. }
|
||||
|
|
@ -581,46 +581,6 @@ impl ActionPrompt {
|
|||
_ => 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 {
|
||||
|
|
|
|||
|
|
@ -12,20 +12,32 @@
|
|||
//
|
||||
// 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::fmt::Display;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
id_impl,
|
||||
limited::ClampedString,
|
||||
message::PublicIdentity,
|
||||
role::{Role, RoleTitle},
|
||||
token::TokenString,
|
||||
};
|
||||
|
||||
id_impl!(PlayerId);
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
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)]
|
||||
pub struct Player {
|
||||
|
|
@ -61,48 +73,3 @@ pub struct RoleChange {
|
|||
pub new_role: RoleTitle,
|
||||
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::{
|
||||
character::CharacterId,
|
||||
diedto::DiedTo,
|
||||
game::{Category, GameTime, Village},
|
||||
game::{GameTime, Village},
|
||||
message::CharacterIdentity,
|
||||
};
|
||||
|
||||
|
|
@ -122,59 +122,50 @@ pub enum Role {
|
|||
#[checks(Alignment::Village)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks(Powerful::NotPowerful)]
|
||||
#[checks(Category::Villager)]
|
||||
Villager,
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Category::Villager)]
|
||||
Scapegoat { redeemed: bool },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
#[checks(Category::Intel)]
|
||||
Seer,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
#[checks(Category::Intel)]
|
||||
Arcanist,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
#[checks(Category::Intel)]
|
||||
Adjudicator,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
#[checks(Category::Intel)]
|
||||
PowerSeer,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
#[checks(Category::Intel)]
|
||||
Mortician,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
#[checks(Category::Intel)]
|
||||
Beholder,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks(Category::Intel)]
|
||||
MasonLeader {
|
||||
recruits_available: u8,
|
||||
recruits: Box<[CharacterId]>,
|
||||
|
|
@ -182,69 +173,58 @@ pub enum Role {
|
|||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks(Category::Intel)]
|
||||
Empath { cursed: bool },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks(Category::Defensive)]
|
||||
Vindicator,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks(Category::Defensive)]
|
||||
Diseased,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks(Category::Defensive)]
|
||||
BlackKnight { attacked: Option<DiedTo> },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks(Category::Offensive)]
|
||||
Weightlifter,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks(Category::Offensive)]
|
||||
PyreMaster { villagers_killed: u8 },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
#[checks(Category::Intel)]
|
||||
Gravedigger,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks]
|
||||
#[checks(Category::Offensive)]
|
||||
Hunter { target: Option<CharacterId> },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks(Category::Offensive)]
|
||||
Militia { targeted: Option<CharacterId> },
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks(Category::Offensive)]
|
||||
MapleWolf { last_kill_on_night: u8 },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks(Category::Defensive)]
|
||||
Guardian {
|
||||
last_protected: Option<PreviousGuardianAction>,
|
||||
},
|
||||
|
|
@ -252,17 +232,14 @@ pub enum Role {
|
|||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks(Category::Defensive)]
|
||||
Protector { last_protected: Option<CharacterId> },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks(Category::StartsAsVillager)]
|
||||
Apprentice(RoleTitle),
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks(Category::StartsAsVillager)]
|
||||
Elder {
|
||||
knows_on_night: NonZeroU8,
|
||||
woken_for_reveal: bool,
|
||||
|
|
@ -272,7 +249,6 @@ pub enum Role {
|
|||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
#[checks(Category::Intel)]
|
||||
Insomniac,
|
||||
|
||||
#[checks(Alignment::Wolves)]
|
||||
|
|
@ -280,39 +256,33 @@ pub enum Role {
|
|||
#[checks(Powerful::Powerful)]
|
||||
#[checks("wolf")]
|
||||
#[checks("killing_wolf")]
|
||||
#[checks(Category::Wolves)]
|
||||
Werewolf,
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks("wolf")]
|
||||
#[checks("killing_wolf")]
|
||||
#[checks(Category::Wolves)]
|
||||
AlphaWolf { killed: Option<CharacterId> },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks("wolf")]
|
||||
#[checks(Category::Wolves)]
|
||||
DireWolf { last_blocked: Option<CharacterId> },
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks("wolf")]
|
||||
#[checks("killing_wolf")]
|
||||
#[checks(Category::Wolves)]
|
||||
Shapeshifter { shifted_into: Option<CharacterId> },
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks("wolf")]
|
||||
#[checks(Category::Wolves)]
|
||||
LoneWolf,
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks(Killer::Killer)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks("wolf")]
|
||||
#[checks(Category::Wolves)]
|
||||
Bloodletter,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright (C) 2026 Emilis Bliūdžius
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{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")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
|
||||
|
|
@ -3,83 +3,45 @@ name = "werewolves"
|
|||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[dependencies]
|
||||
leptos = { workspace = true }
|
||||
leptos_router = { workspace = true }
|
||||
axum = { workspace = true, optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
leptos_axum = { workspace = true, optional = true }
|
||||
leptos_meta = { workspace = true }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
getrandom = { version = "=0.3.4", optional = true }
|
||||
colored = { workspace = true, optional = true }
|
||||
pretty_env_logger = { workspace = true, optional = true }
|
||||
sqlx = { workspace = true, optional = true }
|
||||
tower-http = { workspace = true, optional = true }
|
||||
mime-sniffer = { version = "0.1", optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
wasm-logger = { version = "0.2" }
|
||||
gloo = { version = "0.11" }
|
||||
leptos-use = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
rand = { workspace = true }
|
||||
reactive_stores = { version = "0.3" }
|
||||
axum-extra = { workspace = true, optional = true }
|
||||
anyhow = { workspace = true, optional = true }
|
||||
bytes = { workspace = true, optional = true }
|
||||
fast_qr = { workspace = true, optional = true }
|
||||
argon2 = { workspace = true, optional = true }
|
||||
log.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
werewolves-macros.workspace = true
|
||||
werewolves-proto.workspace = true
|
||||
codee.workspace = true
|
||||
convert_case.workspace = true
|
||||
thiserror.workspace = true
|
||||
sorted-vec.workspace = true
|
||||
web-sys = { version = "0.3", features = [
|
||||
"HtmlTableCellElement",
|
||||
"Event",
|
||||
"EventTarget",
|
||||
"HtmlImageElement",
|
||||
"HtmlDivElement",
|
||||
"HtmlSelectElement",
|
||||
"HtmlDialogElement",
|
||||
"DomRect",
|
||||
"WheelEvent",
|
||||
] }
|
||||
wasm-bindgen = { version = "=0.2.100" }
|
||||
log = "0.4"
|
||||
rand = { version = "0.9", features = ["small_rng"] }
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
uuid = { version = "*", features = ["js"] }
|
||||
yew = { version = "0.22", features = ["csr"] }
|
||||
yew-router = "0.19"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
gloo = "0.11"
|
||||
wasm-logger = "0.2"
|
||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||
once_cell = "1"
|
||||
chrono = { version = "0.4" }
|
||||
werewolves-macros = { path = "../werewolves-macros" }
|
||||
werewolves-proto = { path = "../werewolves-proto" }
|
||||
futures = "0.3"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
thiserror = { version = "2" }
|
||||
convert_case = { version = "0.10" }
|
||||
ciborium = { version = "0.2", optional = true }
|
||||
chrono-humanize = { version = "0.2.3", features = ["wasmbind"] }
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:console_error_panic_hook",
|
||||
"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"
|
||||
default = ["cbor"]
|
||||
# default = ["json"]
|
||||
cbor = ["dep:ciborium"]
|
||||
json = ["dep:serde_json"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
[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)
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
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();
|
||||
}
|
||||
|
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 |
|
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 |
|
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 |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
|
@ -0,0 +1,20 @@
|
|||
<!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>
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
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>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
// Copyright (C) 2026 Emilis Bliūdžius
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use core::ops::{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");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn DebugMarker() -> impl IntoView {
|
||||
option_env!("LOCAL_DEBUG").map(|_| {
|
||||
view! { <div class="debug-marker">"DEBUG"</div> }
|
||||
})
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
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} }
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
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>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
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>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
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
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::use_url;
|
||||
|
||||
#[component]
|
||||
pub fn LinkButton(href: String, mut children: ChildrenFnMut) -> impl IntoView {
|
||||
let url = use_url();
|
||||
let link = move || {
|
||||
let already_open = url.get().path() == href;
|
||||
match already_open {
|
||||
true => view! {
|
||||
<button
|
||||
class:current=already_open
|
||||
disabled=already_open
|
||||
class:no-hover=already_open
|
||||
>
|
||||
{children()}
|
||||
</button>
|
||||
}
|
||||
.into_any(),
|
||||
false => view! {
|
||||
<a href=href.clone()>
|
||||
<button>{children()}</button>
|
||||
</a>
|
||||
}
|
||||
.into_any(),
|
||||
}
|
||||
};
|
||||
view! { {link} }
|
||||
}
|
||||