From b564520d569a56bf113ad9f5da3f2d5ad81f6ee5 Mon Sep 17 00:00:00 2001 From: Adrian Gaudebert Date: Mon, 21 Oct 2024 17:35:32 +0200 Subject: [PATCH] Add resource production buildings and a way to upgrade them. --- package-lock.json | 11 +++++++ package.json | 1 + src/App.svelte | 50 +++++++++++++++++++++++++++++ src/app.css | 4 +-- src/buildings.ts | 62 ++++++++++++++++++++++++++++++++++++ src/main.ts | 10 +++--- src/moves/index.ts | 31 ++++++++++++++++++ src/moves/upgradeBuilding.ts | 29 +++++++++++++++++ src/types.ts | 21 ++++++++++++ src/update.ts | 29 +++++++++++++++++ src/village.ts | 44 +++++++++++++++++++++++++ 11 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 src/buildings.ts create mode 100644 src/moves/index.ts create mode 100644 src/moves/upgradeBuilding.ts create mode 100644 src/types.ts create mode 100644 src/update.ts create mode 100644 src/village.ts diff --git a/package-lock.json b/package-lock.json index aef93b9..4609fa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.4", + "immer": "^10.1.1", "svelte": "^4.2.19", "svelte-check": "^4.0.4", "tslib": "^2.7.0", @@ -877,6 +878,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/is-reference": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", diff --git a/package.json b/package.json index 7fe91a0..67a12e3 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.4", + "immer": "^10.1.1", "svelte": "^4.2.19", "svelte-check": "^4.0.4", "tslib": "^2.7.0", diff --git a/src/App.svelte b/src/App.svelte index be2cd56..72933a1 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,8 +1,58 @@
+
+
    +
  • Wood: { Math.floor($village.resources.wood) }
  • +
  • Stone: { Math.floor($village.resources.stone) }
  • +
  • Iron: { Math.floor($village.resources.iron) }
  • +
  • Food: { Math.floor($village.resources.food) }
  • +
