Files
Battle-for-Tismo/shared/game_state/orders.js
T
adrien e7ee5dbf0e Added the resource economy, its works and their icons
- Five resources (energy, steel, food, luxury, high-tech) with per-city stores, a world market, delivery and material upkeep.
- Resource-producing tile works built outside cities; every nation opens with the works its economy needs.
- Real game-icons.net art replacing the dummy icons for the new resources and works, with the credits updated.
2026-09-22 05:27:11 +02:00

310 lines
13 KiB
JavaScript

// Player-issued orders that are not movement: training, building, government,
// research and pillaging.
import { key } from "../hex.js";
import { ECONOMY } from "../data/economy.js";
import { GOVERNMENTS } from "../data/governments.js";
import { PILLAGE, pillageSteps } from "../data/combat.js";
import { transportImprovement } from "../data/improvements.js";
import { buildingBuildCost, buildingRefund } from "../rules.js";
import { tileImprovementResourceCost } from "../resources.js";
import { TRAINING_QUEUE_LIMIT } from "./constants.js";
export const orderMethods = {
// Adds one unit to the city's production queue. The queue is shared with
// buildings and holds at most TRAINING_QUEUE_LIMIT entries; the first entry
// is produced while the rest wait. The unit's price is paid up front from the
// budget, then it takes production capacity to finish.
requestTrain(cityId, protoIndex, free = false) {
const city = this.findCity(cityId);
if (!city) return false;
if (protoIndex < 0 || protoIndex >= this.protoUnits.length) return false;
const proto = this.protoUnits[protoIndex];
// Aircraft and warships need the right installation in the city first.
if (proto.requiresBuilding && !this.hasCityBuilding(city, proto.requiresBuilding)) return false;
// The local testing shortcut: no cost, no production time, no queue.
if (free) {
this._spawnTrainedUnit(city, proto);
this._visibilityDirty = true;
this._emitChanged();
return true;
}
const queue = this.training.get(cityId);
if (queue && queue.length >= TRAINING_QUEUE_LIMIT) return false;
// A free unit (the worker) needs neither money nor production time.
if (proto.cost <= 0) {
this._spawnTrainedUnit(city, proto);
this._visibilityDirty = true;
this._emitChanged();
return true;
}
const capacity = ECONOMY.productionCapacity(this.getPlayerGdp(city.civ));
if (capacity <= 0) return false;
if (!this._spendBudget(city.civ, proto.cost, "training")) return false;
const materials = this.constructionResourceCost(proto, 0, proto.cost);
if (!this._payConstructionResources(city.civ, city.coords, materials, "training")) {
this._refundBudget(city.civ, proto.cost, "training");
return false;
}
const entry = {
kind: "unit",
protoIndex,
capacity,
elapsedHours: 0,
totalHours: (proto.cost / capacity) *
this.constructionApprovalMultiplier(city) *
this.constructionSpeedMultiplier(city.civ),
};
if (queue) queue.push(entry);
else this.training.set(cityId, [entry]);
this._emitChanged();
return true;
},
// Cancels the queued entry at `index` in the city's production queue.
// Nothing is refunded: the money and time already spent on it are lost.
cancelTrain(cityId, index) {
const queue = this.training.get(cityId);
if (!queue) return false;
const at = Number(index);
if (!Number.isInteger(at) || at < 0 || at >= queue.length) return false;
queue.splice(at, 1);
if (queue.length === 0) this.training.delete(cityId);
this._emitChanged();
return true;
},
// Queues one more level of a building in the city's production queue. Like a
// unit, the level is paid for up front from the budget and only rises once
// its production time has elapsed. Repeated orders stack levels: the second
// queued level is priced off the level the first will leave behind.
requestBuild(cityId, protoIndex, free = false) {
const city = this.findCity(cityId);
if (!city || protoIndex < 0 || protoIndex >= this.protoBuildings.length) return false;
const proto = this.protoBuildings[protoIndex];
// Placement rules: a port needs the sea, a shipyard needs a port.
if (proto.coastal && !this.isCoastalCity(city)) return false;
if (proto.requiresBuilding && !this.hasCityBuilding(city, proto.requiresBuilding)) return false;
if (proto.requiresTechnology && !this.hasTechnology(city.civ, proto.requiresTechnology)) return false;
// The local testing shortcut: raise the next level at once, for free.
if (free) {
const level =
this.getCityBuildingLevel(city, protoIndex) +
this._pendingBuildingLevels(cityId, protoIndex);
city.buildings[protoIndex] = level + 1;
this._gdpPerCapitaCache.clear();
this._touchModifiers();
this._emitChanged();
return true;
}
const queue = this.training.get(cityId);
if (queue && queue.length >= TRAINING_QUEUE_LIMIT) return false;
const level =
this.getCityBuildingLevel(city, protoIndex) +
this._pendingBuildingLevels(cityId, protoIndex);
const cost = buildingBuildCost(proto, level);
// A free improvement is raised at once.
if (cost <= 0) {
city.buildings[protoIndex] = level + 1;
this._gdpPerCapitaCache.clear();
this._touchModifiers();
this._emitChanged();
return true;
}
const capacity = ECONOMY.productionCapacity(this.getPlayerGdp(city.civ));
if (capacity <= 0) return false;
if (!this._spendBudget(city.civ, cost, "construction")) return false;
const materials = this.constructionResourceCost(proto, level, cost);
if (!this._payConstructionResources(city.civ, city.coords, materials)) {
this._refundBudget(city.civ, cost, "construction");
return false;
}
const entry = {
kind: "building",
protoIndex,
level,
capacity,
elapsedHours: 0,
totalHours: (cost / capacity) *
this.constructionApprovalMultiplier(city) *
this.constructionSpeedMultiplier(city.civ),
};
if (queue) queue.push(entry);
else this.training.set(cityId, [entry]);
this._emitChanged();
return true;
},
// Lays a road or railway on a land tile the civilisation controls. A tile
// holds at most one transport improvement, so building one replaces whatever
// is already there; the full price is still paid. The cost is charged up front
// and the tile changes at once, and afterwards the nation pays an hourly
// maintenance for every road and railway tile it holds.
requestBuildImprovement(civ, coords, id, free = false) {
if (civ < 0 || civ >= this.civilisations.length) return false;
const improvement = transportImprovement(id);
if (!improvement || !coords) return false;
if (!Number.isFinite(coords.x) || !Number.isFinite(coords.y)) return false;
const k = key(coords.x, coords.y);
const tile = this.tiles[k];
if (!tile || tile.terrainClass !== "Land") return false;
if (this.civAt(coords) !== civ) return false;
const current = this.railways.has(k) ? "railway" : this.roads.has(k) ? "road" : null;
if (current === improvement.id) return false;
if (!free) {
if (!this._spendBudget(civ, improvement.buildCost, "tilebuild")) return false;
const materials = tileImprovementResourceCost(improvement);
if (!this._payConstructionResources(civ, coords, materials, "tilebuild")) {
this._refundBudget(civ, improvement.buildCost, "tilebuild");
return false;
}
}
this.roads.delete(k);
this.railways.delete(k);
if (improvement.id === "railway") this.railways.add(k);
else this.roads.add(k);
this._improvementVersion += 1;
this._emitChanged();
return true;
},
// Removes whatever transport improvement stands on a land tile the
// civilisation controls. Removal is immediate and free, so a mis-built road
// can be cleared without a new one replacing it.
requestRemoveImprovement(civ, coords) {
if (civ < 0 || civ >= this.civilisations.length) return false;
if (!coords) return false;
if (!Number.isFinite(coords.x) || !Number.isFinite(coords.y)) return false;
const k = key(coords.x, coords.y);
const tile = this.tiles[k];
if (!tile || tile.terrainClass !== "Land") return false;
if (this.civAt(coords) !== civ) return false;
const present = this.railways.has(k) || this.roads.has(k);
if (!present) return false;
this.roads.delete(k);
this.railways.delete(k);
this._improvementVersion += 1;
this._emitChanged();
return true;
},
requestDemolish(cityId, protoIndex) {
const city = this.findCity(cityId);
if (!city || protoIndex < 0 || protoIndex >= this.protoBuildings.length) return false;
const proto = this.protoBuildings[protoIndex];
const civ = city.civ;
const level = this.getCityBuildingLevel(city, protoIndex);
if (level <= 0) return false;
const refund = buildingRefund(proto, level);
if (refund > 0) this._refundBudget(civ, refund, "construction");
if (level <= 1) delete city.buildings[protoIndex];
else city.buildings[protoIndex] = level - 1;
this._gdpPerCapitaCache.clear();
this._touchModifiers();
this._emitChanged();
return true;
},
requestSetGovernment(civ, index) {
if (civ < 0 || civ >= this.civilisations.length) return false;
if (index < 0 || index >= this.governments.length) return false;
if (this.getGovernment(civ) === index) return false;
const cost = GOVERNMENTS[index].baseCost +
GOVERNMENTS[index].costGdpFraction * Math.max(this.getPlayerGdp(civ), 0);
if (cost > 0) this._spendBudget(civ, cost, "campaign");
this.government.set(civ, index);
this._gdpPerCapitaCache.clear();
this._touchModifiers();
this._emitChanged();
return true;
},
// Focuses a nation's research on one technology. Every point it creates from
// now on flows straight into it; nothing is charged up front and nothing is
// banked while no technology is focused.
requestFocusTechnology(civ, index) {
if (!this.canResearchTechnology(civ, index)) return false;
if (this.getFocusedTechnology(civ) === index) return true;
// Progress is kept per technology, so switching focus never discards work
// already done on either the old focus or the new one.
this.focusTechnology.set(civ, index);
this._emitChanged();
return true;
},
// Whether the units may pillage the tile they stand on: all of them are on
// one hostile tile where no battle is raging.
canPillage(unitIds) {
const units = this._unitsForOrder(unitIds);
if (units.length === 0) return false;
const coords = units[0].coords;
const owner = this.civAt(coords);
if (owner < 0 || owner === units[0].civ) return false;
if (!units.every((unit) => unit.coords.x === coords.x && unit.coords.y === coords.y)) {
return false;
}
return !this._battleTiles().some(
(battle) => battle.coords.x === coords.x && battle.coords.y === coords.y
);
},
// Pillages the tile the units stand on. Every unit in the stack takes a turn,
// the weakest first (least HP relative to maximum): each removes 10% of the
// remaining population, cuts the tile's production per head by 30% and heals
// one hit point per 1000 people it drives off.
requestPillage(unitIds) {
const units = this._unitsForOrder(unitIds);
if (!this.canPillage(unitIds)) return false;
const coords = units[0].coords;
const owner = this.civAt(coords);
const k = key(coords.x, coords.y);
const ordered = units.slice().sort((a, b) => a.hp / a.maxHp - b.hp / b.maxHp);
let population = this.tilePopulation.get(k) || 0;
const { steps } = pillageSteps(population, ordered.length);
let penalty = this.tileProductionPenalty.get(k) !== undefined
? this.tileProductionPenalty.get(k)
: 1;
let drivenOff = 0;
for (let i = 0; i < ordered.length; i++) {
penalty *= PILLAGE.productionPenalty;
const unit = ordered[i];
unit.hp = Math.min(unit.maxHp, unit.hp + steps[i].heal);
drivenOff += steps[i].removed;
}
population = steps.length > 0 ? steps[steps.length - 1].remaining : population;
this.tilePopulation.set(k, population);
this._populationVersion += 1;
this.tileProductionPenalty.set(k, penalty);
this._gdpPerCapitaCache.delete(k);
// The people driven off the tile are civilians lost to its owner in the war
// against the pillaging civilisation.
if (owner >= 0) this._recordCasualties(owner, units[0].civ, "civilians", drivenOff);
// Pillaging the defenceless is a war crime the world remembers.
this._recordWarCrime(units[0].civ, coords, owner);
this._emitChanged();
return true;
},
// How many levels of `protoIndex` already sit in the city's production queue,
// so the next order is priced off the level the waiting ones will reach.
_pendingBuildingLevels(cityId, protoIndex) {
const queue = this.training.get(cityId);
if (!queue) return 0;
let count = 0;
for (const entry of queue) {
if (entry.kind === "building" && entry.protoIndex === protoIndex) count += 1;
}
return count;
},
_unitsForOrder(unitIds) {
const ids = Array.isArray(unitIds) ? unitIds : [unitIds];
const units = [];
for (const id of ids) {
const unit = this.findUnit(Number(id));
if (unit) units.push(unit);
}
return units;
},
};