feat: persist berry bush state (depletion + regrowth) across save/load

Replace foodPositions with berryBushes in TileData, saving per-bush
berryCount, maxBerries, and ticksSinceLastRegrowth to the metadata table.
On load, restore berry bush state into GameMap; fall back to initializing
from resource tiles for old saves without berry data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-10 20:35:53 +00:00
parent 1b0da25887
commit b75f3a05e2
6 changed files with 39 additions and 16 deletions
+13 -1
View File
@@ -109,7 +109,10 @@ export class GameLoop {
trunkDecorations: this.map.trunkDecorations,
resourceTiles: this.map.resourceTiles,
obstacles: new Set(this.map.getObstacles().map(p => `${p.x},${p.y}`)),
foodPositions: [],
berryBushes: [...this.map.berryBushes.entries()].map(([key, state]) => {
const [x, y] = key.split(',').map(Number);
return { x, y, ...state };
}),
});
this.saveManager.saveEntityState(this.world, 0, this.runtimeConstants);
this.wirePersistenceCallbacks();
@@ -123,6 +126,15 @@ export class GameLoop {
// Load map
this.saveManager!.loadMapState(this.map);
// If no berry bushes were loaded (old save), initialize from resource tiles
if (this.map.berryBushes.size === 0) {
for (const rt of this.map.resourceTiles) {
if (rt.resourceType === 'berries') {
this.map.initBerryBush(rt.x, rt.y, industryConfig.berryBushMaxCount);
}
}
}
// Set non-entity singletons
this.world.setSingleton('itemRegistry', ItemRegistry.createDefault());
this.world.setSingleton('recipeRegistry', RecipeRegistry.createDefault());
@@ -94,7 +94,7 @@ describe('integration: full save/load cycle', () => {
trunkDecorations: generated.trunkDecorations,
resourceTiles: generated.resourceTiles,
obstacles: generated.obstacles,
foodPositions: generated.foodPositions,
berryBushes: [],
};
manager.initNewWorld(world, map, tileData);
manager.saveEntityState(world, 500);
@@ -47,7 +47,7 @@ describe('SaveManager', () => {
trunkDecorations: new Array(16).fill(-1),
resourceTiles: [],
obstacles: new Set<string>(),
foodPositions: [],
berryBushes: [],
};
manager.initNewWorld(world, map, tileData);
@@ -71,7 +71,7 @@ describe('SaveManager', () => {
trunkDecorations: new Array(16).fill(-1),
resourceTiles: [{ x: 2, y: 3, resourceType: 'tree' }],
obstacles: new Set<string>(['1,1', '2,2']),
foodPositions: [],
berryBushes: [],
};
manager.initNewWorld(world, map, tileData);
@@ -103,7 +103,7 @@ describe('SaveManager', () => {
trunkDecorations: new Array(16).fill(-1),
resourceTiles: [],
obstacles: new Set<string>(),
foodPositions: [],
berryBushes: [],
};
manager.initNewWorld(world, map, tileData);
@@ -152,7 +152,7 @@ describe('SaveManager', () => {
trunkDecorations: new Array(16).fill(-1),
resourceTiles: [],
obstacles: new Set<string>(),
foodPositions: [],
berryBushes: [],
};
manager.initNewWorld(world, map, tileData);
@@ -177,7 +177,7 @@ describe('SaveManager', () => {
trunkDecorations: new Array(16).fill(-1),
resourceTiles: [],
obstacles: new Set<string>(),
foodPositions: [],
berryBushes: [],
};
// Open and close, then clear tiles to simulate empty DB
@@ -44,7 +44,10 @@ describe('worldSerializer', () => {
{ x: 2, y: 1, resourceType: 'stone' },
],
obstacles: new Set(['1,0', '2,1']),
foodPositions: [{ x: 0, y: 0 }, { x: 2, y: 1 }],
berryBushes: [
{ x: 0, y: 0, berryCount: 3, maxBerries: 5, ticksSinceLastRegrowth: 0 },
{ x: 2, y: 1, berryCount: 1, maxBerries: 5, ticksSinceLastRegrowth: 10 },
],
};
saveTiles(width, height, data);
@@ -56,7 +59,7 @@ describe('worldSerializer', () => {
expect(loaded!.trunkDecorations).toEqual(data.trunkDecorations);
expect(loaded!.resourceTiles).toEqual(data.resourceTiles);
expect(loaded!.obstacles).toEqual(data.obstacles);
expect(loaded!.foodPositions).toEqual(data.foodPositions);
expect(loaded!.berryBushes).toEqual(data.berryBushes);
});
it('returns null when no tiles are saved', () => {
@@ -76,7 +79,7 @@ describe('worldSerializer', () => {
trunkDecorations: [-1, -1],
resourceTiles: [],
obstacles: new Set(),
foodPositions: [],
berryBushes: [],
};
saveTiles(width, height, data1);
@@ -86,7 +89,7 @@ describe('worldSerializer', () => {
trunkDecorations: [7, 8],
resourceTiles: [{ x: 0, y: 0, resourceType: 'iron' }],
obstacles: new Set(['0,0']),
foodPositions: [{ x: 1, y: 0 }],
berryBushes: [{ x: 1, y: 0, berryCount: 5, maxBerries: 5, ticksSinceLastRegrowth: 0 }],
};
saveTiles(width, height, data2);
+8
View File
@@ -86,6 +86,14 @@ export class SaveManager {
map.resourceTiles = tileData.resourceTiles;
map.loadObstacles(tileData.obstacles);
for (const bush of tileData.berryBushes) {
map.berryBushes.set(`${bush.x},${bush.y}`, {
berryCount: bush.berryCount,
maxBerries: bush.maxBerries,
ticksSinceLastRegrowth: bush.ticksSinceLastRegrowth,
});
}
return true;
}
+5 -5
View File
@@ -6,7 +6,7 @@ export interface TileData {
trunkDecorations: number[];
resourceTiles: Array<{ x: number; y: number; resourceType: string }>;
obstacles: Set<string>;
foodPositions: Array<{ x: number; y: number }>;
berryBushes: Array<{ x: number; y: number; berryCount: number; maxBerries: number; ticksSinceLastRegrowth: number }>;
}
export function saveTiles(width: number, height: number, data: TileData): void {
@@ -45,7 +45,7 @@ export function saveTiles(width: number, height: number, data: TileData): void {
"INSERT INTO metadata (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
);
upsertMeta.run('obstacles', JSON.stringify([...data.obstacles]));
upsertMeta.run('food_positions', JSON.stringify(data.foodPositions));
upsertMeta.run('berry_bushes', JSON.stringify(data.berryBushes));
});
transaction();
@@ -88,10 +88,10 @@ export function loadTiles(width: number, height: number): TileData | null {
const obstaclesRow = db.prepare("SELECT value FROM metadata WHERE key = 'obstacles'").get() as { value: string } | undefined;
const obstacles = new Set<string>(obstaclesRow ? JSON.parse(obstaclesRow.value) as string[] : []);
const foodRow = db.prepare("SELECT value FROM metadata WHERE key = 'food_positions'").get() as { value: string } | undefined;
const foodPositions: Array<{ x: number; y: number }> = foodRow ? JSON.parse(foodRow.value) : [];
const berryRow = db.prepare("SELECT value FROM metadata WHERE key = 'berry_bushes'").get() as { value: string } | undefined;
const berryBushes: TileData['berryBushes'] = berryRow ? JSON.parse(berryRow.value) : [];
return { terrain, decorations, trunkDecorations, resourceTiles, obstacles, foodPositions };
return { terrain, decorations, trunkDecorations, resourceTiles, obstacles, berryBushes };
}
export function saveMetadata(data: { tick: number; lastSavedAt: string }): void {