Back to City Breaker
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.

src/game/config.ts
// 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[];