426 lines
14 KiB
JavaScript
426 lines
14 KiB
JavaScript
import React, { useState, useEffect, useContext, useRef, useMemo, useCallback } from "react";
|
|
import equal from "fast-deep-equal";
|
|
|
|
import Paper from '@material-ui/core/Paper';
|
|
import Button from '@material-ui/core/Button';
|
|
import Switch from '@material-ui/core/Switch';
|
|
|
|
import "./HouseRules.css";
|
|
|
|
import { GlobalContext } from "./GlobalContext.js";
|
|
import { Placard } from "./Placard.js";
|
|
|
|
/* Volcano based on https://www.ultraboardgames.com/catan/the-volcano.php */
|
|
const Volcano = ({ 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((field in rules && 'number' in rules[field]) ? rules[field].number : init);
|
|
const [gold, setGold] =
|
|
useState((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 : false);
|
|
setNumber('number' in rules[field] ? rules[field].number : init);
|
|
let update = false;
|
|
if (!('gold' in rules[field])) {
|
|
rules[field].gold = false;
|
|
update = true;
|
|
}
|
|
if (!('number' in rules[field])) {
|
|
rules[field].number = init;
|
|
update = true;
|
|
}
|
|
|
|
if (update) {
|
|
ws.send(JSON.stringify({
|
|
type: 'rules',
|
|
rules: rules
|
|
}));
|
|
}
|
|
}
|
|
}, [rules, field, init, ws]);
|
|
|
|
const toggleGold = () => {
|
|
rules[field].gold = !gold;
|
|
rules[field].number = number;
|
|
setGold(rules[field].gold);
|
|
|
|
ws.send(JSON.stringify({
|
|
type: 'rules',
|
|
rules: rules
|
|
}));
|
|
};
|
|
|
|
const update = (delta) => {
|
|
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={(e) => 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>;
|
|
}
|
|
|
|
const VictoryPoints = ({ ws, rules, field }) => {
|
|
const minVP = 10;
|
|
const [points, setPoints] = useState(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) => {
|
|
let 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>;
|
|
}
|
|
|
|
const NotImplemented = () => {
|
|
return <div>Not yet implemented.</div>;
|
|
}
|
|
|
|
/*
|
|
The Jungle
|
|
Setup
|
|
On any normally assembled map, replace all deserts with jungles. Select an extra number token with the value of 3, 4, 5, 9, 10, or 11. (All players must agree on which number to use.) Place this number token on the jungle hex. The Robber begins the game on the jungle hex.
|
|
Special rules
|
|
The Robber may be played on the jungle hex when a player has robber control.
|
|
However, when the number on an unblocked jungle tile is rolled, adjacent players may explore the jungle and make discoveries that aid them in developing their principalities. Each adjacent settlement will receive one discovery, while each adjacent city will receive two discoveries.
|
|
Discoveries are represented by Discovery Counters, instead of resource cards. Discovery Counters do not count against a player's hand limit of resources when a 7 is rolled. Discovery Counters can not be stolen by the Robber, can not be claimed by a Monopoly, can not be earned through a Year of Plenty, and may not be used in any trades.
|
|
Discovery Counters may be used to aid the purchase of development cards only. Each Discovery Counter can be used to replace any one of the three resources needed to purchase a card. Up to three Discovery Counters may be used on each card purchase. Any combination of Discovery Counters and the three usual resources may be used. For example, a player may purchase a development card with one ore and two Discovery Counters. Similarly, a development card could be purchased with one wool, one grain, and one Discovery Counter.
|
|
*/
|
|
const HouseRules = ({ houseRulesActive, setHouseRulesActive }) => {
|
|
const { ws } = useContext(GlobalContext);
|
|
const [rules, setRules] = useState(undefined);
|
|
const [state, setState] = useState(undefined);
|
|
const [ruleElements, setRuleElements] = useState([]);
|
|
const fields = useMemo(() => [
|
|
'state', 'rules'
|
|
], []);
|
|
const onWsMessage = (event) => {
|
|
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 !== state) {
|
|
setState(data.update.state);
|
|
}
|
|
if ('rules' in data.update
|
|
&& !equal(data.update.rules, rules)) {
|
|
console.log(`house-rules - setting house rules to `,
|
|
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 => 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((event) => {
|
|
setHouseRulesActive(false);
|
|
}, [setHouseRulesActive]);
|
|
|
|
console.log(`house-rules - render - `, { rules });
|
|
|
|
const setRule = useCallback((event, key) => {
|
|
if (!(key in rules)) {
|
|
rules[key] = { enabled: false };
|
|
}
|
|
rules[key].enabled = !rules[key].enabled;
|
|
console.log(`house-rules - set ${key} - ${rules[key].enabled}`);
|
|
setRules(Object.assign({}, rules));
|
|
ws.send(JSON.stringify({
|
|
type: 'rules',
|
|
rules
|
|
}));
|
|
}, [ws, rules]);
|
|
|
|
useEffect(() => {
|
|
/* https://icebreaker.com/games/catan-1/feature/catan-house-rules */
|
|
setRuleElements([{
|
|
title: `Why you play so slowf`,
|
|
key: `slowest-turn`,
|
|
description: `The player with the longest turn idle time (longer than 2 minutes) so far loses 2VP.`,
|
|
element: <Placard type="longest-turn"/>,
|
|
implemented: false
|
|
}, {
|
|
title: `You are so developed`,
|
|
key: `most-developed`,
|
|
description:
|
|
`The player with the most development cards (more than 4) receives 2VP.`,
|
|
element: <Placard type="most-developed" />,
|
|
implemented: true
|
|
}, {
|
|
title: `Another round of port`,
|
|
key: `port-of-call`,
|
|
description:
|
|
`The player with the most harbor ports (more than 2) receives 2VP.`,
|
|
element: <Placard type="port-of-call" />,
|
|
implemented: true
|
|
}, {
|
|
title: `More victory points`,
|
|
key: `victory-points`,
|
|
description: `Customize how many Victory Points are required to win.`,
|
|
element: <VictoryPoints {...{
|
|
ws, rules,
|
|
field: `victory-points`
|
|
}} />,
|
|
implemented: true
|
|
}, {
|
|
title: `Tiles start facing down`,
|
|
key: `tiles-start-facing-down`,
|
|
description: `Resource tiles start upside-down while placing starting settlements.`,
|
|
element: <div>Once all players have placed their initial settlements
|
|
and roads, the tiles are flipped and you discover what the
|
|
resources are.</div>,
|
|
implemented: true
|
|
}, {
|
|
title: `Bribery`,
|
|
key: `bribery`,
|
|
description: `Dissuade enemies from robbing you by offering resources voluntarily.`,
|
|
element: <NotImplemented {...{
|
|
ws, rules,
|
|
field: `bribery`
|
|
}} />,
|
|
}, {
|
|
title: `King of the Hill`,
|
|
key: `king-of-the-hill`,
|
|
description: `Keep your lead for one full turn after you reach max victory points.`,
|
|
element: <NotImplemented {...{
|
|
ws, rules,
|
|
field: `king-of-the-hill`
|
|
}} />,
|
|
}, {
|
|
title: `Everyone gets one re-roll`,
|
|
key: `everyone-gets-one-reroll`,
|
|
description: `Each player gets one chance re-roll at any point.`,
|
|
element: <NotImplemented {...{
|
|
ws, rules,
|
|
field: `everyone-gets-one-reroll`
|
|
}} />,
|
|
}, {
|
|
title: `The Bridge`,
|
|
key: `the-bridge`,
|
|
description: `Build a super-bridge across one resource tile.`,
|
|
element: <NotImplemented {...{
|
|
ws, rules,
|
|
field: `the-bridge`
|
|
}} />,
|
|
}, {
|
|
title: `Discard desert`,
|
|
key: `discard-desert`,
|
|
description: `Scrap the desert in favour of an additional resource tile.`,
|
|
element: <NotImplemented {...{
|
|
ws, rules,
|
|
field: `discard-desert`
|
|
}} />,
|
|
}, {
|
|
title: `Roll double, roll again`,
|
|
key: `roll-double-roll-again`,
|
|
description: `Roll again if you roll two of the same number.`,
|
|
element: < div>If you roll doubles, players get those resources and
|
|
then you must roll again.</div >,
|
|
implemented: true,
|
|
}, {
|
|
title: `Twelve and Two are synonyms`,
|
|
key: `twelve-and-two-are-synonyms`,
|
|
description: `If twelve is rolled, two scores as well. And vice-versa.`,
|
|
element: < div>If you roll a twelve or two, resources are triggered
|
|
for both.</div >,
|
|
implemented: true,
|
|
}, {
|
|
title: `Robin Hood robber`,
|
|
key: `robin-hood-robber`,
|
|
description: `Robbers can't steal from players with two or less victory points.`,
|
|
element: <></>,
|
|
implemented: true
|
|
}, {
|
|
title: `Crime and Punishment`,
|
|
key: `crime-and-punishment`,
|
|
description: `Change how the robber works to make Catan more or less competitive.`,
|
|
element: <NotImplemented {...{
|
|
ws, rules,
|
|
field: `crime-and-punishment`
|
|
}} />,
|
|
}, {
|
|
title: `Credit? Debt? You bebt!`,
|
|
key: `credit`,
|
|
description: `Trade with resources you don't have.`,
|
|
element: <NotImplemented {...{
|
|
ws, rules,
|
|
field: `credit`
|
|
}} />,
|
|
}, {
|
|
title: `Volcanoes are a lava fun!`,
|
|
key: `volcano`,
|
|
description: `A volcano is on the island! Let the lava flow!`,
|
|
element: <Volcano {
|
|
...{
|
|
ws, rules,
|
|
field: `volcano`,
|
|
disabled: state !== 'lobby'
|
|
}
|
|
} />,
|
|
implemented: true,
|
|
}, {
|
|
title: `Don't keep paying those soldiers!`,
|
|
key: `mercenaries`,
|
|
description: `Disband a soldier and pick two resources to receive as tribute. If you no longer have the Longest Army, you lose it.`,
|
|
element: <NotImplemented {...{
|
|
ws, rules,
|
|
field: `credit`
|
|
}} />,
|
|
} ]
|
|
.filter(item => item.implemented)
|
|
.sort((A, B) => {
|
|
if (A.implemented && B.implemented) {
|
|
return A.title.localeCompare();
|
|
}
|
|
if (A.implemented) {
|
|
return -1;
|
|
}
|
|
if (B.implemented) {
|
|
return +1;
|
|
}
|
|
return A.title.localeCompare();
|
|
})
|
|
.map(item => {
|
|
const disabled = (state !== 'lobby' || !item.implemented),
|
|
defaultChecked = rules
|
|
&& (item.key in rules)
|
|
? rules[item.key].enabled
|
|
: false;
|
|
console.log(`house-rules - ${item.key} - `,
|
|
{ rules, defaultChecked, disabled });
|
|
return <div className="HouseRule"
|
|
data-enabled={
|
|
rules
|
|
&& item.key in rules
|
|
&& rules[item.key].enabled}
|
|
data-disabled={disabled}
|
|
key={item.key}
|
|
data-key={item.key}>
|
|
<div className="HouseSelector">
|
|
<div><b>{item.title}</b>: {item.description}</div>
|
|
<Switch
|
|
size={'small'}
|
|
className="RuleSwitch"
|
|
checked={defaultChecked}
|
|
id={item.key}
|
|
onChange={(e) => setRule(e, item.key)}
|
|
{...{ disabled }} />
|
|
</div>
|
|
{ defaultChecked && item.element }
|
|
</div>
|
|
}));
|
|
}, [rules, setRules, setRuleElements, state, ws, setRule ]);
|
|
|
|
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>
|
|
</Paper>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export { HouseRules }; |