161 lines
6.3 KiB
JavaScript
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;
|
|
},
|
|
};
|