Show an actual board for the village and allow building on specific tiles.

This commit is contained in:
Adrian 2024-10-24 11:32:31 +02:00
parent d6f6a0bd00
commit 5a7fb8ddcb
13 changed files with 442 additions and 80 deletions

View File

@ -1,63 +1,7 @@
<script lang="ts">
import { onMount } from "svelte";
import Resources from "./hud/Resources.svelte";
import moves from "./moves";
import update from "./update";
import village from "./village";
import BuildingCreator from "./hud/BuildingCreator.svelte";
onMount(() => {
let frame: number;
function loop(timestamp: number) {
frame = requestAnimationFrame(loop);
update(timestamp);
}
frame = requestAnimationFrame(loop);
return () => {
cancelAnimationFrame(frame);
}
});
function upgradeBuilding(id: number) {
moves.upgradeBuilding(id);
}
let showBuildingCreator = false;
import Game from "./hud/Game.svelte";
</script>
{ #if showBuildingCreator }
<BuildingCreator close={ () => { showBuildingCreator = false } } />
{ /if }
<main>
<header>
<Resources />
</header>
<div>
<button on:click={ () => { showBuildingCreator = true } }>Create building</button>
</div>
<div class="buildings">
{ #each $village.buildings as building }
<div>
<p>{ building.name } ({ building.level })</p>
<p>
<button on:click={ () => upgradeBuilding(building.id) }>
Upgrade
</button>
</p>
</div>
{ /each }
</div>
<Game />
</main>
<style>
.buildings {
display: grid;
gap: 1em;
grid-template-columns: repeat(4, 1fr);
}
</style>

View File

@ -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;

88
src/board/Tile.svelte Normal file
View File

@ -0,0 +1,88 @@
<script lang="ts">
export let empty = false;
export let onTileClick: () => void;
</script>
<button
class="invisible"
on:click={ onTileClick }
>
<div
class={ "hexagon" }
class:empty
>
<div class="content">
<slot></slot>
</div>
</div>
</button>
<style>
/* Source: https://codepen.io/gpyne/pen/iElhp */
.hexagon {
background-color: hsl(220, 75%, 75%);
cursor: pointer;
display: inline-block;
font-size: 1.2vmin;
margin: -0.8em 2.7em;
position: relative;
text-align: center;
rotate: 90deg;
}
.hexagon,
.hexagon::before,
.hexagon::after {
width: 6.7em;
height: 11.6em;
border-radius: 20%/5%;
}
.hexagon::before {
background-color: inherit;
content: "";
position: absolute;
left: 0;
transform: rotate(-60deg);
}
.hexagon::after {
background-color: inherit;
content: "";
position: absolute;
left: 0;
transform: rotate(60deg);
}
.hexagon:nth-child(even) {
top: 5.9em;
}
.hexagon:hover {
background-color: hsl(60, 75%, 75%);
z-index: 105;
}
.hexagon:active {
background-color: hsl(60, 75%, 50%);
z-index: 110;
}
.hexagon.empty {
position: relative;
display: none;
width: 6.7em;
height: 11.6em;
margin: 0.1em 1.8em;
}
.hexagon.empty:nth-child(even) {
top: 5.9em;
}
.hexagon .content {
position: absolute;
display: grid;
top: 5%;
left: -40%;
font-size: 4vmin;
height: 90%;
line-height: 1.2;
width: 180%;
z-index: 100;
}
.hexagon .content > * {
margin: auto;
}
</style>

52
src/board/Village.svelte Normal file
View File

@ -0,0 +1,52 @@
<script lang="ts">
import { Hex } from "../hexgrid";
import moves from "../moves";
import showBuildingCreator from "../stores/showBuildingCreator";
import { getKeysAsNumbers } from "../utils";
import village from "../village";
import Tile from "./Tile.svelte";
function upgradeBuilding(id: number) {
moves.upgradeBuilding(id);
}
function openBuildingCreator(tile: Hex) {
showBuildingCreator.set(tile);
}
</script>
<section class="village-map">
<div>
{ #each getKeysAsNumbers($village.villageTiles) as y }
<div>
{ #each getKeysAsNumbers($village.villageTiles[y]) as x }
{ #if $village.villageTiles[y][x] >= 0 }
<Tile
onTileClick={ () => {} }
>
<p>{ $village.buildings.find(b => b.id === $village.villageTiles[y][x])?.name }</p>
</Tile>
{ :else }
<Tile
onTileClick={ () => openBuildingCreator(new Hex(x, y)) }
/>
{ /if }
{ /each }
</div>
{ /each }
</div>
</section>
<style>
.village-map {
display: grid;
height: 100%;
position: relative;
}
.village-map > div {
margin: auto;
}
</style>

View File

@ -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),
};
}

135
src/hexgrid.ts Normal file
View File

@ -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;
}

View File

@ -1,17 +1,25 @@
<script lang="ts">
import buildings from "../buildings";
import moves from "../moves";
import showBuildingCreator from "../stores/showBuildingCreator";
export let close: () => void;
function close() {
showBuildingCreator.set(null);
}
function build(type: string) {
if (moves.build(type)) {
if ($showBuildingCreator === null) {
return;
}
if (moves.build(type, $showBuildingCreator)) {
close();
}
}
</script>
{ #if $showBuildingCreator !== null }
<section>
<div class="building-creator">
<header>
@ -30,6 +38,7 @@
</div>
</div>
</section>
{ /if }
<style>
section {

47
src/hud/Game.svelte Normal file
View File

@ -0,0 +1,47 @@
<script lang="ts">
import { onMount } from "svelte";
import Village from "../board/Village.svelte";
import update from "../update";
import BuildingCreator from "./BuildingCreator.svelte";
import Resources from "./Resources.svelte";
onMount(() => {
let frame: number;
function loop(timestamp: number) {
frame = requestAnimationFrame(loop);
update(timestamp);
}
frame = requestAnimationFrame(loop);
return () => {
cancelAnimationFrame(frame);
}
});
</script>
<section class="hud">
<header>
<Resources />
</header>
<div class="buildings">
<Village />
</div>
</section>
<section class="overlay">
<BuildingCreator />
</section>
<style>
.buildings {
margin-top: 2em;
}
.overlay {
left: 0;
position: absolute;
top: 0;
}
</style>

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
import { writable } from "svelte/store";
import type { Hex } from "../hexgrid";
export default writable<Hex | null>(null);

View File

@ -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;
}

View File

@ -44,3 +44,8 @@ export function getStorage(villageState: VillageState): Production {
})
.reduce(_reduceResources, getEmptyResources());
}
export function getKeysAsNumbers(dict: Object): Array<number> {
return Object.keys(dict).map(i => parseInt(i)).sort((a, b) => a - b);
}

View File

@ -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,16 +22,54 @@ export interface VillageState {
food: number;
culture: number;
};
villageTiles: Board;
outsideTiles: Board;
}
const village = writable<VillageState>({
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: [
createBuilding(buildings.townhall),
createBuilding(buildings.woodcutter),
createBuilding(buildings.pit),
createBuilding(buildings.mine),
createBuilding(buildings.field),
townhall,
],
resources: {
wood: 60,
@ -32,7 +78,17 @@ const village = writable<VillageState>({
food: 50,
culture: 0,
},
});
villageTiles: getInitialVillageBoard(),
outsideTiles: getInitialOutsideBoard(),
};
state.villageTiles[0][0] = townhall.id;
return state;
}
const village = writable<VillageState>(getInitialState());
export default village;