// 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; }, };