1015 lines
29 KiB
TypeScript
1015 lines
29 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment */
|
|
// @ts-nocheck
|
|
import React, { useEffect, useState, useContext, useRef, useMemo } from "react";
|
|
import equal from "fast-deep-equal";
|
|
import { assetsPath } from "./Common";
|
|
import "./Board.css";
|
|
import { GlobalContext } from "./GlobalContext";
|
|
import { Flock } from "./Bird";
|
|
import { Herd } from "./Sheep";
|
|
|
|
type BoardProps = {
|
|
animations: boolean;
|
|
};
|
|
|
|
type CornerData = {
|
|
index: number;
|
|
x: number;
|
|
y: number;
|
|
// Add other properties as needed
|
|
};
|
|
|
|
type RoadData = {
|
|
index: number;
|
|
x: number;
|
|
y: number;
|
|
// Add other properties as needed
|
|
};
|
|
|
|
type PipData = {
|
|
order: number;
|
|
x: number;
|
|
y: number;
|
|
// Add other properties as needed
|
|
};
|
|
|
|
type TileData = {
|
|
order: number;
|
|
x: number;
|
|
y: number;
|
|
// Add other properties as needed
|
|
};
|
|
|
|
type TurnData = {
|
|
// Define based on usage
|
|
};
|
|
|
|
type RulesData = {
|
|
// Define based on usage
|
|
};
|
|
|
|
type PlacementData = Record<string, unknown>;
|
|
|
|
type CornerProps = {
|
|
corner: CornerData;
|
|
};
|
|
|
|
type RoadProps = {
|
|
road: RoadData;
|
|
};
|
|
|
|
type PipProps = {
|
|
pip: PipData;
|
|
className?: string;
|
|
};
|
|
|
|
type TileProps = {
|
|
tile: TileData;
|
|
};
|
|
|
|
const rows = [3, 4, 5, 4, 3, 2]; /* The final row of 2 is to place roads and corners */
|
|
|
|
const hexRatio = 1.1547, // eslint-disable-line @typescript-eslint/no-loss-of-precision
|
|
tileWidth = 67,
|
|
tileHalfWidth = tileWidth * 0.5,
|
|
tileHeight = tileWidth * hexRatio,
|
|
tileHalfHeight = tileHeight * 0.5,
|
|
radius = tileHeight * 2,
|
|
borderOffsetX = 86 /* ~1/10th border image width... hand tuned */,
|
|
borderOffsetY = 3;
|
|
|
|
/* Actual sizing */
|
|
const tileImageWidth = 90 /* Based on hand tuned and image width */,
|
|
tileImageHeight = tileImageWidth / hexRatio,
|
|
borderImageWidth = (2 + 2 / 3) * tileImageWidth /* 2.667 * .Tile.width */,
|
|
borderImageHeight = borderImageWidth * 0.29; /* 0.29 * .Border.height */
|
|
|
|
const showTooltip = () => {
|
|
const tooltip = document.querySelector(".Board .Tooltip") as HTMLElement;
|
|
if (tooltip) tooltip.style.display = "flex";
|
|
};
|
|
|
|
const clearTooltip = () => {
|
|
const tooltip = document.querySelector(".Board .Tooltip") as HTMLElement;
|
|
if (tooltip) tooltip.style.display = "none";
|
|
};
|
|
|
|
const Board: React.FC<BoardProps> = ({ animations }) => {
|
|
const { ws, sendJsonMessage } = useContext(GlobalContext);
|
|
const board = useRef();
|
|
const [transform, setTransform] = useState(1);
|
|
const [pipElements, setPipElements] = useState<React.ReactElement[]>([]);
|
|
const [borderElements, setBorderElements] = useState<React.ReactElement[]>([]);
|
|
const [tileElements, setTileElements] = useState<React.ReactElement[]>([]);
|
|
const [cornerElements, setCornerElements] = useState<React.ReactElement[]>([]);
|
|
const [roadElements, setRoadElements] = useState<React.ReactElement[]>([]);
|
|
const [signature, setSignature] = useState<string>("");
|
|
const [generated, setGenerated] = useState<string>("");
|
|
const [robber, setRobber] = useState<number>(-1);
|
|
const [robberName, setRobberName] = useState<string[]>([]);
|
|
const [pips, setPips] = useState<any>(undefined); // Keep as any for now, complex structure
|
|
const [pipOrder, setPipOrder] = useState<any>(undefined);
|
|
const [borders, setBorders] = useState<any>(undefined);
|
|
const [borderOrder, setBorderOrder] = useState<any>(undefined);
|
|
const [animationSeeds, setAnimationSeeds] = useState<any>(undefined);
|
|
const [tiles, setTiles] = useState<any>(undefined);
|
|
const [tileOrder, setTileOrder] = useState<number[]>([]);
|
|
const [placements, setPlacements] = useState<PlacementData | undefined>(undefined);
|
|
const [turn, setTurn] = useState<TurnData>({});
|
|
const [state, setState] = useState<string>("");
|
|
const [color, setColor] = useState<string>("");
|
|
const [rules, setRules] = useState<RulesData>({});
|
|
const [longestRoadLength, setLongestRoadLength] = useState<number>(0);
|
|
const fields = useMemo(
|
|
() => [
|
|
"signature",
|
|
"robber",
|
|
"robberName",
|
|
"pips",
|
|
"pipOrder",
|
|
"borders",
|
|
"borderOrder",
|
|
"tiles",
|
|
"tileOrder",
|
|
"placements",
|
|
"turn",
|
|
"state",
|
|
"color",
|
|
"longestRoadLength",
|
|
"rules",
|
|
"animationSeeds",
|
|
],
|
|
[]
|
|
);
|
|
|
|
const onWsMessage = (event) => {
|
|
if (ws && ws !== event.target) {
|
|
console.error(`Disconnect occur?`);
|
|
}
|
|
const data = JSON.parse(event.data);
|
|
switch (data.type) {
|
|
case "game-update":
|
|
console.log(`board - game update`, data.update);
|
|
if ("robber" in data.update && data.update.robber !== robber) {
|
|
setRobber(data.update.robber);
|
|
}
|
|
|
|
if ("robberName" in data.update && data.update.robberName !== robberName) {
|
|
setRobberName(data.update.robberName);
|
|
}
|
|
|
|
if ("state" in data.update && data.update.state !== state) {
|
|
setState(data.update.state);
|
|
}
|
|
|
|
if ("rules" in data.update && !equal(data.update.rules, rules)) {
|
|
setRules(data.update.rules);
|
|
}
|
|
|
|
if ("color" in data.update && data.update.color !== color) {
|
|
setColor(data.update.color);
|
|
}
|
|
|
|
if ("longestRoadLength" in data.update && data.update.longestRoadLength !== longestRoadLength) {
|
|
setLongestRoadLength(data.update.longestRoadLength);
|
|
}
|
|
|
|
if ("turn" in data.update) {
|
|
if (!equal(data.update.turn, turn)) {
|
|
console.log(`board - turn`, data.update.turn);
|
|
setTurn(data.update.turn);
|
|
}
|
|
}
|
|
|
|
if ("placements" in data.update && !equal(data.update.placements, placements)) {
|
|
console.log(`board - placements`, data.update.placements);
|
|
setPlacements(data.update.placements);
|
|
}
|
|
|
|
/* The following are only updated if there is a new game
|
|
* signature */
|
|
|
|
if ("pipOrder" in data.update && !equal(data.update.pipOrder, pipOrder)) {
|
|
console.log(`board - setting new pipOrder`);
|
|
setPipOrder(data.update.pipOrder);
|
|
}
|
|
|
|
if ("borderOrder" in data.update && !equal(data.update.borderOrder, borderOrder)) {
|
|
console.log(`board - setting new borderOrder`);
|
|
setBorderOrder(data.update.borderOrder);
|
|
}
|
|
|
|
if ("animationSeeds" in data.update && !equal(data.update.animationSeeds, animationSeeds)) {
|
|
console.log(`board - setting new animationSeeds`);
|
|
setAnimationSeeds(data.update.animationSeeds);
|
|
}
|
|
|
|
if ("tileOrder" in data.update && !equal(data.update.tileOrder, tileOrder)) {
|
|
console.log(`board - setting new tileOrder`);
|
|
setTileOrder(data.update.tileOrder);
|
|
}
|
|
|
|
if (data.update.signature !== signature) {
|
|
console.log(`board - setting new signature`);
|
|
setSignature(data.update.signature);
|
|
}
|
|
|
|
/* This is permanent static data from the server -- do not update
|
|
* once set */
|
|
if ("pips" in data.update && !pips) {
|
|
console.log(`board - setting new static pips`);
|
|
setPips(data.update.pips);
|
|
}
|
|
if ("tiles" in data.update && !tiles) {
|
|
console.log(`board - setting new static tiles`);
|
|
setTiles(data.update.tiles);
|
|
}
|
|
if ("borders" in data.update && !borders) {
|
|
console.log(`board - setting new static borders`);
|
|
setBorders(data.update.borders);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
const refWsMessage = useRef(onWsMessage);
|
|
useEffect(() => {
|
|
refWsMessage.current = onWsMessage;
|
|
});
|
|
useEffect(() => {
|
|
if (!ws) {
|
|
return;
|
|
}
|
|
console.log("board - bind");
|
|
const cbMessage = (e) => refWsMessage.current(e);
|
|
ws.addEventListener("message", cbMessage);
|
|
return () => {
|
|
console.log("board - unbind");
|
|
ws.removeEventListener("message", cbMessage);
|
|
};
|
|
}, [ws]);
|
|
useEffect(() => {
|
|
if (!sendJsonMessage) {
|
|
return;
|
|
}
|
|
sendJsonMessage({
|
|
type: "get",
|
|
fields,
|
|
});
|
|
}, [sendJsonMessage, fields]);
|
|
|
|
useEffect(() => {
|
|
const boardBox = board.current.querySelector(".BoardBox");
|
|
if (boardBox) {
|
|
console.log(`board - setting transform scale to ${transform}`);
|
|
boardBox.style.transform = `scale(${transform})`;
|
|
}
|
|
}, [transform]);
|
|
|
|
const onResize = () => {
|
|
if (!board.current) {
|
|
return;
|
|
}
|
|
|
|
/* Adjust the 'transform: scale' for the BoardBox
|
|
* so the board fills the Board
|
|
*
|
|
* The board is H tall, and H * hexRatio wide */
|
|
const width = board.current.offsetWidth,
|
|
height = board.current.offsetHeight;
|
|
let _transform;
|
|
if (height * hexRatio > width) {
|
|
_transform = width / (450 * hexRatio); // eslint-disable-line @typescript-eslint/no-loss-of-precision
|
|
} else {
|
|
_transform = height / 450; // eslint-disable-line @typescript-eslint/no-loss-of-precision
|
|
}
|
|
|
|
if (transform !== _transform) {
|
|
setTransform(_transform);
|
|
}
|
|
};
|
|
|
|
const refOnResize = useRef(onResize);
|
|
useEffect(() => {
|
|
refOnResize.current = onResize;
|
|
});
|
|
useEffect(() => {
|
|
const cbOnResize = (e) => refOnResize.current(e);
|
|
window.addEventListener("resize", cbOnResize);
|
|
return () => {
|
|
window.removeEventListener("resize", cbOnResize);
|
|
};
|
|
}, [refOnResize]);
|
|
|
|
onResize();
|
|
|
|
useEffect(() => {
|
|
if (!ws) {
|
|
return;
|
|
}
|
|
|
|
console.log(`Generating static corner data... should only occur once per reload or socket reconnect.`);
|
|
const onCornerClicked = (event, corner) => {
|
|
let type;
|
|
if (event.currentTarget.getAttribute("data-type") === "settlement") {
|
|
type = "place-city";
|
|
} else {
|
|
type = "place-settlement";
|
|
}
|
|
ws.send(
|
|
JSON.stringify({
|
|
type,
|
|
index: corner.index,
|
|
})
|
|
);
|
|
};
|
|
const Corner: React.FC<CornerProps> = ({ corner }) => {
|
|
return (
|
|
<div
|
|
className="Corner"
|
|
onMouseMove={(e) => {
|
|
if (e.shiftPressed) {
|
|
const tooltip = document.querySelector(".Board .Tooltip") as HTMLElement;
|
|
tooltip.innerHTML = `<pre>${corner}</pre>`;
|
|
showTooltip();
|
|
}
|
|
}}
|
|
onClick={(event) => {
|
|
onCornerClicked(event, corner);
|
|
}}
|
|
data-index={corner.index}
|
|
style={{
|
|
top: `${corner.top}px`,
|
|
left: `${corner.left}px`,
|
|
}}
|
|
>
|
|
<div className="Corner-Shape" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const generateCorners = () => {
|
|
let row = 0,
|
|
rowCount = 0;
|
|
let y = -8 + 0.5 * tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth,
|
|
x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight;
|
|
let index = 0;
|
|
const corners = [];
|
|
let corner;
|
|
|
|
for (let i = 0; i < 21; i++) {
|
|
if (row > 2 && rowCount === 0) {
|
|
corner = {
|
|
index: index++,
|
|
top: y - 0.5 * tileHalfHeight,
|
|
left: x - tileHalfHeight,
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner} />);
|
|
}
|
|
|
|
corner = {
|
|
index: index++,
|
|
top: y,
|
|
left: x,
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner} />);
|
|
|
|
corner = {
|
|
index: index++,
|
|
top: y - 0.5 * tileHalfHeight,
|
|
left: x + tileHalfHeight,
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner} />);
|
|
|
|
if (++rowCount === rows[row]) {
|
|
corner = {
|
|
index: index++,
|
|
top: y,
|
|
left: x + 2 * tileHalfHeight,
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner} />);
|
|
|
|
if (row > 2) {
|
|
corner = {
|
|
index: index++,
|
|
top: y - 0.5 * tileHalfHeight,
|
|
left: x + 3 * tileHalfHeight,
|
|
};
|
|
corners.push(<Corner key={`corner-${index}}`} corner={corner} />);
|
|
}
|
|
|
|
row++;
|
|
rowCount = 0;
|
|
y += tileHeight - 10.5;
|
|
x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight;
|
|
} else {
|
|
x += tileHeight;
|
|
}
|
|
}
|
|
return corners;
|
|
};
|
|
|
|
setCornerElements(generateCorners());
|
|
}, [ws, setCornerElements]);
|
|
|
|
useEffect(() => {
|
|
if (!ws) {
|
|
return;
|
|
}
|
|
|
|
console.log(`Generating static road data... should only occur once per reload or socket reconnect.`);
|
|
const Road: React.FC<RoadProps> = ({ road }) => {
|
|
const onRoadClicked = (road) => {
|
|
console.log(`Road clicked: ${road.index}`);
|
|
if (!ws) {
|
|
console.error(`board - onRoadClicked - ws is NULL`);
|
|
return;
|
|
}
|
|
ws.send(
|
|
JSON.stringify({
|
|
type: "place-road",
|
|
index: road.index,
|
|
})
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="Road"
|
|
onClick={() => {
|
|
onRoadClicked(road);
|
|
}}
|
|
data-index={road.index}
|
|
style={{
|
|
transform: `translate(-50%, -50%) rotate(${road.angle}deg)`,
|
|
top: `${road.top}px`,
|
|
left: `${road.left}px`,
|
|
}}
|
|
>
|
|
<div className="Road-Shape" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const generateRoads = () => {
|
|
let row = 0,
|
|
rowCount = 0;
|
|
let y = -2.5 + tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth,
|
|
x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight;
|
|
|
|
let index = 0;
|
|
let road;
|
|
|
|
const corners = [];
|
|
|
|
for (let i = 0; i < 21; i++) {
|
|
const lastRow = row === rows.length - 1;
|
|
if (row > 2 && rowCount === 0) {
|
|
road = {
|
|
index: index++,
|
|
angle: -60,
|
|
top: y - 0.5 * tileHalfHeight,
|
|
left: x - tileHalfHeight,
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road} />);
|
|
}
|
|
|
|
road = {
|
|
index: index++,
|
|
angle: 240,
|
|
top: y,
|
|
left: x,
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road} />);
|
|
|
|
road = {
|
|
index: index++,
|
|
angle: -60,
|
|
top: y - 0.5 * tileHalfHeight,
|
|
left: x + tileHalfHeight,
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road} />);
|
|
|
|
if (!lastRow) {
|
|
road = {
|
|
index: index++,
|
|
angle: 0,
|
|
top: y,
|
|
left: x,
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road} />);
|
|
}
|
|
|
|
if (++rowCount === rows[row]) {
|
|
if (!lastRow) {
|
|
road = {
|
|
index: index++,
|
|
angle: 0,
|
|
top: y,
|
|
left: x + 2 * tileHalfHeight,
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road} />);
|
|
}
|
|
|
|
if (row > 2) {
|
|
road = {
|
|
index: index++,
|
|
angle: 60,
|
|
top: y - 0.5 * tileHalfHeight,
|
|
left: x + 3 * tileHalfHeight,
|
|
};
|
|
corners.push(<Road key={`road-${index}}`} road={road} />);
|
|
}
|
|
|
|
row++;
|
|
rowCount = 0;
|
|
y += tileHeight - 10.5;
|
|
x = -tileHalfHeight - (rows[row] - 1) * 0.5 * tileHeight;
|
|
} else {
|
|
x += tileHeight;
|
|
}
|
|
}
|
|
return corners;
|
|
};
|
|
setRoadElements(generateRoads());
|
|
}, [ws, setRoadElements]);
|
|
|
|
/* Generate Pip, Tile, and Border elements */
|
|
useEffect(() => {
|
|
if (!ws) {
|
|
return;
|
|
}
|
|
console.log(`board - Generate pip, border, and tile elements`);
|
|
const Pip: React.FC<PipProps> = ({ pip, className }) => {
|
|
const onPipClicked = (pip) => {
|
|
if (!ws) {
|
|
console.error(`board - sendPlacement - ws is NULL`);
|
|
return;
|
|
}
|
|
ws.send(
|
|
JSON.stringify({
|
|
type: "place-robber",
|
|
index: pip.index,
|
|
})
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`Pip ${className}`}
|
|
onClick={() => {
|
|
onPipClicked(pip);
|
|
}}
|
|
data-roll={pip.roll}
|
|
data-index={pip.index}
|
|
style={{
|
|
top: `${pip.top}px`,
|
|
left: `${pip.left}px`,
|
|
backgroundImage: `url(${assetsPath}/gfx/pip-numbers.png)`,
|
|
backgroundPositionX: `${(100 * (pip.order % 6)) / 5}%`, // eslint-disable-line @typescript-eslint/no-loss-of-precision
|
|
backgroundPositionY: `${(100 * Math.floor(pip.order / 6)) / 5}%`,
|
|
}}
|
|
>
|
|
<div className="Pip-Shape" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const generatePips = function (pipOrder) {
|
|
let row = 0,
|
|
rowCount = 0;
|
|
let y = tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth,
|
|
x = -(rows[row] - 1) * 0.5 * tileHeight;
|
|
let index = 0;
|
|
let pip;
|
|
return pipOrder.map((order) => {
|
|
let volcano = false;
|
|
pip = {
|
|
roll: pips[order].roll,
|
|
index: index++,
|
|
top: y,
|
|
left: x,
|
|
order: order,
|
|
};
|
|
if ("volcano" in rules && rules[`volcano`].enabled && pip.roll === 7) {
|
|
pip.order = pips.findIndex((pip) => pip.roll === rules[`volcano`].number);
|
|
pip.roll = rules[`volcano`].number;
|
|
volcano = true;
|
|
}
|
|
const div = <Pip className={volcano ? "Volcano" : ""} pip={pip} key={`pip-${order}`} />;
|
|
|
|
if (++rowCount === rows[row]) {
|
|
row++;
|
|
rowCount = 0;
|
|
y += tileWidth;
|
|
x = -(rows[row] - 1) * 0.5 * tileHeight;
|
|
} else {
|
|
x += tileHeight;
|
|
}
|
|
|
|
return div;
|
|
});
|
|
};
|
|
|
|
const Tile: React.FC<TileProps> = ({ tile }) => {
|
|
const style: React.CSSProperties = {
|
|
top: `${tile.top}px`,
|
|
left: `${tile.left}px`,
|
|
width: `${tileImageWidth}px`,
|
|
height: `${tileImageHeight}px`,
|
|
backgroundImage: `url(${assetsPath}/gfx/tiles-${tile.type}.png)`,
|
|
backgroundPositionY: `-${tile.card * tileHeight}px`,
|
|
};
|
|
|
|
if (tile.type === "volcano") {
|
|
style.transform = `rotate(-90deg)`;
|
|
style.top = `${tile.top + 6}px`;
|
|
style.transformOrigin = "0% 50%";
|
|
}
|
|
|
|
return (
|
|
<div className="Tile" data-index={tile.index} style={{ ...style }}>
|
|
<div className="Tile-Shape" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const generateTiles = function (tileOrder, animationSeeds) {
|
|
let row = 0,
|
|
rowCount = 0;
|
|
let y = tileHalfWidth - (rows.length - 1) * 0.5 * tileWidth,
|
|
x = -(rows[row] - 1) * 0.5 * tileHeight;
|
|
let index = 0;
|
|
return tileOrder.map((order) => {
|
|
const tile = Object.assign({}, tiles[order], { index: index++, left: x, top: y });
|
|
const volcanoActive = "volcano" in rules && rules[`volcano`].enabled;
|
|
|
|
if (
|
|
"tiles-start-facing-down" in rules &&
|
|
rules[`tiles-start-facing-down`].enabled &&
|
|
state !== "normal" &&
|
|
state !== "volcano" &&
|
|
state !== "winner" &&
|
|
(!volcanoActive || tile.type !== "desert")
|
|
) {
|
|
tile.type = "jungle";
|
|
tile.card = 0;
|
|
}
|
|
if (volcanoActive && tile.type === "desert") {
|
|
tile.type = "volcano";
|
|
tile.card = 0;
|
|
}
|
|
let div;
|
|
|
|
if (tile.type === "wheat") {
|
|
div = (
|
|
<div key={`tile-${order}`}>
|
|
{animations && (
|
|
<Flock
|
|
count={Math.floor(1 + animationSeeds[index] * 2)}
|
|
style={{
|
|
top: `${tile.top - tileImageHeight * 0.5}px`,
|
|
left: `${tile.left - tileImageWidth * 0.5}px`,
|
|
width: `${tileImageWidth}px`,
|
|
height: `${tileImageHeight}px`,
|
|
}}
|
|
/>
|
|
)}{" "}
|
|
<Tile tile={tile} />
|
|
</div>
|
|
);
|
|
} else if (tile.type === "sheep") {
|
|
div = (
|
|
<div key={`tile-${order}`}>
|
|
{animations && (
|
|
<Herd
|
|
count={Math.floor(1 + animationSeeds[index] * 4)}
|
|
style={{
|
|
top: `${tile.top - tileImageHeight * 0.5}px`,
|
|
left: `${tile.left - tileImageWidth * 0.5}px`,
|
|
width: `${tileImageWidth}px`,
|
|
height: `${tileImageHeight}px`,
|
|
}}
|
|
/>
|
|
)}
|
|
<Tile tile={tile} />
|
|
</div>
|
|
);
|
|
} else {
|
|
div = <Tile key={`tile-${order}`} tile={tile} />;
|
|
}
|
|
|
|
if (++rowCount === rows[row]) {
|
|
row++;
|
|
rowCount = 0;
|
|
y += tileWidth;
|
|
x = -(rows[row] - 1) * 0.5 * tileHeight;
|
|
} else {
|
|
x += tileHeight;
|
|
}
|
|
return div;
|
|
});
|
|
};
|
|
|
|
const calculateBorderSlot = (side, e) => {
|
|
const borderBox = document.querySelector(".Borders").getBoundingClientRect();
|
|
let angle =
|
|
((360 + Math.floor(90 + (Math.atan2(e.pageY - borderBox.top, e.pageX - borderBox.left) * 180) / Math.PI)) %
|
|
360) -
|
|
side * 60;
|
|
if (angle > 180) {
|
|
angle = angle - 360;
|
|
}
|
|
let slot = 0;
|
|
if (angle > -20 && angle < 5) {
|
|
slot = 1;
|
|
} else if (angle > 5) {
|
|
slot = 2;
|
|
}
|
|
return slot;
|
|
};
|
|
|
|
const mouseEnter = (border, side, e) => {
|
|
const slot = calculateBorderSlot(side, e);
|
|
if (!border[slot]) {
|
|
clearTooltip();
|
|
return;
|
|
}
|
|
const tooltip = document.querySelector(".Board .Tooltip") as HTMLElement;
|
|
tooltip.textContent =
|
|
border[slot] === "bank" ? "3 of one kind for 1 resource" : `2 ${border[slot]} for 1 resource`;
|
|
tooltip.style.top = `${e.pageY}px`;
|
|
tooltip.style.left = `${e.pageX + 16}px`;
|
|
showTooltip();
|
|
};
|
|
|
|
const mouseMove = (border, side, e) => {
|
|
const slot = calculateBorderSlot(side, e);
|
|
if (!border[slot]) {
|
|
clearTooltip();
|
|
return;
|
|
}
|
|
const tooltip = document.querySelector(".Board .Tooltip") as HTMLElement;
|
|
tooltip.textContent =
|
|
border[slot] === "bank" ? "3 of one kind for 1 resource" : `2 ${border[slot]} for 1 resource`;
|
|
tooltip.style.top = `${e.pageY}px`;
|
|
tooltip.style.left = `${e.pageX + 16}px`;
|
|
showTooltip();
|
|
};
|
|
|
|
const mouseLeave = () => {
|
|
clearTooltip();
|
|
};
|
|
|
|
const generateBorders = function (borderOrder) {
|
|
const sides = 6;
|
|
let side = -1;
|
|
return borderOrder.map((order) => {
|
|
const border = borders[order];
|
|
side++;
|
|
const x = +Math.sin(Math.PI - (side / sides) * 2 * Math.PI) * radius,
|
|
y = Math.cos(Math.PI - (side / sides) * 2 * Math.PI) * radius;
|
|
const prev = order === 0 ? 6 : order;
|
|
const file = `borders-${order + 1}.${prev}.png`;
|
|
const value = side;
|
|
return (
|
|
<div
|
|
key={`border-${order}`}
|
|
className="Border"
|
|
border={border}
|
|
onMouseEnter={(e) => {
|
|
mouseEnter(border, value, e);
|
|
}}
|
|
onMouseMove={(e) => {
|
|
mouseMove(border, value, e);
|
|
}}
|
|
onMouseLeave={mouseLeave}
|
|
style={{
|
|
width: `${borderImageWidth}px`,
|
|
height: `${borderImageHeight}px`,
|
|
top: `${y}px`,
|
|
left: `${x}px`,
|
|
transform: `rotate(${
|
|
side * (360 / sides)
|
|
}deg) translate(${borderOffsetX}px, ${borderOffsetY}px) scale(-1, -1)`,
|
|
backgroundImage: `url(${assetsPath}/gfx/${file} )`,
|
|
}}
|
|
/>
|
|
);
|
|
});
|
|
};
|
|
|
|
if (borders && borderOrder) {
|
|
console.log(`board - Generate board - borders`);
|
|
setBorderElements(generateBorders(borderOrder));
|
|
}
|
|
|
|
if (tiles && tileOrder && animationSeeds) {
|
|
console.log(`board - Generate board - tiles`);
|
|
setTileElements(generateTiles(tileOrder, animationSeeds));
|
|
}
|
|
|
|
/* Regenerate pips every time; it uses ws */
|
|
if (pips && pipOrder) {
|
|
console.log(`board - Generate board - pips`);
|
|
setPipElements(generatePips(pipOrder));
|
|
}
|
|
|
|
if (signature && signature !== generated) {
|
|
console.log(`board - Regnerating for ${signature}`);
|
|
setGenerated(signature);
|
|
}
|
|
}, [
|
|
signature,
|
|
generated,
|
|
pips,
|
|
pipOrder,
|
|
borders,
|
|
borderOrder,
|
|
tiles,
|
|
tileOrder,
|
|
animationSeeds,
|
|
ws,
|
|
state,
|
|
rules,
|
|
animations,
|
|
]);
|
|
|
|
/* Re-render turn info after every render */
|
|
useEffect(() => {
|
|
if (!turn) {
|
|
return;
|
|
}
|
|
let nodes = document.querySelectorAll(".Active");
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
nodes[i].classList.remove("Active");
|
|
}
|
|
if (turn.roll) {
|
|
nodes = document.querySelectorAll(`.Pip[data-roll="${turn.roll}"]`);
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const index = nodes[i].getAttribute("data-index");
|
|
if (index !== null) {
|
|
const tile = document.querySelector(`.Tile[data-index="${index}"]`);
|
|
if (tile) {
|
|
tile.classList.add("Active");
|
|
}
|
|
}
|
|
nodes[i].classList.add("Active");
|
|
}
|
|
}
|
|
});
|
|
|
|
/* Re-render placements after every render */
|
|
useEffect(() => {
|
|
if (!placements) {
|
|
return;
|
|
}
|
|
/* Set color and type based on placement data from the server */
|
|
placements.corners.forEach((corner, index) => {
|
|
const el = document.querySelector(`.Corner[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
if (turn.volcano === index) {
|
|
el.classList.add("Lava");
|
|
} else {
|
|
el.classList.remove("Lava");
|
|
}
|
|
if (!corner.color) {
|
|
el.removeAttribute("data-color");
|
|
el.removeAttribute("data-type");
|
|
} else {
|
|
el.setAttribute("data-color", corner.color);
|
|
el.setAttribute("data-type", corner.type);
|
|
}
|
|
});
|
|
placements.roads.forEach((road, index) => {
|
|
const el = document.querySelector(`.Road[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
if (!road.color) {
|
|
el.removeAttribute("data-color");
|
|
} else {
|
|
if (road.longestRoad) {
|
|
if (road.longestRoad === longestRoadLength) {
|
|
el.classList.add("LongestRoad");
|
|
} else {
|
|
el.classList.remove("LongestRoad");
|
|
}
|
|
el.setAttribute("data-longest", road.longestRoad);
|
|
} else {
|
|
el.removeAttribute("data-longest");
|
|
}
|
|
el.setAttribute("data-color", road.color);
|
|
}
|
|
});
|
|
|
|
/* Clear all 'Option' targets */
|
|
let nodes = document.querySelectorAll(`.Option`);
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
nodes[i].classList.remove("Option");
|
|
}
|
|
|
|
/* Add 'Option' based on turn.limits */
|
|
if (turn && turn.limits) {
|
|
if (turn.limits["roads"]) {
|
|
turn.limits["roads"].forEach((index) => {
|
|
const el = document.querySelector(`.Road[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.add("Option");
|
|
el.setAttribute("data-color", turn.color);
|
|
});
|
|
}
|
|
if (turn.limits["corners"]) {
|
|
turn.limits["corners"].forEach((index) => {
|
|
const el = document.querySelector(`.Corner[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.add("Option");
|
|
el.setAttribute("data-color", turn.color);
|
|
});
|
|
}
|
|
if (turn.limits["tiles"]) {
|
|
turn.limits["tiles"].forEach((index) => {
|
|
const el = document.querySelector(`.Tile[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.add("Option");
|
|
});
|
|
}
|
|
if (turn.limits["pips"]) {
|
|
turn.limits["pips"].forEach((index) => {
|
|
const el = document.querySelector(`.Pip[data-index="${index}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.add("Option");
|
|
});
|
|
}
|
|
}
|
|
|
|
/* Clear the robber */
|
|
nodes = document.querySelectorAll(`.Pip.Robber`);
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
nodes[i].classList.remove("Robber");
|
|
["Robert", "Roberta", "Velocirobber"].forEach((robberName) => nodes[i].classList.remove(robberName));
|
|
}
|
|
|
|
/* Place the robber */
|
|
if (robber !== undefined) {
|
|
const el = document.querySelector(`.Pip[data-index="${robber}"]`);
|
|
if (el) {
|
|
el.classList.add("Robber");
|
|
el.classList.add(robberName);
|
|
}
|
|
}
|
|
});
|
|
|
|
const canAction = (action) => {
|
|
return turn && Array.isArray(turn.actions) && turn.actions.indexOf(action) !== -1;
|
|
};
|
|
|
|
const canRoad =
|
|
canAction("place-road") && turn.color === color && (state === "initial-placement" || state === "normal");
|
|
|
|
const canCorner =
|
|
(canAction("place-settlement") || canAction("place-city")) &&
|
|
turn.color === color &&
|
|
(state === "initial-placement" || state === "normal");
|
|
|
|
const canPip =
|
|
canAction("place-robber") && turn.color === color && (state === "initial-placement" || state === "normal");
|
|
|
|
console.log(`board - tile elements`, tileElements);
|
|
return (
|
|
<div className="Board" ref={board}>
|
|
<div className="Tooltip">tooltip</div>
|
|
<div className="BoardBox">
|
|
<div className="Borders" disabled>
|
|
{borderElements}
|
|
</div>
|
|
<div className="Tiles" disabled>
|
|
{tileElements}
|
|
</div>
|
|
<div className="Pips" disabled={!canPip}>
|
|
{pipElements}
|
|
</div>
|
|
<div className="Corners" disabled={!canCorner}>
|
|
{cornerElements}
|
|
</div>
|
|
<div className="Roads" disabled={!canRoad}>
|
|
{roadElements}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export { Board };
|