From 078aa0ba6d2f5d1e7cc2197881fcdf8dc439d958 Mon Sep 17 00:00:00 2001 From: Jack Papel Date: Wed, 18 Dec 2024 22:32:17 -0500 Subject: [PATCH 1/8] Separate Popover into its own element --- client/src/components/Popover.tsx | 159 ++++++++++++++++++++++++++ client/src/components/Select.tsx | 183 +++++++----------------------- client/src/components/select.css | 3 +- client/src/menu/main/Credits.tsx | 2 +- 4 files changed, 201 insertions(+), 146 deletions(-) create mode 100644 client/src/components/Popover.tsx diff --git a/client/src/components/Popover.tsx b/client/src/components/Popover.tsx new file mode 100644 index 000000000..78dd578f1 --- /dev/null +++ b/client/src/components/Popover.tsx @@ -0,0 +1,159 @@ +import React, { ReactElement, useCallback, useEffect, useMemo, useRef } from "react"; +import "./select.css"; +import ReactDOM from "react-dom/client"; +import { THEME_CSS_ATTRIBUTES } from ".."; + +export type PopoverController = { + setOpenOrClosed: (open: boolean) => void, + open: boolean, +} + +const PopoverContext = React.createContext(undefined); + +export default function Popover(props: Readonly<{ + open: boolean, + children: JSX.Element, + setOpenOrClosed: (open: boolean) => void, + anchorRef?: React.RefObject, + className?: string +}>): ReactElement { + const handleSetOpen = useCallback((isOpen: boolean) => { + props.setOpenOrClosed(isOpen); + }, [props]); + + const thisRef = useRef(null); + const popoverRef = useRef(document.createElement('div')); + + const popoverRoot = useMemo(() => { + const popoverElement = popoverRef.current; + popoverElement.style.position = "absolute"; + + document.body.appendChild(popoverElement); + return ReactDOM.createRoot(popoverElement); + }, []) + + //set ref + useEffect(() => { + const initialPopover = popoverRef.current; + return () => { + setTimeout(() => { + popoverRoot.unmount(); + }) + initialPopover.remove(); + + popoverRef.current = document.createElement('div'); + } + }, [popoverRoot]) + + //match css styles + useEffect(() => { + const styleBenefactor = props.anchorRef?.current ?? thisRef.current; + const popoverElement = popoverRef.current; + + if (styleBenefactor) { + // Match styles + THEME_CSS_ATTRIBUTES.forEach(prop => { + popoverElement.style.setProperty(`--${prop}`, getComputedStyle(styleBenefactor).getPropertyValue(`--${prop}`)) + }) + + popoverElement.className = 'popover ' + (props.className ?? '') + } + }, [props.anchorRef, props.className]) + + // This is for the popover's anchor, not the element named Anchor + const [anchorLocation, setAnchorLocation] = React.useState(() => { + const bounds = props.anchorRef?.current?.getBoundingClientRect(); + + if (bounds) { + return { top: bounds.top, left: bounds.left } + } else { + return {top: 0, left: 0} + } + }); + + //close on scroll + useEffect(() => { + const listener = () => { + const bounds = props.anchorRef?.current?.getBoundingClientRect(); + if ( + bounds && + props.open && + ( + anchorLocation.top !== bounds?.top || + anchorLocation.left !== bounds?.left + ) + ) + handleSetOpen(false); + }; + + window.addEventListener("scroll", listener, true); + window.addEventListener("resize", listener); + return () => { + window.removeEventListener("scroll", listener, true); + window.removeEventListener("resize", listener); + } + }) + + const PopoverContextToBeProvided = useMemo(() => ({ + setOpenOrClosed: props.setOpenOrClosed, + open: props.open + }), [props.open, props.setOpenOrClosed]) + + //open and set position + useEffect(() => { + const anchorElement = props.anchorRef?.current; + const popoverElement = popoverRef.current; + + if (anchorElement && props.open) { + popoverRoot.render( + + {props.children} + + ); + + + popoverElement.hidden = false; + + const buttonBounds = anchorElement.getBoundingClientRect(); + // Position + popoverElement.style.width = `${buttonBounds.width}px`; + popoverElement.style.left = `${buttonBounds.left}px`; + setAnchorLocation({top: buttonBounds.top, left: buttonBounds.left}); + + const spaceAbove = buttonBounds.top; + const spaceBelow = window.innerHeight - buttonBounds.bottom; + + const oneRem = parseFloat(getComputedStyle(anchorElement).fontSize); + + if (spaceAbove > spaceBelow) { + const newHeight = Math.min((25 - .25) * oneRem, spaceAbove - .25 * oneRem); + popoverElement.style.height = `${newHeight}px`; + popoverElement.style.top = `unset`; + popoverElement.style.bottom = `${spaceBelow + buttonBounds.height + .25 * oneRem}px`; + } else { + const newHeight = Math.min((25 - .25) * oneRem, spaceBelow - .25 * oneRem); + popoverElement.style.height = `${newHeight}px`; + popoverElement.style.top = `${spaceAbove + buttonBounds.height + .25 * oneRem}px`; + popoverElement.style.bottom = `unset`; + } + } else { + popoverElement.hidden = true; + } + }, [handleSetOpen, props.open, props.children, popoverRoot, props.anchorRef, PopoverContextToBeProvided]) + + //close on click outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (!popoverRef.current?.contains(event.target as Node) && props.open) { + handleSetOpen(false); + } + }; + + setTimeout(() => { + document.addEventListener("click", handleClickOutside); + }) + return () => document.removeEventListener("click", handleClickOutside); + }, [handleSetOpen, props.open]); + + return
+} \ No newline at end of file diff --git a/client/src/components/Select.tsx b/client/src/components/Select.tsx index a0169c24f..d0aecc75a 100644 --- a/client/src/components/Select.tsx +++ b/client/src/components/Select.tsx @@ -4,6 +4,7 @@ import "./select.css"; import Icon from "./Icon"; import ReactDOM from "react-dom/client"; import { THEME_CSS_ATTRIBUTES } from ".."; +import Popover from "./Popover"; export type SelectOptionsNoSearch = Map; export type SelectOptionsSearch = Map; @@ -45,7 +46,7 @@ export default function Select(props: Readonly< } }, [props]); - const [open, setOpen]= React.useState(false); + const [open, setOpen] = React.useState(false); const [searchString, setSearchString] = React.useState(""); @@ -101,156 +102,52 @@ export default function Select(props: Readonly< } } - const buttonRef = useRef(null); - const dropdownRef = useRef(document.createElement('div')); + const ref = useRef(null); - const dropdownRoot = useMemo(() => { - const dropdownElement = dropdownRef.current; - dropdownElement.style.position = "absolute"; - - document.body.appendChild(dropdownElement); - return ReactDOM.createRoot(dropdownElement); - }, []) - - //set ref - useEffect(() => { - const initialDropdown = dropdownRef.current; - return () => { - setTimeout(() => { - dropdownRoot.unmount(); - }) - initialDropdown.remove(); - - dropdownRef.current = document.createElement('div'); - } - }, [dropdownRoot]) - - //match css styles - useEffect(() => { - const buttonElement = buttonRef.current; - const dropdownElement = dropdownRef.current; - - if (buttonElement) { - // Match styles - THEME_CSS_ATTRIBUTES.forEach(prop => { - dropdownElement.style.setProperty(`--${prop}`, getComputedStyle(buttonElement).getPropertyValue(`--${prop}`)) - }) - - dropdownElement.className = 'custom-select-options' - } - }, []) - - const [buttonLocation, setButtonLocation] = React.useState({top: 0, left: 0}); - - //close on scroll - useEffect(() => { - const listener = (ev: Event) => { - const bounds = buttonRef.current?.getBoundingClientRect(); - if ( - open && - ( - buttonLocation.top !== bounds?.top || - buttonLocation.left !== bounds?.left - ) - ) - handleSetOpen(false); - }; - - window.addEventListener("scroll", listener, true); - window.addEventListener("resize", listener); - return () => { - window.removeEventListener("scroll", listener, true); - window.removeEventListener("resize", listener); - } - }) - - //open and set position - useEffect(() => { - const buttonElement = buttonRef.current; - const dropdownElement = dropdownRef.current; + const value = optionsSearch.get(props.value); + if(value === undefined) { + console.error(`Value not found in options ${props.value}`); + } - if (buttonElement && open) { - dropdownRoot.render( + {handleSetOpen(!open)}} + className={"custom-select "+(props.className?props.className:"")} + onKeyDown={(e)=>{ + if(props.disabled) return; + if(e.key === "Enter" && !open) { + e.preventDefault(); + handleSetOpen(true); + }else if(e.key === "Tab") { + handleSetOpen(false); + }else{ + e.preventDefault(); + handleKeyInput(e.key); + } + }} + > + {open === true ? + keyboard_arrow_up : + keyboard_arrow_down} + {value !== undefined?value[0]:props.value.toString()} + + + { if(props.disabled) return; handleSetOpen(false); handleOnChange(value); }} - />); - - - dropdownElement.hidden = false; - - const buttonBounds = buttonElement.getBoundingClientRect(); - // Position - dropdownElement.style.width = `${buttonBounds.width}px`; - dropdownElement.style.left = `${buttonBounds.left}px`; - setButtonLocation({top: buttonBounds.top, left: buttonBounds.left}); - - const spaceAbove = buttonBounds.top; - const spaceBelow = window.innerHeight - buttonBounds.bottom; - - const oneRem = parseFloat(getComputedStyle(buttonElement).fontSize); - - if (spaceAbove > spaceBelow) { - const newHeight = Math.min((25 - .25) * oneRem, spaceAbove - .25 * oneRem); - dropdownElement.style.height = `${newHeight}px`; - dropdownElement.style.top = `unset`; - dropdownElement.style.bottom = `${spaceBelow + buttonBounds.height + .25 * oneRem}px`; - } else { - const newHeight = Math.min((25 - .25) * oneRem, spaceBelow - .25 * oneRem); - dropdownElement.style.height = `${newHeight}px`; - dropdownElement.style.top = `${spaceAbove + buttonBounds.height + .25 * oneRem}px`; - dropdownElement.style.bottom = `unset`; - } - } else { - dropdownElement.hidden = true; - } - }, [handleOnChange, handleSetOpen, open, props.disabled, optionsNoSearch, dropdownRoot]) - - //close on click outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (!dropdownRef.current?.contains(event.target as Node) && open) { - handleSetOpen(false); - } - }; - - setTimeout(() => { - document.addEventListener("click", handleClickOutside); - }) - return () => document.removeEventListener("click", handleClickOutside); - }, [handleSetOpen, open]); - - const value = optionsSearch.get(props.value); - if(value === undefined) { - console.error(`Value not found in options ${props.value}`); - } - - return {handleSetOpen(!open)}} - className={"custom-select "+(props.className?props.className:"")} - onKeyDown={(e)=>{ - if(props.disabled) return; - if(e.key === "Enter" && !open) { - e.preventDefault(); - handleSetOpen(true); - }else if(e.key === "Tab") { - handleSetOpen(false); - }else{ - e.preventDefault(); - handleKeyInput(e.key); - } - }} - > - {open === true ? - keyboard_arrow_up : - keyboard_arrow_down} - {value !== undefined?value[0]:props.value.toString()} - + /> + + } function SelectOptions(props: Readonly<{ diff --git a/client/src/components/select.css b/client/src/components/select.css index 31f9ec555..4f3ac7374 100644 --- a/client/src/components/select.css +++ b/client/src/components/select.css @@ -9,10 +9,9 @@ .custom-select-options { position: absolute; min-width: max-content; - max-height: min-content; overflow-y: scroll; - padding: .13rem .25rem; + padding: .13rem .13rem; background-color: var(--secondary-color); border-radius: .25rem; border: .13rem solid var(--primary-border-color); diff --git a/client/src/menu/main/Credits.tsx b/client/src/menu/main/Credits.tsx index 751f32169..5186f4d05 100644 --- a/client/src/menu/main/Credits.tsx +++ b/client/src/menu/main/Credits.tsx @@ -13,7 +13,7 @@ export default function Credits(): ReactElement {

