Files
builazoo/web/js/visitor-incidents.js
ncantu c7d389ecbb Lint: fix errors and remove unused variables
**Motivations:**
- Ensure lint config is not degraded and fix all lint errors for pousse workflow.

**Root causes:**
- Unused variables kept with _ prefix instead of removed (_row, _questReward, _i).
- getAnimalBlockOrigin had 5 parameters (max 4).
- use of continue statement (no-continue rule).

**Correctifs:**
- ESLint config verified; no eslint-disable in codebase.
- Removed unused variable _row (biome-rules); removed dead function _questReward (quests); removed unused map param _i (state.js).
- getAnimalBlockOrigin refactored to 4 params (pos object instead of x, y).
- Replaced continue with if (cell) block in normalizeLoadedCells (state.js).
- JSDoc param names aligned with _height, _y (biome-rules).

**Evolutions:**
- (none)

**Pages affectées:**
- web/js/biome-rules.js
- web/js/quests.js
- web/js/state.js
- web/js/placement.js
2026-03-04 15:32:27 +01:00

148 lines
5.3 KiB
JavaScript

/**
* 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 truck (egg or sale) is in progress.
* @param {import("./types.js").GameState} state
* @returns {boolean}
*/
function hasTruckWait(state) {
if (state.eggPurchaseTruck && state.eggPurchaseTruck.startAt) return true;
if (state.truckSale && state.truckSale.startAt) return true;
return false;
}
/**
* True when there are API sales as buyer undelivered with pending validation.
* @param {import("./types.js").GameState} state
* @returns {boolean}
*/
function hasApiUndeliveredWait(state) {
const api = state.salesFromApi;
if (!api || !api.asBuyerUndelivered || api.asBuyerUndelivered.length === 0) return false;
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;
}
/**
* 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) {
return hasTruckWait(state) || hasApiUndeliveredWait(state);
}
/**
* Expire timed-out incidents and apply penalty. Returns indices to remove from arrivals.
* @param {import("./types.js").VisitorArrival[]} arrivals
* @param {{ nowUnix: number, timeoutSec: number, penalty: number, stateRef: { attractivityBonusFromIncidents: number } }} opts
* @returns {number[]}
*/
function expireIncidents(arrivals, opts) {
const { nowUnix, timeoutSec, penalty, stateRef } = opts;
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) {
stateRef.attractivityBonusFromIncidents = (stateRef.attractivityBonusFromIncidents ?? 0) - penalty;
toRemove.push(i);
}
}
}
return toRemove;
}
/**
* Spawn new incidents on visitors without one (by chance).
* @param {import("./types.js").VisitorArrival[]} arrivals
* @param {number} chance
* @param {number} nowUnix
*/
function spawnIncidents(arrivals, chance, nowUnix) {
for (let i = 0; i < arrivals.length; i++) {
const v = arrivals[i];
const hasNoIncident = v.incidentType === null || v.incidentType === undefined;
if (hasNoIncident && Math.random() < chance) {
v.incidentType = INCIDENT_TYPES[Math.floor(Math.random() * INCIDENT_TYPES.length)];
v.incidentSince = nowUnix;
}
}
}
/**
* @returns {{ baseChance: number, waitMult: number, timeoutSec: number, penalty: number }}
*/
function getIncidentConfig() {
const cfg = GameConfig.Visitor;
return {
baseChance: cfg?.IncidentChanceBase ?? 0.002,
waitMult: cfg?.IncidentChanceWaitMultiplier ?? 4,
timeoutSec: cfg?.IncidentTimeoutSeconds ?? 45,
penalty: cfg?.IncidentUnresolvedAttractivityPenalty ?? 0.2,
};
}
/**
* 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 { baseChance, waitMult, timeoutSec, penalty } = getIncidentConfig();
const inWait = isInWaitPhase(state);
const chance = inWait ? baseChance * waitMult : baseChance;
const toRemove = expireIncidents(arrivals, { nowUnix, timeoutSec, penalty, stateRef: state });
for (let r = toRemove.length - 1; r >= 0; r--) {
arrivals.splice(toRemove[r], 1);
}
spawnIncidents(arrivals, chance, nowUnix);
}
/**
* Apply resolve bonus (coins + attractivity) to state. Mutates state and v.
* @param {import("./types.js").GameState} state
* @param {import("./types.js").VisitorArrival} v
*/
function applyResolveBonus(state, v) {
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;
}
/**
* 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;
applyResolveBonus(state, v);
return true;
}