Compare commits
8 Commits
16db8ee0be
...
fe34ffee8d
Author | SHA1 | Date | |
---|---|---|---|
fe34ffee8d | |||
21952c2024 | |||
756f2bc152 | |||
a47486973c | |||
25f281028c | |||
5a7fb8ddcb | |||
d6f6a0bd00 | |||
11bcbbc8a6 |
1
public/img/icons/field.svg
Normal file
1
public/img/icons/field.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g class="" style="" transform="translate(0,0)"><path d="M257.625 16.75c-132.32 0-239.78 107.46-239.78 239.78s107.46 239.783 239.78 239.783 239.78-107.462 239.78-239.782-107.46-239.78-239.78-239.78zm0 17.906c58.24 0 111.19 22.37 150.75 59-53.35-22.728-121.28 4.247-156.97-18.594 15.65 28.19 42.047 29.17 74.032 27.438 22.816 27.9 61.838 17.83 106.782 17 17.17 21.857 30.288 47.033 38.31 74.406-23.755 6.825-72.6 4.008-92.374-.875 8.236 8.03 19.117 12.027 32.094 14.595-48.222 1.067-94.365 5.457-124.375-11.688 8.84 14.213 20.115 23.206 33.28 28.625-19.962-.433-38.48-3.21-54.905-11.093 26.83 30.444 69.098 30.62 114.47 26.28 31.063 11.3 66.71 13.98 100.717 12.375.03 1.47.063 2.93.063 4.406 0 19.2-2.428 37.834-7 55.595-9.933-2.477-20.396-4.745-31.313-6.78l6.907-25.44-18.03-4.874-7.377 27.126c-14.308-2.26-29.233-4.163-44.593-5.75l3.344-31.375-18.594-2-3.375 31.626c-17.664-1.49-35.795-2.55-54.095-3.22l1.375-34.623-18.656-.75-1.406 34.812c-11.082-.238-22.19-.33-33.282-.28-6.816.03-13.623.142-20.406.28l-1.5-37.72-18.656.75 1.5 37.47c-17.666.618-35.106 1.605-52.03 2.938l-4.033-37.688-18.56 1.97 3.968 37.342c-14.93 1.44-29.428 3.16-43.22 5.157l-8.812-32.5-18.03 4.906 8.28 30.5c-13.984 2.39-27.106 5.07-39.187 8.063-4.562-17.742-6.97-36.357-6.97-55.532 0-30.21 6.03-58.983 16.938-85.217 45.587 15.482 137.805-12.232 208.062 16.468-13.577-12.7-29.093-20.01-45.53-24.53 42.76 4.614 101.767-13.058 162.343 11.688-13.39-12.526-28.787-19.426-44.97-23.97-31.258-26.39-71.34-28.437-109.812-27.437-36.037-25.845-82.634-23.168-124.31-21.655 40.3-41.466 96.683-67.22 159.155-67.22zm-4.22 275.125c10.84-.048 21.707.064 32.532.283l-1 24.75c-16.738-.202-33.458-.102-50.156.312l-1-25.094c6.533-.125 13.063-.22 19.626-.25zm-38.31.783l1 25.156c-16.37.62-32.73 1.506-49.064 2.655l-2.686-25.094c16.482-1.244 33.504-2.136 50.75-2.717zm89.53.03c17.893.626 35.6 1.606 52.813 3l-2.563 23.97c-17.098-1.158-34.168-1.956-51.22-2.407l.97-24.562zm-158.906 4.25l2.655 24.97c-12.164.997-24.323 2.114-36.47 3.375l-6.436-23.75c12.845-1.773 26.32-3.3 40.25-4.594zm230.343.438c14.38 1.44 28.335 3.135 41.687 5.158l-6.063 22.343c-12.733-1.48-25.45-2.752-38.156-3.81l2.533-23.69zm-289.188 6.97l6.22 23c-12.295 1.406-24.567 2.948-36.845 4.594-2.99-6.453-5.692-13.073-8.063-19.844 11.808-2.878 24.784-5.464 38.688-7.75zm349.375 1.28c10.804 1.955 21.1 4.112 30.813 6.5-2.303 6.576-4.926 13.008-7.813 19.283-9.63-1.538-19.232-2.94-28.844-4.22l5.844-21.562z" fill="#ffffff" fill-opacity="1"></path></g></svg>
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/img/icons/food.png
Normal file
BIN
public/img/icons/food.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 777 B |
BIN
public/img/icons/iron.png
Normal file
BIN
public/img/icons/iron.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 853 B |
BIN
public/img/icons/stone.png
Normal file
BIN
public/img/icons/stone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 957 B |
1
public/img/icons/village.svg
Normal file
1
public/img/icons/village.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g class="" style="" transform="translate(0,0)"><path d="M109.902 35.87l-71.14 59.284h142.28l-71.14-59.285zm288 32l-71.14 59.284h142.28l-71.14-59.285zM228.73 84.403l-108.9 90.75h217.8l-108.9-90.75zm-173.828 28.75v62h36.81l73.19-60.992v-1.008h-110zm23 14h16v18h-16v-18zm265 18v10.963l23 19.166v-16.13h16v18h-13.756l.104.087 19.098 15.914h-44.446v14h78v-39h18v39h14v-62h-110zm-194.345 48v20.08l24.095-20.08h-24.095zm28.158 0l105.1 87.582 27.087-22.574v-65.008H176.715zm74.683 14h35.735v34h-35.735v-34zm-76.714 7.74L30.37 335.153H319l-144.314-120.26zm198.046 13.51l-76.857 64.047 32.043 26.704H481.63l-108.9-90.75zm-23.214 108.75l.103.086 19.095 15.914h-72.248v77.467h60.435v-63.466h50v63.467h46v-93.466H349.516zm-278.614 16V476.13h126v-76.976h50v76.977h31.565V353.155H70.902zm30 30h50v50h-50v-50z" fill="#ffffff" fill-opacity="1"></path></g></svg>
|
After Width: | Height: | Size: 944 B |
BIN
public/img/icons/wood.png
Normal file
BIN
public/img/icons/wood.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 902 B |
@ -1,63 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import Game from "./hud/Game.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;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{ #if showBuildingCreator }
|
|
||||||
<BuildingCreator close={ () => { showBuildingCreator = false } } />
|
|
||||||
{ /if }
|
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<Game />
|
||||||
<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>
|
|
||||||
</main>
|
</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;
|
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) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
color: #213547;
|
color: #213547;
|
||||||
|
8
src/board/BuildingTile.svelte
Normal file
8
src/board/BuildingTile.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Building } from "../types";
|
||||||
|
|
||||||
|
export let building: Building;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>{ building.name }</p>
|
||||||
|
<p>{ building.level }</p>
|
50
src/board/Outside.svelte
Normal file
50
src/board/Outside.svelte
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import gameTab from "../stores/gameTab";
|
||||||
|
import showBuildingPanel from "../stores/showBuildingPanel";
|
||||||
|
import { getBuilding, getKeysAsNumbers } from "../utils";
|
||||||
|
import village, { VILLAGE_TILE } from "../village";
|
||||||
|
import BuildingTile from "./BuildingTile.svelte";
|
||||||
|
import Tile from "./Tile.svelte";
|
||||||
|
|
||||||
|
|
||||||
|
function openBuildingPanel(buildingId: number) {
|
||||||
|
showBuildingPanel.set(buildingId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="outside-map">
|
||||||
|
<div>
|
||||||
|
{ #each getKeysAsNumbers($village.outsideTiles) as y }
|
||||||
|
<div>
|
||||||
|
{ #each getKeysAsNumbers($village.outsideTiles[y]) as x }
|
||||||
|
{ #if $village.outsideTiles[y][x] >= 0 }
|
||||||
|
<Tile
|
||||||
|
onTileClick={ () => openBuildingPanel($village.outsideTiles[y][x]) }
|
||||||
|
>
|
||||||
|
<BuildingTile building={ getBuilding($village, $village.outsideTiles[y][x]) } />
|
||||||
|
</Tile>
|
||||||
|
{ :else if $village.outsideTiles[y][x] === VILLAGE_TILE }
|
||||||
|
<Tile
|
||||||
|
onTileClick={ () => gameTab.set('village') }
|
||||||
|
>
|
||||||
|
<img src="/img/icons/village.svg" alt="">
|
||||||
|
</Tile>
|
||||||
|
{ /if }
|
||||||
|
{ /each }
|
||||||
|
</div>
|
||||||
|
{ /each }
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.outside-map {
|
||||||
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 0.8em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outside-map > div {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
47
src/board/Tile.svelte
Normal file
47
src/board/Tile.svelte
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let onTileClick: () => void;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="invisible"
|
||||||
|
on:click={ onTileClick }
|
||||||
|
>
|
||||||
|
<div class="hexagon">
|
||||||
|
<div class="content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hexagon {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background-color: hsl(220, 75%, 75%);
|
||||||
|
border-radius: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2vmin;
|
||||||
|
height: 12em;
|
||||||
|
margin: -0.8em 0.2em;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hexagon:hover {
|
||||||
|
background-color: hsl(60, 75%, 75%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hexagon:active {
|
||||||
|
background-color: hsl(60, 75%, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hexagon .content {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 1.2;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hexagon .content > * {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
53
src/board/Village.svelte
Normal file
53
src/board/Village.svelte
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Hex } from "../hexgrid";
|
||||||
|
import showBuildingCreator from "../stores/showBuildingCreator";
|
||||||
|
import showBuildingPanel from "../stores/showBuildingPanel";
|
||||||
|
import { getBuilding, getKeysAsNumbers } from "../utils";
|
||||||
|
import village from "../village";
|
||||||
|
import BuildingTile from "./BuildingTile.svelte";
|
||||||
|
import Tile from "./Tile.svelte";
|
||||||
|
|
||||||
|
|
||||||
|
function openBuildingCreator(tile: Hex) {
|
||||||
|
showBuildingCreator.set(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBuildingPanel(buildingId: number) {
|
||||||
|
showBuildingPanel.set(buildingId);
|
||||||
|
}
|
||||||
|
</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={ () => openBuildingPanel($village.villageTiles[y][x]) }
|
||||||
|
>
|
||||||
|
<BuildingTile building={ getBuilding($village, $village.villageTiles[y][x]) } />
|
||||||
|
</Tile>
|
||||||
|
{ :else }
|
||||||
|
<Tile
|
||||||
|
onTileClick={ () => openBuildingCreator(new Hex(x, y)) }
|
||||||
|
/>
|
||||||
|
{ /if }
|
||||||
|
{ /each }
|
||||||
|
</div>
|
||||||
|
{ /each }
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.village-map {
|
||||||
|
display: grid;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 0.8em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.village-map > div {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
@ -3,9 +3,11 @@ import { getEmptyResources } from "./utils";
|
|||||||
import type { VillageState } from "./village";
|
import type { VillageState } from "./village";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default [
|
||||||
'townhall': {
|
{
|
||||||
|
type: 'townhall',
|
||||||
name: 'Town Hall',
|
name: 'Town Hall',
|
||||||
|
autoBuilt: true,
|
||||||
cost: (level: number) => {
|
cost: (level: number) => {
|
||||||
return {
|
return {
|
||||||
wood: level * 10,
|
wood: level * 10,
|
||||||
@ -25,8 +27,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'woodcutter': {
|
{
|
||||||
|
type: 'woodcutter',
|
||||||
name: 'Woodcutter',
|
name: 'Woodcutter',
|
||||||
|
autoBuilt: true,
|
||||||
cost: (level: number) => {
|
cost: (level: number) => {
|
||||||
return {
|
return {
|
||||||
wood: level * 10,
|
wood: level * 10,
|
||||||
@ -48,8 +52,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'mine': {
|
{
|
||||||
|
type: 'mine',
|
||||||
name: 'Mine',
|
name: 'Mine',
|
||||||
|
autoBuilt: true,
|
||||||
cost: (level: number) => {
|
cost: (level: number) => {
|
||||||
return {
|
return {
|
||||||
wood: level * 10,
|
wood: level * 10,
|
||||||
@ -71,8 +77,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'pit': {
|
{
|
||||||
|
type: 'pit',
|
||||||
name: 'Pit',
|
name: 'Pit',
|
||||||
|
autoBuilt: true,
|
||||||
cost: (level: number) => {
|
cost: (level: number) => {
|
||||||
return {
|
return {
|
||||||
wood: level * 10,
|
wood: level * 10,
|
||||||
@ -94,8 +102,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'field': {
|
{
|
||||||
|
type: 'field',
|
||||||
name: 'Field',
|
name: 'Field',
|
||||||
|
autoBuilt: true,
|
||||||
cost: (level: number) => {
|
cost: (level: number) => {
|
||||||
return {
|
return {
|
||||||
wood: level * 10,
|
wood: level * 10,
|
||||||
@ -113,7 +123,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'warehouse': {
|
{
|
||||||
|
type: 'warehouse',
|
||||||
name: 'Warehouse',
|
name: 'Warehouse',
|
||||||
cost: (level: number) => {
|
cost: (level: number) => {
|
||||||
return {
|
return {
|
||||||
@ -136,7 +147,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'granary': {
|
{
|
||||||
|
type: 'granary',
|
||||||
name: 'Granary',
|
name: 'Granary',
|
||||||
cost: (level: number) => {
|
cost: (level: number) => {
|
||||||
return {
|
return {
|
||||||
@ -159,4 +171,4 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
];
|
||||||
|
@ -1,13 +1,29 @@
|
|||||||
|
import buildings from "./buildings";
|
||||||
|
import { Hex } from "./hexgrid";
|
||||||
import type { Building, BuildingSource } from "./types";
|
import type { Building, BuildingSource } from "./types";
|
||||||
|
|
||||||
|
|
||||||
let uid = 0;
|
let uid = 0;
|
||||||
|
|
||||||
|
|
||||||
export function createBuilding(building: BuildingSource): Building {
|
export function getBuildingSource(buildingType: string): BuildingSource {
|
||||||
|
const source: BuildingSource | undefined = buildings.find(b => b.type === buildingType);
|
||||||
|
|
||||||
|
if (source === undefined) {
|
||||||
|
throw new Error(`Unknown building type: "${buildingType}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function createBuilding(buildingType: string): Building {
|
||||||
|
const source: BuildingSource = getBuildingSource(buildingType);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...building,
|
...source,
|
||||||
level: 1,
|
|
||||||
id: uid++,
|
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,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import buildings from "../buildings";
|
import buildings from "../buildings";
|
||||||
import moves from "../moves";
|
import moves from "../moves";
|
||||||
|
import showBuildingCreator from "../stores/showBuildingCreator";
|
||||||
|
|
||||||
export let close: () => void;
|
function close() {
|
||||||
|
showBuildingCreator.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function build(type: string) {
|
function build(type: string) {
|
||||||
if (moves.build(type)) {
|
if ($showBuildingCreator === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moves.build(type, $showBuildingCreator)) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const constructible = buildings.filter(b => !b.autoBuilt);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{ #if $showBuildingCreator !== null }
|
||||||
<section>
|
<section>
|
||||||
<div class="building-creator">
|
<div class="building-creator">
|
||||||
<header>
|
<header>
|
||||||
@ -21,15 +31,16 @@
|
|||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
<div class="buildings">
|
<div class="buildings">
|
||||||
{ #each Object.entries(buildings) as [type, building] }
|
{ #each constructible as building }
|
||||||
<div>
|
<div>
|
||||||
<p>{ building.name }</p>
|
<p>{ building.name }</p>
|
||||||
<button on:click={ () => build(type) }>Build</button>
|
<button on:click={ () => build(building.type) }>Build</button>
|
||||||
</div>
|
</div>
|
||||||
{ /each }
|
{ /each }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{ /if }
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
|
74
src/hud/BuildingPanel.svelte
Normal file
74
src/hud/BuildingPanel.svelte
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import buildings from "../buildings";
|
||||||
|
import moves from "../moves";
|
||||||
|
import showBuildingPanel from "../stores/showBuildingPanel";
|
||||||
|
import { getBuilding } from "../utils";
|
||||||
|
import village from "../village";
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
showBuildingPanel.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function upgrade() {
|
||||||
|
if ($showBuildingPanel === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moves.upgradeBuilding($showBuildingPanel)) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: building = ($showBuildingPanel !== null) ? getBuilding($village, $showBuildingPanel) : null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{ #if building !== null }
|
||||||
|
<section>
|
||||||
|
<div class="building-panel">
|
||||||
|
<header>
|
||||||
|
<h1>Building</h1>
|
||||||
|
<span class="close">
|
||||||
|
<button on:click={ close }>X</button>
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div class="building">
|
||||||
|
<div>
|
||||||
|
<p>{ building.name } ({ building.level })</p>
|
||||||
|
<button on:click={ () => upgrade() }>Upgrade</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{ /if }
|
||||||
|
|
||||||
|
<style>
|
||||||
|
section {
|
||||||
|
background-color: hsl(0, 0%, 10%, 0.8);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-panel {
|
||||||
|
background-color: hsl(0, 0%, 20%);
|
||||||
|
border: 0.2em solid grey;
|
||||||
|
border-radius: .4em;
|
||||||
|
width: 80%;
|
||||||
|
height: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-panel header {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.building-panel header .close {
|
||||||
|
position: absolute;
|
||||||
|
right: 1em;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
67
src/hud/Game.svelte
Normal file
67
src/hud/Game.svelte
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
import Outside from "../board/Outside.svelte";
|
||||||
|
import Village from "../board/Village.svelte";
|
||||||
|
import gameTab from "../stores/gameTab";
|
||||||
|
import type { GameTab } from "../types";
|
||||||
|
import update from "../update";
|
||||||
|
import BuildingCreator from "./BuildingCreator.svelte";
|
||||||
|
import BuildingPanel from "./BuildingPanel.svelte";
|
||||||
|
import Navigation from "./Navigation.svelte";
|
||||||
|
import Resources from "./Resources.svelte";
|
||||||
|
import Queue from "./Queue.svelte";
|
||||||
|
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let frame: number;
|
||||||
|
|
||||||
|
function loop(timestamp: number) {
|
||||||
|
frame = requestAnimationFrame(loop);
|
||||||
|
update(timestamp);
|
||||||
|
}
|
||||||
|
frame = requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setTab(newTab: GameTab) {
|
||||||
|
gameTab.set(newTab);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="hud">
|
||||||
|
<header>
|
||||||
|
<Resources />
|
||||||
|
<Navigation { setTab } />
|
||||||
|
</header>
|
||||||
|
<div class="board">
|
||||||
|
{ #if $gameTab === 'village' }
|
||||||
|
<Village />
|
||||||
|
{ :else if $gameTab === 'resources' }
|
||||||
|
<Outside />
|
||||||
|
{ /if }
|
||||||
|
</div>
|
||||||
|
<div class="queue">
|
||||||
|
<Queue />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="overlay">
|
||||||
|
<BuildingCreator />
|
||||||
|
<BuildingPanel />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
55
src/hud/Navigation.svelte
Normal file
55
src/hud/Navigation.svelte
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { GameTab } from "../types";
|
||||||
|
|
||||||
|
export let setTab: (tab: GameTab) => void;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<button
|
||||||
|
class="invisible"
|
||||||
|
on:click={ () => setTab('village') }
|
||||||
|
>
|
||||||
|
<img src="/img/icons/village.svg" alt="">
|
||||||
|
<span>Village</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="invisible"
|
||||||
|
on:click={ () => setTab('resources') }
|
||||||
|
>
|
||||||
|
<img src="/img/icons/field.svg" alt="">
|
||||||
|
<span>Resources</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 2em;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav button {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 0.4em solid grey;
|
||||||
|
border-radius: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 4em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav button img {
|
||||||
|
height: 4em;
|
||||||
|
width: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav button span {
|
||||||
|
background-color: black;
|
||||||
|
border-radius: 1em;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
translate: -50% 100%;
|
||||||
|
}
|
||||||
|
</style>
|
50
src/hud/Queue.svelte
Normal file
50
src/hud/Queue.svelte
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getBuilding } from "../utils";
|
||||||
|
import village from "../village";
|
||||||
|
|
||||||
|
$: queue = $village.queue.map(q => {
|
||||||
|
return {
|
||||||
|
...q,
|
||||||
|
building: getBuilding($village, q.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="queue">
|
||||||
|
{ #each queue as item }
|
||||||
|
<div>
|
||||||
|
<p>{ item.building.name }</p>
|
||||||
|
<p class="time">{ Math.ceil(item.remainingTime / 1000) }’</p>
|
||||||
|
</div>
|
||||||
|
{ /each }
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.queue {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue div {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border: 0.4em solid grey;
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 4em;
|
||||||
|
max-width: 4em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue div p {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue div .time {
|
||||||
|
background: hsl(0, 0%, 20%);
|
||||||
|
border-radius: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
position: absolute;
|
||||||
|
translate: -50% 150%;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,21 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getStorage } from "../utils";
|
import { getProduction, getStorage } from "../utils";
|
||||||
import village from "../village";
|
import village from "../village";
|
||||||
|
|
||||||
|
|
||||||
$: capacity = getStorage($village);
|
$: capacity = getStorage($village);
|
||||||
|
$: production = getProduction($village);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="resources">
|
<div class="resources">
|
||||||
<div>Wood: { Math.floor($village.resources.wood) } / { capacity.wood }</div>
|
<div>
|
||||||
<div>Stone: { Math.floor($village.resources.stone) } / { capacity.stone }</div>
|
<img src="/img/icons/wood.png" alt="Wood" />
|
||||||
<div>Iron: { Math.floor($village.resources.iron) } / { capacity.iron }</div>
|
{ Math.floor($village.resources.wood) } / { capacity.wood }
|
||||||
<div>Food: { Math.floor($village.resources.food) } / { capacity.food }</div>
|
({ production.wood >= 0 ? '+' : '' }{ production.wood })
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img src="/img/icons/stone.png" alt="Stone" />
|
||||||
|
{ Math.floor($village.resources.stone) } / { capacity.stone }
|
||||||
|
({ production.stone >= 0 ? '+' : '' }{ production.stone })
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img src="/img/icons/iron.png" alt="Iron" />
|
||||||
|
{ Math.floor($village.resources.iron) } / { capacity.iron }
|
||||||
|
({ production.iron >= 0 ? '+' : '' }{ production.iron })
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img src="/img/icons/food.png" alt="Food" />
|
||||||
|
{ Math.floor($village.resources.food) } / { capacity.food }
|
||||||
|
({ production.food >= 0 ? '+' : '' }{ production.food })
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.resources {
|
.resources {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resources div {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resources img {
|
||||||
|
height: 1.5em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import buildings from "../buildings";
|
import buildings from "../buildings";
|
||||||
import { createBuilding } from "../create";
|
import { createBuilding, getBuildingSource } from "../create";
|
||||||
import type { VillageState } from "../village";
|
import type { Hex } from "../hexgrid";
|
||||||
|
import { enqueueBuilding } from "../utils";
|
||||||
|
import { DEFAULT_TILE, type VillageState } from "../village";
|
||||||
|
|
||||||
|
|
||||||
export default function build(V: VillageState, buildingType: keyof typeof buildings) {
|
export default function build(V: VillageState, buildingType: string, tile: Hex) {
|
||||||
const building = buildings[buildingType];
|
const building = getBuildingSource(buildingType);
|
||||||
const cost = building.cost(1);
|
const cost = building.cost(1);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -16,12 +18,22 @@ export default function build(V: VillageState, buildingType: keyof typeof buildi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (V.villageTiles[tile.y][tile.x] !== DEFAULT_TILE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
V.resources.wood -= cost.wood;
|
V.resources.wood -= cost.wood;
|
||||||
V.resources.stone -= cost.stone;
|
V.resources.stone -= cost.stone;
|
||||||
V.resources.iron -= cost.iron;
|
V.resources.iron -= cost.iron;
|
||||||
V.resources.food -= cost.food;
|
V.resources.food -= cost.food;
|
||||||
|
|
||||||
V.buildings.push(createBuilding(building));
|
const newBuilding = createBuilding(buildingType);
|
||||||
|
newBuilding.tile = tile;
|
||||||
|
newBuilding.level = 0;
|
||||||
|
|
||||||
|
V.buildings.push(newBuilding);
|
||||||
|
V.villageTiles[tile.y][tile.x] = newBuilding.id;
|
||||||
|
enqueueBuilding(V, newBuilding);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { enqueueBuilding } from "../utils";
|
||||||
import type { VillageState } from "../village";
|
import type { VillageState } from "../village";
|
||||||
|
|
||||||
|
|
||||||
@ -7,7 +8,10 @@ export default function upgradeBuilding(V: VillageState, buildingId: number) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cost = building.cost(building.level + 1);
|
const ongoingUpgrades = V.queue.filter(q => q.id === building.id);
|
||||||
|
const level = building.level + 1 + ongoingUpgrades.length;
|
||||||
|
|
||||||
|
const cost = building.cost(level);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cost.wood > V.resources.wood
|
cost.wood > V.resources.wood
|
||||||
@ -22,8 +26,7 @@ export default function upgradeBuilding(V: VillageState, buildingId: number) {
|
|||||||
V.resources.stone -= cost.stone;
|
V.resources.stone -= cost.stone;
|
||||||
V.resources.iron -= cost.iron;
|
V.resources.iron -= cost.iron;
|
||||||
V.resources.food -= cost.food;
|
V.resources.food -= cost.food;
|
||||||
|
enqueueBuilding(V, building);
|
||||||
building.level++;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
5
src/stores/gameTab.ts
Normal file
5
src/stores/gameTab.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import type { GameTab } from "../types";
|
||||||
|
|
||||||
|
|
||||||
|
export default writable<GameTab>('village');
|
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);
|
4
src/stores/showBuildingPanel.ts
Normal file
4
src/stores/showBuildingPanel.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
|
||||||
|
export default writable<number | null>(null);
|
@ -1,3 +1,9 @@
|
|||||||
|
import type { Hex } from "./hexgrid";
|
||||||
|
|
||||||
|
|
||||||
|
export type GameTab = 'village' | 'resources';
|
||||||
|
|
||||||
|
|
||||||
export interface Cost {
|
export interface Cost {
|
||||||
wood: number;
|
wood: number;
|
||||||
stone: number;
|
stone: number;
|
||||||
@ -11,6 +17,8 @@ export type Production = Cost;
|
|||||||
|
|
||||||
export interface BuildingSource {
|
export interface BuildingSource {
|
||||||
name: string;
|
name: string;
|
||||||
|
type: string;
|
||||||
|
autoBuilt?: boolean;
|
||||||
cost: (level: number) => Cost;
|
cost: (level: number) => Cost;
|
||||||
behavior: {
|
behavior: {
|
||||||
production?: Function;
|
production?: Function;
|
||||||
@ -22,4 +30,5 @@ export interface BuildingSource {
|
|||||||
export interface Building extends BuildingSource {
|
export interface Building extends BuildingSource {
|
||||||
id: number;
|
id: number;
|
||||||
level: number;
|
level: number;
|
||||||
|
tile: Hex;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
|
|
||||||
import { getProduction, getStorage } from './utils';
|
import { getBuilding, getProduction, getStorage } from './utils';
|
||||||
import village, { type VillageState } from "./village";
|
import village, { type VillageState } from "./village";
|
||||||
import type { Production } from './types';
|
import type { Production } from './types';
|
||||||
|
|
||||||
@ -18,6 +18,17 @@ export default function update(timestamp: number) {
|
|||||||
|
|
||||||
village.update(state => {
|
village.update(state => {
|
||||||
return produce(state, (V: VillageState) => {
|
return produce(state, (V: VillageState) => {
|
||||||
|
// Advance building construction.
|
||||||
|
if (V.queue.length) {
|
||||||
|
V.queue[0].remainingTime -= delta;
|
||||||
|
if (V.queue[0].remainingTime <= 0) {
|
||||||
|
const building = getBuilding(V, V.queue[0].id);
|
||||||
|
building.level++;
|
||||||
|
V.queue.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make all buildings produce and consume.
|
||||||
const productionPerMinute = getProduction(V);
|
const productionPerMinute = getProduction(V);
|
||||||
const storage = getStorage(V);
|
const storage = getStorage(V);
|
||||||
|
|
||||||
@ -30,6 +41,9 @@ export default function update(timestamp: number) {
|
|||||||
if (V.resources[resource] > storage[resource]) {
|
if (V.resources[resource] > storage[resource]) {
|
||||||
V.resources[resource] = storage[resource];
|
V.resources[resource] = storage[resource];
|
||||||
}
|
}
|
||||||
|
else if (V.resources[resource] < 0) {
|
||||||
|
V.resources[resource] = 0;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return V;
|
return V;
|
||||||
|
61
src/utils.ts
61
src/utils.ts
@ -1,4 +1,4 @@
|
|||||||
import type { Production } from "./types";
|
import type { Building, Production } from "./types";
|
||||||
import type { VillageState } from "./village";
|
import type { VillageState } from "./village";
|
||||||
|
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ export function getEmptyResources(): Production {
|
|||||||
|
|
||||||
export function getProduction(villageState: VillageState): Production {
|
export function getProduction(villageState: VillageState): Production {
|
||||||
return villageState.buildings
|
return villageState.buildings
|
||||||
.filter(b => b.behavior.production)
|
.filter(b => b.behavior.production && b.level > 0)
|
||||||
.map(b => {
|
.map(b => {
|
||||||
if (b.behavior.production) {
|
if (b.behavior.production) {
|
||||||
return b.behavior.production(villageState, b);
|
return b.behavior.production(villageState, b);
|
||||||
@ -36,7 +36,7 @@ export function getProduction(villageState: VillageState): Production {
|
|||||||
|
|
||||||
export function getStorage(villageState: VillageState): Production {
|
export function getStorage(villageState: VillageState): Production {
|
||||||
return villageState.buildings
|
return villageState.buildings
|
||||||
.filter(b => b.behavior.storage)
|
.filter(b => b.behavior.storage && b.level > 0)
|
||||||
.map(b => {
|
.map(b => {
|
||||||
if (b.behavior.storage) {
|
if (b.behavior.storage) {
|
||||||
return b.behavior.storage(villageState, b);
|
return b.behavior.storage(villageState, b);
|
||||||
@ -44,3 +44,58 @@ export function getStorage(villageState: VillageState): Production {
|
|||||||
})
|
})
|
||||||
.reduce(_reduceResources, getEmptyResources());
|
.reduce(_reduceResources, getEmptyResources());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getBuilding(V: VillageState, buildingId: number): Building {
|
||||||
|
const building = V.buildings.find(b => b.id === buildingId);
|
||||||
|
if (!building) {
|
||||||
|
throw new Error(`Cannot find building with id "${buildingId}"`);
|
||||||
|
}
|
||||||
|
return building;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getKeysAsNumbers(dict: Object): Array<number> {
|
||||||
|
return Object.keys(dict).map(i => parseInt(i)).sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of shuffled values, using a version of the
|
||||||
|
* [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle).
|
||||||
|
*
|
||||||
|
* @since 0.1.0
|
||||||
|
* @category Array
|
||||||
|
* @param {Array} array The array to shuffle.
|
||||||
|
* @returns {Array} Returns the new shuffled array.
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* shuffle([1, 2, 3, 4])
|
||||||
|
* // => [4, 1, 3, 2]
|
||||||
|
*/
|
||||||
|
export function shuffle<T>(array: Array<T>): Array<T> {
|
||||||
|
const length = array.length;
|
||||||
|
if (!length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let index = -1;
|
||||||
|
const lastIndex = length - 1;
|
||||||
|
const result = [ ...array ];
|
||||||
|
while (++index < length) {
|
||||||
|
const rand = index + Math.floor(Math.random() * (lastIndex - index + 1));
|
||||||
|
[ result[rand], result[index] ] = [ result[index], result[rand] ];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function enqueueBuilding(V: VillageState, building: Building) {
|
||||||
|
const ongoingUpgrades = V.queue.filter(q => q.id === building.id);
|
||||||
|
const level = building.level + 1 + ongoingUpgrades.length;
|
||||||
|
const remainingTime = 1000 * level;
|
||||||
|
|
||||||
|
V.queue.push({
|
||||||
|
id: building.id,
|
||||||
|
remainingTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
107
src/village.ts
107
src/village.ts
@ -3,6 +3,21 @@ import { writable } from "svelte/store";
|
|||||||
import buildings from "./buildings";
|
import buildings from "./buildings";
|
||||||
import { createBuilding } from "./create";
|
import { createBuilding } from "./create";
|
||||||
import type { Building } from "./types";
|
import type { Building } from "./types";
|
||||||
|
import { getTilesAtDistance, Hex } from "./hexgrid";
|
||||||
|
import { getKeysAsNumbers, shuffle } from "./utils";
|
||||||
|
|
||||||
|
|
||||||
|
type Board = {
|
||||||
|
[key: number]: {
|
||||||
|
[key: number]: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface QueuedBuilding {
|
||||||
|
id: number;
|
||||||
|
remainingTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface VillageState {
|
export interface VillageState {
|
||||||
@ -14,17 +29,53 @@ export interface VillageState {
|
|||||||
food: number;
|
food: number;
|
||||||
culture: number;
|
culture: number;
|
||||||
};
|
};
|
||||||
|
villageTiles: Board;
|
||||||
|
outsideTiles: Board;
|
||||||
|
queue: QueuedBuilding[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const village = writable<VillageState>({
|
export const DEFAULT_TILE = -1;
|
||||||
buildings: [
|
export const VILLAGE_TILE = -2;
|
||||||
createBuilding(buildings.townhall),
|
|
||||||
createBuilding(buildings.woodcutter),
|
|
||||||
createBuilding(buildings.pit),
|
function getInitialVillageBoard() {
|
||||||
createBuilding(buildings.mine),
|
const board: Board = {
|
||||||
createBuilding(buildings.field),
|
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 state: VillageState = {
|
||||||
|
buildings: [],
|
||||||
resources: {
|
resources: {
|
||||||
wood: 60,
|
wood: 60,
|
||||||
stone: 60,
|
stone: 60,
|
||||||
@ -32,7 +83,45 @@ const village = writable<VillageState>({
|
|||||||
food: 50,
|
food: 50,
|
||||||
culture: 0,
|
culture: 0,
|
||||||
},
|
},
|
||||||
});
|
villageTiles: getInitialVillageBoard(),
|
||||||
|
outsideTiles: getInitialOutsideBoard(),
|
||||||
|
queue: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the Town hall.
|
||||||
|
const townhall = createBuilding('townhall');
|
||||||
|
state.villageTiles[0][0] = townhall.id;
|
||||||
|
state.buildings.push(townhall);
|
||||||
|
|
||||||
|
// Create all the resource buildings.
|
||||||
|
const resourceBuildingTypes: Array<string> = shuffle([
|
||||||
|
'woodcutter', 'woodcutter', 'woodcutter', 'woodcutter',
|
||||||
|
'mine', 'mine', 'mine', 'mine',
|
||||||
|
'pit', 'pit', 'pit', 'pit',
|
||||||
|
'field', 'field', 'field', 'field', 'field', 'field',
|
||||||
|
]);
|
||||||
|
getKeysAsNumbers(state.outsideTiles).forEach(y => {
|
||||||
|
getKeysAsNumbers(state.outsideTiles[y]).forEach(x => {
|
||||||
|
if (state.outsideTiles[y][x] !== DEFAULT_TILE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = resourceBuildingTypes.pop();
|
||||||
|
if (type === undefined) {
|
||||||
|
throw new Error("Not enough building types for outside resource buildings");
|
||||||
|
}
|
||||||
|
const newBuilding = createBuilding(type);
|
||||||
|
newBuilding.tile = new Hex(x, y);
|
||||||
|
state.outsideTiles[y][x] = newBuilding.id;
|
||||||
|
state.buildings.push(newBuilding);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const village = writable<VillageState>(getInitialState());
|
||||||
|
|
||||||
|
|
||||||
export default village;
|
export default village;
|
||||||
|
Loading…
Reference in New Issue
Block a user