diff --git a/public/tile-artillery.svg b/public/tile-artillery.svg new file mode 100644 index 0000000..8469187 --- /dev/null +++ b/public/tile-artillery.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/tile-defender.svg b/public/tile-defender.svg index 3a15f3c..e6f8395 100644 --- a/public/tile-defender.svg +++ b/public/tile-defender.svg @@ -8,7 +8,7 @@ version="1.1" id="svg7723" inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" - sodipodi:docname="tile-pawn.svg" + sodipodi:docname="tile-defender.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -24,13 +24,13 @@ inkscape:deskcolor="#505050" inkscape:document-units="px" showgrid="false" - inkscape:zoom="2.8284271" - inkscape:cx="54.623999" - inkscape:cy="121.44559" + inkscape:zoom="5.6568542" + inkscape:cx="61.429902" + inkscape:cy="70.799067" inkscape:window-width="1920" - inkscape:window-height="1022" + inkscape:window-height="1058" inkscape:window-x="0" - inkscape:window-y="36" + inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="layer1" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Game.tsx b/src/components/Game.tsx index 1e150a0..39f71a2 100644 --- a/src/components/Game.tsx +++ b/src/components/Game.tsx @@ -49,6 +49,7 @@ export default function Game(props: GameProps) { ...(props.playerID ? { playerID: props.playerID } : {}), ...("credentials" in props ? { credentials: props.credentials } : {}), }); + const [gameOver, setGameOver] = createSignal(client.getState()?.ctx.gameover ?? false); const [state, setState] = createStore(clone(client.getState()?.G) ?? { resources: {}, cells: {}, @@ -62,7 +63,7 @@ export default function Game(props: GameProps) { start: startAnimation, running: animationRunning, get: getAnimationState, - } = useAnimation(state, 250); + } = useAnimation(state, 333); const [hoveredCell, setHoveredCell] = createSignal<[number, number] | null>(null); const [selectedTool, setSelectedTool] = createSignal({ @@ -91,6 +92,7 @@ export default function Game(props: GameProps) { state, getTransform, hoveredCell, + gameOver, selectedTool, getAnimationState, playerID: props.playerID, @@ -115,7 +117,7 @@ export default function Game(props: GameProps) { getTransform, clickTile(x: number, y: number) { if (!props.playerID) return; - if (animationRunning()) return; + if (animationRunning() || gameOver()) return; const tool = selectedTool(); @@ -159,6 +161,8 @@ export default function Game(props: GameProps) { startAnimation(); } + setGameOver(state.ctx.gameover ?? false); + // For some ungodly reason, I need to clone the state, because otherwise the frozenness sporadically gets // saved in the store, triggering various issues later down the line. // My guess for as to why is that `state.G` can be mutated and some of its properties are passed to Object.freeze @@ -196,7 +200,7 @@ export default function Game(props: GameProps) { }} />
- Turn {turn()} + Turn {turn()}{gameOver() ? "- Game over" : ""}
diff --git a/src/components/Game/useDrawBoard.ts b/src/components/Game/useDrawBoard.ts index c70840e..104ecfe 100644 --- a/src/components/Game/useDrawBoard.ts +++ b/src/components/Game/useDrawBoard.ts @@ -14,6 +14,7 @@ export type UseDrawBoardOptions = { getAnimationState: Accessor>, playerID?: string | undefined, targetTiles: Accessor<[x: number, y: number][]>, + gameOver: Accessor, }; export default function useDrawBoard(options: UseDrawBoardOptions) { @@ -25,7 +26,8 @@ export default function useDrawBoard(options: UseDrawBoardOptions) { selectedTool, getAnimationState, playerID, - targetTiles + targetTiles, + gameOver } = options; const [tileImages] = createResource(loadTileImages); @@ -54,7 +56,7 @@ export default function useDrawBoard(options: UseDrawBoardOptions) { const transform = getTransform(); const transformedCtx = new draw.TransformedCanvas2DCtx(ctx, transform); - if (hoveredCell() && !animationRunning()) { + if (hoveredCell() && !animationRunning() && !gameOver()) { const [x, y] = hoveredCell()!; const tool = selectedTool(); let valid = true; diff --git a/src/game.ts b/src/game.ts index 438ac3f..62370f3 100644 --- a/src/game.ts +++ b/src/game.ts @@ -1,5 +1,6 @@ import type { Game } from "boardgame.io"; import { INVALID_MOVE } from "boardgame.io/core"; +import hexNeighborhood from "./utils/hexNeighborhood.ts"; // TODO: partial information @@ -60,7 +61,7 @@ export const Buildings = { description: "Your first line of defense: has good HP and can attack a single adjacent tile to prevent enemy expansion to the border of your territory.", humanName: "Defender", cost: 2, - hp: 2, + hp: 3, placedOn: ["road"], attack: { power: 1, @@ -85,23 +86,27 @@ export const Buildings = { }, laser: { name: "laser", - description: "A long-range defensive unit, able to cause moderate damage to 3 tiles in a row", + description: "A long-range defensive unit, able to cause moderate damage to 2 tiles in a row. Does not pierce", humanName: "Laser", - cost: 5, - hp: 2, + cost: 4, + hp: 3, placedOn: ["road", "defender"], attack: { power: 2, - cost: 2, + cost: 1, maxMoves: 1, targets(_cells, x, y) { return [...adjacentCells(x, y)]; }, - damageZone(_cells, x, y, targetX, targetY) { + damageZone(cells, x, y, targetX, targetY) { const dx = targetX - x; const dy = targetY - y; + const MAX_DIST = 2; + + const squares = new Array(MAX_DIST).fill(null).map((_, dist): [number, number] => [x + dx * (dist + 1), y + dy * (dist + 1)]); + const maxDist = squares.findIndex(([x, y]) => !!getCell(cells, x, y)?.owner); - return new Array(3).fill(null).map((_, dist) => [x + dx * (dist + 1), y + dy * (dist + 1)]); + return squares.slice(0, maxDist === -1 ? MAX_DIST : maxDist + 1); } } }, @@ -113,7 +118,37 @@ export const Buildings = { gain: 2, hp: 2, placedOn: ["road"] - } + }, + wall: { + name: "wall", + description: "A strong defensive unit, that is unable to attack.", + humanName: "Wall", + cost: 3, + hp: 5, + placedOn: ["road", "defender"] + }, + artillery: { + name: "artillery", + description: "A long-range unit, capable of shooting over defenses for moderate damage up to 3 tiles away.", + humanName: "Artillery", + cost: 6, + hp: 1, + placedOn: ["road", "defender"], + attack: { + cost: 2, + power: 2, + maxMoves: 1, + targets(_cells, x, y) { + return [ + ...hexNeighborhood(x, y, 2), + ...hexNeighborhood(x, y, 3), + ]; + }, + damageZone(_cells, _x, _y, targetX, targetY) { + return [[targetX, targetY]]; + } + } + }, } as const satisfies Readonly, SetupData> = { } applyMoves(ctx.G); + + if (isGameOver(ctx.G)) { + ctx.events.endGame(); + } }, endIf(ctx) { if (!ctx.ctx.activePlayers) return false; @@ -752,3 +791,13 @@ export function getLocalState(state: State, playerID: string): State["cells"] { return res; } + +export function isGameOver(state: State): boolean { + const playersAlive: string[] = []; + for (const [_x, _y, cell] of iterateCells(state.cells)) { + if (cell.owner && cell.building === "base") { + if (!playersAlive.includes(cell.owner)) playersAlive.push(cell.owner); + } + } + return playersAlive.length <= 1; +} diff --git a/src/utils/hexNeighborhood.ts b/src/utils/hexNeighborhood.ts new file mode 100644 index 0000000..8ecd64a --- /dev/null +++ b/src/utils/hexNeighborhood.ts @@ -0,0 +1,39 @@ +import { adjacentCells } from "../game.ts"; + +type Neighborhood = [x: number, y: number][]; + +const neighborhoodCache: Neighborhood[] = []; + +function offset(neighborhood: Neighborhood, cx: number, cy: number): Neighborhood { + return neighborhood.map(([x2, y2]) => [x2 + cx, y2 + cy]); +} + +export default function hexNeighborhood(cx: number, cy: number, radius: number): Neighborhood { + if (neighborhoodCache[radius]) { + return offset(neighborhoodCache[radius]!, cx, cy); + } else { + const open: [x: number, y: number, hop: number][] = [[0, 0, 0]]; + const closed: Set = new Set(); + const result: Neighborhood = []; + + while (open.length > 0) { + const [x, y, hop] = open.shift()!; + const key = `${x}:${y}`; + if (closed.has(key)) continue; + closed.add(key); + + if (hop === radius) { + result.push([x, y]); + } + + if (hop < radius) { + for (const [nx, ny] of adjacentCells(x, y)) { + open.push([nx, ny, hop + 1]); + } + } + } + + neighborhoodCache[radius] = result; + return offset(result, cx, cy); + } +}