/** * Visitor incidents (thirst, bin full, bench required, animal too far, want photo). * Appear more often during wait phases; resolve by click for bonus, or timeout applies penalty. */ import { GameConfig } from "./config.js"; /** Incident type keys for i18n and display. */ export const INCIDENT_TYPES = ["thirst", "bin", "bench", "animalFar", "photo"]; /** Emoji per incident type for bubble display. */ export const INCIDENT_EMOJI = { thirst: "💧", bin: "🗑️", bench: "🪑", animalFar: "🦌", photo: "📷" }; /** * True when player is in a wait phase (truck moving, sale pending validation, etc.). * @param {import("./types.js").GameState} state * @returns {boolean} */ export function isInWaitPhase(state) { if (state.eggPurchaseTruck && state.eggPurchaseTruck.startAt) return true; if (state.truckSale && state.truckSale.startAt) return true; const api = state.salesFromApi; if (api && api.asBuyerUndelivered && api.asBuyerUndelivered.length > 0) { const nowMs = Date.now(); for (const s of api.asBuyerUndelivered) { const validatedAtMs = s.validated_at ? new Date(s.validated_at).getTime() : 0; const pending = (s.status === "sold" || s.status === "validated") && validatedAtMs > nowMs; if (pending) return true; } } return false; } /** * Spawn and expire incidents. Call after tickVisitorArrivals. * @param {import("./types.js").GameState} state * @param {number} nowUnix */ export function tickVisitorIncidents(state, nowUnix) { const arrivals = state.visitorArrivals ?? []; const cfg = GameConfig.Visitor; const baseChance = cfg?.IncidentChanceBase ?? 0.002; const waitMult = cfg?.IncidentChanceWaitMultiplier ?? 4; const timeoutSec = cfg?.IncidentTimeoutSeconds ?? 45; const penalty = cfg?.IncidentUnresolvedAttractivityPenalty ?? 0.2; const inWait = isInWaitPhase(state); const chance = inWait ? baseChance * waitMult : baseChance; const toRemove = []; for (let i = 0; i < arrivals.length; i++) { const v = arrivals[i]; if (v.incidentType !== null && v.incidentType !== undefined) { if (nowUnix - (v.incidentSince ?? nowUnix) >= timeoutSec) { state.attractivityBonusFromIncidents = (state.attractivityBonusFromIncidents ?? 0) - penalty; toRemove.push(i); } } else if (Math.random() < chance) { v.incidentType = INCIDENT_TYPES[Math.floor(Math.random() * INCIDENT_TYPES.length)]; v.incidentSince = nowUnix; } } for (let r = toRemove.length - 1; r >= 0; r--) { arrivals.splice(toRemove[r], 1); } } /** * Resolve incident for visitor at index: clear incident, add coins and attractivity bonus. Mutates state. * @param {import("./types.js").GameState} state * @param {number} visitorIndex * @returns {boolean} true if an incident was resolved */ export function resolveIncident(state, visitorIndex) { const arrivals = state.visitorArrivals ?? []; const v = arrivals[visitorIndex]; if (!v || (v.incidentType === null || v.incidentType === undefined)) return false; const cfg = GameConfig.Visitor; const coinBonus = cfg?.IncidentResolveCoinBonus ?? 8; const attractivityBonus = cfg?.IncidentResolveAttractivityBonus ?? 0.15; state.coins += coinBonus; state.attractivityBonusFromIncidents = (state.attractivityBonusFromIncidents ?? 0) + attractivityBonus; if (state.stats) state.stats.coinsEarned = (state.stats.coinsEarned ?? 0) + coinBonus; delete v.incidentType; delete v.incidentSince; return true; }