diff --git a/client/src/components/ChatMessage.tsx b/client/src/components/ChatMessage.tsx index 1f739ef99..f11790a03 100644 --- a/client/src/components/ChatMessage.tsx +++ b/client/src/components/ChatMessage.tsx @@ -156,9 +156,7 @@ const ChatElement = React.memo(( } defaultOpen={GAME_MANAGER.getMySpectator()} > -
- -
+ ; } diff --git a/client/src/components/DetailsSummary.tsx b/client/src/components/DetailsSummary.tsx index 92202cf28..2fa242b86 100644 --- a/client/src/components/DetailsSummary.tsx +++ b/client/src/components/DetailsSummary.tsx @@ -24,7 +24,7 @@ export default function DetailsSummary(props: Readonly<{ }, [props.open, openState, props.disabled]); return
-
{ if(props.disabled) return; setOpen(!open); @@ -33,7 +33,7 @@ export default function DetailsSummary(props: Readonly<{ > {(props.dropdownArrow === undefined || props.dropdownArrow === true) ? ((props.disabled === undefined || props.disabled===false) ? - {open ? "expand_more" : "expand_less"} + {open ? "keyboard_arrow_down" : "keyboard_arrow_right"} : close ) : diff --git a/client/src/components/TextAreaDropdown.tsx b/client/src/components/TextAreaDropdown.tsx index 84d425acb..1913b7f07 100644 --- a/client/src/components/TextAreaDropdown.tsx +++ b/client/src/components/TextAreaDropdown.tsx @@ -1,17 +1,19 @@ -import { ReactElement, useEffect, useMemo, useState } from "react"; +import React, { ReactElement, useEffect, useMemo, useState } from "react"; import StyledText from "./StyledText"; import { sanitizePlayerMessage } from "./ChatMessage"; import GAME_MANAGER, { replaceMentions } from ".."; -import React from "react"; import { Button } from "./Button"; import Icon from "./Icon"; import translate from "../game/lang"; import "./textAreaDropdown.css"; +import DetailsSummary from "./DetailsSummary"; export function TextDropdownArea(props: Readonly<{ titleString: string, savedText: string, + defaultOpen?: boolean, open?: boolean, + dropdownArrow?: boolean, onAdd?: () => void, onSubtract?: () => void, onSave: (text: string) => void, @@ -37,18 +39,21 @@ export function TextDropdownArea(props: Readonly<{ } return ( -
- - - + } + > {unsaved ? "Unsaved" : ""} -
+ ) } @@ -144,11 +149,12 @@ function PrettyTextArea(props: Readonly<{ const [writing, setWriting] = useState(false); const [hover, setHover] = useState(false); - return
setHover(true)} onMouseLeave={() => setHover(false)} + onTouchEnd={() => setWriting(true)} onFocus={() => setWriting(true)} - onBlur={() => setWriting(false)} + onBlur={() => {setWriting(false); setHover(false)}} > {(!writing && !hover) ?
diff --git a/client/src/components/chatMessage.css b/client/src/components/chatMessage.css index e7d7f7947..00e348b1f 100644 --- a/client/src/components/chatMessage.css +++ b/client/src/components/chatMessage.css @@ -101,14 +101,6 @@ .chat-message.warning { background-color: #660000; } -.grave-message { - border: .13rem solid var(--background-border-color); - border-bottom-color: var(--background-border-shadow-color); - border-right-color: var(--background-border-shadow-color); - border-radius: .5rem; - margin: .13rem; - padding: .25rem; -} .kira-guess-results { display: flex; @@ -134,4 +126,7 @@ } .kira-guess-result.notInGame{ border-color: red; +} +.chat-message-div .details-summary-container { + width: 100%; } \ No newline at end of file diff --git a/client/src/components/detailsSummary.css b/client/src/components/detailsSummary.css index 6d5152ba1..c42d88b41 100644 --- a/client/src/components/detailsSummary.css +++ b/client/src/components/detailsSummary.css @@ -5,19 +5,24 @@ align-items: center; background-color: var(--primary-color); - border: .13rem solid var(--primary-border-color); - border-bottom-color: var(--primary-border-shadow-color); - border-right-color: var(--primary-border-shadow-color); - margin: .13rem; - padding: .13rem; border-radius: .5rem; + padding: .13rem; +} +.details-summary-summary-container.open { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom: .13rem solid var(--primary-border-shadow-color); } .details-summary-container{ display: flex; flex-direction: column; - width: 100%; - margin: 0; + margin: .13rem; padding: 0; + background-color: var(--secondary-color); + border: .13rem solid var(--primary-border-color); + border-bottom-color: var(--primary-border-shadow-color); + border-right-color: var(--primary-border-shadow-color); + border-radius: 0.5rem; /* justify-content: left; align-items: center; */ } \ No newline at end of file diff --git a/client/src/components/dragAndDrop.css b/client/src/components/dragAndDrop.css index 595212c2e..d95c16212 100644 --- a/client/src/components/dragAndDrop.css +++ b/client/src/components/dragAndDrop.css @@ -1,5 +1,6 @@ div.draggable { - cursor: grab + cursor: grab; + touch-action: none; } div.draggable.dragged { diff --git a/client/src/components/gameModeSettings/EnabledModifiersDisplay.tsx b/client/src/components/gameModeSettings/EnabledModifiersDisplay.tsx deleted file mode 100644 index 11f65ee49..000000000 --- a/client/src/components/gameModeSettings/EnabledModifiersDisplay.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { ReactElement, useContext } from "react"; -import { MODIFIERS, ModifierType } from "../../game/gameState.d"; -import React from "react"; -import translate from "../../game/lang"; -import StyledText from "../StyledText"; -import { GameModeContext } from "./GameModesEditor"; -import Select, { SelectOptionsSearch } from "../Select"; - -export function EnabledModifiersDisplay(props: { - disabled: boolean, - enabledModifiers?: ModifierType[], - onChange?: (modifiers: ModifierType[]) => void, -}): ReactElement { - let {enabledModifiers} = useContext(GameModeContext); - - enabledModifiers = props.enabledModifiers ?? (enabledModifiers as ModifierType[]); - - const dropdownsSelected = props.disabled === true ? enabledModifiers : (enabledModifiers as (ModifierType | null)[]).concat([null]) - - return
-

{translate("modifiers")}

- {dropdownsSelected.map((currentModifier, index) => { - return !enabledModifiers.includes(m)||m===currentModifier) - } - onChange={newModifier => { - if (props.onChange === undefined) - return; - - let currentModifiers: (ModifierType | null)[] = [...enabledModifiers]; - currentModifiers.splice(index, 1, newModifier); - - //make sure to remove duplicates & null - const out = currentModifiers - .filter((value, index, self) => self.indexOf(value) === index) - .filter(modifier => modifier !== null) as ModifierType[]; - - props.onChange(out); - }} - /> - - })} -
-} - -function EnabledModifierDisplay(props: { - modifier: ModifierType | null, - choosableModifiers?: ModifierType[], - disabled: boolean, - onChange: (modifier: ModifierType | null) => void, -}): ReactElement { - - const optionMap: SelectOptionsSearch = new Map(); - optionMap.set("none", [ - {translate("none")}, - translate("none") - ]); - for (let modifier of (props.choosableModifiers ?? MODIFIERS)) { - optionMap.set(modifier, [ - {translate(modifier)}, - translate(modifier) - ]); - } - - return { - const value = Number(e.target.value); + {props.disabled + ? props.time + : { + const value = Number(e.target.value); - if (!isValidPhaseTime(value)) return - - props.onChange(props.phase, value); - - }} - onKeyUp={(e)=>{ - if(e.key !== 'Enter') return; - - props.onChange(props.phase, props.time); - }} - /> + if (!isValidPhaseTime(value)) return + + props.onChange(props.phase, value); + + }} + onKeyUp={(e)=>{ + if(e.key !== 'Enter') return; + + props.onChange(props.phase, props.time); + }} + /> + }
} \ No newline at end of file diff --git a/client/src/components/gameModeSettings/disabledRoleSelector.css b/client/src/components/gameModeSettings/disabledRoleSelector.css index 4d1c79417..e8b81c953 100644 --- a/client/src/components/gameModeSettings/disabledRoleSelector.css +++ b/client/src/components/gameModeSettings/disabledRoleSelector.css @@ -12,7 +12,7 @@ flex-grow: 1; width: 5rem; } -.disabled-role-element { +.placard { display: inline-block; background-color: var(--primary-color); border: .13rem solid var(--primary-border-color); @@ -21,6 +21,6 @@ border-radius: .25rem; white-space: pre; } -.disabled-role-element.disabled { +.placard.disabled { background-color: var(--secondary-color); } \ No newline at end of file diff --git a/client/src/components/gameModeSettings/gameMode/dataFixer/v3.ts b/client/src/components/gameModeSettings/gameMode/dataFixer/v3.ts index 75391a400..1ff195bfd 100644 --- a/client/src/components/gameModeSettings/gameMode/dataFixer/v3.ts +++ b/client/src/components/gameModeSettings/gameMode/dataFixer/v3.ts @@ -1,6 +1,6 @@ import { VersionConverter } from "."; import { GameMode, GameModeData, GameModeStorage, ShareableGameMode } from ".."; -import { Settings } from "../../../../game/localStorage"; +import { getDefaultSettings, Settings } from "../../../../game/localStorage"; import { RoleOutline, RoleOutlineOption } from "../../../../game/roleListState.d"; import { Role } from "../../../../game/roleState.d"; import { Failure, ParseResult, ParseSuccess, Success, isFailure } from "../parse"; @@ -27,6 +27,13 @@ function parseSettings(json: NonNullable): ParseResult { } } + for(const key of ['maxMenus', 'menuOrder']) { + if (!Object.keys(json).includes(key)) { + json.maxMenus = getDefaultSettings().maxMenus + json.menuOrder = getDefaultSettings().menuOrder + } + } + if (json.format !== "v3") { return Failure("settingsFormatNotV3", json); } diff --git a/client/src/components/gameModeSettings/outlineSelector.css b/client/src/components/gameModeSettings/outlineSelector.css index 3d8134c37..d20db8d87 100644 --- a/client/src/components/gameModeSettings/outlineSelector.css +++ b/client/src/components/gameModeSettings/outlineSelector.css @@ -40,6 +40,7 @@ align-items:center; gap: .25rem; display: flex; + overflow-x: auto; } .role-list-setter-add-button-div { justify-content: center; diff --git a/client/src/components/gameModeSettings/phaseTimeSelector.css b/client/src/components/gameModeSettings/phaseTimeSelector.css index 5196e056f..d772649e2 100644 --- a/client/src/components/gameModeSettings/phaseTimeSelector.css +++ b/client/src/components/gameModeSettings/phaseTimeSelector.css @@ -2,7 +2,6 @@ max-height: 50vh; overflow-y: auto; display: flex; - gap: .25rem; flex-direction: row; flex-wrap: wrap; margin-bottom: 0.25rem; @@ -12,20 +11,15 @@ display: flex; flex-basis: 11rem; flex-grow: 1; - border: .13rem solid var(--primary-border-color); background-color: var(--secondary-color); - border-radius: 0.5rem; align-items: center; justify-content: space-between; } .phase-times-selector > .phase-times > div > span { - margin: 0 .25rem; white-space: pre; } .phase-times-selector > .phase-times > div > input { - border-top-color: var(--primary-border-shadow-color); - border-left-color: var(--primary-border-shadow-color); - border-bottom-color: var(--primary-border-color); - border-right-color: var(--primary-border-color); width: 3rem; + margin: 0; + padding: 0; } \ No newline at end of file diff --git a/client/src/components/grave.css b/client/src/components/grave.css index 438e890b9..ebb468b39 100644 --- a/client/src/components/grave.css +++ b/client/src/components/grave.css @@ -1,9 +1,11 @@ .grave { white-space: normal; - width: 100%; background-color: var(--primary-color); - border: 1px solid var(--primary-border-color); + margin: .25rem; + padding: .25rem; + border-radius: .5rem; + border: .13rem solid var(--primary-border-color); } .grave .note-area { background-color: var(--secondary-color); diff --git a/client/src/components/textAreaDropdown.css b/client/src/components/textAreaDropdown.css index 094d925ef..e42eeb9ee 100644 --- a/client/src/components/textAreaDropdown.css +++ b/client/src/components/textAreaDropdown.css @@ -1,28 +1,29 @@ -.text-area-dropdown details > div { - padding: .13rem; +.pretty-text-area { + display: flex; + flex-direction: column; + align-items: stretch; } .text-area-dropdown .textarea, .text-area-dropdown textarea { text-align: left; height: 20rem; - width: 100%; - margin: 0; + margin: .13rem; text-wrap: wrap; + resize: none; } .text-area-dropdown .textarea > * { text-wrap: pretty; } -.text-area-dropdown summary > div { - display: inline-flex; +.text-area-dropdown .details-summary-summary-container > div { + display: flex; width: 100%; flex-direction: row; flex-wrap: wrap; + padding-left: .25rem; justify-content: space-between; text-align: left; align-items: baseline; - /* This is a little hacky. This whole thing is a little hacky.*/ - padding-right: .75rem; } -.text-area-dropdown summary > div > div { +.text-area-dropdown .details-summary-summary-container > div > div { display:flex; flex-wrap: wrap; gap: 0.25rem; diff --git a/client/src/components/useHooks.tsx b/client/src/components/useHooks.tsx index 4c6c3cf11..d7ab73e48 100644 --- a/client/src/components/useHooks.tsx +++ b/client/src/components/useHooks.tsx @@ -23,6 +23,22 @@ function usePacketListener(listener: (type?: StateEventType) => void) { }); } +// https://stackoverflow.com/a/77278013/9157590 +function deepEqual(a: T, b: T): boolean { + if (a === b) { + return true; + } + + const bothAreObjects = + a && b && typeof a === "object" && typeof b === "object"; + + return ( + bothAreObjects && + Object.keys(a).length === Object.keys(b).length && + Object.entries(a).every(([k, v]) => deepEqual(v, b[k as keyof T])) + ); +}; + export function useGameState( getValue: (gameState: GameState) => T, events?: StateEventType[], @@ -39,7 +55,7 @@ export function useGameState( usePacketListener((type?: StateEventType) => { if (GAME_MANAGER.state.stateType === "game" && (events ?? []).includes(type as StateEventType)) { const value = getValue(GAME_MANAGER.state); - if (value !== state) { + if (!deepEqual(value, state)) { setState(value); } } @@ -63,7 +79,10 @@ export function useLobbyState( usePacketListener((type?: StateEventType) => { if (GAME_MANAGER.state.stateType === "lobby" && (events ?? []).includes(type as StateEventType)) { - setState(getValue(GAME_MANAGER.state)); + const value = getValue(GAME_MANAGER.state); + if (!deepEqual(value, state)) { + setState(value); + } } }); @@ -88,7 +107,10 @@ export function useLobbyOrGameState( (GAME_MANAGER.state.stateType === "lobby" || GAME_MANAGER.state.stateType === "game") && (events ?? []).includes(type as StateEventType) ) { - setState(getValue(GAME_MANAGER.state)); + const value = getValue(GAME_MANAGER.state); + if (!deepEqual(value, state)) { + setState(value); + } } }); diff --git a/client/src/game/localStorage.tsx b/client/src/game/localStorage.tsx index b216017e4..31fc50bd3 100644 --- a/client/src/game/localStorage.tsx +++ b/client/src/game/localStorage.tsx @@ -3,6 +3,7 @@ import { CurrentFormat, GameModeStorage } from "../components/gameModeSettings/g import { Language } from "./lang"; import { Role } from "./roleState.d"; import parseFromJson from "../components/gameModeSettings/gameMode/dataFixer"; +import { ContentMenu } from "../menu/game/GameScreen"; export function saveReconnectData(roomCode: number, playerId: number) { localStorage.setItem( @@ -49,6 +50,8 @@ export type Settings = { accessibilityFont: boolean; defaultName: string | null; language: Language; + maxMenus: number; + menuOrder: ContentMenu[] roleSpecificMenus: Role[] // RoleSpecificMenuType=standalone for all listed roles, otherwise it should be playerlist }; @@ -59,7 +62,7 @@ export type RoleSpecificMenuType = "playerList" | "standalone"; export function loadSettingsParsed(): Settings { const result = parseFromJson("Settings", loadSettings()); if(result.type === "failure") { - return DEFAULT_SETTINGS; + return getDefaultSettings(); }else{ return result.value; } @@ -74,7 +77,7 @@ export function loadSettings(): unknown { return null; } } - return DEFAULT_SETTINGS; + return getDefaultSettings(); } export function saveSettings(newSettings: Partial) { const currentSettings = parseFromJson("Settings", loadSettings()); @@ -82,7 +85,7 @@ export function saveSettings(newSettings: Partial) { if(currentSettings.type === "failure") { localStorage.setItem("settings", JSON.stringify({ - ...DEFAULT_SETTINGS, + ...getDefaultSettings(), ...newSettings, })); }else{ @@ -117,13 +120,23 @@ export function deleteGameModes() { localStorage.removeItem("savedGameModes"); } - -export const DEFAULT_SETTINGS: Readonly = { - format: "v3", - volume: 0.5, - fontSize: 1, - accessibilityFont: false, - language: "en_us", - defaultName: null, - roleSpecificMenus: [] -}; \ No newline at end of file +export function getDefaultSettings(): Readonly { + return { + format: "v3", + volume: 0.5, + fontSize: 1, + accessibilityFont: false, + language: "en_us", + defaultName: null, + maxMenus: window.innerWidth < 600 ? 1 : 6, + menuOrder: [ + ContentMenu.WikiMenu, + ContentMenu.GraveyardMenu, + ContentMenu.PlayerListMenu, + ContentMenu.ChatMenu, + ContentMenu.WillMenu, + ContentMenu.RoleSpecificMenu + ], + roleSpecificMenus: [] + } +} \ No newline at end of file diff --git a/client/src/game/messageListener.tsx b/client/src/game/messageListener.tsx index 61b2346ec..03a5de720 100644 --- a/client/src/game/messageListener.tsx +++ b/client/src/game/messageListener.tsx @@ -279,6 +279,7 @@ export default function messageListener(packet: ToClientPacket){ case "roleOutline": //role list entriy if(GAME_MANAGER.state.stateType === "lobby" || GAME_MANAGER.state.stateType === "game") { + GAME_MANAGER.state.roleList = structuredClone(GAME_MANAGER.state.roleList); GAME_MANAGER.state.roleList[packet.index] = packet.roleOutline; GAME_MANAGER.state.roleList = [...GAME_MANAGER.state.roleList]; } diff --git a/client/src/index.css b/client/src/index.css index 4a951d07f..40bddbf63 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -177,8 +177,8 @@ button, select, summary { user-select: none; } input, option, textarea, .textarea { - border-top-color: var(--background-border-shadow-color); - border-left-color: var(--background-border-shadow-color); + border-top-color: var(--primary-border-shadow-color); + border-left-color: var(--primary-border-shadow-color); } :has(input):focus-within > button.clear { diff --git a/client/src/index.tsx b/client/src/index.tsx index db5236e2b..5d1a14c7e 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -6,7 +6,9 @@ import { GameManager, createGameManager } from './game/gameManager'; import LoadingScreen from './menu/LoadingScreen'; import route from './routing'; -export type Theme = "player-list-menu-colors" | "will-menu-colors" | "role-specific-colors" | "graveyard-menu-colors" | "wiki-menu-colors" +export const DEV_ENV = process.env.NODE_ENV !== 'production'; + +export type Theme = "chat-menu-colors" | "player-list-menu-colors" | "will-menu-colors" | "role-specific-colors" | "graveyard-menu-colors" | "wiki-menu-colors" const THEME_CSS_ATTRIBUTES = [ 'background-color', 'fade-color', 'primary-color', 'secondary-color', diff --git a/client/src/menu/Anchor.tsx b/client/src/menu/Anchor.tsx index 7aefd54d9..53b365415 100644 --- a/client/src/menu/Anchor.tsx +++ b/client/src/menu/Anchor.tsx @@ -3,7 +3,6 @@ import "../index.css"; import "./anchor.css"; import translate, { switchLanguage } from "../game/lang"; import GlobalMenu from "./GlobalMenu"; -import SettingsMenu from './Settings'; import { loadSettingsParsed } from "../game/localStorage"; import LoadingScreen from "./LoadingScreen"; import { Theme } from ".."; @@ -159,8 +158,6 @@ export default function Anchor(props: Readonly<{ let coverCardTheme: Theme | null = null; if (coverCard.type === WikiCoverCard || coverCard.type === WikiArticle) { coverCardTheme = "wiki-menu-colors" - } else if (coverCard.type === SettingsMenu) { - coverCardTheme = "graveyard-menu-colors" } if (callback) { diff --git a/client/src/menu/Settings.tsx b/client/src/menu/Settings.tsx index c27d1fe68..fbd075353 100644 --- a/client/src/menu/Settings.tsx +++ b/client/src/menu/Settings.tsx @@ -3,20 +3,20 @@ import "./settings.css"; import translate, { Language, languageName, LANGUAGES, switchLanguage } from "../game/lang"; import StyledText, { computeKeywordData } from "../components/StyledText"; import Icon from "../components/Icon"; -import { loadSettingsParsed, RoleSpecificMenuType, saveSettings } from "../game/localStorage"; -import { MobileContext, AnchorControllerContext, ANCHOR_CONTROLLER } from "./Anchor"; -import { Role } from "../game/roleState.d"; +import { loadSettingsParsed, saveSettings } from "../game/localStorage"; +import { AnchorControllerContext, ANCHOR_CONTROLLER } from "./Anchor"; import AudioController from "./AudioController"; -import { getAllRoles } from "../game/roleListState.d"; import CheckBox from "../components/CheckBox"; +import { DragAndDrop } from "../components/DragAndDrop"; +import { MENU_THEMES, MENU_TRANSLATION_KEYS } from "./game/GameScreen"; export default function SettingsMenu(): ReactElement { const [volume, setVolume] = useState(loadSettingsParsed().volume); const [fontSizeState, setFontSize] = useState(loadSettingsParsed().fontSize); const [defaultName, setDefaultName] = useState(loadSettingsParsed().defaultName); - const [roleSpecificMenuSettings, setRoleSpecificMenuSettings] = useState(loadSettingsParsed().roleSpecificMenus); const [accessibilityFontEnabled, setAccessibilityFontEnabled] = useState(loadSettingsParsed().accessibilityFont); - const mobile = useContext(MobileContext)!; + const [menuOrder, setMenuOrder] = useState(loadSettingsParsed().menuOrder); + const [maxMenus, setMaxMenus] = useState(loadSettingsParsed().maxMenus); const anchorController = useContext(AnchorControllerContext)!; useEffect(() => { @@ -31,21 +31,23 @@ export default function SettingsMenu(): ReactElement {
-
-
-
-

volume_up {translate("menu.settings.volume")}

- { - const volume = parseFloat(e.target.value); - saveSettings({volume}); - setVolume(volume); - } - }/> -
-
-

{translate("menu.settings.fontSize")}

+
+

{translate("menu.settings.general")}

+
+

volume_up {translate("menu.settings.volume")}

+ { + const volume = parseFloat(e.target.value); + saveSettings({volume}); + setVolume(volume); + } + }/> +
+
+

{translate("menu.settings.font")}

+
-
-

language {translate("menu.settings.language")}

- -
+ +
-

{translate("menu.settings.dangerZone")}

- +

language {translate("menu.settings.language")}

+
-
-
+
+

{translate("menu.settings.gameplay")}

+
+

{translate("menu.settings.menus")}

+ +
+ {translate("menu.settings.menuOrder")} +
+
+ {translate(MENU_TRANSLATION_KEYS[menu] + ".icon")} +
} + onDragEnd={newItems => { + saveSettings({menuOrder: [...newItems]}) + setMenuOrder([...newItems]) + }} + /> +
+
+
+

{translate("menu.settings.defaultName")}

-
-

{translate("menu.settings.accessibility")}

- -
- {mobile &&

{translate("menu.settings.advanced")}

} -
- - {translate("menu.settings.roleSpecificMenus")} - - { - getAllRoles().map(role => { - // const roleSpecificMenuExists = type.roleSpecificMenu; - const menuType: RoleSpecificMenuType = roleSpecificMenuSettings.includes(role as Role) ? "standalone" : "playerList"; - - - return
- {translate(`role.${role}.name`)} - -
- }) - } -
+

{translate("menu.settings.dangerZone")}

+
diff --git a/client/src/menu/anchor.css b/client/src/menu/anchor.css index ea3c3eb8e..6cfc24c83 100644 --- a/client/src/menu/anchor.css +++ b/client/src/menu/anchor.css @@ -67,8 +67,8 @@ .anchor .global-menu-button { z-index: 101; position: fixed; - right: .2rem; - top: .2rem; + right: .25rem; + top: .25rem; height: fit-content; width: fit-content; font-size: 1.5rem; @@ -89,7 +89,7 @@ .anchor-cover-card-content { display: flex; - padding: 1.5rem; + padding: 1rem; width: 100%; height: 100%; } @@ -112,7 +112,7 @@ left: 0; width: 100%; height: 100%; - padding: 4rem 2rem; + padding: 3rem 1rem; } .anchor-cover-card-background-cover::before { diff --git a/client/src/menu/game/GameScreen.tsx b/client/src/menu/game/GameScreen.tsx index 16f236527..833c2c719 100644 --- a/client/src/menu/game/GameScreen.tsx +++ b/client/src/menu/game/GameScreen.tsx @@ -1,10 +1,10 @@ import React, { createContext, ReactElement, useCallback, useContext, useEffect, useState } from "react"; -import HeaderMenu from "./HeaderMenu"; +import HeaderMenu, { MenuButtons } from "./HeaderMenu"; import GraveyardMenu from "./gameScreenContent/GraveyardMenu"; import ChatMenu from "./gameScreenContent/ChatMenu"; import PlayerListMenu from "./gameScreenContent/PlayerListMenu"; import WillMenu from "./gameScreenContent/WillMenu"; -import GAME_MANAGER, { modulus } from "../.."; +import GAME_MANAGER, { DEV_ENV, modulus, Theme } from "../.."; import WikiMenu from "./gameScreenContent/WikiMenu"; import "../../index.css"; import "./gameScreen.css"; @@ -17,6 +17,7 @@ import { Button } from "../../components/Button"; import translate from "../../game/lang"; import { useGameState } from "../../components/useHooks"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; +import { loadSettingsParsed } from "../../game/localStorage"; export enum ContentMenu { ChatMenu = "ChatMenu", @@ -36,6 +37,24 @@ export const MENU_ELEMENTS = { [ContentMenu.WikiMenu]: WikiMenu } +export const MENU_THEMES: Record = { + [ContentMenu.ChatMenu]: "chat-menu-colors", + [ContentMenu.PlayerListMenu]: "player-list-menu-colors", + [ContentMenu.RoleSpecificMenu]: "role-specific-colors", + [ContentMenu.WillMenu]: "will-menu-colors", + [ContentMenu.GraveyardMenu]: "graveyard-menu-colors", + [ContentMenu.WikiMenu]: "wiki-menu-colors" +} + +export const MENU_TRANSLATION_KEYS: Record = { + [ContentMenu.ChatMenu]: "menu.chat", + [ContentMenu.PlayerListMenu]: "menu.playerList", + [ContentMenu.RoleSpecificMenu]: "menu.ability", + [ContentMenu.WillMenu]: "menu.will", + [ContentMenu.GraveyardMenu]: "menu.gameMode", + [ContentMenu.WikiMenu]: "menu.wiki" +} + const ALL_CONTENT_MENUS = Object.values(ContentMenu); export interface MenuController { @@ -45,6 +64,8 @@ export interface MenuController { menusOpen(): ContentMenu[]; menuOpen(menu: ContentMenu): boolean; canOpen(menu: ContentMenu): boolean; + menus(): ContentMenu[] + maxMenus: number } export function useMenuController>>( @@ -121,7 +142,11 @@ export function useMenuController }, canOpen(menu): boolean { return contentMenus[menu] !== undefined - } + }, + menus(): ContentMenu[] { + return Object.keys(contentMenus).filter(menu => contentMenus[menu as ContentMenu] !== undefined) as ContentMenu[]; + }, + maxMenus: maxContent }) }, [contentMenus, getMenuController, maxContent, setMenuController]); @@ -144,17 +169,22 @@ export { MenuControllerContext } export default function GameScreen(): ReactElement { const mobile = useContext(MobileContext)!; + const { maxMenus, menuOrder } = loadSettingsParsed(); + + const menusOpen: [ContentMenu, boolean | undefined][] = [ + [ContentMenu.WikiMenu, mobile ? undefined : false ], + [ContentMenu.GraveyardMenu, maxMenus > 4 ], + [ContentMenu.PlayerListMenu, maxMenus > 1 ], + [ContentMenu.ChatMenu, true ], + [ContentMenu.WillMenu, maxMenus > 3 ], + [ContentMenu.RoleSpecificMenu, maxMenus > 2 ], + ]; + + menusOpen.sort((a, b) => menuOrder.indexOf(a[0]) - menuOrder.indexOf(b[0])) const menuController = useMenuController( - mobile ? 2 : Infinity, - { - WikiMenu: false, - GraveyardMenu: !mobile, - PlayerListMenu: true, - ChatMenu: true, - WillMenu: !mobile, - RoleSpecificMenu: !mobile, - }, + maxMenus, + Object.fromEntries(menusOpen), () => MENU_CONTROLLER_HOLDER.controller!, menuController => MENU_CONTROLLER_HOLDER.controller = menuController ); @@ -164,6 +194,15 @@ export default function GameScreen(): ReactElement { ["addChatMessages"] )!; + useEffect(() => { + const onBeforeUnload = (e: BeforeUnloadEvent) => { + if (!DEV_ENV) e.preventDefault() + }; + + window.addEventListener("beforeunload", onBeforeUnload); + return () => window.removeEventListener("beforeunload", onBeforeUnload); + }, []) + useEffect(() => { const swipeEventListener = (right: boolean) => { // Close the furthest right menu, open the next one to the left or right @@ -174,7 +213,7 @@ export default function GameScreen(): ReactElement { } const allowedMenus = ALL_CONTENT_MENUS.filter(menu => { - return !menusOpen.includes(menu) + return !menusOpen.includes(menu) && menuController.menus().includes(menu) }); const rightMostMenu = menusOpen[menusOpen.length - 1]; @@ -198,6 +237,7 @@ export default function GameScreen(): ReactElement {
+ {mobile && }
} @@ -225,11 +265,11 @@ export function GameScreenMenus(): ReactElement { className="panel" minSize={minSize} defaultSize={mobile===false?defaultSizes[menu]:undefined} - key={index} - > + key={menu} + > - {menusOpen.some((_, i) => i > index) && } + {!mobile && menusOpen.some((_, i) => i > index) && } })} {menuController.menusOpen().length === 0 &&
diff --git a/client/src/menu/game/HeaderMenu.tsx b/client/src/menu/game/HeaderMenu.tsx index d1e747684..b6c6a8824 100644 --- a/client/src/menu/game/HeaderMenu.tsx +++ b/client/src/menu/game/HeaderMenu.tsx @@ -2,7 +2,7 @@ import React, { ReactElement, useContext, useMemo } from "react"; import translate from "../../game/lang"; import GAME_MANAGER from "../../index"; import { PhaseState, Player, Verdict } from "../../game/gameState.d"; -import { MenuControllerContext, ContentMenu } from "./GameScreen"; +import { MenuControllerContext, ContentMenu, MENU_THEMES, MENU_TRANSLATION_KEYS } from "./GameScreen"; import "./headerMenu.css"; import StyledText from "../../components/StyledText"; import Icon from "../../components/Icon"; @@ -30,7 +30,7 @@ export default function HeaderMenu(props: Readonly<{ return
{!(GAME_MANAGER.getMySpectator() && !GAME_MANAGER.getMyHost()) && } - {!(GAME_MANAGER.getMySpectator() && !mobile) && } + {!mobile && }
} @@ -189,56 +189,23 @@ function VerdictButton(props: Readonly<{ verdict: Verdict }>) { } -function MenuButtons(props: Readonly<{ chatMenuNotification: boolean }>): ReactElement | null { +export function MenuButtons(props: Readonly<{ chatMenuNotification: boolean }>): ReactElement | null { const menuController = useContext(MenuControllerContext)!; return
- - - - - {GAME_MANAGER.getMySpectator() || } - {!GAME_MANAGER.getMySpectator() && - } + })}
} diff --git a/client/src/menu/game/gameScreen.css b/client/src/menu/game/gameScreen.css index 65a651147..a2ed20c9f 100644 --- a/client/src/menu/game/gameScreen.css +++ b/client/src/menu/game/gameScreen.css @@ -4,6 +4,12 @@ flex-direction: column; } +.game-screen > .menu-buttons > button { + padding: 0.5rem; + font-size: 1.5em; + aspect-ratio: 1/1; +} + .game-screen > .leave-button { position: absolute; margin: 0; diff --git a/client/src/menu/game/gameScreenContent/AbilityMenu/AbilityMenu.tsx b/client/src/menu/game/gameScreenContent/AbilityMenu/AbilityMenu.tsx index acabe89c1..ff07d8fa7 100644 --- a/client/src/menu/game/gameScreenContent/AbilityMenu/AbilityMenu.tsx +++ b/client/src/menu/game/gameScreenContent/AbilityMenu/AbilityMenu.tsx @@ -17,10 +17,10 @@ export default function AbilityMenu(): ReactElement { {translate("menu.ability.title")} {!mySpectator && - <> +
- +
}
diff --git a/client/src/menu/game/gameScreenContent/AbilityMenu/abilityMenu.css b/client/src/menu/game/gameScreenContent/AbilityMenu/abilityMenu.css index e92cf4d71..a151b4aaa 100644 --- a/client/src/menu/game/gameScreenContent/AbilityMenu/abilityMenu.css +++ b/client/src/menu/game/gameScreenContent/AbilityMenu/abilityMenu.css @@ -1,6 +1,10 @@ .ability-menu { overflow-y: auto; } +.ability-menu .abilities { + display: flex; + flex-direction: column; +} .game-screen .content > .ability-menu { width: 10%; } diff --git a/client/src/menu/game/gameScreenContent/AbilityMenu/genericAbilityMenu.css b/client/src/menu/game/gameScreenContent/AbilityMenu/genericAbilityMenu.css index 2dd331ef2..0fc9e83c9 100644 --- a/client/src/menu/game/gameScreenContent/AbilityMenu/genericAbilityMenu.css +++ b/client/src/menu/game/gameScreenContent/AbilityMenu/genericAbilityMenu.css @@ -1,8 +1,3 @@ -.generic-ability-menu { - margin-top: .4rem; -} - - .generic-ability-menu-tab-summary { display: flex; flex-direction: row; @@ -20,6 +15,7 @@ border-color: var(--primary-border-color); border-radius: .4rem; border-width: .13rem; + margin-top: 0.25rem; } .generic-ability-menu-tab-no-summary > span{ display: inline-flex; diff --git a/client/src/menu/game/gameScreenContent/GraveyardMenu.tsx b/client/src/menu/game/gameScreenContent/GraveyardMenu.tsx index 88da450a8..e555cda6a 100644 --- a/client/src/menu/game/gameScreenContent/GraveyardMenu.tsx +++ b/client/src/menu/game/gameScreenContent/GraveyardMenu.tsx @@ -7,17 +7,20 @@ import StyledText from "../../../components/StyledText"; import { EnabledRolesDisplay } from "../../../components/gameModeSettings/EnabledRoleSelector"; import { useGameState, usePlayerState } from "../../../components/useHooks"; import { translateRoleOutline } from "../../../game/roleListState.d"; -import { EnabledModifiersDisplay } from "../../../components/gameModeSettings/EnabledModifiersDisplay"; import { Button } from "../../../components/Button"; import DetailsSummary from "../../../components/DetailsSummary"; +import { EnabledModifiersDisplay } from "../../../components/gameModeSettings/EnabledModifiersSelector"; export default function GraveyardMenu(): ReactElement { return
{translate("menu.gameMode.title")} -
+ -
+
@@ -38,7 +41,7 @@ function RoleListDisplay(): ReactElement { const roleOutlineName = translateRoleOutline(entry); return
+ {mobile === true && } ); diff --git a/client/src/resources/lang/en_us.json b/client/src/resources/lang/en_us.json index b101ea7aa..fcea2055b 100644 --- a/client/src/resources/lang/en_us.json +++ b/client/src/resources/lang/en_us.json @@ -43,12 +43,16 @@ "menu.globalMenu.gameSettingsEditor": "Game Mode Editor", "menu.settings.title": "Settings", - "menu.settings.advanced": "Advanced Settings", + "menu.settings.general": "General", + "menu.settings.gameplay": "Gameplay", + "menu.settings.menus": "Menus", + "menu.settings.maxMenus": "Max Menus Open", + "menu.settings.menuOrder": "Menu Order", "menu.settings.dangerZone": "Danger zone", "menu.settings.volume": "Volume", "menu.settings.font": "Font", "menu.settings.fontSize": "Font Size", - "menu.settings.accessibility": "Accessibility", + "menu.settings.accessibilityFont": "Accessibility Font", "menu.settings.language": "Language", "menu.settings.defaultName": "Default Name", "menu.settings.eraseSaveData": "Erase Save Data", @@ -124,7 +128,7 @@ "menu.gameMode.icon": "⚙️", "killedBy": "Killed by", - "menu.gameScreen.noContent": "Click on one of the tabs above to open", + "menu.gameScreen.noContent": "Click on one of the tabs to open a menu", "notification.connectionFailed": "Connection failed", "notification.serverNotFound": "Server not found, it could be offline", diff --git a/server/src/lobby/lobby_client.rs b/server/src/lobby/lobby_client.rs index 15432082e..b2697aceb 100644 --- a/server/src/lobby/lobby_client.rs +++ b/server/src/lobby/lobby_client.rs @@ -24,6 +24,7 @@ pub struct LobbyClient{ } #[derive(Clone, Debug, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] pub enum Ready { Host, Ready, diff --git a/server/src/lobby/on_client_message.rs b/server/src/lobby/on_client_message.rs index d987db02d..5bd0d6e58 100644 --- a/server/src/lobby/on_client_message.rs +++ b/server/src/lobby/on_client_message.rs @@ -68,7 +68,7 @@ impl Lobby { return }; - let text = text.trim_newline().trim_whitespace().truncate(100).truncate_lines(1); + let text = text.trim_newline().trim_whitespace().truncate(100); if text.is_empty() {return} let name = if let Some(