1
0

Improving House Rules UX

This commit is contained in:
James Ketr 2025-09-25 11:49:02 -07:00
parent 5e5f4ed9a9
commit a3425b3178
2 changed files with 138 additions and 99 deletions

View File

@ -1,26 +1,4 @@
.HouseRules {
display: flex;
position: absolute;
left: 0;
right: 30rem;
bottom: 0;
top: 0;
justify-content: center;
align-items: center;
background: rgba(0,0,0,0.5);
z-index: 1000;
max-height: 100vh;
overflow: auto;
}
.HouseRules > * {
max-height: calc(100vh - 2em);
overflow: auto;
margin: 0.5em;
width: 40em;
display: inline-flex;
flex-direction: column;
}
.HouseRules .HouseSelector { .HouseRules .HouseSelector {
display: flex; display: flex;

View File

@ -1,14 +1,22 @@
import React, { useState, useEffect, useContext, useRef, useMemo, useCallback } from "react"; import React, {
useState,
useEffect,
useContext,
useRef,
useMemo,
useCallback,
} from "react";
import equal from "fast-deep-equal"; import equal from "fast-deep-equal";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import "./HouseRules.css"; // import "./HouseRules.css";
import { GlobalContext } from "./GlobalContext"; import { GlobalContext } from "./GlobalContext";
import { Placard } from "./Placard"; import { Placard } from "./Placard";
import { Box } from "@mui/material";
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
@ -25,8 +33,12 @@ const Volcano: React.FC<VolcanoProps> = ({ ws, rules, field, disabled }) => {
Math.random() > 0.5 Math.random() > 0.5
? Math.floor(8 + Math.random() * 5) /* Do not include 7 */ ? Math.floor(8 + Math.random() * 5) /* Do not include 7 */
: Math.floor(2 + Math.random() * 5); /* Do not include 7 */ : Math.floor(2 + Math.random() * 5); /* Do not include 7 */
const [number, setNumber] = useState<number>(field in rules && "number" in rules[field] ? rules[field].number : init); const [number, setNumber] = useState<number>(
const [gold, setGold] = useState<boolean>(field in rules && "gold" in rules[field] ? rules[field].gold : false); field in rules && "number" in rules[field] ? rules[field].number : init
);
const [gold, setGold] = useState<boolean>(
field in rules && "gold" in rules[field] ? rules[field].gold : false
);
console.log(`house-rules - ${field} - `, rules[field]); console.log(`house-rules - ${field} - `, rules[field]);
@ -93,16 +105,21 @@ const Volcano: React.FC<VolcanoProps> = ({ ws, rules, field, disabled }) => {
return ( return (
<div className="Volcano"> <div className="Volcano">
<div> <div>
The Volcano replaces the Desert. When the Volcano erupts, roll a die to determine the direction the lava will The Volcano replaces the Desert. When the Volcano erupts, roll a die to
flow. One of the six intersections on the Volcano tile will be affected. If there is a settlement on the selected determine the direction the lava will flow. One of the six intersections
intersection, it is destroyed! on the Volcano tile will be affected. If there is a settlement on the
selected intersection, it is destroyed!
</div> </div>
<div> <div>
Remove it from the board (its owner may rebuild it later). If a city is located there, it is reduced to a Remove it from the board (its owner may rebuild it later). If a city is
settlement! Replace the city with a settlement of its owner&apos;s color. If he has no settlements remaining, the located there, it is reduced to a settlement! Replace the city with a
city is destroyed instead. settlement of its owner&apos;s color. If he has no settlements
remaining, the city is destroyed instead.
</div>
<div>
The presence of the Robber on the Volcano does not prevent the Volcano
from erupting.
</div> </div>
<div>The presence of the Robber on the Volcano does not prevent the Volcano from erupting.</div>
<div> <div>
Roll {number} and the Volcano erupts! Roll {number} and the Volcano erupts!
<button onClick={() => update(+1)}>up</button>&nbsp;/&nbsp; <button onClick={() => update(+1)}>up</button>&nbsp;/&nbsp;
@ -110,20 +127,30 @@ const Volcano: React.FC<VolcanoProps> = ({ ws, rules, field, disabled }) => {
</div> </div>
<div className="HouseSelector"> <div className="HouseSelector">
<div> <div>
<b>Volcanoes have gold!</b>: Volcano can produce resources when its number is rolled. <b>Volcanoes have gold!</b>: Volcano can produce resources when its
number is rolled.
</div> </div>
<div> <div>
<Switch size={"small"} className="RuleSwitch" checked={gold} onChange={() => toggleGold()} {...{ disabled }} /> <Switch
size={"small"}
className="RuleSwitch"
checked={gold}
onChange={() => toggleGold()}
{...{ disabled }}
/>
</div> </div>
</div> </div>
<div> <div>
Volcanoes tend to be rich in valuable minerals such as gold or gems. Each settlement that is adjacent to the Volcanoes tend to be rich in valuable minerals such as gold or gems.
Volcano when it erupts may produce any one of the five resources it&apos;s owner desires. Each settlement that is adjacent to the Volcano when it erupts may
produce any one of the five resources it&apos;s owner desires.
</div> </div>
<div> <div>
Each city adjacent to the Volcano may produce any two resources. This resource production is taken before the Each city adjacent to the Volcano may produce any two resources. This
results of the volcano eruption are resolved. Note that while the Robber can not prevent the Volcano from resource production is taken before the results of the volcano eruption
erupting, he does prevent any player from producing resources from the Volcano hex if he has been placed there. are resolved. Note that while the Robber can not prevent the Volcano
from erupting, he does prevent any player from producing resources from
the Volcano hex if he has been placed there.
</div> </div>
</div> </div>
); );
@ -186,8 +213,7 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
const { ws, name } = useContext(GlobalContext); const { ws, name } = useContext(GlobalContext);
const [rules, setRules] = useState<any>({}); const [rules, setRules] = useState<any>({});
const [state, setState] = useState<any>({}); const [state, setState] = useState<any>({});
const [gameState, setGameState] = useState<string>(''); const [gameState, setGameState] = useState<string>("");
const [ruleElements, setRuleElements] = useState<React.ReactElement[]>([]);
const fields = useMemo(() => ["state", "rules"], []); const fields = useMemo(() => ["state", "rules"], []);
const onWsMessage = (event: MessageEvent) => { const onWsMessage = (event: MessageEvent) => {
@ -259,8 +285,8 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
[rules, ws] [rules, ws]
); );
useEffect(() => { const ruleList = useMemo(
const ruleList = [ () => [
{ {
key: "volcano", key: "volcano",
label: "Volcanoes are a lava fun!", label: "Volcanoes are a lava fun!",
@ -268,7 +294,12 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
category: "board", category: "board",
defaultChecked: false, defaultChecked: false,
element: ( element: (
<Volcano ws={ws} rules={rules} field={"volcano"} disabled={gameState !== 'lobby'} /> <Volcano
ws={ws}
rules={rules}
field={"volcano"}
disabled={gameState !== "lobby"}
/>
), ),
}, },
{ {
@ -277,20 +308,29 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
description: "Customize how many Victory Points are required to win.", description: "Customize how many Victory Points are required to win.",
category: "rules", category: "rules",
defaultChecked: false, defaultChecked: false,
element: <VictoryPoints ws={ws} rules={rules} field={"victory-points"} />, element: (
<VictoryPoints ws={ws} rules={rules} field={"victory-points"} />
),
}, },
{ {
key: "tiles-start-facing-down", key: "tiles-start-facing-down",
label: "Tiles start facing down", label: "Tiles start facing down",
description: "Resource tiles start upside-down while placing starting settlements.", description:
"Resource tiles start upside-down while placing starting settlements.",
category: "board", category: "board",
defaultChecked: false, defaultChecked: false,
element: <div>Once all players have placed their initial settlements and roads, the tiles are flipped and you discover what the resources are.</div>, element: (
<div>
Once all players have placed their initial settlements and roads,
the tiles are flipped and you discover what the resources are.
</div>
),
}, },
{ {
key: "most-developed", key: "most-developed",
label: "You are so developed", label: "You are so developed",
description: "The player with the most development cards (more than 4) receives 2VP.", description:
"The player with the most development cards (more than 4) receives 2VP.",
category: "expansion", category: "expansion",
defaultChecked: false, defaultChecked: false,
element: <Placard type="most-developed" />, element: <Placard type="most-developed" />,
@ -298,7 +338,8 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
{ {
key: "port-of-call", key: "port-of-call",
label: "Another round of port", label: "Another round of port",
description: "The player with the most harbor ports (more than 2) receives 2VP.", description:
"The player with the most harbor ports (more than 2) receives 2VP.",
category: "expansion", category: "expansion",
defaultChecked: false, defaultChecked: false,
element: <Placard type="port-of-call" />, element: <Placard type="port-of-call" />,
@ -306,7 +347,8 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
{ {
key: "slowest-turn", key: "slowest-turn",
label: "Why you play so slowf", label: "Why you play so slowf",
description: "The player with the longest turn idle time (longer than 2 minutes) so far loses 2VP.", description:
"The player with the longest turn idle time (longer than 2 minutes) so far loses 2VP.",
category: "expansion", category: "expansion",
defaultChecked: false, defaultChecked: false,
element: <Placard type="longest-turn" />, element: <Placard type="longest-turn" />,
@ -317,7 +359,12 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
description: "Roll again if you roll two of the same number.", description: "Roll again if you roll two of the same number.",
category: "rolling", category: "rolling",
defaultChecked: false, defaultChecked: false,
element: <div>If you roll doubles, players get those resources and then you must roll again.</div>, element: (
<div>
If you roll doubles, players get those resources and then you must
roll again.
</div>
),
}, },
{ {
key: "twelve-and-two-are-synonyms", key: "twelve-and-two-are-synonyms",
@ -325,20 +372,44 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
description: "If twelve is rolled, two scores as well. And vice-versa.", description: "If twelve is rolled, two scores as well. And vice-versa.",
category: "rolling", category: "rolling",
defaultChecked: false, defaultChecked: false,
element: <div>If you roll a twelve or two, resources are triggered for both.</div>, element: (
<div>
If you roll a twelve or two, resources are triggered for both.
</div>
),
}, },
{ {
key: "robin-hood-robber", key: "robin-hood-robber",
label: "Robin Hood robber", label: "Robin Hood robber",
description: "Robbers can't steal from players with two or less victory points.", description:
"Robbers can't steal from players with two or less victory points.",
category: "rules", category: "rules",
defaultChecked: false, defaultChecked: false,
element: <></>, element: <></>,
}, },
]; ],
[rules, setRules, state, ws, setRule, name, gameState]
);
setRuleElements( if (!houseRulesActive) {
ruleList.map((item) => { return <></>;
}
return (
<Paper
className="HouseRules"
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
maxHeight: "100dvh",
overflow: "hidden",
}}
>
<Box className="Title">House Rules</Box>
<Box sx={{ display: "flex", flexDirection: "column" }}>
{ruleList.map((item) => {
const defaultChecked = item.defaultChecked; const defaultChecked = item.defaultChecked;
if (!(item.key in rules)) { if (!(item.key in rules)) {
rules[item.key] = { rules[item.key] = {
@ -352,7 +423,9 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
return ( return (
<div key={item.key} className="HouseSelector"> <div key={item.key} className="HouseSelector">
<div><b>{item.label}</b>: {item.description}</div> <div>
<b>{item.label}</b>: {item.description}
</div>
<div> <div>
<Switch <Switch
size={"small"} size={"small"}
@ -360,28 +433,16 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
checked={checked} checked={checked}
id={item.key} id={item.key}
onChange={(e) => setRule(e, item.key)} onChange={(e) => setRule(e, item.key)}
disabled={gameState !== 'lobby' || !name} disabled={gameState !== "lobby" || !name}
/> />
</div> </div>
{checked && item.element} {checked && item.element}
</div> </div>
); );
}) })}
); </Box>
}, [rules, setRules, setRuleElements, state, ws, setRule, name, gameState]);
if (!houseRulesActive) {
return <></>;
}
return (
<div className="HouseRules">
<Paper>
<div className="Title">House Rules</div>
<div style={{ display: "flex", flexDirection: "column" }}>{ruleElements}</div>
<Button onClick={dismissClicked}>Close</Button> <Button onClick={dismissClicked}>Close</Button>
</Paper> </Paper>
</div>
); );
}; };