Show an actual board for the village and allow building on specific tiles.
This commit is contained in:
parent
d6f6a0bd00
commit
5a7fb8ddcb
@ -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>
|
||||
|
@ -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
88
src/board/Tile.svelte
Normal 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
52
src/board/Village.svelte
Normal 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>
|
@ -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
135
src/hexgrid.ts
Normal 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;
|
||||
}
|
@ -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
47
src/hud/Game.svelte
Normal 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>
|
@ -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;
|
||||
}
|
||||
|
5
src/stores/showBuildingCreator.ts
Normal file
5
src/stores/showBuildingCreator.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { writable } from "svelte/store";
|
||||
import type { Hex } from "../hexgrid";
|
||||
|
||||
|
||||
export default writable<Hex | null>(null);
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user