{translate("leadDevelopers")}

- Jack Papel + Jack Papel (Website) Sammy

{translate("otherContributors")}

From 8f06b390e20b083d561b32a1268d1f777cc876bc Mon Sep 17 00:00:00 2001 From: Jack Papel Date: Wed, 18 Dec 2024 23:38:07 -0500 Subject: [PATCH 2/8] Move select-specific code from popover --- client/src/components/Popover.tsx | 86 +++++++++++-------------------- client/src/components/Select.tsx | 28 +++++++++- client/src/components/select.css | 2 +- 3 files changed, 59 insertions(+), 57 deletions(-) diff --git a/client/src/components/Popover.tsx b/client/src/components/Popover.tsx index 78dd578f1..8c7d1281b 100644 --- a/client/src/components/Popover.tsx +++ b/client/src/components/Popover.tsx @@ -1,26 +1,15 @@ -import React, { ReactElement, useCallback, useEffect, useMemo, useRef } from "react"; +import React, { ReactElement, useEffect, useMemo, useRef } from "react"; import "./select.css"; import ReactDOM from "react-dom/client"; import { THEME_CSS_ATTRIBUTES } from ".."; - -export type PopoverController = { - setOpenOrClosed: (open: boolean) => void, - open: boolean, -} - -const PopoverContext = React.createContext(undefined); - -export default function Popover(props: Readonly<{ +export default function Popover(props: Readonly<{ open: boolean, children: JSX.Element, setOpenOrClosed: (open: boolean) => void, - anchorRef?: React.RefObject, + onRender?: (popoverElement: HTMLDivElement, anchorElement?: T | undefined) => void + anchorRef?: React.RefObject, className?: string }>): ReactElement { - const handleSetOpen = useCallback((isOpen: boolean) => { - props.setOpenOrClosed(isOpen); - }, [props]); - const thisRef = useRef(null); const popoverRef = useRef(document.createElement('div')); @@ -83,7 +72,7 @@ export default function Popover(props: Readonly<{ anchorLocation.left !== bounds?.left ) ) - handleSetOpen(false); + props.setOpenOrClosed(false); }; window.addEventListener("scroll", listener, true); @@ -94,58 +83,45 @@ export default function Popover(props: Readonly<{ } }) - const PopoverContextToBeProvided = useMemo(() => ({ - setOpenOrClosed: props.setOpenOrClosed, - open: props.open - }), [props.open, props.setOpenOrClosed]) + const popoverContext = useMemo(() => { + return { + popoverElement: popoverRef.current, + anchorElement: props.anchorRef?.current ?? undefined, + open: props.open + } + }, [props.anchorRef, props.open]) //open and set position useEffect(() => { - const anchorElement = props.anchorRef?.current; const popoverElement = popoverRef.current; + const anchorElement = props.anchorRef?.current; + + if (props.open) { + popoverRoot.render(props.children); + + if (anchorElement) { + const anchorBounds = anchorElement.getBoundingClientRect(); - if (anchorElement && props.open) { - popoverRoot.render( - - {props.children} - - ); - - - popoverElement.hidden = false; - - const buttonBounds = anchorElement.getBoundingClientRect(); - // Position - popoverElement.style.width = `${buttonBounds.width}px`; - popoverElement.style.left = `${buttonBounds.left}px`; - setAnchorLocation({top: buttonBounds.top, left: buttonBounds.left}); - - const spaceAbove = buttonBounds.top; - const spaceBelow = window.innerHeight - buttonBounds.bottom; - - const oneRem = parseFloat(getComputedStyle(anchorElement).fontSize); - - if (spaceAbove > spaceBelow) { - const newHeight = Math.min((25 - .25) * oneRem, spaceAbove - .25 * oneRem); - popoverElement.style.height = `${newHeight}px`; - popoverElement.style.top = `unset`; - popoverElement.style.bottom = `${spaceBelow + buttonBounds.height + .25 * oneRem}px`; - } else { - const newHeight = Math.min((25 - .25) * oneRem, spaceBelow - .25 * oneRem); - popoverElement.style.height = `${newHeight}px`; - popoverElement.style.top = `${spaceAbove + buttonBounds.height + .25 * oneRem}px`; - popoverElement.style.bottom = `unset`; + setAnchorLocation({top: anchorBounds.top, left: anchorBounds.left}); } + + setTimeout(() => { + popoverElement.hidden = false; + + if (props.onRender) { + props.onRender(popoverElement, anchorElement ?? undefined) + } + }) } else { popoverElement.hidden = true; } - }, [handleSetOpen, props.open, props.children, popoverRoot, props.anchorRef, PopoverContextToBeProvided]) + }, [props, popoverRoot, popoverContext]) //close on click outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (!popoverRef.current?.contains(event.target as Node) && props.open) { - handleSetOpen(false); + props.setOpenOrClosed(false); } }; @@ -153,7 +129,7 @@ export default function Popover(props: Readonly<{ document.addEventListener("click", handleClickOutside); }) return () => document.removeEventListener("click", handleClickOutside); - }, [handleSetOpen, props.open]); + }, [props]); return
} \ No newline at end of file diff --git a/client/src/components/Select.tsx b/client/src/components/Select.tsx index d644db88e..6ab415352 100644 --- a/client/src/components/Select.tsx +++ b/client/src/components/Select.tsx @@ -134,6 +134,33 @@ export default function Select(props: Readonly< { + if (!buttonElement) return; + + const buttonBounds = buttonElement.getBoundingClientRect(); + dropdownElement.style.width = `${buttonBounds.width}px`; + dropdownElement.style.left = `${buttonBounds.left}px`; + + const spaceAbove = buttonBounds.top; + const spaceBelow = window.innerHeight - buttonBounds.bottom; + + const oneRem = parseFloat(getComputedStyle(buttonElement).fontSize); + + const maxHeight = (25 - .25) * oneRem; + const optionsHeight = 1 + .5 * oneRem + (dropdownElement.firstElementChild?.clientHeight ?? Infinity); + + if (spaceAbove > spaceBelow) { + const newHeight = Math.min(maxHeight, spaceAbove - .25 * oneRem, optionsHeight); + dropdownElement.style.height = `${newHeight}px`; + dropdownElement.style.top = `unset`; + dropdownElement.style.bottom = `${spaceBelow + buttonBounds.height + .25 * oneRem}px`; + } else { + const newHeight = Math.min(maxHeight, spaceBelow - .25 * oneRem, optionsHeight); + dropdownElement.style.height = `${newHeight}px`; + dropdownElement.style.top = `${spaceAbove + buttonBounds.height + .25 * oneRem}px`; + dropdownElement.style.bottom = `unset`; + } + }} anchorRef={ref} > (props: Readonly<{ options: SelectOptionsNoSearch, onChange?: (value: K)=>void, }>) { - return
{props.searchString ?? null} {[...props.options.entries()] diff --git a/client/src/components/select.css b/client/src/components/select.css index 4f3ac7374..d46339433 100644 --- a/client/src/components/select.css +++ b/client/src/components/select.css @@ -9,7 +9,7 @@ .custom-select-options { position: absolute; min-width: max-content; - overflow-y: scroll; + overflow-y: auto; padding: .13rem .13rem; background-color: var(--secondary-color); From 024261f9d76b782faf2bf430fe959e7d008162ee Mon Sep 17 00:00:00 2001 From: Jack Papel Date: Wed, 18 Dec 2024 23:41:22 -0500 Subject: [PATCH 3/8] Remove unused code --- client/src/components/Popover.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/client/src/components/Popover.tsx b/client/src/components/Popover.tsx index 8c7d1281b..f32997987 100644 --- a/client/src/components/Popover.tsx +++ b/client/src/components/Popover.tsx @@ -83,14 +83,6 @@ export default function Popover(props: Read } }) - const popoverContext = useMemo(() => { - return { - popoverElement: popoverRef.current, - anchorElement: props.anchorRef?.current ?? undefined, - open: props.open - } - }, [props.anchorRef, props.open]) - //open and set position useEffect(() => { const popoverElement = popoverRef.current; @@ -115,7 +107,7 @@ export default function Popover(props: Read } else { popoverElement.hidden = true; } - }, [props, popoverRoot, popoverContext]) + }, [props, popoverRoot]) //close on click outside useEffect(() => { From 59efad944bd6447d779ca39511a8691da20b0814 Mon Sep 17 00:00:00 2001 From: Jack Papel Date: Wed, 18 Dec 2024 23:49:57 -0500 Subject: [PATCH 4/8] Remove select styling import --- client/src/components/Popover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Popover.tsx b/client/src/components/Popover.tsx index f32997987..b84a41044 100644 --- a/client/src/components/Popover.tsx +++ b/client/src/components/Popover.tsx @@ -1,7 +1,7 @@ import React, { ReactElement, useEffect, useMemo, useRef } from "react"; -import "./select.css"; import ReactDOM from "react-dom/client"; import { THEME_CSS_ATTRIBUTES } from ".."; + export default function Popover(props: Readonly<{ open: boolean, children: JSX.Element, From 4048c429874b3d4ae4a37a537a0a016525225a15 Mon Sep 17 00:00:00 2001 From: Sam Maselli Date: Sat, 4 Jan 2025 20:56:16 -0500 Subject: [PATCH 5/8] priority encyclopedia page --- client/src/resources/lang/en_us.json | 2 +- server/src/game/player/player_helper_functions.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/resources/lang/en_us.json b/client/src/resources/lang/en_us.json index 10efbecc9..5f7eb1985 100644 --- a/client/src/resources/lang/en_us.json +++ b/client/src/resources/lang/en_us.json @@ -1283,7 +1283,7 @@ "wiki.article.standard.priority.title":"Priority", "wiki.article.standard.priority.title:var.0":"Priorities", - "wiki.article.standard.priority.text":"***Some information in this article may be old, or incorrect***\n\nAbilities are used in a specific order called priority order. If 2 players have abilities that go on the same priority, the player with the lower player number will go first. It is recommended that you read a role's manual page before this page. Some abilities go before night starts. Attacking someone doesn't kill them until the end of the night. Here are all the priorities and the abilities that happen in them.\n\n**Before Night (Not a priority)**\n\n- Jailor chooses who to jail\n- Reporter chooses who to interview\n- Spy finds if an apostle can convert\n- Create Visits\n - All visits are decided by who is selected by each players. During each priority, each associated ability is activated using who is being visited by who, which is not necessarily who the player selected (in the case of a transporter, witch, etc...).\n\n**1: Top Priority**\n\n- Ojo (Visit every player with certain role)\n- Doomsayer (Kill)\n- Revolutionary (Suicide)\n- Jester (Haunt)\n- Veteran (Decide to Rampage)\n- Vigilante (Suicide)\n\n**2: Transporter**\n\n- Transporter (Swap Visits)\n\n**3: Possess**\n\n- Syndicate Witch (Possess)\n\n**4: Necromancy**\n\n- Retributionist (Possess/Change Visits)\n- Necromancer (Possess/Change Visits)\n\n**5: roleblock**\n\n- Bouncer (roleblock)\n- Hypnotist (roleblock)\n- Escort (roleblock)\n- Jailor (Invisible roleblock)\n\n**6: Deception**\n\n- Hypnotist (Hypnotize)\n- Framer (Frame)\n- Blackmailer (Silence)\n- Mortician (Tag)\n- Arsonist (Douse)\n\n**7: Bodyguard**\n\n- Bodyguard (Change visits)\n\n**8: Heal**\n\n- Engineer (Intentionally dismantle, set, or use trap)\n- Bodyguard (Defend themselves)\n- Sentinel (Protect target)\n- Guardian (Defend)\n- Veteran (Defend themselves)\n\n**9: Kill**\n\n- Ojo (Kill)\n- Apostle (Kill)\n- Zealot (Kill)\n- Martyr (Kill)\n- Engineer (Indirect Kill)\n- Arsonist (Ignite)\n- Bodyguard (Attack attacker)\n- Sentinel (Attack Visitor)\n- Godfather (Attack or Make Backup Attack)\n- Jailor (Attack)\n- Mafioso (Attack)\n- Veteran (Rampage)\n- Vigilante (Attack)\n- Werewolf (Rampage and attacks)\n\n**10: Investigative**\n\n- Ojo (See)\n- Reporter (Add Report Message)\n- Psychic (Vision)\n- Engineer (Traps report info)\n- Engineer (Dismantle if triggered) \n- Arsonist (Find who is doused)\n- Bodyguard (Find if protected)\n- Informant (Investigate)\n- Sentinel (Find if protected)\n- Guardian (Find if protected)\n- Mortician (Find role and alibi)\n- Forger (Find role and alibi)\n- Lookout (Investigate)\n- Philosopher (Investigate)\n- Detective (Investigate)\n- Spy (Find who the Syndicate Visits)\n- Tracker (Investigate)\n- Werewolf (Track players scent)\n\n**11: Spy Bug**\n\n- Spy (Bug)\n\n**12: Steal Messages**\n\n- Syndicate Witch (Get target messages)\n- Necromancer (Get target messages)\n- Retributionist (Get target messages)\n\n**13: Convert**\n\n- Apostle (Convert)", + "wiki.article.standard.priority.text":"At the end of night many abilities happen at nearly the same time. They happen in a specific order called priority. Knowing the order of some things can help understanding of some game mechanics.\nThis is the general priority order, with some additional notes:\n- TopPriority\n- Ward\n - Bouncer, Scarecrow, Kidnapper, Jailor, and Warden all ward here\n- Transporter\n- Warper\n- Possess\n - Witch gets second visit deleted here\n- Roleblock\n - Hypnotist and Escort go here\n- Deception\n - Forger, Framer, Blackmailer go here\n - Framer gets the second visit deleted here\n - Syndicate backup visit is erased if the killing is blocked\n- Bodyguard\n - Bodyguard moves the attacking visit to be towards themself\n- Heal\n - Protective roles like Doctor and Armorsmith go here\n- Kill\n- Convert\n - Win condition changes go here\n- Poison\n- Investigative\n - Roles like Detective, Snoop, Lookout, and Spy go here\n- Cupid\n- SpyBug\n- StealMessages\n - Witch steals messages\n", "wiki.article.standard.chat.title:var.0":"Chat Messages", "wiki.article.standard.chat.title":"Chat", diff --git a/server/src/game/player/player_helper_functions.rs b/server/src/game/player/player_helper_functions.rs index 2b4ae811b..c074617fc 100644 --- a/server/src/game/player/player_helper_functions.rs +++ b/server/src/game/player/player_helper_functions.rs @@ -182,8 +182,6 @@ impl PlayerReference{ AbilitySelection::Integer { .. } => {}, AbilitySelection::Kira { .. } => {}, } - - } possessed_visit.target.set_night_visits(game, From 76f5055bf28afc3ff72e5b8a3e18f3575a32fe1e Mon Sep 17 00:00:00 2001 From: Jack Papel Date: Tue, 7 Jan 2025 17:36:37 -0800 Subject: [PATCH 6/8] Get rid of weird stateType-specific functions on gameManager --- client/src/components/ChatMessage.tsx | 85 +++++++++++-------- client/src/components/TextAreaDropdown.tsx | 8 +- client/src/components/WikiArticle.tsx | 4 +- client/src/components/useHooks.tsx | 28 ++++++ client/src/game/gameManager.d.tsx | 8 +- client/src/game/gameManager.tsx | 53 ------------ client/src/game/messageListener.tsx | 48 ++++++----- client/src/index.tsx | 10 +-- client/src/menu/game/HeaderMenu.tsx | 26 ++++-- .../menu/game/gameScreenContent/ChatMenu.tsx | 16 ++-- .../game/gameScreenContent/GraveyardMenu.tsx | 6 +- .../game/gameScreenContent/PlayerListMenu.tsx | 11 ++- client/src/menu/lobby/LobbyNamePane.tsx | 11 ++- 13 files changed, 160 insertions(+), 154 deletions(-) diff --git a/client/src/components/ChatMessage.tsx b/client/src/components/ChatMessage.tsx index 47f6823d2..c05f5462b 100644 --- a/client/src/components/ChatMessage.tsx +++ b/client/src/components/ChatMessage.tsx @@ -10,7 +10,7 @@ import DOMPurify from "dompurify"; import GraveComponent from "./grave"; import { RoleOutline, translateRoleOutline } from "../game/roleListState.d"; import { CopyButton } from "./ClipboardButtons"; -import { useGameState, useLobbyOrGameState, usePlayerState } from "./useHooks"; +import { useGameState, useLobbyOrGameState, usePlayerNames, usePlayerState, useSpectator } from "./useHooks"; import { KiraResult, KiraResultDisplay } from "../menu/game/gameScreenContent/AbilityMenu/AbilitySelectionTypes/KiraSelectionMenu"; import { AuditorResult } from "../menu/game/gameScreenContent/AbilityMenu/RoleSpecificMenus/AuditorMenu"; import { ControllerID, AbilitySelection, translateControllerID, controllerIdToLink } from "../game/abilityInput"; @@ -37,7 +37,8 @@ const ChatElement = React.memo(( const [mouseHovering, setMouseHovering] = React.useState(false); const message = props.message; - const playerNames = props.playerNames ?? GAME_MANAGER.getPlayerNames(); + const realPlayerNames = usePlayerNames(); + const playerNames = props.playerNames ?? realPlayerNames; const chatMessageStyles = require("../resources/styling/chatMessage.json"); if(message.variant === undefined){ console.error("ChatElement message with undefined variant:"); @@ -132,33 +133,13 @@ const ChatElement = React.memo(( />
case "playerDied": - - let graveRoleString: string; - switch (message.variant.grave.information.type) { - case "obscured": - graveRoleString = translate("obscured"); - break; - case "normal": - graveRoleString = translate("role."+message.variant.grave.information.role+".name"); - break; - } - - return
- - {(chatGroupIcon??"")} {translate("chatMessage.playerDied", - playerNames[message.variant.grave.player], graveRoleString - )} - - } - defaultOpen={GAME_MANAGER.getMySpectator()} - > - - -
; + return } return
; }); +function PlayerDiedChatMessage(props: Readonly<{ + playerKeywordData?: KeywordDataMap, + style: string, + chatGroupIcon: string | null, + playerNames: string[], + message: ChatMessage & { variant: { type: "playerDied" } } +}>): ReactElement { + let graveRoleString: string; + switch (props.message.variant.grave.information.type) { + case "obscured": + graveRoleString = translate("obscured"); + break; + case "normal": + graveRoleString = translate("role."+props.message.variant.grave.information.role+".name"); + break; + } + + const spectator = useSpectator(); + + return
+ + {(props.chatGroupIcon ?? "")} {translate("chatMessage.playerDied", + props.playerNames[props.message.variant.grave.player], graveRoleString + )} + + } + defaultOpen={spectator} + > + + +
; +} + function LobbyChatMessage(props: Readonly<{ message: ChatMessage & { variant: { type: "lobbyMessage" } } playerNames: string[], @@ -326,14 +344,9 @@ export function sanitizePlayerMessage(text: string): string { export function translateChatMessage( message: ChatMessageVariant, - playerNames?: string[], + playerNames: string[], roleList?: RoleOutline[] ): string { - - if (playerNames === undefined) { - playerNames = GAME_MANAGER.getPlayerNames(); - } - switch (message.type) { case "lobbyMessage": return sanitizePlayerMessage(replaceMentions(message.text, playerNames)); @@ -522,7 +535,7 @@ export function translateChatMessage( out = translate("chatMessage.abilityUsed.selection.twoRoleOutlineOption", first, second); break; case "string": - out = translate("chatMessage.abilityUsed.selection.string", sanitizePlayerMessage(replaceMentions(message.selection.selection))); + out = translate("chatMessage.abilityUsed.selection.string", sanitizePlayerMessage(replaceMentions(message.selection.selection, playerNames))); break; case "integer": let text = translateChecked("controllerId."+controllerIdToLink(message.abilityId).replace(/\//g, ".") + ".integer." + message.selection.selection); diff --git a/client/src/components/TextAreaDropdown.tsx b/client/src/components/TextAreaDropdown.tsx index 5207ef69f..37f3b5652 100644 --- a/client/src/components/TextAreaDropdown.tsx +++ b/client/src/components/TextAreaDropdown.tsx @@ -7,6 +7,7 @@ import Icon from "./Icon"; import translate from "../game/lang"; import "./textAreaDropdown.css"; import DetailsSummary from "./DetailsSummary"; +import { usePlayerNames } from "./useHooks"; export function TextDropdownArea(props: Readonly<{ titleString: string, @@ -82,6 +83,8 @@ function TextDropdownLabel( return props.savedText !== props.field }, [props.field, props.savedText]); + const playerNames = usePlayerNames(); + function save(field: string) { props.onSave(field); } @@ -92,7 +95,7 @@ function TextDropdownLabel( } return
- {replaceMentions(props.titleString)} + {replaceMentions(props.titleString, playerNames)} {props.onSubtract ?