163 lines
6.3 KiB
JavaScript
163 lines
6.3 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 { buildingBuildCost, buildingRefund } from "../rules.js";
|
|
import { TRAINING_QUEUE_LIMIT } from "./constants.js";
|
|
|
|
export const orderMethods = {
|
|
// Adds one unit to the city's training queue. The queue holds at most
|
|
// TRAINING_QUEUE_LIMIT units; the first entry is built while the rest wait.
|
|
requestTrain(cityId, protoIndex) {
|
|
const city = this.findCity(cityId);
|
|
if (!city) return false;
|
|
if (protoIndex < 0 || protoIndex >= this.protoUnits.length) return false;
|
|
const proto = this.protoUnits[protoIndex];
|
|
const queue = this.training.get(cityId);
|
|
if (queue && queue.length >= TRAINING_QUEUE_LIMIT) return false;
|
|
const capacity = ECONOMY.productionCapacity(this.getPlayerGdp(city.civ));
|
|
if (capacity <= 0 || proto.cost <= 0) {
|
|
this._spawnUnit(city.coords, city.civ, proto);
|
|
this._visibilityDirty = true;
|
|
this._emitChanged();
|
|
return true;
|
|
}
|
|
const entry = {
|
|
protoIndex,
|
|
capacity,
|
|
elapsedHours: 0,
|
|
totalHours: proto.cost / capacity,
|
|
};
|
|
if (queue) queue.push(entry);
|
|
else this.training.set(cityId, [entry]);
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
// Cancels the queued unit at `index` in the city's training queue. Nothing is
|
|
// refunded: the time already spent on it is 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;
|
|
},
|
|
|
|
requestBuild(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);
|
|
const cost = buildingBuildCost(proto, level);
|
|
if (cost > 0 && this.getBudget(civ) < cost) return false;
|
|
if (cost > 0) this.budgets.set(civ, this.getBudget(civ) - cost);
|
|
city.buildings[protoIndex] = level + 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.budgets.set(civ, this.getBudget(civ) + refund);
|
|
if (level <= 1) delete city.buildings[protoIndex];
|
|
else city.buildings[protoIndex] = level - 1;
|
|
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.getBudget(civ) < cost) return false;
|
|
if (cost > 0) this.budgets.set(civ, this.getBudget(civ) - cost);
|
|
this.government.set(civ, index);
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
requestResearchTechnology(civ, index) {
|
|
if (!this.canResearchTechnology(civ, index)) return false;
|
|
const technology = this.technologies[index];
|
|
if (this.getResearch(civ) < technology.cost) return false;
|
|
this.research.set(civ, this.getResearch(civ) - technology.cost);
|
|
this.researched.get(civ).add(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 GDP per capita 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.tileGdpPenalty.get(k) !== undefined ? this.tileGdpPenalty.get(k) : 1;
|
|
let drivenOff = 0;
|
|
for (let i = 0; i < ordered.length; i++) {
|
|
penalty *= PILLAGE.gdpPenalty;
|
|
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.tileGdpPenalty.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);
|
|
this._emitChanged();
|
|
return true;
|
|
},
|
|
|
|
_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;
|
|
},
|
|
};
|