From 5a7fb8ddcb6ef78eb13decfcfe99fd3efcea8a1d Mon Sep 17 00:00:00 2001 From: Adrian Gaudebert Date: Thu, 24 Oct 2024 11:32:31 +0200 Subject: [PATCH] Show an actual board for the village and allow building on specific tiles. --- src/App.svelte | 60 +------------ src/app.css | 7 ++ src/board/Tile.svelte | 88 +++++++++++++++++++ src/board/Village.svelte | 52 ++++++++++++ src/create.ts | 4 +- src/hexgrid.ts | 135 ++++++++++++++++++++++++++++++ src/hud/BuildingCreator.svelte | 13 ++- src/hud/Game.svelte | 47 +++++++++++ src/moves/build.ts | 15 +++- src/stores/showBuildingCreator.ts | 5 ++ src/types.ts | 3 + src/utils.ts | 5 ++ src/village.ts | 88 +++++++++++++++---- 13 files changed, 442 insertions(+), 80 deletions(-) create mode 100644 src/board/Tile.svelte create mode 100644 src/board/Village.svelte create mode 100644 src/hexgrid.ts create mode 100644 src/hud/Game.svelte create mode 100644 src/stores/showBuildingCreator.ts diff --git a/src/App.svelte b/src/App.svelte index 1dce74a..88c01d7 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,63 +1,7 @@ -{ #if showBuildingCreator } - { showBuildingCreator = false } } /> -{ /if }
-
- -
-
- -
-
- { #each $village.buildings as building } -
-

{ building.name } ({ building.level })

-

- -

-
- { /each } -
+
- - diff --git a/src/app.css b/src/app.css index b584976..b92576c 100644 --- a/src/app.css +++ b/src/app.css @@ -62,6 +62,13 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +button.invisible, +button:disabled.invisible, +button:active.invisible, +button:focus.invisible { + all: unset; +} + @media (prefers-color-scheme: light) { :root { color: #213547; diff --git a/src/board/Tile.svelte b/src/board/Tile.svelte new file mode 100644 index 0000000..82f6245 --- /dev/null +++ b/src/board/Tile.svelte @@ -0,0 +1,88 @@ + + + + + diff --git a/src/board/Village.svelte b/src/board/Village.svelte new file mode 100644 index 0000000..a372ee1 --- /dev/null +++ b/src/board/Village.svelte @@ -0,0 +1,52 @@ + + +
+
+ { #each getKeysAsNumbers($village.villageTiles) as y } +
+ { #each getKeysAsNumbers($village.villageTiles[y]) as x } + { #if $village.villageTiles[y][x] >= 0 } + {} } + > +

{ $village.buildings.find(b => b.id === $village.villageTiles[y][x])?.name }

+
+ { :else } + openBuildingCreator(new Hex(x, y)) } + /> + { /if } + { /each } +
+ { /each } +
+
+ + diff --git a/src/create.ts b/src/create.ts index c2c92e1..06b9a86 100644 --- a/src/create.ts +++ b/src/create.ts @@ -1,3 +1,4 @@ +import { Hex } from "./hexgrid"; import type { Building, BuildingSource } from "./types"; @@ -7,7 +8,8 @@ let uid = 0; export function createBuilding(building: BuildingSource): Building { return { ...building, - level: 1, id: uid++, + level: 1, + tile: new Hex(0, 0), }; } diff --git a/src/hexgrid.ts b/src/hexgrid.ts new file mode 100644 index 0000000..702a3ea --- /dev/null +++ b/src/hexgrid.ts @@ -0,0 +1,135 @@ +/** + * A class representing coordinates in a hexagonal grid with 2 dimensions. + * Offset system is "odd-r". + */ +export class Hex { + x: number; + y: number; + + constructor(x: number | { x: number, y: number }, y?: number) { + if (typeof x === "number") { + this.x = x as number; + this.y = y || 0; + } + else { + this.x = x.x; + this.y = x.y; + } + } + + toCube() { + const q = this.x - (this.y - (this.y&1)) / 2; + const r = this.y; + return new Cube(q, r, -q - r); + } +} + + +/** + * A class representing coordinates in a hexagonal grid with 3 dimensions. + * Mostly used as a transient value for easier algorithms. + */ +class Cube { + q: number; + r: number; + s: number; + + constructor(q: number, r: number, s: number) { + this.q = q; + this.r = r; + this.s = s; + } + + toHex() { + return new Hex( + this.q + (this.r - (this.r&1)) / 2, + this.r + ); + } + + add(cube: Cube) { + return new Cube(this.q + cube.q, this.r + cube.r, this.s + cube.s); + } + + subtract(cube: Cube) { + return new Cube(this.q - cube.q, this.r - cube.r, this.s - cube.s); + } +} + + +export function getCornersAtDistance(distance: number) { + const results: Hex[] = []; + for (let q = -distance; q <= distance; q++) { + for (let r = Math.max(-distance, -q - distance); r <= Math.min(distance, -q + distance); r++) { + const s = -q - r; + if ( + (q === 0 || r === 0 || s === 0) + && Math.max(Math.abs(q), Math.abs(r), Math.abs(s)) === distance + ) { + const cube = new Cube(q, r, s); + results.push(cube.toHex()); + } + } + } + return results; +} + + +export function getTilesAtDistance(distance: number) { + const results: Hex[] = []; + for (let q = -distance; q <= distance; q++) { + for (let r = Math.max(-distance, -q - distance); r <= Math.min(distance, -q + distance); r++) { + const s = -q - r; + if (Math.max(Math.abs(q), Math.abs(r), Math.abs(s)) === distance) { + const cube = new Cube(q, r, s); + results.push(cube.toHex()); + } + } + } + return results; +} + + +const adjacentVectors = [ + new Cube(+1, 0, -1), new Cube(+1, -1, 0), new Cube(0, -1, +1), + new Cube(-1, 0, +1), new Cube(-1, +1, 0), new Cube(0, +1, -1), +]; + + +/** + * Return all coordinates adjacent to a given one. + * @param tile Hex - 2D coordinates in a hexagonal grid. + * @returns Array of 6 2D coordinates. + */ +export function getAdjacentCoords(tile: Hex): Hex[] { + const origin = tile.toCube(); + return adjacentVectors.map(vec => origin.add(vec).toHex()); +} + + +/** + * Return a position on the 3D board based on coordinates of a tile on our grid. + * @param x The x coordinate in the grid. + * @param y The y coordinate in the grid. + * @returns An array containing the 3 coordinates (x, y, z) of that tile on a 3D board. + */ +export function getPositionOnBoard(x: number, y: number): [number, number, number] { + const posX = x * 2 + (y % 2 ? 1 : 0); + const posY = 0; + const posZ = y * 1.72; + return [ posX, posY, posZ ]; +} + + +/** + * Return the distance between two cells. + * @param a Hex - 2D coordinates in a hexagonal grid. + * @param b Hex - 2D coordinates in a hexagonal grid. + * @returns Distance between the two coordinates. + */ +export function distanceBetween(a: Hex, b: Hex): number { + const aCube = a.toCube(); + const bCube = b.toCube(); + const vector = aCube.subtract(bCube); + return (Math.abs(vector.q) + Math.abs(vector.r) + Math.abs(vector.s)) / 2; +} diff --git a/src/hud/BuildingCreator.svelte b/src/hud/BuildingCreator.svelte index 55cdf25..c12c127 100644 --- a/src/hud/BuildingCreator.svelte +++ b/src/hud/BuildingCreator.svelte @@ -1,17 +1,25 @@ +{ #if $showBuildingCreator !== null }
@@ -30,6 +38,7 @@
+{ /if } diff --git a/src/moves/build.ts b/src/moves/build.ts index d34d21e..fa4f790 100644 --- a/src/moves/build.ts +++ b/src/moves/build.ts @@ -1,9 +1,10 @@ import buildings from "../buildings"; import { createBuilding } from "../create"; -import type { VillageState } from "../village"; +import type { Hex } from "../hexgrid"; +import { DEFAULT_TILE, type VillageState } from "../village"; -export default function build(V: VillageState, buildingType: keyof typeof buildings) { +export default function build(V: VillageState, buildingType: keyof typeof buildings, tile: Hex) { const building = buildings[buildingType]; const cost = building.cost(1); @@ -16,12 +17,20 @@ export default function build(V: VillageState, buildingType: keyof typeof buildi return false; } + if (V.villageTiles[tile.y][tile.x] !== DEFAULT_TILE) { + return false; + } + V.resources.wood -= cost.wood; V.resources.stone -= cost.stone; V.resources.iron -= cost.iron; V.resources.food -= cost.food; - V.buildings.push(createBuilding(building)); + const newBuilding = createBuilding(building); + newBuilding.tile = tile; + + V.buildings.push(newBuilding); + V.villageTiles[tile.y][tile.x] = newBuilding.id; return true; } diff --git a/src/stores/showBuildingCreator.ts b/src/stores/showBuildingCreator.ts new file mode 100644 index 0000000..945a8cc --- /dev/null +++ b/src/stores/showBuildingCreator.ts @@ -0,0 +1,5 @@ +import { writable } from "svelte/store"; +import type { Hex } from "../hexgrid"; + + +export default writable(null); diff --git a/src/types.ts b/src/types.ts index 969a032..a5e2de7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import type { Hex } from "./hexgrid"; + export interface Cost { wood: number; stone: number; @@ -22,4 +24,5 @@ export interface BuildingSource { export interface Building extends BuildingSource { id: number; level: number; + tile: Hex; } diff --git a/src/utils.ts b/src/utils.ts index 48d7f73..2a6c8de 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -44,3 +44,8 @@ export function getStorage(villageState: VillageState): Production { }) .reduce(_reduceResources, getEmptyResources()); } + + +export function getKeysAsNumbers(dict: Object): Array { + return Object.keys(dict).map(i => parseInt(i)).sort((a, b) => a - b); +} diff --git a/src/village.ts b/src/village.ts index beb7e36..bb6547d 100644 --- a/src/village.ts +++ b/src/village.ts @@ -3,6 +3,14 @@ import { writable } from "svelte/store"; import buildings from "./buildings"; import { createBuilding } from "./create"; import type { Building } from "./types"; +import { getTilesAtDistance } from "./hexgrid"; + + +type Board = { + [key: number]: { + [key: number]: number; + } +} export interface VillageState { @@ -14,25 +22,73 @@ export interface VillageState { food: number; culture: number; }; + villageTiles: Board; + outsideTiles: Board; } -const village = writable({ - buildings: [ - createBuilding(buildings.townhall), - createBuilding(buildings.woodcutter), - createBuilding(buildings.pit), - createBuilding(buildings.mine), - createBuilding(buildings.field), - ], - resources: { - wood: 60, - stone: 60, - iron: 60, - food: 50, - culture: 0, - }, -}); +export const DEFAULT_TILE = -1; +export const VILLAGE_TILE = -2; + + +function getInitialVillageBoard() { + const board: Board = { + 0: { 0: DEFAULT_TILE }, + }; + for (let i = 1; i <= 2; i++) { + getTilesAtDistance(i).forEach(tile => { + if (board[tile.y] === undefined) { + board[tile.y] = {}; + } + + board[tile.y][tile.x] = DEFAULT_TILE; + }); + } + return board; +} + + +function getInitialOutsideBoard() { + const board: Board = { + 0: { 0: VILLAGE_TILE }, + }; + for (let i = 1; i <= 2; i++) { + getTilesAtDistance(i).forEach(tile => { + if (board[tile.y] === undefined) { + board[tile.y] = {}; + } + + board[tile.y][tile.x] = DEFAULT_TILE; + }); + } + return board; +} + + +function getInitialState() { + const townhall = createBuilding(buildings.townhall); + const state = { + buildings: [ + townhall, + ], + resources: { + wood: 60, + stone: 60, + iron: 60, + food: 50, + culture: 0, + }, + villageTiles: getInitialVillageBoard(), + outsideTiles: getInitialOutsideBoard(), + }; + + state.villageTiles[0][0] = townhall.id; + + return state; +} + + +const village = writable(getInitialState()); export default village;