+
+
+ { #each $village.buildings as building } +
+

{ building.name }

+

Level: { building.level }

+

+ +

+
+ { /each } +
diff --git a/src/app.css b/src/app.css index 865860a..0baf90b 100644 --- a/src/app.css +++ b/src/app.css @@ -4,8 +4,8 @@ font-weight: 400; color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + color: hsla(0, 0%, 100%, 0.87); + background-color: hsl(0, 0%, 14%); font-synthesis: none; text-rendering: optimizeLegibility; diff --git a/src/buildings.ts b/src/buildings.ts new file mode 100644 index 0000000..07b77dc --- /dev/null +++ b/src/buildings.ts @@ -0,0 +1,62 @@ +import type { VillageState } from "./village"; + + +export default { + 'woodcutter': { + name: 'Woodcutter', + level: 1, + cost: (level: number) => { + return { + wood: level * 10, + stone: level * 10, + iron: level * 10, + food: 0, + }; + }, + behavior: { + production: (V: VillageState, self: any, delta: number) => { + const outputPerMinute = 5 * (self.level * self.level); + const outputPerMilisecond = outputPerMinute / 60.0 / 1000.0; + V.resources.wood += outputPerMilisecond * delta; + } + } + }, + 'mine': { + name: 'Mine', + level: 1, + cost: (level: number) => { + return { + wood: level * 10, + stone: level * 10, + iron: level * 10, + food: 0, + }; + }, + behavior: { + production: (V: VillageState, self: any, delta: number) => { + const outputPerMinute = 5 * (self.level * self.level); + const outputPerMilisecond = outputPerMinute / 60.0 / 1000.0; + V.resources.iron += outputPerMilisecond * delta; + } + } + }, + 'pit': { + name: 'Pit', + level: 1, + cost: (level: number) => { + return { + wood: level * 10, + stone: level * 10, + iron: level * 10, + food: 0, + }; + }, + behavior: { + production: (V: VillageState, self: any, delta: number) => { + const outputPerMinute = 5 * (self.level * self.level); + const outputPerMilisecond = outputPerMinute / 60.0 / 1000.0; + V.resources.stone += outputPerMilisecond * delta; + } + } + }, +}; diff --git a/src/main.ts b/src/main.ts index 4d67e2a..bba7235 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,10 @@ import './app.css' import App from './App.svelte' -const app = new App({ - target: document.getElementById('app')!, -}) -export default app +const app = new App({ + target: document.getElementById('app')!, +}); + + +export default app; diff --git a/src/moves/index.ts b/src/moves/index.ts new file mode 100644 index 0000000..f5f3061 --- /dev/null +++ b/src/moves/index.ts @@ -0,0 +1,31 @@ +import { produce } from 'immer'; + +import village, { type VillageState } from '../village'; +import upgradeBuilding from './upgradeBuilding'; + + +// Encapsulates a move function into a store update, where the data is made +// immutable by immer. This allows moves to be much, much simpler to write, +// while ensuring that states are always immutable. We'll benefit from this +// when we want to implement an undo option. +export function makeMove(move: (...args: any[]) => boolean) { + // Create a new function. + return (...args: any[]) => { + let result: boolean = false; + + // That updates the game state. + village.update(state => { + // With an immutable state. + return produce(state, (G: VillageState) => { + result = move(G, ...args); + }); + }); + + return result; + }; +} + + +export default { + upgradeBuilding: makeMove(upgradeBuilding), +}; diff --git a/src/moves/upgradeBuilding.ts b/src/moves/upgradeBuilding.ts new file mode 100644 index 0000000..3843b66 --- /dev/null +++ b/src/moves/upgradeBuilding.ts @@ -0,0 +1,29 @@ +import type { VillageState } from "../village"; + + +export default function upgradeBuilding(V: VillageState, buildingId: number) { + const building = V.buildings.find(b => b.id === buildingId); + if (!building) { + return false; + } + + const cost = building.cost(building.level); + + 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; + + building.level++; + + return true; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..1096336 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,21 @@ +export interface Cost { + wood: number; + stone: number; + iron: number; + food: number; +} + + +export interface BuildingSource { + name: string; + level: number; + cost: (level: number) => Cost; + behavior: { + production?: Function; + }; +} + + +export interface Building extends BuildingSource { + id: number; +} diff --git a/src/update.ts b/src/update.ts new file mode 100644 index 0000000..14e8f45 --- /dev/null +++ b/src/update.ts @@ -0,0 +1,29 @@ +import { produce } from 'immer'; + +import village, { type VillageState } from "./village"; + + +let lastFrame: number; + + +export default function update(timestamp: number) { + if (!lastFrame) { + lastFrame = timestamp; + return; + } + + const delta = timestamp - lastFrame; + + village.update(state => { + return produce(state, (V: VillageState) => { + V.buildings.forEach(b => { + if (b.behavior.production) { + b.behavior.production(V, b, delta); + } + }); + return V; + }); + }); + + lastFrame = timestamp; +} diff --git a/src/village.ts b/src/village.ts new file mode 100644 index 0000000..f8726b8 --- /dev/null +++ b/src/village.ts @@ -0,0 +1,44 @@ +import { writable } from "svelte/store"; +import buildings from "./buildings"; +import type { Building } from "./types"; + + +export interface VillageState { + buildings: Building[]; + resources: { + wood: number; + stone: number; + iron: number; + food: number; + culture: number; + } +} + +let uid = 0; + +const village = writable({ + buildings: [ + { ...buildings.woodcutter, id: uid++ }, + { ...buildings.woodcutter, id: uid++ }, + { ...buildings.woodcutter, id: uid++ }, + { ...buildings.woodcutter, id: uid++ }, + { ...buildings.mine, id: uid++ }, + { ...buildings.mine, id: uid++ }, + { ...buildings.mine, id: uid++ }, + { ...buildings.mine, id: uid++ }, + { ...buildings.pit, id: uid++ }, + { ...buildings.pit, id: uid++ }, + { ...buildings.pit, id: uid++ }, + { ...buildings.pit, id: uid++ }, + ], + resources: { + wood: 100, + stone: 100, + iron: 100, + food: 0, + culture: 0, + }, +}); + + +export default village;