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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user