diff --git a/client/public/assets/gfx/pip-numbers.png b/client/public/assets/gfx/pip-numbers.png index 4109bde..4c6b11c 100644 Binary files a/client/public/assets/gfx/pip-numbers.png and b/client/public/assets/gfx/pip-numbers.png differ diff --git a/client/public/assets/gfx/tabletop.png b/client/public/assets/gfx/tabletop.png index bdf89eb..e08cc14 100644 Binary files a/client/public/assets/gfx/tabletop.png and b/client/public/assets/gfx/tabletop.png differ diff --git a/client/src/Board.css b/client/src/Board.css index 6e47f96..e3458df 100644 --- a/client/src/Board.css +++ b/client/src/Board.css @@ -5,3 +5,27 @@ height: 100%; } +.Tile { + width: 90.75px; + height: 77.5px; + position: absolute; + background-position-y: 0px; + background-size: cover; + transform: translate(-45.375px, -38.75px) rotate(-30deg); +} + +.Pip { + width: 50px; + height: 50px; + position: absolute; + background-size: 600% auto; /* pip-numbers is a 6x6 grid of pip images */ + transform: translate(-25px, -25px); +} + +.Border { + width: 242px; + height: 70.6px; + position: absolute; + transform-origin: 0 0; + background-size: cover; +} \ No newline at end of file diff --git a/client/src/Board.js b/client/src/Board.js index bc53169..e9ee008 100644 --- a/client/src/Board.js +++ b/client/src/Board.js @@ -82,48 +82,7 @@ const loadImage = (file, drawFrame) => { return image; } -const Tiles = (game, drawFrame) => { - if (!game) { - return; - } - const tiles = game.tiles; - - [ "robber", "brick", "wood", "sheep", "stone", "wheat" ].forEach((type) => { - const file = "tiles-" + type + ".png", - image = loadImage(file, drawFrame); - - tiles.forEach((tile) => { - if (tile.type === type) { - tile.image = image; - tile.x = 0; - tile.pos = { x: 0, y: 0 }; - } - tile.jitter = 0; - }); - }); - - return tiles; -}; - -const gameSignature = (game) => { - if (!game) { - return ""; - } - const signature = - game.borders.map(border => border.file.replace(/borders-(.).*/, "$1")).join('') + - game.pips.map(pip => pip.roll.toString()).join('') + - game.tiles.map(tile => tile.type.charAt(0)).join(''); - - return signature; -}; - const Board = ({ game }) => { - const [signature, setSignature] = useState(gameSignature(game)); - const [mouse, setMouse] = useState({x: 0, y: 0, timer: null}); - const [pips, setPips] = useState([]); - const [borders, setBorders] = useState([]); - const [tabletop, setTabletop] = useState(null); - const [tiles, setTiles] = useState([]); const [closest, setClosest] = useState({ info: {}, tile: null, @@ -131,408 +90,134 @@ const Board = ({ game }) => { tradeToken: null, settlement: null }); - const [minSize,setMinSize] = useState(0); - const radius = 0.317; - const canvasRef = useRef(null); + const center = { x: 300, y: 350 }, rows = [3, 4, 5, 4, 3]; - const drawPips = useCallback((ctx) => { - const image = pips.image, pipSize = 0.06; - - function drawTile(tile, angle, radius) { - tile.pos.x = Math.sin(-angle) * radius; - tile.pos.y = Math.cos(-angle) * radius; - const image = tile.image; - ctx.save(); - ctx.rotate(angle); - ctx.translate(0., radius); - ctx.rotate(-angle + Math.PI * 1. / 6.); - ctx.drawImage(image, - tile.x * image.width, tile.y * image.height, - image.width, image.height / 4., - -tileWidth * 0.5, -tileHeight * 0.5, - tileWidth, tileHeight); - ctx.restore(); + const generatePips = () => { + let row = 0, rowCount = 0; + let y = center.y - rows.length * 0.5 * 67, + x = center.x - (rows[row] - 1) * 0.5 * 77.5; + let divs = [], count = 19; + for (let i = 0; i < count; i++) { + let code = (i === (count - 1)) + ? 'robber' + : String.fromCharCode(65 + i); + divs.push( +
+ ); + + if (++rowCount === rows[row]) { + row++; + rowCount = 0; + y += 67; + x = center.x - (rows[row] - 1) * 0.5 * 77.5; + } else { + x += 77.5; + } } + return divs; + }; - function drawPip(pip, angle, radius, jitter) { - ctx.save(); - ctx.rotate(angle); - ctx.translate(0., radius); - /* Offset into a random direction by a random amount */ - ctx.rotate(Math.PI * 2. * jitter); - ctx.translate(0, tileHeight * 0.1 * jitter); - /* Undo random rotation for position, and add random rotation - * for pip placement */ - ctx.rotate(-angle - Math.PI * 2. * jitter + jitter * 0.4); - ctx.drawImage(image, - pip.x * image.width, pip.y * image.height, - image.width / 6., image.height / 6., - -pipSize * 0.5, -pipSize * 0.5, - pipSize, pipSize); - ctx.restore(); - } + const generateTiles = () => { + let row = 0, rowCount = 0; + let y = center.y - rows.length * 0.5 * 67, + x = center.x - (rows[row] - 1) * 0.5 * 77.5; + + return [ "robber", "brick", "wood", "sheep", "stone", "wheat" ].map((type) => { + let count; + switch (type) { + case "robber": + count = 1; + break; + case "brick": + count = 3; + break; + case "wood": + count = 4; + break; + case "sheep": + count = 4; + break; + case "stone": + count = 3; + break; + case "wheat": + count = 4; + break; + default: + console.error(`Invalid type: ${type}`); + break; + } + let divs = []; - let angle, - rotation = radius, - index = 0, pip; //, roll = dice[0].pips + dice[1].pips; + for (let i = 0; i < count; i++) { + divs.push( +
+ ); - /* Outer row */ - angle = 0; - for (let i = 0; i < 12; i++) { - angle -= Math.PI * 2. / 12.; - pip = pips.pips[index++]; - tiles[i].pip = pip; - drawTile(tiles[i], angle, rotation - (i % 2) * 0.04); - drawPip(pip, angle, rotation - (i % 2) * 0.04, tiles[i].jitter); - } - - /* Middle row */ - angle = Math.PI * 2. / 12.; - rotation = radius * 0.5; - for (let i = 12; i < 18; i++) { - angle -= Math.PI * 2. / 6.; - pip = pips.pips[index++]; - tiles[i].pip = pip; - drawTile(tiles[i], angle, rotation); - drawPip(pip, angle, rotation, tiles[i].jitter); - } - - /* Center */ - let i = 18; - pip = pips.pips[index++]; - tiles[i].pip = pip; - drawTile(tiles[i], 0, 0); - drawPip(pip, 0, 0, tiles[i].jitter); - }, [ tiles, pips]); - - const drawBorders = useCallback((ctx) => { - ctx.rotate(Math.PI); - - const offset = 0.18; - borders.forEach((border, index) => { - ctx.translate(0, radius); - - const image = border.image; - - ctx.drawImage(image, - -offset, 0, - 0.5, 0.5 * image.height / image.width); - - ctx.translate(0, -radius); - ctx.rotate(Math.PI * 2. / 6.); - }); - }, [borders]); - - const drawFrame = useCallback(() => { - if (!canvasRef || !tabletop || !canvasRef.current) { - return; - } - - if (!game) { - console.log("Nothing to render if there is no game!"); - return; - } - - const canvas = canvasRef.current; - - if (canvas.width === 0 || canvas.height === 0) { - console.log("No dimensions to render in"); - return; - } - - const ctx = canvas.getContext("2d"); - // ctx.clearRect(0, 0, canvas.width, canvas.height); - - ctx.save(); - ctx.strokeStyle = 'white'; - ctx.fillStyle = 'rgba(0, 0, 0, 0)'; - - /* - * Tabletop tiling: - * Image width: 1080 - * Left start: 32 - * Right edge: 1010 (1010 - 32 = 978) - * - * If the view is wider than taller, then - */ - const tabletopLeft = 32 * tabletop.width / 1080, - tabletopRight = 1010 * tabletop.width / 1080, - tabletopLeaf = 978 * tabletop.width / 1080; - - /* If view is taller than wide, tile the tabletop vertically */ - ctx.save(); - if (canvas.height > canvas.width) { - const tabletopHeight = canvas.width * tabletop.height / tabletop.width; - for (let top = 0, step = 0; top < canvas.height; top += tabletopHeight, step++) { - if (step % 2) { - ctx.save(); - ctx.translate(0, tabletopHeight - 1); - ctx.transform(1, 0, 0, -1, 0, 0); - ctx.drawImage(tabletop, - 0, 0, - tabletop.width, tabletop.height, - 0, 0, canvas.width, canvas.width * tabletop.height / tabletop.width); - ctx.restore(); + if (++rowCount === rows[row]) { + row++; + rowCount = 0; + y += 67; + x = center.x - (rows[row] - 1) * 0.5 * 77.5; } else { - ctx.drawImage(tabletop, - 0, 0, - tabletop.width, tabletop.height, - 0, 0, - canvas.width, canvas.width * tabletop.height / tabletop.width); - } - ctx.translate(0, tabletopHeight); - } - } else { - //const tabletopWidth = canvas.height * tabletop.width / tabletop.height; - ctx.drawImage(tabletop, - 0, 0, - tabletopRight, tabletop.height, - 0, 0, - canvas.height * tabletopRight / tabletop.height, canvas.height); - let left = canvas.height * tabletopRight / tabletop.height; - while (left < canvas.width) { - ctx.drawImage(tabletop, - tabletopLeft, 0, - tabletopLeaf, tabletop.height, - left, 0, - canvas.height * tabletopLeaf / tabletop.height, canvas.height); - left += canvas.height * tabletopLeaf / tabletop.height; - } - } - ctx.restore(); - - ctx.scale(minSize / hexagonRatio, minSize / hexagonRatio); - ctx.translate(0.5 * hexagonRatio, 0.5 * hexagonRatio); - ctx.lineWidth = 2. / minSize; - - /* Board dimensions: - * ________ - * /___1__| \ - * / / \6\ - * /2/ \ \ - * / / \/\ - * \/\ / / - * \ \ /5/ - * \3\______/_/ - * \_|__4___/ - * 0 0.3 0.6 1 - */ - - ctx.save(); - drawBorders(ctx); - ctx.restore(); - - ctx.save(); - drawPips(ctx); - ctx.restore(); - - ctx.fillStyle = "rgba(128, 128, 0, 0.125)"; - ctx.strokeStyle = "rgba(255, 255, 0, 0.5)"; - - if (game.state !== 'lobby') { - const roll = - (game.dice[0] ? game.dice[0] : 0) + (game.dice[1] ? game.dice[1] : 0); - if (roll) tiles.forEach((tile) => { - if (tile.pip.roll === roll) { - ctx.save(); - ctx.beginPath(); - ctx.arc(tile.pos.x, tile.pos.y, tileHeight * 0.5, 0, Math.PI * 2); - ctx.fill(); - ctx.stroke(); - ctx.restore(); - } - }); - } - - if (closest.tile) { - ctx.save(); - - Object.assign(ctx, getPlayerColors(game.color)); - - ctx.translate(closest.tile.pos.x, closest.tile.pos.y); - /* draw circle hovered current tile - ctx.beginPath(); - ctx.arc(0, 0, tileHeight * 0.5, 0, Math.PI * 2.); - ctx.stroke(); - */ - - /* road */ - let angle = Math.round(closest.info.angle / (Math.PI / 3.)) * (Math.PI / 3.); - ctx.rotate(angle); - ctx.translate(-tileHeight * 0.5, 0); - ctx.beginPath(); - ctx.rect(-roadSize * 0.125, -roadSize * 0.5, roadSize * 0.25, roadSize); - ctx.fill(); - ctx.stroke(); - ctx.translate(tileHeight * 0.5, 0); - ctx.rotate(-angle); - - /* village */ - angle = (closest.info.angle - Math.PI / 6.); - angle = Math.round(angle / (Math.PI / 3.)) * (Math.PI / 3.); - angle += Math.PI / 6.; - ctx.rotate(angle); - ctx.translate(-tileWidth * 0.5, 0); - ctx.rotate(-angle); - ctx.beginPath(); - ctx.rect(-settlementSize * 0.5, -settlementSize * 0.5, settlementSize, settlementSize); - ctx.fill(); - ctx.stroke(); - ctx.rotate(angle); - ctx.translate(+tileWidth * 0.5, 0); - ctx.rotate(-angle); - - ctx.restore(); - } - - /* For 0.5 after mouse movement, there is an on - * screen mouse helper. */ - if (mouse.timer) { - ctx.strokeStyle = "rgba(0, 255, 255)"; - ctx.fillStyle = "rgba(0, 255, 255, 0.25)"; - ctx.beginPath(); - ctx.arc(mouse.x, mouse.y, - tileHeight * 0.5, 0, Math.PI * 2.); - ctx.stroke(); - ctx.fill(); - } - - ctx.restore(); - }, [ game, canvasRef, closest, mouse, minSize, drawBorders, drawPips, tabletop, tiles ]); - - const mouseMove = useCallback((event) => { - let x, y; - - if (event.changedTouches && event.changedTouches.length > 0) { - x = event.changedTouches[0].clientX; - y = event.changedTouches[0].clientY; - } else { - x = event.clientX; - y = event.clientY; - } - - /* Hide the mouse cursor circle after 0.5s */ - if (mouse.timer) { - window.clearTimeout(mouse.timer); - } - let timer = window.setTimeout(() => { - mouse.timer = null; - window.requestAnimationFrame(drawFrame); - }, 500); - - /* Scale mouse.x and mouse.y relative to board */ - setMouse({ - x: x / (minSize / hexagonRatio) - 0.5 - tileHeight * 0.5, - y: y / (minSize / hexagonRatio) - 0.5 - tileHeight * 0.5, - timer: timer - }); - - let tmp = null; - - tiles.forEach((tile) => { - const dX = tile.pos.x - mouse.x, - dY = tile.pos.y - mouse.y; - const distance = Math.sqrt(dX * dX + dY * dY); - if (distance > tileHeight * 0.75) { - return; - } - if (!tmp || tmp.distance > distance) { - tmp = { - tile: tile, - distance: distance, - angle: (distance !== 0.0) ? Math.atan2(dY, dX) : 0 + x += 77.5; } } + + return divs; }); + }; - if (!tmp) { - closest.tile = null; - closest.info.distance = -1; - closest.road = null; - closest.angle = 0; - closest.settlement = null; - closest.tradeToken = null; - } else { - closest.info.distance = closest.distance; - closest.info.angle = closest.angle; + const generateBorders = () => { + const divs = [], + radius = 77.5 * 2; + const sides = 6; + for (let side = 0; side < sides; side++) { + let x = center.x + Math.sin(Math.PI - side / sides * 2. * Math.PI) * radius, + y = -33.5 + center.y + Math.cos(Math.PI - side / sides * 2. * Math.PI) * radius; + let prev = (side == 0) ? 6 : side; + const file = `borders-${side+1}.${prev}.png`; + divs.push(
{side}
); } + return divs; + }; - setClosest(closest); - - window.requestAnimationFrame(drawFrame); - }, [ drawFrame, closest, setClosest, setMouse, minSize, mouse, tiles ]); - - const updateDimensions = useCallback(() => { - if (canvasRef.current.updateSizeTimer) { - clearTimeout(canvasRef.current.updateSizeTimer); - } - - canvasRef.current.updateSizeTimer = setTimeout(() => { - const width = canvasRef.current.offsetWidth, - height = canvasRef.current.offsetHeight; - - if (width !== canvasRef.current.width || - height !== canvasRef.current.height) { - canvasRef.current.setAttribute("width", width); - canvasRef.current.setAttribute("height", height); - setMinSize(Math.min(height, width)); - } - - canvasRef.current.updateSizeTimer = 0; - }, 250); - - window.requestAnimationFrame(drawFrame); - }, [ drawFrame, setMinSize ]); - - const updateGame = useCallback((game) => { - if (!game || game.state === "invalid") { - return; - } - - setSignature(gameSignature(game)); - - setTiles(Tiles(game, drawFrame)); - setPips({ - image: loadImage('pip-numbers.png', drawFrame), - pips: game.pips - }); - setTabletop(loadImage('tabletop.png', drawFrame)); - setBorders(game.borders.map((border) => { - return { - image: loadImage(border.file, drawFrame) - }; - })); - }, [drawFrame, setTiles, setPips, setTabletop, setBorders, setSignature]); - - useEffect(() => { - if (!canvasRef.current) { - return; - } - - if (signature !== gameSignature(game)) { - updateGame(game); - } - - const canvas = canvasRef.current; - - canvas.addEventListener('mousemove', mouseMove); - canvas.addEventListener('touchmove', mouseMove); - window.addEventListener('resize', updateDimensions); - - updateDimensions(); - - return () => { - canvas.removeEventListener('mousemove', mouseMove); - canvas.removeEventListener('touchmove', mouseMove); - window.removeEventListener('resize', updateDimensions); - }; - }, [ mouseMove, updateDimensions, updateGame, signature, game ]); + const tiles = generateTiles(), + pips = generatePips(), + borders = generateBorders(); return ( - - +
+ { borders } + { tiles } + { pips } +
); }; diff --git a/client/src/Table.js b/client/src/Table.js index 4b85044..0527042 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -411,7 +411,8 @@ class Table extends React.Component { wheat: 0, game: null, message: "", - error: "" + error: "", + signature: "" }; this.componentDidMount = this.componentDidMount.bind(this); this.updateDimensions = this.updateDimensions.bind(this); @@ -426,7 +427,8 @@ class Table extends React.Component { this.setPlayerName = this.setPlayerName.bind(this); this.setSelected = this.setSelected.bind(this); this.updateMessage = this.updateMessage.bind(this); - + this.gameSignature = this.gameSignature.bind(this); + this.mouse = { x: 0, y: 0 }; this.radius = 0.317; @@ -470,7 +472,7 @@ class Table extends React.Component { const error = (game.status !== 'success') ? game.status : undefined; this.updateGame(game); this.updateMessage(); - this.setState({ game: game, error: error }); + this.setState({ error: error }); }).catch((error) => { console.error(error); this.setState({error: error.message}); @@ -507,7 +509,7 @@ class Table extends React.Component { this.updateGame(game); this.updateMessage(); - this.setState({ game: game, error: message}); + this.setState({ error: message}); }).catch((error) => { console.error(error); this.setState({error: error.message}); @@ -538,7 +540,7 @@ class Table extends React.Component { console.log (`Table shuffled!`); this.updateGame(game); this.updateMessage(); - this.setState({ game: game, error: "Table shuffled!" }); + this.setState({ error: "Table shuffled!" }); }).catch((error) => { console.error(error); this.setState({error: error.message}); @@ -573,7 +575,7 @@ class Table extends React.Component { } this.updateGame(game); this.updateMessage(); - this.setState({ game: { ...this.state.game, dice: game.dice }, error: error } ); + this.setState({ /*game: { ...this.state.game, dice: game.dice },*/ error: error } ); }).catch((error) => { console.error(error); this.setState({error: error.message}); @@ -614,7 +616,7 @@ class Table extends React.Component { this.updateGame(game); this.updateMessage(); - this.setState({ game: game, error: error }); + this.setState({ error: error }); }).catch((error) => { console.error(error); this.setState({error: error.message}); @@ -655,7 +657,7 @@ class Table extends React.Component { return res.json(); }).then((game) => { this.updateGame(game); - this.setState({ game: game, error: "" }); + this.setState({ error: "" }); }).catch((error) => { console.error(error); this.setState({error: error.message}); @@ -687,7 +689,7 @@ class Table extends React.Component { console.log (`Game state set to ${game.state}!`); this.updateGame(game); this.updateMessage(); - this.setState({ game: { ...this.state.game, state: game.state }, error: `Game state now ${game.state}.` }); + this.setState({ /*game: { ...this.state.game, state: game.state }, */error: `Game state now ${game.state}.` }); }).catch((error) => { console.error(error); this.setState({error: error.message}); @@ -780,10 +782,26 @@ class Table extends React.Component { }, 250); } + + gameSignature(game) { + if (!game) { + return ""; + } + const signature = + game.borders.map(border => border.file.replace(/borders-(.).*/, "$1")).join('') + + game.pips.map(pip => pip.roll.toString()).join('') + + game.tiles.map(tile => tile.type.charAt(0)).join(''); + + return signature; + }; + updateGame(game) { + + if (this.state.signature !== this.gameSignature(game)) { + this.setState({ signature: this.gameSignature(game), game: game }); + } // console.log("Update Game", game); this.game = game; - this.setState({ game: game }); } updateMessage() { @@ -901,7 +919,7 @@ class Table extends React.Component { this.updateGame(game); this.updateMessage(); - this.setState({ game: game, error: "" }); + this.setState({ error: "" }); }).catch((error) => { console.error(error); this.setState({error: error.message});