SOURCE / PINNED RELEASE
Made of little things.
City Breaker
- Release
- 94d2b8664b10…
- Author-recorded commit
- df23103cbcc1…
- License
- LICENSE
- Author’s source reference
- nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-cf8b858653e
Archive hash verified: eeeed725668c1ece…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// City Breaker — every balance number lives here.
// Distances are game-km on the theater map, speeds are game-km per game-second in
// the mission view (time is compressed so a raid takes a few minutes to fly).
// Strategic time advances in 6-hour turns.
export const CONFIG = {
campaign: {
days: 60,
turnHours: 6,
startDay: 1,
startHour: 6,
startPolitical: 70,
winOutput: 0.15,
partialOutput: 0.45,
criticalNodesDaysForWin: 5,
criticalNodeDownThreshold: 0.3,
// Hard minimum turnaround after any sortie, before another raid can launch.
minHoursBetweenRaids: 6,
maxLaunchDelayHours: 96,
launchHourStep: 1,
missionDurationHours: 6,
},
// ── Political capital & ROE (Rolling Thunder as difficulty) ─────────────
politics: {
max: 100,
regenPerDay: 0.6,
civilianDeathsPer1000: 0.6,
highProfileCivilianEvent: 4, // a single raid killing > highProfileThreshold
highProfileThreshold: 800,
restrictedHospital: 14,
restrictedDam: 28,
restrictedGovt: 4,
aircraftLost: 0.6,
bearLostExtra: 2.0, // a Tu-95 down is on every newsreel in the West
capturedCrewParade: 1.2,
militarySuccessBonus: 20, // × war-output drop achieved by a raid (0.1 drop → +2)
roeViolation: 7,
linebackerDrainPerDay: 2.2,
firestormPenalty: 12,
roeTiers: [
{ min: 60, name: 'OPEN', noGo: [] as string[], minAlt: 0, dayOnly: false, standDownHours: 0, maxAircraft: 99 },
{ min: 45, name: 'RESTRICTED', noGo: ['housing', 'hospital', 'altstadt', 'govt'], minAlt: 0, dayOnly: false, standDownHours: 0, maxAircraft: 99 },
{ min: 30, name: 'TIGHT', noGo: ['housing', 'hospital', 'altstadt', 'govt', 'dam'], minAlt: 2.5, dayOnly: true, standDownHours: 12, maxAircraft: 32 },
{ min: 15, name: 'GRADUAL PRESSURE', noGo: ['housing', 'hospital', 'altstadt', 'govt', 'dam', 'port'], minAlt: 2.5, dayOnly: true, standDownHours: 24, maxAircraft: 20 },
{ min: 0, name: 'BOMBING HALT', noGo: ['housing', 'hospital', 'altstadt', 'govt', 'dam', 'port', 'chem'], minAlt: 4, dayOnly: true, standDownHours: 48, maxAircraft: 12 },
],
},
// ── Fleet ────────────────────────────────────────────────────────────────
aircraft: {
'Tu-16': { name: 'Tu-16 "Badger"', role: 'bomber', count: 22, loadKg: 9000, speed: 0.85, dash: 1.0, turn: 0.55, climb: 0.22, hp: 100, rcs: 1.0, rangeKm: 2400, tailGun: true, tailGunRange: 1.4, crew: 6, maintHours: 18, cost: 3, fighter: false, lowSpeedPenalty: 0 },
'Tu-22': { name: 'Tu-22 "Blinder"', role: 'bomber', count: 8, loadKg: 9000, speed: 1.05, dash: 1.55, turn: 0.38, climb: 0.3, hp: 85, rcs: 0.9, rangeKm: 2200, tailGun: true, tailGunRange: 1.2, crew: 3, maintHours: 30, cost: 5, fighter: false, lowSpeedPenalty: 0.35 },
'Tu-95': { name: 'Tu-95 "Bear"', role: 'bomber', count: 6, loadKg: 15000, speed: 0.72, dash: 0.8, turn: 0.4, climb: 0.15, hp: 160, rcs: 1.7, rangeKm: 6000, tailGun: true, tailGunRange: 1.4, crew: 9, maintHours: 30, cost: 8, fighter: false, lowSpeedPenalty: 0 },
'Il-28': { name: 'Il-28 "Beagle"', role: 'bomber', count: 18, loadKg: 3000, speed: 0.82, dash: 0.9, turn: 0.85, climb: 0.3, hp: 60, rcs: 0.6, rangeKm: 1100, tailGun: true, tailGunRange: 1.0, crew: 3, maintHours: 10, cost: 1, fighter: false, lowSpeedPenalty: 0 },
'MiG-21': { name: 'MiG-21PFM "Fishbed"', role: 'escort', count: 20, loadKg: 0, speed: 1.15, dash: 2.0, turn: 1.6, climb: 0.9, hp: 35, rcs: 0.35, rangeKm: 650, tailGun: false, tailGunRange: 0, crew: 1, maintHours: 8, cost: 1.5, fighter: true, lowSpeedPenalty: 0 },
'Su-7': { name: 'Su-7B "Fitter"', role: 'suppress', count: 14, loadKg: 2000, speed: 1.0, dash: 1.6, turn: 1.1, climb: 0.7, hp: 45, rcs: 0.45, rangeKm: 700, tailGun: false, tailGunRange: 0, crew: 1, maintHours: 10, cost: 1.5, fighter: true, lowSpeedPenalty: 0 },
'Yak-28': { name: 'Yak-28 "Brewer" (weasel)', role: 'weasel', count: 8, loadKg: 3000, speed: 1.0, dash: 1.4, turn: 0.9, climb: 0.55, hp: 55, rcs: 0.55, rangeKm: 1100, tailGun: false, tailGunRange: 0, crew: 2, maintHours: 12, cost: 2, fighter: false, lowSpeedPenalty: 0 },
'Tu-16PP': { name: 'Tu-16PP "Badger-J" (EW)', role: 'jammer', count: 4, loadKg: 0, speed: 0.85, dash: 0.95, turn: 0.5, climb: 0.2, hp: 100, rcs: 1.0, rangeKm: 2400, tailGun: true, tailGunRange: 1.4, crew: 7, maintHours: 20, cost: 4, fighter: false, lowSpeedPenalty: 0 },
'MiG-25R': { name: 'MiG-25R "Foxbat-B"', role: 'recon', count: 0, loadKg: 0, speed: 2.2, dash: 2.9, turn: 0.35, climb: 1.2, hp: 50, rcs: 0.8, rangeKm: 1600, tailGun: false, tailGunRange: 0, crew: 1, maintHours: 14, cost: 6, fighter: false, lowSpeedPenalty: 0 },
},
foxbatArrivalDay: 20,
foxbatCount: 2,
replacements: {
everyDays: 7,
progressNeeded: 0.08, // war output must have dropped this much since last delivery
politicalNeeded: 40,
bombers: 3,
fighters: 4,
},
crew: {
startXp: 0.45,
greenXp: 0.12,
xpPerSortie: 0.06,
xpCap: 1,
captureChance: 0.45, // of crews shot down over hostile territory, fraction captured
rescueSuccess: 0.55,
rescueHeloLoss: 0.25,
},
// ── Munitions ────────────────────────────────────────────────────────────
munitions: {
'FAB-250': { name: 'FAB-250', kg: 250, stock: 2400, weekly: 600, blast: 1.0, fire: 0.05, vsHard: 0.5, vsSoft: 1.0, vsRail: 1.0, radius: 0.18, guided: false, delay: false, cluster: false, missile: false },
'FAB-500': { name: 'FAB-500', kg: 500, stock: 1600, weekly: 400, blast: 1.05, fire: 0.05, vsHard: 0.8, vsSoft: 1.0, vsRail: 1.0, radius: 0.24, guided: false, delay: false, cluster: false, missile: false },
'FAB-1500': { name: 'FAB-1500', kg: 1500, stock: 140, weekly: 30, blast: 1.25, fire: 0.08, vsHard: 1.6, vsSoft: 1.1, vsRail: 0.9, radius: 0.4, guided: false, delay: false, cluster: false, missile: false },
'RBK-500': { name: 'RBK-500 cluster', kg: 500, stock: 360, weekly: 80, blast: 0.8, fire: 0.08, vsHard: 0.15, vsSoft: 2.6, vsRail: 1.7, radius: 0.45, guided: false, delay: false, cluster: true, missile: false },
'ZAB-500': { name: 'ZAB-500 incendiary', kg: 500, stock: 700, weekly: 150, blast: 0.25, fire: 1.0, vsHard: 0.1, vsSoft: 0.6, vsRail: 0.2, radius: 0.35, guided: false, delay: false, cluster: false, missile: false },
'FAB-500-LD': { name: 'FAB-500 long-delay', kg: 500, stock: 160, weekly: 40, blast: 0.6, fire: 0.0, vsHard: 0.6, vsSoft: 0.6, vsRail: 0.8, radius: 0.24, guided: false, delay: true, cluster: false, missile: false },
'UPAB-1500': { name: 'UPAB-1500 guided', kg: 1500, stock: 4, weekly: 4, blast: 1.4, fire: 0.05, vsHard: 2.4, vsSoft: 1.1, vsRail: 1.4, radius: 0.35, guided: true, delay: false, cluster: false, missile: false },
'S-24': { name: 'S-24 rockets', kg: 235, stock: 420, weekly: 100, blast: 0.8, fire: 0.05, vsHard: 0.3, vsSoft: 2.0, vsRail: 0.8, radius: 0.12, guided: false, delay: false, cluster: false, missile: false },
'KSR-11': { name: 'KSR-11 (AS-5 Kelt) anti-radar', kg: 1500, stock: 18, weekly: 4, blast: 1.0, fire: 0.05, vsHard: 1.0, vsSoft: 2.0, vsRail: 0.5, radius: 0.3, guided: true, delay: false, cluster: false, missile: true },
'K-10S': { name: 'K-10S standoff', kg: 4500, stock: 10, weekly: 2, blast: 1.6, fire: 0.1, vsHard: 2.0, vsSoft: 1.5, vsRail: 1.2, radius: 0.5, guided: true, delay: false, cluster: false, missile: true },
'Kh-22': { name: 'Kh-22 (AS-4) standoff', kg: 5800, stock: 6, weekly: 1, blast: 1.9, fire: 0.1, vsHard: 2.4, vsSoft: 1.6, vsRail: 1.2, radius: 0.6, guided: true, delay: false, cluster: false, missile: true },
},
// Which munitions each type may carry, and max missiles where load is not weight-limited.
loadouts: {
'Tu-16': ['FAB-250', 'FAB-500', 'FAB-1500', 'RBK-500', 'ZAB-500', 'FAB-500-LD', 'UPAB-1500', 'K-10S'],
'Tu-22': ['FAB-250', 'FAB-500', 'FAB-1500', 'RBK-500', 'ZAB-500', 'FAB-500-LD', 'Kh-22'],
'Tu-95': ['FAB-250', 'FAB-500', 'FAB-1500', 'RBK-500', 'ZAB-500', 'FAB-500-LD'],
'Il-28': ['FAB-250', 'FAB-500', 'RBK-500', 'ZAB-500', 'FAB-500-LD'],
'Su-7': ['S-24', 'FAB-250', 'RBK-500'],
'Yak-28': ['KSR-11'],
'MiG-21': [],
'Tu-16PP': [],
'MiG-25R': [],
} as Record<string, string[]>,
missileLimit: { 'K-10S': 1, 'Kh-22': 1, 'KSR-11': 2, 'UPAB-1500': 2 } as Record<string, number>,
pres: { stock: 500, weekly: 120, perJammerSortie: 40, perAircraft: 8, corridorCost: 60 },
drones: { stock: 24, weekly: 4 },
fuelTons: { stock: 4000, weekly: 1800, perSortieTon: 30, perBearTon: 70, perFighterTon: 8 },
// ── Mission physics (game units) ────────────────────────────────────────
mission: {
worldMinX: -75, worldMaxX: 135, worldMinY: -95, worldMaxY: 95,
egressX: 128,
altLow: 0.15, altMed: 4, altHigh: 10, altMax: 13,
playerSpeedScale: 1,
gravityFall: 2.0, // fall seconds = gravityFall * sqrt(altKm)
throwFactor: 0.85,
windKmS: 0.18,
windDriftFactor: 1.0,
windForecastError: 0.35, // fraction of wind unknown to the sight, grows with altitude
scatterBase: 0.12,
scatterPerKmAlt: 0.07,
scatterNight: 0.25,
scatterJink: 0.25,
scatterXpReduction: 0.55,
sticks: 8, // bomb entities per aircraft load
ripple: 1.4,
guidedSteerRate: 0.9,
timeScaleOptions: [1, 2, 4],
maxSeconds: 900,
timeCompression: 10, // displayed minutes = game seconds × this / 60
pace: 1.35, // base speed of the flown mission (time multiplier)
crew: {
doorsOpenSec: 1.2,
doorsDrag: 0.08,
doorsRcs: 1.35,
markers: 3,
markerLife: 90,
markerPullKm: 3.5,
extinguishers: 2,
engineFireExplodeSec: 14,
engineExplodeDmg: 40,
hungChance: 0.04,
hungChanceDamaged: 0.14,
fuelReserve: 1.7, // fuel load as a multiple of the planned route time
fuelLeakChance: 0.15,
bingo: 0.2,
beamWindowDeg: 28,
beamBreakPerSec: 0.55,
beamRangeKm: 9,
tailGunManualMult: 3,
tailGunAutoMult: 0.6,
tailGunRange: 1.8,
formation: { tight: 0.55, normal: 1, loose: 1.7 },
crashAglKm: 0.025,
pullUpAglKm: 0.25,
},
prerollToBarrierKm: 24, // the flown raid starts this far before the picket line
barragePerSecDay: 7, // unguided tracer streams over the city ('Baghdad, 1991')
barragePerSecNight: 16,
damageScale: 0.4,
waveOffsets: [-36, -24, -12, 0, 12, 24],
armRange: 34, armCone: 0.7,
standoffRange: 45, standoffMin: 12,
lowAltTerrainFollow: 0.15,
lowAltCrashBelow: 0.04,
tanker: { fighterBingoNoTanker: 0.72, fighterBingoTanker: 1.05 },
routeLength: { center: 1, north: 1.45, south: 1.4 },
},
// ── Enemy air defense ───────────────────────────────────────────────────
sam: {
nike: { name: 'Nike Hercules', range: 42, minAlt: 1.2, searchRange: 70, speed: 2.6, turn: 1.3, lethal: 0.4, kill: 60, pk: 0.55, trackTime: 5, reloadSec: 45, ready: 2, stock: 6, guidanceSec: 22 },
bloodhound: { name: 'Bloodhound', range: 26, minAlt: 0.35, searchRange: 50, speed: 2.2, turn: 1.8, lethal: 0.35, kill: 50, pk: 0.5, trackTime: 4, reloadSec: 40, ready: 2, stock: 6, guidanceSec: 18 },
lowAltDetectFactor: 0.35,
lowAltBelow: 0.6,
jamRangeFactor: 0.55,
burnThroughKm: 9,
chaffBreakChance: 0.55,
chaffCloudLife: 18,
chaffRadius: 1.4,
trackLossPerSecJammed: 0.08,
homeOnJamChance: 0.35,
relocateChance: 0.4,
radarRepairDays: 2,
siteRebuildDays: 5,
emconChance: 0.25, // radar shutdown when an anti-radar missile is detected
maxSimultaneousSam: 6,
missSigma: 0.36, // km of guidance error at intercept (1960s semi-active homing)
missSigmaJammed: 0.5,
reloadPerTurn: 2,
},
aaa: {
heavy: { name: '90mm radar AAA', range: 7, maxAlt: 11, minAlt: 1, rof: 0.9, burstR: 0.38, dmg: 13, walk: 0.6, err0: 1.8, errMin: 0.22 },
bofors: { name: '40mm Bofors', range: 3.4, maxAlt: 3.2, minAlt: 0, rof: 2.4, burstR: 0.2, dmg: 9, walk: 0.7, err0: 0.9, errMin: 0.12 },
vulcan: { name: '20mm Vulcan', range: 1.8, maxAlt: 1.5, minAlt: 0, rof: 6, burstR: 0.12, dmg: 6, walk: 0.8, err0: 0.6, errMin: 0.06 },
nightPenalty: 0.6,
searchlightBonus: 1.6,
radarAAAgrowlRange: 8,
suppressionPerHit: 0.35,
},
interceptors: {
'F-104G': { name: 'F-104G Starfighter', speed: 1.6, turn: 1.0, hp: 30, aim9: 2, aim7: 0, gun: true, count: 16 },
'F-4': { name: 'F-4 Phantom II', speed: 1.4, turn: 1.15, hp: 45, aim9: 4, aim7: 4, gun: false, count: 12 },
'Lightning': { name: 'Lightning F.6', speed: 1.5, turn: 1.3, hp: 35, aim9: 0, aim7: 0, red: 2, gun: true, count: 8 },
aim9: { range: 3.2, minRange: 0.6, cone: 0.5, speed: 2.4, turn: 2.4, pk: 0.32, dmg: 55, life: 7 },
aim7: { range: 9, minRange: 1.5, cone: 0.8, speed: 2.6, turn: 1.6, pk: 0.22, dmg: 70, life: 10 },
r3s: { range: 3.0, minRange: 0.6, cone: 0.45, speed: 2.3, turn: 2.2, pk: 0.3, dmg: 60, life: 7 },
gunRange: 0.9, gunCone: 0.18, gunDmgPerSec: 60,
tailGunDmgPerSec: 22,
scrambleDelaySec: 14,
scramblePerRaidSize: 0.35,
barrierCap: 4,
nightEffectiveness: 0.55,
fuelRationPerDepot: 1,
},
ew: {
jammerRadius: 18,
jammerStrength: 0.65,
freqHopPerLevel: 0.12, // adaptation erodes jamming
chaffCorridorDuration: 120,
droneLaunchLead: 50,
},
// ── City simulation (6h ticks) ──────────────────────────────────────────
city: {
population: 1200000,
workers: 400000,
workshopShare: 0.4,
batteryReserveHours: 10,
fireSpread: 0.22,
fireDamagePerTick: 0.06,
fireSuppressPerBrigade: 0.02,
firestormThreshold: 1.5, // summed fire across adjacent districts
firestormDamage: 0.45,
civiliansPerKgInHousing: 0.0045,
civiliansPerKgElsewhere: 0.0012,
fireCasualtiesPerTick: 150,
fireBurnoutPerTick: 0.2, // fires consume what they burn
evacuationThreshold: 0.25, // fresh destruction inside the window (Hamburg/Tokyo scale)
evacuationHousingLoss: 0.35, // and this share of homes must be gone
evacuationWindowDays: 7,
evacuationWorkforceLoss: 0.35,
moraleHardenPerRaid: 0.06,
resolveMobilizationBonus: 0.45,
repairPerCrewTick: 0.008,
substationRepairMult: 5,
railRepairMult: 2.4,
bridgeRepairMult: 0.12,
pontoonTicks: 12, // 3 days to float a ferry
pontoonCapacity: 0.45,
mutualAidEveryDays: 4,
mutualAidCrews: 6,
ldbHoursPerBomb: 3,
ldbClearPerTick: 4,
rebuildDispersalGain: 0.18,
stockpileDays: { coal: 14, fuel: 20, samReloads: 10, steel: 12 },
satelliteGrowthPerDispersal: 0.25,
pools: { fireBrigades: 30, railBattalions: 24, generators: 10, labor: 60 },
generatorPowerShare: 0.06,
interceptorReplacePerTick: 0.5,
samReloadsPerTick: 3,
},
output: {
weights: { steel: 0.16, machine: 0.2, chem: 0.08, workshops: 0.32, satellites: 0.14, port: 0.1 },
logisticsBottleneck: 0.55,
powerBottleneck: 0.5,
regionalImport: 0.35, // share of load the regional grid can feed through intact substations
workforceBottleneck: 0.7,
bottleneckThreshold: 0.3,
bottleneckPenalty: 0.8,
},
adaptation: {
pointsPerTick: 0.55,
resolveBonus: 1.5,
lagTicks: 4,
historyLength: 5,
maxLevel: 5,
costs: {
samDensity: 6, aaaDensity: 4, lowAltRadar: 5, barrierPatrol: 6, nightNet: 5, railResilience: 4,
firebreaks: 4, dispersal: 6, decoys: 3, hiddenFuel: 4, humanShields: 5, freqHop: 6, emcon: 4,
} as Record<string, number>,
decoyBasePerLevel: 0.09,
intelFreshDays: 3,
intelStaleDecayPerDay: 0.06,
reconRevealMiG25: 0.85,
reconRevealIl28: 0.55,
},
fx: {
maxParticles: 2200,
shakeFlakNear: 6,
shakeSamNear: 14,
shakeBomb: 3,
slowmoSeconds: 0.5,
slowmoScale: 0.25,
whiteFlash: 0.35,
vignettePulse: 0.6,
chaffBundlesPerPop: 26,
},
audio: {
master: 0.55,
rwr: 0.35,
radio: 0.5,
ambience: 0.35,
},
};
export type AircraftType = keyof typeof CONFIG.aircraft;
export type MunitionId = keyof typeof CONFIG.munitions;
export type InterceptorType = 'F-104G' | 'F-4' | 'Lightning';
export const AIRCRAFT_TYPES = Object.keys(CONFIG.aircraft) as AircraftType[];
export const MUNITION_IDS = Object.keys(CONFIG.munitions) as MunitionId[];
