**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
148 lines
5.3 KiB
JavaScript
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;
|
|
}
|