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:
288
web/js/income.js
Normal file
288
web/js/income.js
Normal file
@@ -0,0 +1,288 @@
|
||||
import { LootTables } from "./loot-tables.js";
|
||||
import { getIncomeMultiplier } from "./mutation-rules.js";
|
||||
import { getLevelMultiplier, getSellValue } from "./economy.js";
|
||||
import { GameConfig } from "./config.js";
|
||||
import { getPrestigeIncomeMultiplier } from "./prestige.js";
|
||||
import { isOriginCell } from "./grid-utils.js";
|
||||
import { getOriginAnimalCount } from "./food.js";
|
||||
|
||||
/**
|
||||
* Total sell value of all animals in the zoo (used for visitor attraction). Counts each animal block once (origin cell only).
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @returns {number}
|
||||
*/
|
||||
function getTotalAnimalValue(state) {
|
||||
let total = 0;
|
||||
for (const [key, cell] of Object.entries(state.grid.cells)) {
|
||||
if (cell.kind !== "animal" || !isOriginCell(key, cell)) {
|
||||
// skip non-origin animals
|
||||
} else {
|
||||
const animalDef = LootTables.Animals[cell.id];
|
||||
if (animalDef !== null && animalDef !== undefined) {
|
||||
const mutationMult = getIncomeMultiplier(cell.mutation);
|
||||
total += getSellValue(
|
||||
animalDef.baseIncomePerSecond,
|
||||
cell.level,
|
||||
mutationMult,
|
||||
animalDef.sellFactor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Max simultaneous visitors allowed by billeterie capacity. Entry is only via billeterie.
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getBilleterieCapacity(state) {
|
||||
const cfg = GameConfig.Billeterie;
|
||||
if (!cfg) return 0;
|
||||
const unit = cfg.VisitorsPerUnit ?? 20;
|
||||
let total = 0;
|
||||
for (const cell of Object.values(state.grid.cells)) {
|
||||
if (cell !== null && cell !== undefined && cell.kind === "billeterie") {
|
||||
total += (cell.level ?? 1) * unit;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attraction from cities: per-city contribution = min(maxVisitorsTowardZoos, rawWeight * 100), summed then scaled.
|
||||
* Closer cities contribute more, but each city is capped by maxVisitorsTowardZoos.
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @returns {number}
|
||||
*/
|
||||
function getCityAttraction(state) {
|
||||
const cities = GameConfig.WorldMap?.Cities;
|
||||
if (!cities || cities.length === 0) return 0;
|
||||
const zoos = state.worldZoos ?? [];
|
||||
const player = zoos.find((z) => z.id === "player");
|
||||
if (!player) return 0;
|
||||
const scale = GameConfig.Visitor.CityAttractionScale ?? 0.002;
|
||||
const rawMultiplier = 100;
|
||||
let sum = 0;
|
||||
for (const city of cities) {
|
||||
const dx = (city.x - player.x) / 100;
|
||||
const dy = (city.y - player.y) / 100;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy) || 0.01;
|
||||
const raw = 1 / (1 + dist);
|
||||
const maxFromCity = city.maxVisitorsTowardZoos ?? 999;
|
||||
const contrib = Math.min(maxFromCity, raw * rawMultiplier);
|
||||
sum += contrib;
|
||||
}
|
||||
return sum * scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decay multiplier when the zoo has not evolved (upgrade/place/sell) for a while.
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @param {number} nowUnix
|
||||
* @returns {number}
|
||||
*/
|
||||
function getStagnationMultiplier(state, nowUnix) {
|
||||
const after = GameConfig.Visitor.StagnationDecayAfterSeconds ?? 60;
|
||||
const perMin = GameConfig.Visitor.StagnationDecayPerMinute ?? 0.05;
|
||||
const last = state.lastEvolutionAt ?? 0;
|
||||
const elapsed = Math.max(0, nowUnix - last);
|
||||
if (elapsed <= after) return 1;
|
||||
const minutesStagnant = (elapsed - after) / 60;
|
||||
const decay = Math.min(0.9, minutesStagnant * perMin);
|
||||
return Math.max(0.1, 1 - decay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stay duration multiplier from boutiques and animal diversity (visitors stay longer).
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @returns {number}
|
||||
*/
|
||||
function getStayMultiplier(state) {
|
||||
let shopBonus = 0;
|
||||
for (const cell of Object.values(state.grid.cells)) {
|
||||
if (cell !== null && cell !== undefined && cell.kind === "souvenirShop") {
|
||||
shopBonus += (cell.level ?? 1) * (GameConfig.Visitor.StayMultiplierPerShopLevel ?? 0.15);
|
||||
}
|
||||
}
|
||||
const speciesSet = new Set();
|
||||
for (const [key, cell] of Object.entries(state.grid.cells)) {
|
||||
if (cell !== null && cell !== undefined && cell.kind === "animal" && isOriginCell(key, cell)) speciesSet.add(cell.id);
|
||||
}
|
||||
const diversityBonus = speciesSet.size * (GameConfig.Visitor.StayMultiplierPerSpecies ?? 0.02);
|
||||
return Math.max(0.5, 1 + shopBonus + diversityBonus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stay duration in seconds (base 1 day × stay multiplier). Visitors leave when now > arrivedAt + this.
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @returns {number}
|
||||
*/
|
||||
function getStayDurationSeconds(state) {
|
||||
const base = GameConfig.Time?.DayLengthSeconds ?? 120;
|
||||
return base * getStayMultiplier(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Demand for visitors (before billeterie cap).
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @param {number} nowUnix
|
||||
* @returns {number}
|
||||
*/
|
||||
function getVisitorDemand(state, nowUnix) {
|
||||
let animalCount = 0;
|
||||
for (const [key, cell] of Object.entries(state.grid.cells)) {
|
||||
if (cell.kind === "animal" && isOriginCell(key, cell)) animalCount += 1;
|
||||
}
|
||||
const visitorsPerAnimal = GameConfig.Visitor.VisitorsPerAnimal;
|
||||
const plotBonus = (state.plotLevel ?? 1) * GameConfig.Visitor.PlotLevelBonus;
|
||||
let demand = Math.floor(animalCount * visitorsPerAnimal + plotBonus);
|
||||
const cityAttraction = getCityAttraction(state);
|
||||
const animalValue = getTotalAnimalValue(state);
|
||||
const animalValueScale = GameConfig.Visitor.AnimalValueScale ?? 0.00015;
|
||||
demand *= 1 + cityAttraction;
|
||||
demand *= 1 + animalValue * animalValueScale;
|
||||
demand *= getStagnationMultiplier(state, nowUnix);
|
||||
return Math.max(0, Math.floor(demand));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update visitor entities: remove those who exceeded stay duration, add new arrivals up to min(cap, demand).
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @param {number} nowUnix
|
||||
*/
|
||||
export function tickVisitorArrivals(state, nowUnix) {
|
||||
state.visitorArrivals = state.visitorArrivals ?? [];
|
||||
const stayDuration = getStayDurationSeconds(state);
|
||||
state.visitorArrivals = state.visitorArrivals.filter(
|
||||
(v) => nowUnix < v.arrivedAt + stayDuration
|
||||
);
|
||||
const demand = getVisitorDemand(state, nowUnix);
|
||||
const cap = getBilleterieCapacity(state);
|
||||
const target = Math.min(cap, demand);
|
||||
const current = state.visitorArrivals.length;
|
||||
for (let i = 0; i < target - current; i++) {
|
||||
state.visitorArrivals.push({ arrivedAt: nowUnix });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor count and average payment per visitor per second. Includes luxury guest effect (LuxuryGuestChance, LuxuryEntryMultiplier, LuxuryShopMultiplier) in the average.
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @returns {{ visitorCount: number, paymentPerVisitor: number }}
|
||||
*/
|
||||
function getVisitorParams(state) {
|
||||
const arrivals = state.visitorArrivals ?? [];
|
||||
let visitorCount = arrivals.length;
|
||||
if (visitorCount === 0 && getBilleterieCapacity(state) === 0) {
|
||||
let animalCount = 0;
|
||||
for (const [key, cell] of Object.entries(state.grid.cells)) {
|
||||
if (cell.kind === "animal" && isOriginCell(key, cell)) animalCount += 1;
|
||||
}
|
||||
const visitorsPerAnimal = GameConfig.Visitor.VisitorsPerAnimal;
|
||||
const plotBonus = (state.plotLevel ?? 1) * GameConfig.Visitor.PlotLevelBonus;
|
||||
visitorCount = Math.max(0, Math.floor(animalCount * visitorsPerAnimal + plotBonus));
|
||||
}
|
||||
const billeterieCap = getBilleterieCapacity(state);
|
||||
if (billeterieCap > 0 && visitorCount > billeterieCap) visitorCount = billeterieCap;
|
||||
let paymentPerVisitor = GameConfig.Visitor.BasePaymentPerVisitor;
|
||||
let souvenirBonus = 1;
|
||||
let shopCount = 0;
|
||||
for (const cell of Object.values(state.grid.cells)) {
|
||||
if (cell && cell.kind === "souvenirShop") shopCount += (cell.level ?? 1);
|
||||
}
|
||||
if (shopCount > 0) {
|
||||
const bonusPerShop = GameConfig.Visitor.SouvenirShopBonusPerShop ?? 0.2;
|
||||
souvenirBonus = 1 + shopCount * bonusPerShop;
|
||||
const luxuryChance = GameConfig.Visitor.LuxuryGuestChance ?? 0;
|
||||
const luxuryShopMult = GameConfig.Visitor.LuxuryShopMultiplier ?? 1;
|
||||
if (luxuryChance > 0 && luxuryShopMult > 1) {
|
||||
souvenirBonus *= 1 + luxuryChance * (luxuryShopMult - 1);
|
||||
}
|
||||
}
|
||||
paymentPerVisitor *= souvenirBonus;
|
||||
const luxuryChance = GameConfig.Visitor.LuxuryGuestChance ?? 0;
|
||||
const luxuryEntryMult = GameConfig.Visitor.LuxuryEntryMultiplier ?? 1;
|
||||
if (luxuryChance > 0 && luxuryEntryMult > 1) {
|
||||
paymentPerVisitor *= 1 + luxuryChance * (luxuryEntryMult - 1);
|
||||
}
|
||||
return { visitorCount, paymentPerVisitor };
|
||||
}
|
||||
|
||||
export function getVisitorCount(state) {
|
||||
return getVisitorParams(state).visitorCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attractivity score for display and future city allocation. Formula: value + species + rarity + fill rate, minus death penalty, plus birth bonus.
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getAttractivityScore(state) {
|
||||
const value = getTotalAnimalValue(state);
|
||||
const originCount = getOriginAnimalCount(state);
|
||||
const grid = state.grid;
|
||||
const cellCount = grid.width * grid.height;
|
||||
const fillRate = cellCount > 0 ? originCount / cellCount : 0;
|
||||
const speciesSet = new Set();
|
||||
let raritySum = 0;
|
||||
for (const [key, cell] of Object.entries(state.grid.cells)) {
|
||||
if (cell === null || cell === undefined || cell.kind !== "animal" || !isOriginCell(key, cell)) {
|
||||
// skip
|
||||
} else {
|
||||
speciesSet.add(cell.id);
|
||||
const def = LootTables.Animals[cell.id];
|
||||
if (def) raritySum += def.rarityLevel ?? 1;
|
||||
}
|
||||
}
|
||||
const speciesCount = speciesSet.size;
|
||||
const avgRarity = originCount > 0 ? raritySum / originCount : 0;
|
||||
const valueNorm = value * 0.001;
|
||||
const speciesNorm = speciesCount * 2;
|
||||
const rarityNorm = avgRarity * 0.5;
|
||||
const fillNorm = fillRate * 10;
|
||||
let score = valueNorm + speciesNorm + rarityNorm + fillNorm;
|
||||
const deathPenalty = GameConfig.Visitor?.AttractivityDeathPenalty ?? 0.5;
|
||||
const birthBonus = GameConfig.Visitor?.AttractivityBirthBonus ?? 0.2;
|
||||
const deaths = state.deathCountRecent ?? 0;
|
||||
const births = state.birthCount ?? 0;
|
||||
score -= deathPenalty * deaths;
|
||||
score += birthBonus * births;
|
||||
const incidentBonus = state.attractivityBonusFromIncidents ?? 0;
|
||||
score += incidentBonus;
|
||||
return Math.max(0, score);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("./types.js").AnimalCell} cell
|
||||
* @returns {number}
|
||||
*/
|
||||
function incomePerSecond(cell) {
|
||||
const animalDef = LootTables.Animals[cell.id];
|
||||
if (animalDef === null || animalDef === undefined) throw new Error("IncomeService: unknown animal");
|
||||
const mutationMult = getIncomeMultiplier(cell.mutation);
|
||||
const levelMult = getLevelMultiplier(cell.level);
|
||||
return animalDef.baseIncomePerSecond * mutationMult * levelMult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("./types.js").GameState} state
|
||||
* @param {number} dt
|
||||
* @param {{ incomeMultiplier: number }} eventModifiers
|
||||
* @returns {{ animal: number, visitor: number }}
|
||||
*/
|
||||
export function tick(state, dt, eventModifiers) {
|
||||
const prestigeMult = getPrestigeIncomeMultiplier(state.prestigeLevel);
|
||||
let animalTotal = 0;
|
||||
for (const [key, cell] of Object.entries(state.grid.cells)) {
|
||||
if (cell.kind === "animal" && isOriginCell(key, cell))
|
||||
animalTotal += incomePerSecond(cell) * dt * eventModifiers.incomeMultiplier * prestigeMult;
|
||||
}
|
||||
const { visitorCount, paymentPerVisitor } = getVisitorParams(state);
|
||||
const visitorTotal = visitorCount * paymentPerVisitor * dt * prestigeMult;
|
||||
const total = animalTotal + visitorTotal;
|
||||
state.coins += total;
|
||||
if (state.stats) state.stats.coinsEarned = (state.stats.coinsEarned ?? 0) + total;
|
||||
return { animal: animalTotal, visitor: visitorTotal };
|
||||
}
|
||||
Reference in New Issue
Block a user