Add a first unit, Philosophers, that you can recruit, but they do nothing for now.

This commit is contained in:
Adrian 2024-10-25 19:03:32 +02:00
parent fe34ffee8d
commit cd872d76e8
16 changed files with 318 additions and 35 deletions

View File

@ -1,7 +1,7 @@
<script lang="ts">
import type { Building } from "../types";
import type { BuildingType } from "../types";
export let building: Building;
export let building: BuildingType;
</script>
<p>{ building.name }</p>

View File

@ -1,6 +1,6 @@
import buildings from "./buildings";
import buildings from "./data/buildings";
import { Hex } from "./hexgrid";
import type { Building, BuildingSource } from "./types";
import type { BuildingType, BuildingSource } from "./types";
let uid = 0;
@ -17,7 +17,7 @@ export function getBuildingSource(buildingType: string): BuildingSource {
}
export function createBuilding(buildingType: string): Building {
export function createBuilding(buildingType: string): BuildingType {
const source: BuildingSource = getBuildingSource(buildingType);
return {
@ -25,5 +25,6 @@ export function createBuilding(buildingType: string): Building {
id: uid++,
level: 1,
tile: new Hex(0, 0),
state: {},
};
}

View File

@ -1,6 +1,6 @@
import type { Building } from "./types";
import { getEmptyResources } from "./utils";
import type { VillageState } from "./village";
import type { BuildingType } from "../types";
import { getEmptyResources } from "../utils";
import type { VillageState } from "../village";
export default [
@ -17,7 +17,7 @@ export default [
};
},
behavior: {
storage: (_V: VillageState, _self: Building) => {
storage: (_V: VillageState, _self: BuildingType) => {
return {
'wood': 100,
'stone': 100,
@ -40,7 +40,7 @@ export default [
};
},
behavior: {
production: (V: VillageState, self: Building) => {
production: (V: VillageState, self: BuildingType) => {
const prod = getEmptyResources();
const outputPerMinute = 5 * (self.level * self.level);
prod.wood = outputPerMinute;
@ -65,7 +65,7 @@ export default [
};
},
behavior: {
production: (V: VillageState, self: Building) => {
production: (V: VillageState, self: BuildingType) => {
const prod = getEmptyResources();
const outputPerMinute = 5 * (self.level * self.level);
prod.iron = outputPerMinute;
@ -90,7 +90,7 @@ export default [
};
},
behavior: {
production: (V: VillageState, self: Building) => {
production: (V: VillageState, self: BuildingType) => {
const prod = getEmptyResources();
const outputPerMinute = 5 * (self.level * self.level);
prod.stone = outputPerMinute;
@ -115,7 +115,7 @@ export default [
};
},
behavior: {
production: (V: VillageState, self: Building) => {
production: (V: VillageState, self: BuildingType) => {
const prod = getEmptyResources();
const outputPerMinute = 5 * (self.level * self.level);
prod.food = outputPerMinute;
@ -135,7 +135,7 @@ export default [
};
},
behavior: {
storage: (V: VillageState, self: Building) => {
storage: (V: VillageState, self: BuildingType) => {
const x = self.level;
const capacity = ( ( ( x + ( x * x ) ) / 2 ) + 3 ) * 25;
return {
@ -159,7 +159,7 @@ export default [
};
},
behavior: {
storage: (V: VillageState, self: Building) => {
storage: (V: VillageState, self: BuildingType) => {
const x = self.level;
const capacity = ( ( ( x + ( x * x ) ) / 2 ) + 3 ) * 25;
return {
@ -171,4 +171,28 @@ export default [
},
},
},
{
type: 'university',
name: 'University',
cost: (level: number) => {
return {
wood: level * 100,
stone: level * 100,
iron: level * 100,
food: 0,
};
},
behavior: {
production: (V: VillageState, self: BuildingType) => {
const prod = getEmptyResources();
const intakePerMinute = self.level * 2;
prod.food = -intakePerMinute;
return prod;
},
units: {
type: 'philosopher',
recruitmentTime: (V: VillageState, self: BuildingType) => 2 - 0.06 * self.level,
},
},
}
];

16
src/data/units.ts Normal file
View File

@ -0,0 +1,16 @@
export default [
{
type: 'philosopher',
name: 'Philosopher',
cost: {
wood: 20,
stone: 50,
iron: 0,
food: 0,
},
behavior: {
culturePerMinute: 1,
foodIntakePerMinute: 2,
},
},
];

View File

@ -1,9 +1,9 @@
<script lang="ts">
import buildings from "../buildings";
import moves from "../moves";
import showBuildingPanel from "../stores/showBuildingPanel";
import { getBuilding } from "../utils";
import village from "../village";
import UniversityPanel from "./UniversityPanel.svelte";
function close() {
showBuildingPanel.set(null);
@ -32,7 +32,14 @@
<button on:click={ close }>X</button>
</span>
</header>
<div class="building">
<div class="content">
{ #if building.level === 0 }
<p>Building in construction…</p>
{ :else if building.type === 'university' }
<UniversityPanel { building } />
{ /if }
</div>
<div class="upgrade">
<div>
<p>{ building.name } ({ building.level })</p>
<button on:click={ () => upgrade() }>Upgrade</button>

39
src/hud/Cost.svelte Normal file
View File

@ -0,0 +1,39 @@
<script lang="ts">
import type { CostType } from "../types";
export let cost: CostType;
</script>
<div class="cost">
<div>
<img src="/img/icons/wood.png" alt="Wood" />
{ cost.wood }
</div>
<div>
<img src="/img/icons/stone.png" alt="Stone" />
{ cost.stone }
</div>
<div>
<img src="/img/icons/iron.png" alt="Iron" />
{ cost.iron }
</div>
<div>
<img src="/img/icons/food.png" alt="Food" />
{ cost.food }
</div>
</div>
<style>
.cost {
align-items: center;
display: flex;
gap: 1em;
justify-content: center;
}
.cost div {
display: flex;
align-items: center;
gap: 0.2em;
}
</style>

View File

@ -9,8 +9,9 @@
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";
import Resources from "./Resources.svelte";
import Units from "./Units.svelte";
onMount(() => {
@ -34,6 +35,7 @@
<section class="hud">
<header>
<Units />
<Resources />
<Navigation { setTab } />
</header>

18
src/hud/Units.svelte Normal file
View File

@ -0,0 +1,18 @@
<script lang="ts">
import units from "../data/units";
import village from "../village";
$: currentUnits = Object.entries($village.units).map(([type, count]) => {
const unit = units.find(u => u.type === type);
return {
...unit,
count,
};
});
</script>
<section>
{ #each currentUnits as unit }
<p>{ unit.name }: { unit.count }</p>
{ /each }
</section>

View File

@ -0,0 +1,72 @@
<script lang="ts">
import moves from "../moves";
import type { BuildingType, CostType, UnitType } from "../types";
import { getUnitSource } from "../utils";
import type { VillageState } from "../village";
import village from "../village";
import Cost from "./Cost.svelte";
export let building: BuildingType;
let numberOfUnits = 1;
$: unitType = building.behavior.units?.type || '';
$: unit = getUnitSource(unitType);
$: maximumUnits = getMaxCountOfUnits($village, unit);
$: if (numberOfUnits > maximumUnits) {
numberOfUnits = maximumUnits || 1;
}
$: cost = {
wood: unit.cost.wood * numberOfUnits,
stone: unit.cost.stone * numberOfUnits,
iron: unit.cost.iron * numberOfUnits,
food: unit.cost.food * numberOfUnits,
};
function recruit() {
moves.recruitUnits(building.id, unitType, numberOfUnits);
}
function getMaxCountOfUnits(V: VillageState, unitData: UnitType): number {
let res = Infinity;
Object.entries(V.resources).forEach(([resource, value]) => {
const cost = unitData.cost[resource as keyof CostType] || 0;
if (cost > 0) {
res = Math.min(
res,
Math.floor(value / cost)
);
}
});
return res;
}
</script>
<div class="university">
<p>Create Philosophers</p>
<div class="cost"><Cost { cost } /></div>
<input
type="range"
name="units"
min="1"
max={ maximumUnits }
bind:value={ numberOfUnits }
/>
<input
type="number"
name="units"
min="1"
max={ maximumUnits }
bind:value={ numberOfUnits }
/>
<button on:click={ recruit } disabled={ numberOfUnits > maximumUnits }>Recruit</button>
</div>
<style>
.university .cost {
margin: 1em;
}
</style>

View File

@ -1,4 +1,4 @@
import buildings from "../buildings";
import buildings from "../data/buildings";
import { createBuilding, getBuildingSource } from "../create";
import type { Hex } from "../hexgrid";
import { enqueueBuilding } from "../utils";

View File

@ -3,6 +3,7 @@ import { produce } from 'immer';
import village, { type VillageState } from '../village';
import build from './build';
import upgradeBuilding from './upgradeBuilding';
import recruitUnits from './recruitUnits';
// Encapsulates a move function into a store update, where the data is made
@ -30,4 +31,5 @@ export function makeMove(move: (...args: any[]) => boolean) {
export default {
build: makeMove(build),
upgradeBuilding: makeMove(upgradeBuilding),
recruitUnits: makeMove(recruitUnits),
};

46
src/moves/recruitUnits.ts Normal file
View File

@ -0,0 +1,46 @@
import units from "../data/units";
import { getBuilding } from "../utils";
import type { VillageState } from "../village";
export default function recruitUnits(
V: VillageState, buildingId: number, unitType: string, unitNumber: number
) {
const unit = units.find(u => u.type === unitType);
if (!unit) {
return false;
}
const cost = {
wood: unit.cost.wood * unitNumber,
stone: unit.cost.stone * unitNumber,
iron: unit.cost.iron * unitNumber,
food: unit.cost.food * unitNumber,
};
if (
cost.wood > V.resources.wood
|| cost.stone > V.resources.stone
|| cost.iron > V.resources.iron
|| cost.food > V.resources.food
) {
return false;
}
V.resources.wood -= cost.wood;
V.resources.stone -= cost.stone;
V.resources.iron -= cost.iron;
V.resources.food -= cost.food;
const building = getBuilding(V, buildingId);
if (!building.state.recruitment) {
building.state.recruitment = {
count: 0,
elapsedTime: 0,
};
}
building.state.recruitment.count += unitNumber;
return true;
}

View File

@ -4,7 +4,7 @@ import type { Hex } from "./hexgrid";
export type GameTab = 'village' | 'resources';
export interface Cost {
export interface CostType {
wood: number;
stone: number;
iron: number;
@ -12,23 +12,44 @@ export interface Cost {
}
export type Production = Cost;
export type ProductionType = CostType;
export interface BuildingSource {
name: string;
type: string;
autoBuilt?: boolean;
cost: (level: number) => Cost;
cost: (level: number) => CostType;
behavior: {
production?: Function;
storage?: Function;
units?: {
type: string;
recruitmentTime: Function;
};
};
}
export interface Building extends BuildingSource {
export interface BuildingType extends BuildingSource {
id: number;
level: number;
tile: Hex;
state: {
recruitment?: {
count: number;
elapsedTime: number;
};
};
}
export interface UnitType {
type: string;
name: string;
cost: CostType;
behavior: {
foodIntakePerMinute: number;
culturePerMinute?: number;
}
}

View File

@ -2,7 +2,7 @@ import { produce } from 'immer';
import { getBuilding, getProduction, getStorage } from './utils';
import village, { type VillageState } from "./village";
import type { Production } from './types';
import type { ProductionType } from './types';
let lastFrame: number;
@ -33,7 +33,7 @@ export default function update(timestamp: number) {
const storage = getStorage(V);
Object.keys(productionPerMinute).forEach((key) => {
const resource = key as keyof Production;
const resource = key as keyof ProductionType;
const outputPerMinute = productionPerMinute[resource];
const outputPerMilisecond = outputPerMinute / 60.0 / 1000.0;
V.resources[resource] += outputPerMilisecond * delta;
@ -46,6 +46,27 @@ export default function update(timestamp: number) {
}
});
// Recruit units.
V.buildings.forEach(b => {
if (!b.state.recruitment || !b.state.recruitment.count) {
return;
}
const recruitment = b.state.recruitment;
recruitment.elapsedTime += delta;
const timeToRecruit = b.behavior.units?.recruitmentTime(V, b) * 1000;
if (recruitment.elapsedTime >= timeToRecruit) {
const unitType = b.behavior.units?.type || '';
if (!V.units.hasOwnProperty(unitType)) {
V.units[unitType] = 0;
}
V.units[unitType]++;
recruitment.count--;
recruitment.elapsedTime = (recruitment.count === 0) ? 0 : timeToRecruit - recruitment.elapsedTime;
}
});
return V;
});
});

View File

@ -1,8 +1,9 @@
import type { Building, Production } from "./types";
import units from "./data/units";
import type { BuildingType, ProductionType } from "./types";
import type { VillageState } from "./village";
function _reduceResources(acc: Production, item: Production): Production {
function _reduceResources(acc: ProductionType, item: ProductionType): ProductionType {
return {
wood: acc.wood + item.wood,
stone: acc.stone + item.stone,
@ -12,7 +13,7 @@ function _reduceResources(acc: Production, item: Production): Production {
}
export function getEmptyResources(): Production {
export function getEmptyResources(): ProductionType {
return {
wood: 0,
stone: 0,
@ -22,7 +23,7 @@ export function getEmptyResources(): Production {
}
export function getProduction(villageState: VillageState): Production {
export function getProduction(villageState: VillageState): ProductionType {
return villageState.buildings
.filter(b => b.behavior.production && b.level > 0)
.map(b => {
@ -34,7 +35,7 @@ export function getProduction(villageState: VillageState): Production {
}
export function getStorage(villageState: VillageState): Production {
export function getStorage(villageState: VillageState): ProductionType {
return villageState.buildings
.filter(b => b.behavior.storage && b.level > 0)
.map(b => {
@ -46,7 +47,7 @@ export function getStorage(villageState: VillageState): Production {
}
export function getBuilding(V: VillageState, buildingId: number): Building {
export function getBuilding(V: VillageState, buildingId: number): BuildingType {
const building = V.buildings.find(b => b.id === buildingId);
if (!building) {
throw new Error(`Cannot find building with id "${buildingId}"`);
@ -55,6 +56,15 @@ export function getBuilding(V: VillageState, buildingId: number): Building {
}
export function getUnitSource(unitType: string) {
const unit = units.find(u => u.type === unitType);
if (unit === undefined) {
throw new Error(`Unknown unit type: "${unitType}"`);
}
return unit;
}
export function getKeysAsNumbers(dict: Object): Array<number> {
return Object.keys(dict).map(i => parseInt(i)).sort((a, b) => a - b);
}
@ -89,7 +99,7 @@ export function shuffle<T>(array: Array<T>): Array<T> {
}
export function enqueueBuilding(V: VillageState, building: Building) {
export function enqueueBuilding(V: VillageState, building: BuildingType) {
const ongoingUpgrades = V.queue.filter(q => q.id === building.id);
const level = building.level + 1 + ongoingUpgrades.length;
const remainingTime = 1000 * level;

View File

@ -1,9 +1,8 @@
import { writable } from "svelte/store";
import buildings from "./buildings";
import { createBuilding } from "./create";
import type { Building } from "./types";
import { getTilesAtDistance, Hex } from "./hexgrid";
import type { BuildingType } from "./types";
import { getKeysAsNumbers, shuffle } from "./utils";
@ -21,7 +20,10 @@ interface QueuedBuilding {
export interface VillageState {
buildings: Building[];
buildings: BuildingType[];
units: {
[key: string]: number;
},
resources: {
wood: number;
stone: number;
@ -76,6 +78,7 @@ function getInitialOutsideBoard() {
function getInitialState() {
const state: VillageState = {
buildings: [],
units: {},
resources: {
wood: 60,
stone: 60,
@ -112,6 +115,7 @@ function getInitialState() {
}
const newBuilding = createBuilding(type);
newBuilding.tile = new Hex(x, y);
newBuilding.level = 10;
state.outsideTiles[y][x] = newBuilding.id;
state.buildings.push(newBuilding);
});