449 lines
13 KiB
TypeScript
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'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> /
|
|
<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'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> /
|
|
<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 };
|