/* 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; 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 = ({ animations }) => { const { ws, sendJsonMessage } = useContext(GlobalContext); const board = useRef(); const [transform, setTransform] = useState(1); const [pipElements, setPipElements] = useState([]); const [borderElements, setBorderElements] = useState([]); const [tileElements, setTileElements] = useState([]); const [cornerElements, setCornerElements] = useState([]); const [roadElements, setRoadElements] = useState([]); const [signature, setSignature] = useState(""); const [generated, setGenerated] = useState(""); const [robber, setRobber] = useState(-1); const [robberName, setRobberName] = useState([]); const [pips, setPips] = useState(undefined); // Keep as any for now, complex structure const [pipOrder, setPipOrder] = useState(undefined); const [borders, setBorders] = useState(undefined); const [borderOrder, setBorderOrder] = useState(undefined); const [animationSeeds, setAnimationSeeds] = useState(undefined); const [tiles, setTiles] = useState(undefined); const [tileOrder, setTileOrder] = useState([]); const [placements, setPlacements] = useState(undefined); const [turn, setTurn] = useState({}); const [state, setState] = useState(""); const [color, setColor] = useState(""); const [rules, setRules] = useState({}); const [longestRoadLength, setLongestRoadLength] = useState(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 = ({ corner }) => { return (
{ if (e.shiftPressed) { const tooltip = document.querySelector(".Board .Tooltip") as HTMLElement; tooltip.innerHTML = `
${corner}
`; showTooltip(); } }} onClick={(event) => { onCornerClicked(event, corner); }} data-index={corner.index} style={{ top: `${corner.top}px`, left: `${corner.left}px`, }} >
); }; 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 = { index: index++, top: y, left: x, }; corners.push(); corner = { index: index++, top: y - 0.5 * tileHalfHeight, left: x + tileHalfHeight, }; corners.push(); if (++rowCount === rows[row]) { corner = { index: index++, top: y, left: x + 2 * tileHalfHeight, }; corners.push(); if (row > 2) { corner = { index: index++, top: y - 0.5 * tileHalfHeight, left: x + 3 * tileHalfHeight, }; corners.push(); } 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 = ({ 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 (
{ onRoadClicked(road); }} data-index={road.index} style={{ transform: `translate(-50%, -50%) rotate(${road.angle}deg)`, top: `${road.top}px`, left: `${road.left}px`, }} >
); }; 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 = { index: index++, angle: 240, top: y, left: x, }; corners.push(); road = { index: index++, angle: -60, top: y - 0.5 * tileHalfHeight, left: x + tileHalfHeight, }; corners.push(); if (!lastRow) { road = { index: index++, angle: 0, top: y, left: x, }; corners.push(); } if (++rowCount === rows[row]) { if (!lastRow) { road = { index: index++, angle: 0, top: y, left: x + 2 * tileHalfHeight, }; corners.push(); } if (row > 2) { road = { index: index++, angle: 60, top: y - 0.5 * tileHalfHeight, left: x + 3 * tileHalfHeight, }; corners.push(); } 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 = ({ 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 (
{ 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}%`, }} >
); }; 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 = ; 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 = ({ 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 (
); }; 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 = (
{animations && ( )}{" "}
); } else if (tile.type === "sheep") { div = (
{animations && ( )}
); } else { div = ; } 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 (
{ 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 (
tooltip
{borderElements}
{tileElements}
{pipElements}
{cornerElements}
{roadElements}
); }; export { Board };