import { GameConfig } from "./config.js"; import { refreshOffers, shouldRefresh } from "./conveyor.js"; import { run as runHatching } from "./hatching.js"; import { tick as incomeTick, tickVisitorArrivals, getAttractivityScore } from "./income.js"; import { getActiveModifiers } from "./event-service.js"; import { tickTime, tickWeather } from "./time-weather.js"; import { tickQuests } from "./quests.js"; import { saveState } from "./state.js"; import { playSound } from "./audio.js"; import { pruneTruckSales, addNpcTruckSale, shouldAddNpcTruck, tickLaboratory } from "./world-map.js"; import { tickAnimalVisits } from "./animal-visits.js"; import { tickPlayerAutoMode } from "./bot-zoo.js"; import { tickFeeding, checkDeathCauses, getFeedingRate } from "./food.js"; import { tickReproduction, getReproductionScore } from "./reproduction.js"; import { tickVisitorIncidents } from "./visitor-incidents.js"; import { tickSaleListings } from "./trade.js"; import { getCurrentSeason } from "./seasons.js"; /** * @param {import("./types.js").GameState} state * @param {number} pointsPerLevelPerSecond * @param {number} dt * @returns {number} */ function sumResearchPointsFromCells(state, pointsPerLevelPerSecond, dt) { let total = 0; for (const [, cell] of Object.entries(state.grid.cells)) { if (cell !== null && cell !== undefined && cell.kind === "research") { const level = cell.level ?? 1; total += pointsPerLevelPerSecond * level * dt; } } return total; } /** * Add research points from all research cells. PointsPerTickPerLevel * level per second. * @param {import("./types.js").GameState} state * @param {number} dt * @returns {void} */ function tickResearch(state, dt) { const cfg = GameConfig.Research; if (!cfg || cfg.PointsPerTickPerLevel === null || cfg.PointsPerTickPerLevel === undefined) return; const total = sumResearchPointsFromCells(state, cfg.PointsPerTickPerLevel, dt); if (total > 0) state.researchPoints = (state.researchPoints ?? 0) + total; } /** * Run one simulation tick: time, feeding, reproduction, visitors, income, research, hatching, quests. * @param {import("./types.js").GameState} state * @param {number} nowUnix * @param {number} nowMs * @param {number} dt * @returns {{ hatched: Array<{ x: number, y: number }>, questEarned: number }} */ function doOneTick(state, nowUnix, nowMs, dt) { if (shouldRefresh(state, nowUnix)) refreshOffers(state, nowUnix); pruneTruckSales(state, nowMs); const eventModifiers = getActiveModifiers(nowUnix); tickTime(state, dt); tickWeather(state, nowUnix); tickAnimalVisits(state, nowUnix, nowMs); if (state.autoMode) tickPlayerAutoMode(state, nowUnix); tickFeeding(state, nowUnix); state.feedingRate = getFeedingRate(state, nowUnix); checkDeathCauses(state, nowUnix); tickReproduction(state, nowUnix); state.reproductionScore = getReproductionScore(state); tickSaleListings(state, nowUnix); tickVisitorArrivals(state, nowUnix); tickVisitorIncidents(state, nowUnix); incomeTick(state, dt, eventModifiers); tickResearch(state, dt); state.attractivityScore = getAttractivityScore(state); const { hatched } = runHatching(state, nowUnix, eventModifiers); const questEarned = tickQuests(state); return { hatched, questEarned }; } /** * @param {() => import("./types.js").GameState} getState * @param {(state: import("./types.js").GameState, payload: { lastHatched?: Array<{ x: number, y: number }> }) => void} onUpdate * @param {(state: import("./types.js").GameState) => void} [saveStateFn] * @returns {void} */ export function startGameLoop(getState, onUpdate, saveStateFn) { const save = saveStateFn || saveState; let lastWall = performance.now() / 1000; let saveAccum = 0; let lastNpcTruckAt = 0; function loop() { const state = getState(); const nowWall = performance.now() / 1000; const dt = Math.min(nowWall - lastWall, 2); lastWall = nowWall; const nowUnix = Math.floor(Date.now() / 1000); const nowMs = Date.now(); if (shouldAddNpcTruck(nowMs, lastNpcTruckAt)) { addNpcTruckSale(state, nowMs); lastNpcTruckAt = nowMs; } tickLaboratory(state, nowUnix); const { hatched, questEarned } = doOneTick(state, nowUnix, nowMs, dt); const newSeason = getCurrentSeason(state); if (state.lastSeason !== undefined && state.lastSeason !== newSeason) { state.seasonChangeMessage = newSeason; } state.lastSeason = newSeason; if (questEarned > 0) playSound("quest"); onUpdate(state, { lastHatched: hatched }); saveAccum += dt; if (saveAccum >= GameConfig.SaveIntervalMs / 1000) { saveAccum = 0; save(state); } } const intervalMs = Math.max(100, GameConfig.IncomeTickMs); setInterval(loop, intervalMs); loop(); }