Initial commit

**Motivations:**
- Initialisation du versionning git pour le projet

**Root causes:**
- N/A (Nouveau projet)

**Correctifs:**
- N/A

**Evolutions:**
- Structure initiale du projet
- Ajout du .gitignore

**Pages affectées:**
- Tous les fichiers
This commit is contained in:
2026-03-03 22:24:17 +01:00
commit e031c9a1d2
155 changed files with 22334 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
/**
* 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;
}