Files
Battle-for-Tismo/shared/game_state/wmd.js
T

161 lines
6.3 KiB
JavaScript

// Weapons of mass destruction: one-use missiles and the nuclear warhead they
// can carry. A missile is trained like any other unit but never marches; firing
// it consumes the rocket and lands its warhead -- conventional or nuclear -- on
// a tile within reach. An intercontinental ballistic missile carries its
// nuclear payload to any tile on the map.
import { key } from "../hex.js";
import { WMD } from "../data/combat.js";
import { tileImprovementById } from "../data/improvements.js";
export const wmdMethods = {
// ------------------------------------------------------------- missiles --
_isMissileProto(proto) {
return !!(proto && proto.missile);
},
_isMissileUnit(unit) {
return this._isMissileProto(this.unitProto(unit));
},
// How far a missile reaches: a global rocket reaches anywhere.
_missileRange(proto) {
if (!proto) return 0;
return proto.globalRange ? Infinity : (proto.range || 0);
},
// Whether a nation may arm a missile with a nuclear warhead: it needs the
// bomb, which in turn only became researchable once a uranium enrichment
// centre stood somewhere in the country.
_canUseNuclearPayload(civ) {
return this.hasTechnology(civ, "nuclear_weapons");
},
// Fires one or more missiles at `goal`. A conventional warhead strikes the
// unit or city there; a nuclear one flattens the whole tile. `nuclear` is
// honoured only for a nation that has researched the bomb, and an ICBM is
// always nuclear. Every missile is consumed, but only when its straight-line
// flight reaches the target: `_detonateMissile` runs the warhead on arrival,
// so a missile can be shot down by air defense on the way.
requestMissileStrike(unitIds, goal, { nuclear = false } = {}) {
const units = this._unitsForOrder(unitIds).filter((unit) => this._isMissileUnit(unit));
if (units.length === 0) return false;
const civ = units[0].civ;
if (units.some((unit) => unit.civ !== civ)) return false;
// An ICBM carries a nuclear warhead by design; a plain missile may be
// up-armed only by a nuclear power.
const anyNuclear = units.some((unit) => {
const proto = this.unitProto(unit);
return !!(proto && proto.nuclear);
});
const useNuclear = nuclear || anyNuclear;
if (useNuclear && !this._canUseNuclearPayload(civ)) return false;
const target = this._strikeTargetAt(goal, civ);
const city = target ? null : this._strikeCityAt(goal, civ);
if (!target && !city) return false;
if (units.some((unit) => !this._missileInRange(unit, goal))) return false;
this._declareWar(civ, target ? target.civ : city.civ);
for (const unit of units) {
const path = this.findPath(unit, unit.coords, goal);
if (path.length < 2) return false;
unit.path = path;
unit.pathIndex = 0;
unit.progressHours = 0;
unit.moveSpeed = null;
unit.waypoints = [];
unit.missileTarget = { x: goal.x, y: goal.y };
unit.nuclearPayload = useNuclear;
}
this._invalidateUnitCaches();
this._visibilityDirty = true;
this._emitChanged();
return true;
},
_missileInRange(unit, goal) {
const proto = this.unitProto(unit);
if (!proto) return false;
const distance = this.topology.tileDistance(unit.coords, goal);
return distance <= this._missileRange(proto) + 1e-9;
},
// A conventional warhead is simply a very heavy ranged strike: it hits the
// garrison if one is there, otherwise the city itself.
_resolveConventionalMissile(unit, goal) {
const target = this._strikeTargetAt(goal, unit.civ);
if (target) {
const damage = Math.max(1, this._unitAttack(unit) - this._effectiveDefense(target, true));
this._applyStrikeDamage(target, damage);
return;
}
const city = this._strikeCityAt(goal, unit.civ);
if (city) this._bombardCity(city, Math.max(1, this._unitAttack(unit)));
},
// ------------------------------------------------------------------ WMD --
// A nuclear detonation: nine people in ten on the tile die, every building
// and work there is flattened except the ones built to survive a bomb, and
// the world recoils -- every nation, the attacker included, loses a thousand
// points of standing.
_applyNuclearBlast(coords, attackerCiv = -1) {
const k = key(coords.x, coords.y);
const population = this.tilePopulation.get(k) || 0;
if (population > 0) {
this.tilePopulation.set(k, population * WMD.populationSurvivorFraction);
this._populationVersion += 1;
}
this._dropTileGdpCache(k);
const city = this.cityAt(coords);
if (city) this._nuclearRazeCity(city);
this._nuclearDestroyImprovement(coords);
// No unit caught on the ground at the point of detonation survives, whatever
// flag it flies.
for (const unit of this.unitsAt(coords).slice()) this._destroyUnit(unit);
this._invalidateUnitCaches();
for (let civ = 0; civ < this.civilisations.length; civ++) {
this._shiftOwnPopularity(civ, -WMD.popularityPenalty);
}
this._emitChanged();
},
// Every building in the city is destroyed outright, except the bomb-proof
// ones (bunkers). The levels are gone for good, not queued for repair.
_nuclearRazeCity(city) {
let razed = false;
for (const index of Object.keys(city.buildings || {})) {
const level = city.buildings[index];
if (level <= 0) continue;
const proto = this.protoBuildings[index];
if (proto && proto.nuclearProof) continue;
delete city.buildings[index];
razed = true;
}
if (razed) {
this._clearTileGdpCache();
this._touchModifiers();
}
},
// The works and roads on a nuked tile are destroyed, save the fortifications
// (and any other nuclear-proof work) that were built to survive one.
_nuclearDestroyImprovement(coords) {
const k = key(coords.x, coords.y);
if (this.tileImprovements.has(k)) {
const id = this.tileImprovements.get(k);
const proto = tileImprovementById(id);
if (!(proto && proto.nuclearProof)) {
this.tileImprovements.delete(k);
this.tileImprovementHp.delete(k);
this.tileImprovementOwner.delete(k);
this.tileRepairDebt.delete(k);
}
}
// Roads and railways do not survive a direct hit.
this.roads.delete(k);
this.railways.delete(k);
this._improvementVersion += 1;
},
};