import { GameConfig } from "./config.js"; import { plotSizeFromLevel } from "./grid-utils.js"; import { LootTables, getColorNames, zeroAnimalWeights } from "./loot-tables.js"; import { ensureBotState } from "./bot-zoo.js"; import { buildDefaultRow1Cells, addStarterAnimals } from "./default-grid-layout.js"; export function defaultAnimalWeights() { const w = zeroAnimalWeights(); w[getColorNames()[0]] = 1; return w; } export function normalizeZooWeights(legacy) { if (!legacy || typeof legacy !== "object") return defaultAnimalWeights(); const keys = getColorNames(); const w = zeroAnimalWeights(); const map = { Basic: keys[0], Ocean: keys[5], Mountain: keys[10] }; for (const [oldKey, val] of Object.entries(legacy)) { const newKey = map[oldKey] ?? oldKey; if (keys.includes(newKey)) w[newKey] = Number(val) || 0; } return w; } /** * @returns {import("./types.js").GameState} */ export function defaultState() { const [width, height] = plotSizeFromLevel(1); const worldZoos = buildDefaultWorldZoos(); const cells = buildDefaultCells(); const state = buildStatePayload(width, height, worldZoos, cells); addStarterAnimals(state); return state; } function buildDefaultWorldZoos() { const configZoos = GameConfig.WorldMap?.Zoos; if (configZoos && configZoos.length > 0) { const worldZoos = configZoos.map((z, i) => ({ id: z.id, name: z.name, x: z.x, y: z.y, animalWeights: i === 0 ? defaultAnimalWeights() : normalizeZooWeights(z.animalWeights), })); worldZoos.forEach((zoo) => ensureBotState(zoo, zoo.id === "player")); return worldZoos; } return [{ id: "player", name: "Mon zoo", x: 25, y: 50, animalWeights: defaultAnimalWeights() }]; } function buildDefaultCells() { return buildDefaultRow1Cells(); } function getDefaultStats() { return { eggsPlaced: 0, animalsSold: 0, conveyorUpgrades: 0, plotUpgrades: 0, truckUpgrades: 0, coinsEarned: 0 }; } function buildStatePayload(width, height, worldZoos, cells) { const grid = { width, height, cells }; return { version: GameConfig.StateVersion, coins: 200, conveyorLevel: 1, plotLevel: 1, truckLevel: 1, grid, pendingEggTokens: [], nextTokenId: 1, conveyorOffers: [], lastOfferRefreshAt: 0, worldZoos, truckSale: undefined, worldTruckSales: [], lastEvolutionAt: Math.floor(Date.now() / 1000), laboratoryOffer: null, prestigeLevel: 0, timeOfDay: 6, gameDayTotal: 0, lastSeason: "spring", weather: "sun", lastWeatherChangeAt: 0, quests: [], lastQuestDay: "", stats: getDefaultStats(), mapZoom: 1, mapPanX: 0, mapPanY: 0, worldMapLevel: 1, autoMode: false, autoModeProfile: "balanced", researchPoints: 0, pendingBabies: [], receptionAnimals: [], saleListings: [], deathCountRecent: 0, birthCount: 0, reproductionTimers: [], visitorArrivals: [], }; } const STORAGE_KEY = "builazoo_state"; /** * @param {import("./types.js").GameState} state */ export function saveState(state) { try { const toSave = { ...state }; delete toSave.autoProfilePickerOpen; delete toSave.autoProfilePickerFamily; localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave)); } catch (e) { console.error("saveState failed", e); } } /** * @returns {import("./types.js").GameState | null} */ export function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw === null || raw === undefined) return null; const data = JSON.parse(raw); if (!data || typeof data.coins !== "number" || !data.grid || typeof data.grid.cells !== "object") return null; applyLoadStateDefaults(data); normalizeLoadedCells(data.grid.cells); ensureSchoolCell(data); return data; } catch (e) { console.error("loadState failed", e); return null; } } function applyLoadStateDefaults(data) { applyLoadStateWorldZoos(data); applyLoadStateScalarDefaults(data); applyLoadStateLegacyCells(data); } function applyLoadStateWorldZoos(data) { if (data.coins < 100) data.coins = 200; if (data.pendingEggTokens === null || data.pendingEggTokens === undefined) data.pendingEggTokens = []; if (data.conveyorOffers === null || data.conveyorOffers === undefined) data.conveyorOffers = []; data.conveyorOffers = data.conveyorOffers.map((o) => ({ ...o, zooId: o.zooId ?? "player" })); if ((data.worldZoos === null || data.worldZoos === undefined) && GameConfig.WorldMap && GameConfig.WorldMap.Zoos) { data.worldZoos = [...GameConfig.WorldMap.Zoos]; } if (data.worldZoos !== null && data.worldZoos !== undefined && Array.isArray(data.worldZoos)) { applyWorldZoosWeightsAndBots(data); } if (data.worldZoos === null || data.worldZoos === undefined) { data.worldZoos = [{ id: "player", name: "Mon zoo", x: 25, y: 50, animalWeights: defaultAnimalWeights() }]; } } function applyWorldZoosWeightsAndBots(data) { const keys = getColorNames(); data.worldZoos = data.worldZoos.map((z) => ({ ...z, animalWeights: z.animalWeights && keys.some((k) => k in (z.animalWeights ?? {})) ? z.animalWeights : normalizeZooWeights(z.animalWeights), })); data.worldZoos.forEach((zoo) => ensureBotState(zoo, zoo.id === "player")); } /** Set data[key] to defaultVal when data[key] is null or undefined. defaultVal may be a function (called for value). */ function setScalarDefault(data, key, defaultVal) { if (data[key] === null || data[key] === undefined) { data[key] = typeof defaultVal === "function" ? defaultVal() : defaultVal; } } const LOAD_STATE_SCALAR_DEFAULTS = [ ["worldTruckSales", []], ["lastEvolutionAt", () => Math.floor(Date.now() / 1000)], ["nextTokenId", 1], ["prestigeLevel", 0], ["timeOfDay", 6], ["gameDayTotal", 0], ["lastSeason", "spring"], ["weather", "sun"], ["lastWeatherChangeAt", 0], ["quests", []], ["lastQuestDay", ""], ["stats", { eggsPlaced: 0, animalsSold: 0, conveyorUpgrades: 0, plotUpgrades: 0, truckUpgrades: 0, coinsEarned: 0 }], ["truckLevel", 1], ["mapZoom", 1], ["mapPanX", 0], ["mapPanY", 0], ["worldMapLevel", 1], ["researchPoints", 0], ["pendingBabies", []], ["receptionAnimals", []], ["saleListings", []], ["deathCountRecent", 0], ["birthCount", 0], ["reproductionTimers", []], ["visitorArrivals", []], ]; function applyLoadStateScalarDefaults(data) { if (data.laboratoryOffer === undefined) data.laboratoryOffer = null; for (const [key, defaultVal] of LOAD_STATE_SCALAR_DEFAULTS) { setScalarDefault(data, key, defaultVal); } if (data.version !== GameConfig.StateVersion) data.version = GameConfig.StateVersion; data.autoProfilePickerOpen = false; data.autoProfilePickerFamily = undefined; if (data.attractivityBonusFromIncidents === null || data.attractivityBonusFromIncidents === undefined) { data.attractivityBonusFromIncidents = 0; } } function applyLoadStateLegacyCells(data) { if (data.grid.cells["2_1"] === null || data.grid.cells["2_1"] === undefined) data.grid.cells["2_1"] = { kind: "nursery", level: 1 }; const c21 = data.grid.cells["2_1"]; if (c21 && (c21.kind === "plotUpgrade" || c21.kind === "worldMapUpgrade")) data.grid.cells["2_1"] = { kind: "nursery", level: 1 }; const c12 = data.grid.cells["1_2"]; if (c12 && (c12.kind === "plotUpgrade" || c12.kind === "worldMapUpgrade")) delete data.grid.cells["1_2"]; } function normalizeOneAnimalCell(cell, now) { if (cell.kind !== "animal") return; if (cell.id && !LootTables.Animals[cell.id]) cell.id = "c0_r0"; if (cell.lastVisitedAt === null || cell.lastVisitedAt === undefined) cell.lastVisitedAt = now; if (cell.lastFedAt === null || cell.lastFedAt === undefined) cell.lastFedAt = cell.placedAt ?? now; } function normalizeOneEggCell(cell) { if (cell.kind === "egg" && cell.eggType && !LootTables.EggTypes[cell.eggType]) cell.eggType = "Color_1"; } function normalizeLoadedCells(cells) { const now = Math.floor(Date.now() / 1000); for (const key of Object.keys(cells)) { const cell = cells[key]; if (cell) { if (cell.kind === "animal") normalizeOneAnimalCell(cell, now); if (cell.kind === "egg") normalizeOneEggCell(cell); } } } function ensureSchoolCell(data) { const hasSchool = Object.values(data.grid.cells).some((c) => c && c.kind === "school"); if (!hasSchool && !data.grid.cells["1_1"] && (data.conveyorLevel || 0) >= 1) { data.grid.cells["1_1"] = { kind: "school", level: data.conveyorLevel || 1 }; } }