1
0
peddlers-of-ketran/client/src/HouseRules.tsx

449 lines
13 KiB
TypeScript

import React, {
useState,
useEffect,
useContext,
useRef,
useMemo,
useCallback,
} from "react";
import equal from "fast-deep-equal";
import Paper from "@mui/material/Paper";
import Button from "@mui/material/Button";
import Switch from "@mui/material/Switch";
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
// import "./HouseRules.css";
import { GlobalContext } from "./GlobalContext";
import { Placard } from "./Placard";
import { Box } from "@mui/material";
/* eslint-disable @typescript-eslint/no-explicit-any */
interface VolcanoProps {
ws: WebSocket | null;
rules: any;
field: string;
disabled: boolean;
}
/* Volcano based on https://www.ultraboardgames.com/catan/the-volcano.php */
const Volcano: React.FC<VolcanoProps> = ({ ws, rules, field, disabled }) => {
const init =
Math.random() > 0.5
? Math.floor(8 + 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 [gold, setGold] = useState<boolean>(
field in rules && "gold" in rules[field] ? rules[field].gold : false
);
console.log(`house-rules - ${field} - `, rules[field]);
useEffect(() => {
if (field in rules) {
setGold("gold" in rules[field] ? rules[field].gold : true);
setNumber("number" in rules[field] ? rules[field].number : init);
let update = false;
if (!("gold" in rules[field])) {
rules[field].gold = true;
update = true;
}
if (!("number" in rules[field])) {
rules[field].number = init;
update = true;
}
if (update && ws) {
ws.send(
JSON.stringify({
type: "rules",
rules: rules,
})
);
}
}
}, [rules, field, init, ws]);
const toggleGold = () => {
if (!ws) return;
rules[field].gold = !gold;
rules[field].number = number;
setGold(rules[field].gold);
ws.send(
JSON.stringify({
type: "rules",
rules: rules,
})
);
};
const update = (delta: number) => {
if (!ws) return;
let value = number + delta;
if (value < 2 || value > 12) {
return;
}
/* Number to trigger Volcano cannot be 7 */
if (value === 7) {
value = delta > 0 ? 8 : 6;
}
setNumber(value);
rules[field].gold = gold;
rules[field].number = value;
ws.send(
JSON.stringify({
type: "rules",
rules: rules,
})
);
};
return (
<div className="Volcano">
<div>
The Volcano replaces the Desert. When the Volcano erupts, roll a die to
determine the direction the lava will flow. One of the six intersections
on the Volcano tile will be affected. If there is a settlement on the
selected intersection, it is destroyed!
</div>
<div>
Remove it from the board (its owner may rebuild it later). If a city is
located there, it is reduced to a settlement! Replace the city with a
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>
Roll {number} and the Volcano erupts!
<button onClick={() => update(+1)}>up</button>&nbsp;/&nbsp;
<button onClick={() => update(-1)}> down</button>
</div>
<div className="HouseSelector">
<div>
<b>Volcanoes have gold!</b>: Volcano can produce resources when its
number is rolled.
</div>
<div>
<Switch
size={"small"}
className="RuleSwitch"
checked={gold}
onChange={() => toggleGold()}
{...{ disabled }}
/>
</div>
</div>
<div>
Volcanoes tend to be rich in valuable minerals such as gold or gems.
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>
Each city adjacent to the Volcano may produce any two resources. This
resource production is taken before the results of the volcano eruption
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>
);
};
interface VictoryPointsProps {
ws: WebSocket | null;
rules: any;
field: string;
}
const VictoryPoints: React.FC<VictoryPointsProps> = ({ ws, rules, field }) => {
const minVP = 10;
const [points, setPoints] = useState<number>(rules[field].points || minVP);
console.log(`house-rules - ${field} - `, rules[field]);
if (!(field in rules)) {
rules[field] = {
points: minVP,
};
}
if (rules[field].points && rules[field].points !== points) {
setPoints(rules[field].points);
}
const update = (value: number) => {
if (!ws) return;
const points = (rules[field].points || minVP) + value;
if (points < minVP) {
return;
}
if (points !== rules[field].points) {
setPoints(points);
rules[field].points = points;
ws.send(
JSON.stringify({
type: "rules",
rules: rules,
})
);
}
};
return (
<div className="VictoryPoints">
{points} points.
<button onClick={() => update(+1)}>up</button>&nbsp;/&nbsp;
<button onClick={() => update(-1)}> down</button>
</div>
);
};
interface HouseRulesProps {
houseRulesActive: boolean;
setHouseRulesActive: React.Dispatch<React.SetStateAction<boolean>>;
}
const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive }) => {
const { ws, name } = useContext(GlobalContext);
const [rules, setRules] = useState<any>({});
const [state, setState] = useState<any>({});
const [gameState, setGameState] = useState<string>("");
const fields = useMemo(() => ["state", "rules"], []);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
switch (data.type) {
case "game-update":
console.log(`house-rules - game-update: `, data.update);
if ("state" in data.update && data.update.state !== gameState) {
setGameState(data.update.state);
}
if ("rules" in data.update && !equal(rules, data.update.rules)) {
setRules(data.update.rules);
}
break;
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
useEffect(() => {
if (!ws) {
return;
}
ws.send(
JSON.stringify({
type: "get",
fields,
})
);
}, [ws, fields]);
const dismissClicked = useCallback(() => {
if (!ws) return;
ws.send(
JSON.stringify({
type: "house-rules",
active: false,
})
);
}, [ws]);
const setRule = useCallback(
(event: React.ChangeEvent<HTMLInputElement>, key: string) => {
if (!ws) return;
const checked = event.target.checked;
console.log(`house-rules - set rule ${key} to ${checked}`);
rules[key].enabled = checked;
setRules({ ...rules });
ws.send(
JSON.stringify({
type: "rules",
rules: rules,
})
);
},
[rules, ws]
);
const ruleList = useMemo(
() => [
{
key: "volcano",
label: "Volcanoes are a lava fun!",
description: "A volcano is on the island! Let the lava flow!",
category: "board",
defaultChecked: false,
element: (
<Volcano
ws={ws}
rules={rules}
field={"volcano"}
disabled={gameState !== "lobby"}
/>
),
},
{
key: "victory-points",
label: "More victory points",
description: "Customize how many Victory Points are required to win.",
category: "rules",
defaultChecked: false,
element: (
<VictoryPoints ws={ws} rules={rules} field={"victory-points"} />
),
},
{
key: "tiles-start-facing-down",
label: "Tiles start facing down",
description:
"Resource tiles start upside-down while placing starting settlements.",
category: "board",
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>
),
},
{
key: "most-developed",
label: "You are so developed",
description:
"The player with the most development cards (more than 4) receives 2VP.",
category: "expansion",
defaultChecked: false,
element: <Placard type="most-developed" />,
},
{
key: "port-of-call",
label: "Another round of port",
description:
"The player with the most harbor ports (more than 2) receives 2VP.",
category: "expansion",
defaultChecked: false,
element: <Placard type="port-of-call" />,
},
{
key: "slowest-turn",
label: "Why you play so slowf",
description:
"The player with the longest turn idle time (longer than 2 minutes) so far loses 2VP.",
category: "expansion",
defaultChecked: false,
element: <Placard type="longest-turn" />,
},
{
key: "roll-double-roll-again",
label: "Roll double, roll again",
description: "Roll again if you roll two of the same number.",
category: "rolling",
defaultChecked: false,
element: (
<div>
If you roll doubles, players get those resources and then you must
roll again.
</div>
),
},
{
key: "twelve-and-two-are-synonyms",
label: "Twelve and Two are synonyms",
description: "If twelve is rolled, two scores as well. And vice-versa.",
category: "rolling",
defaultChecked: false,
element: (
<div>
If you roll a twelve or two, resources are triggered for both.
</div>
),
},
{
key: "robin-hood-robber",
label: "Robin Hood robber",
description:
"Robbers can't steal from players with two or less victory points.",
category: "rules",
defaultChecked: false,
element: <></>,
},
],
[rules, setRules, state, ws, setRule, name, gameState]
);
if (!houseRulesActive) {
return <></>;
}
return (
<Paper sx={{ p: 1, maxWidth: 600, margin: "1rem auto", flexDirection: "column", display: "flex" }} className="HouseRules" elevation={3}>
<DialogTitle>House Rules</DialogTitle>
<DialogContent>
<Box sx={{ display: "flex", flexDirection: "column" }}>
{ruleList.map((item) => {
const defaultChecked = item.defaultChecked;
if (!(item.key in rules)) {
rules[item.key] = {
enabled: defaultChecked,
};
}
const checked = rules[item.key].enabled;
if (checked !== state[item.key]) {
setState({ ...state, [item.key]: checked });
}
return (
<div key={item.key} className="HouseSelector">
<div>
<b>{item.label}</b>: {item.description}
</div>
<div>
<Switch
size={"small"}
className="RuleSwitch"
checked={checked}
id={item.key}
onChange={(e) => setRule(e, item.key)}
disabled={gameState !== "lobby" || !name}
/>
</div>
{checked && item.element}
</div>
);
})}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={dismissClicked}>Close</Button>
</DialogActions>
</Paper>
);
};
export { HouseRules };