Add resource production buildings and a way to upgrade them.

This commit is contained in:
Adrian 2024-10-21 17:35:32 +02:00
parent d5058c4cbe
commit b564520d56
11 changed files with 286 additions and 6 deletions

11
package-lock.json generated
View File

@ -10,6 +10,7 @@
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.2", "@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"immer": "^10.1.1",
"svelte": "^4.2.19", "svelte": "^4.2.19",
"svelte-check": "^4.0.4", "svelte-check": "^4.0.4",
"tslib": "^2.7.0", "tslib": "^2.7.0",
@ -877,6 +878,16 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "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": { "node_modules/is-reference": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",

View File

@ -12,6 +12,7 @@
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.2", "@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"immer": "^10.1.1",
"svelte": "^4.2.19", "svelte": "^4.2.19",
"svelte-check": "^4.0.4", "svelte-check": "^4.0.4",
"tslib": "^2.7.0", "tslib": "^2.7.0",

View File

@ -1,8 +1,58 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import moves from "./moves";
import update from "./update";
import village from "./village";
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);
}
</script> </script>
<main> <main>
<div>
<ul>
<li>Wood: { Math.floor($village.resources.wood) }</li>
<li>Stone: { Math.floor($village.resources.stone) }</li>
<li>Iron: { Math.floor($village.resources.iron) }</li>
<li>Food: { Math.floor($village.resources.food) }</li>
</ul>
</div>
<div class="buildings">
{ #each $village.buildings as building }
<div>
<p>{ building.name }</p>
<p>Level: { building.level }</p>
<p>
<button on:click={ () => upgradeBuilding(building.id) }>
Upgrade
</button>
</p>
</div>
{ /each }
</div>
</main> </main>
<style> <style>
.buildings {
display: flex;
gap: 1em;
}
</style> </style>

View File

@ -4,8 +4,8 @@
font-weight: 400; font-weight: 400;
color-scheme: light dark; color-scheme: light dark;
color: rgba(255, 255, 255, 0.87); color: hsla(0, 0%, 100%, 0.87);
background-color: #242424; background-color: hsl(0, 0%, 14%);
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;

62
src/buildings.ts Normal file
View File

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

View File

@ -1,8 +1,10 @@
import './app.css' import './app.css'
import App from './App.svelte' import App from './App.svelte'
const app = new App({ const app = new App({
target: document.getElementById('app')!, target: document.getElementById('app')!,
}) });
export default app
export default app;

31
src/moves/index.ts Normal file
View File

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

View File

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

21
src/types.ts Normal file
View File

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

29
src/update.ts Normal file
View File

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

44
src/village.ts Normal file
View File

@ -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<VillageState>({
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;