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/map.ts
// Theater geography of Hafenstadt. Game-km, x east, y south (north is up).
// The sea lies west; the river flows east→west through the city to the port.

export type DistrictKind =
  | 'rail' | 'steel' | 'machine' | 'chem' | 'port' | 'fuel' | 'power' | 'substation'
  | 'warehouse' | 'altstadt' | 'housing' | 'military' | 'airbase' | 'govt' | 'bridge'
  | 'hospital' | 'dam' | 'satellite' | 'interdiction';

export interface DistrictDef {
  id: string;
  name: string;
  kind: DistrictKind;
  x: number;
  y: number;
  w: number;
  h: number;
  hardness: number; // 0 soft … 1 bunkered
  flammable: number; // 0 … 1
  popDensity: number; // relative 0 … 1
  substation?: string;
  aaa: { heavy: number; bofors: number; vulcan: number };
  restricted?: boolean;
  militaryValue: number;
  hp: number; // structural "mass" in damage points
}

const A = (heavy: number, bofors: number, vulcan: number) => ({ heavy, bofors, vulcan });

export const DISTRICTS: DistrictDef[] = [
  { id: 'yards', name: 'Rail Marshalling Yards', kind: 'rail', x: 8, y: -5, w: 5, h: 2.2, hardness: 0.25, flammable: 0.3, popDensity: 0.1, substation: 'subE', aaa: A(3, 4, 3), militaryValue: 1, hp: 90 },
  { id: 'steel', name: 'Hütte Steel Works', kind: 'steel', x: 15, y: -8, w: 5, h: 3, hardness: 0.7, flammable: 0.2, popDensity: 0.1, substation: 'subE', aaa: A(3, 3, 2), militaryValue: 1, hp: 160 },
  { id: 'machine', name: 'Kessler Machine Works', kind: 'machine', x: -9, y: -7, w: 4, h: 3, hardness: 0.55, flammable: 0.3, popDensity: 0.1, substation: 'subN', aaa: A(2, 3, 2), militaryValue: 1, hp: 130 },
  { id: 'chem', name: 'Rheinmark Chemical Plant', kind: 'chem', x: -17, y: 4, w: 4, h: 3, hardness: 0.35, flammable: 0.75, popDensity: 0.1, substation: 'subW', aaa: A(2, 2, 2), militaryValue: 1, hp: 100 },
  { id: 'port', name: 'River Port', kind: 'port', x: 6, y: 4, w: 5, h: 2, hardness: 0.35, flammable: 0.4, popDensity: 0.15, substation: 'subS', aaa: A(2, 3, 2), militaryValue: 1, hp: 110 },
  { id: 'fueldepot', name: 'Dockside Fuel Depot', kind: 'fuel', x: 12.5, y: 4.5, w: 2.4, h: 1.8, hardness: 0.2, flammable: 1, popDensity: 0.02, substation: 'subS', aaa: A(2, 3, 3), militaryValue: 1, hp: 60 },
  { id: 'power', name: 'Thermal Power Plant', kind: 'power', x: -3, y: 4.5, w: 2.6, h: 2.4, hardness: 0.6, flammable: 0.3, popDensity: 0.02, aaa: A(4, 4, 3), militaryValue: 1, hp: 120 },
  { id: 'subN', name: 'Substation Nord', kind: 'substation', x: 2, y: -12, w: 1.2, h: 1, hardness: 0.2, flammable: 0.2, popDensity: 0.05, aaa: A(1, 2, 1), militaryValue: 1, hp: 22 },
  { id: 'subE', name: 'Substation Ost', kind: 'substation', x: 12, y: -1.8, w: 1.2, h: 1, hardness: 0.2, flammable: 0.2, popDensity: 0.05, aaa: A(1, 2, 1), militaryValue: 1, hp: 22 },
  { id: 'subW', name: 'Substation West', kind: 'substation', x: -13, y: -1, w: 1.2, h: 1, hardness: 0.2, flammable: 0.2, popDensity: 0.05, aaa: A(1, 1, 1), militaryValue: 1, hp: 22 },
  { id: 'subS', name: 'Substation Süd', kind: 'substation', x: 2, y: 9, w: 1.2, h: 1, hardness: 0.2, flammable: 0.2, popDensity: 0.05, aaa: A(1, 1, 1), militaryValue: 1, hp: 22 },
  { id: 'warehouse', name: 'Speicherstadt Warehousing', kind: 'warehouse', x: 1, y: -2.2, w: 4, h: 1.6, hardness: 0.3, flammable: 0.7, popDensity: 0.2, substation: 'subE', aaa: A(1, 2, 1), militaryValue: 0.6, hp: 80 },
  { id: 'altstadt', name: 'Altstadt (old town)', kind: 'altstadt', x: -4, y: -3.5, w: 3.6, h: 2.4, hardness: 0.15, flammable: 0.95, popDensity: 1, substation: 'subN', aaa: A(1, 1, 1), militaryValue: 0.15, hp: 90 },
  { id: 'housingN', name: 'Nordstadt housing blocks', kind: 'housing', x: -2, y: -12, w: 7, h: 3.6, hardness: 0.45, flammable: 0.25, popDensity: 0.9, substation: 'subN', aaa: A(1, 1, 0), militaryValue: 0.3, hp: 200 },
  { id: 'housingW', name: 'Westend housing blocks', kind: 'housing', x: -15, y: -9, w: 5, h: 4.5, hardness: 0.45, flammable: 0.3, popDensity: 0.85, substation: 'subW', aaa: A(1, 1, 0), militaryValue: 0.3, hp: 170 },
  { id: 'housingS', name: 'Südufer housing blocks', kind: 'housing', x: 0, y: 12, w: 9, h: 3.6, hardness: 0.45, flammable: 0.25, popDensity: 0.9, substation: 'subS', aaa: A(1, 1, 0), militaryValue: 0.3, hp: 220 },
  { id: 'military', name: 'Military District (SAM garrison)', kind: 'military', x: -21, y: -15, w: 4, h: 3, hardness: 0.6, flammable: 0.2, popDensity: 0.05, substation: 'subW', aaa: A(2, 3, 2), militaryValue: 1, hp: 110 },
  { id: 'airbase', name: 'Lindenfeld Interceptor Base', kind: 'airbase', x: -33, y: 6, w: 4.5, h: 1.8, hardness: 0.45, flammable: 0.5, popDensity: 0.02, aaa: A(3, 4, 4), militaryValue: 1, hp: 100 },
  { id: 'govt', name: 'Government & Comms Node', kind: 'govt', x: -7.5, y: -1.8, w: 1.8, h: 1.4, hardness: 0.8, flammable: 0.2, popDensity: 0.4, substation: 'subN', aaa: A(2, 3, 2), militaryValue: 0.8, hp: 80 },
  { id: 'bridge', name: 'Elbbrücke Bridge-Tunnel Complex', kind: 'bridge', x: 3.3, y: 0.6, w: 1.1, h: 2.2, hardness: 0.75, flammable: 0, popDensity: 0.02, aaa: A(3, 4, 3), militaryValue: 1, hp: 70 },
  { id: 'hospital', name: 'St. Georg Hospital', kind: 'hospital', x: -10, y: 2.2, w: 1.2, h: 1, hardness: 0.3, flammable: 0.3, popDensity: 0.8, substation: 'subW', aaa: A(0, 0, 0), restricted: true, militaryValue: 0, hp: 30 },
  { id: 'dam', name: 'Talsperre Ostfluss (dam)', kind: 'dam', x: 55, y: -6.5, w: 1.4, h: 2.4, hardness: 0.95, flammable: 0, popDensity: 0, aaa: A(1, 2, 1), restricted: true, militaryValue: 0.4, hp: 150 },
  { id: 'satN', name: 'Nordheim satellite works', kind: 'satellite', x: 9, y: -30, w: 3, h: 2, hardness: 0.4, flammable: 0.35, popDensity: 0.3, aaa: A(1, 2, 1), militaryValue: 0.9, hp: 70 },
  { id: 'satW', name: 'Westwald satellite works', kind: 'satellite', x: -36, y: -21, w: 3, h: 2, hardness: 0.4, flammable: 0.35, popDensity: 0.3, aaa: A(1, 2, 1), militaryValue: 0.9, hp: 70 },
  { id: 'satS', name: 'Südhang satellite works', kind: 'satellite', x: 16, y: 29, w: 3, h: 2, hardness: 0.4, flammable: 0.35, popDensity: 0.3, aaa: A(1, 2, 1), militaryValue: 0.9, hp: 70 },
  { id: 'railE', name: 'East rail line (Ostbahn viaduct)', kind: 'interdiction', x: 40, y: -10.5, w: 2, h: 1, hardness: 0.5, flammable: 0, popDensity: 0, aaa: A(1, 2, 1), militaryValue: 1, hp: 30 },
  { id: 'railN', name: 'North rail line (Nordbahn cutting)', kind: 'interdiction', x: 9.5, y: -45, w: 1, h: 2, hardness: 0.4, flammable: 0, popDensity: 0, aaa: A(0, 2, 1), militaryValue: 1, hp: 30 },
  { id: 'railS', name: 'South rail line (Südbahn junction)', kind: 'interdiction', x: 18, y: 45, w: 1.5, h: 1.5, hardness: 0.4, flammable: 0, popDensity: 0, aaa: A(0, 2, 1), militaryValue: 1, hp: 30 },
  { id: 'lock', name: 'Barge lock (river traffic)', kind: 'interdiction', x: -42, y: 7.5, w: 1.6, h: 1, hardness: 0.7, flammable: 0, popDensity: 0, aaa: A(1, 2, 1), militaryValue: 1, hp: 50 },
  { id: 'road', name: 'Convoy road (Westwald pass)', kind: 'interdiction', x: -58, y: -26, w: 1.6, h: 1.6, hardness: 0.3, flammable: 0, popDensity: 0, aaa: A(0, 1, 1), militaryValue: 1, hp: 25 },
];

export const DISTRICT_BY_ID: Record<string, DistrictDef> = Object.fromEntries(
  DISTRICTS.map((d) => [d.id, d]),
);

export const SUBSTATIONS = ['subN', 'subE', 'subW', 'subS'];
export const CRITICAL_NODES = ['power', 'yards', 'bridge', 'fueldepot'];
export const FACTORY_IDS = ['steel', 'machine', 'chem'];
export const HOUSING_IDS = ['housingN', 'housingW', 'housingS', 'altstadt'];
export const SATELLITE_IDS = ['satN', 'satW', 'satS'];

// River centerline (east → west).
export const RIVER: [number, number][] = [
  [140, -12], [90, -8], [55, -6], [30, -3], [14, 1.8], [3, 1], [-8, 1.8], [-22, 6], [-42, 7.5], [-80, 9],
];

export interface SupplyRoute {
  id: string;
  name: string;
  mode: 'rail' | 'river' | 'road';
  points: [number, number][];
  node: string; // interdiction point district
  viaBridge: boolean;
  capacity: number;
}

export const SUPPLY_ROUTES: SupplyRoute[] = [
  { id: 'railE', name: 'Ostbahn', mode: 'rail', points: [[140, -14], [70, -12], [40, -10.5], [20, -6], [9, -5]], node: 'railE', viaBridge: false, capacity: 0.3 },
  { id: 'railN', name: 'Nordbahn', mode: 'rail', points: [[10, -95], [9.5, -45], [9, -30], [8, -14], [8, -6]], node: 'railN', viaBridge: false, capacity: 0.25 },
  { id: 'railS', name: 'Südbahn', mode: 'rail', points: [[20, 95], [18, 45], [16, 29], [4, 10], [3.3, 0.6], [6, -4.5]], node: 'railS', viaBridge: true, capacity: 0.2 },
  { id: 'river', name: 'River barges', mode: 'river', points: [[-80, 9], [-42, 7.5], [-22, 6], [-8, 2], [6, 4]], node: 'lock', viaBridge: false, capacity: 0.15 },
  { id: 'road', name: 'Convoy road', mode: 'road', points: [[-95, -30], [-58, -26], [-36, -21], [-14, -6], [-2, 3], [3.3, 0.6]], node: 'road', viaBridge: true, capacity: 0.1 },
];

export const PONTOON_POS: [number, number] = [-1.5, 1.4];

export type SamKind = 'nike' | 'bloodhound';
export interface SamSiteDef { id: string; kind: SamKind; x: number; y: number }
export const SAM_SITES: SamSiteDef[] = [
  { id: 'N1', kind: 'nike', x: -6, y: -19 },
  { id: 'N2', kind: 'nike', x: 19, y: 13 },
  { id: 'N3', kind: 'nike', x: -24, y: 9 },
  { id: 'B1', kind: 'bloodhound', x: 13, y: -15 },
  { id: 'B2', kind: 'bloodhound', x: -12, y: 15 },
  { id: 'B3', kind: 'bloodhound', x: 25, y: -2 },
  { id: 'B4', kind: 'bloodhound', x: -28, y: -7 },
  { id: 'BX1', kind: 'bloodhound', x: 68, y: -28 },
  { id: 'BX2', kind: 'bloodhound', x: 68, y: 26 },
];

export const BARRIER_X = 70;
export const BARRIER_RADARS: [number, number][] = [[72, -55], [72, 0], [72, 50]];

export type RouteId = 'center' | 'north' | 'south';
export const ROUTES: Record<RouteId, { name: string; ingress: [number, number][]; egress: [number, number][]; desc: string }> = {
  center: {
    name: 'Direct — through the barrier',
    ingress: [[132, 2], [90, 0], [45, -2]],
    egress: [[45, 4], [90, 6], [134, 8]],
    desc: 'Shortest. Crosses the picket radars and the Bloodhound line.',
  },
  north: {
    name: 'North — around the barrier',
    ingress: [[132, -70], [80, -85], [30, -70], [15, -40]],
    egress: [[25, -45], [70, -88], [134, -72]],
    desc: 'Skirts the picket line through the hills. Long: fighters need tankers.',
  },
  south: {
    name: 'South — around the barrier',
    ingress: [[132, 70], [80, 85], [35, 68], [20, 40]],
    egress: [[30, 45], [75, 88], [134, 72]],
    desc: 'Over the southern hills past Südhang. Long: fighters need tankers.',
  },
};

export function inRect(d: { x: number; y: number; w: number; h: number }, x: number, y: number, pad = 0): boolean {
  return Math.abs(x - d.x) <= d.w / 2 + pad && Math.abs(y - d.y) <= d.h / 2 + pad;
}

export function riverYAt(x: number): number {
  for (let i = 0; i < RIVER.length - 1; i++) {
    const [x1, y1] = RIVER[i];
    const [x2, y2] = RIVER[i + 1];
    if ((x <= x1 && x >= x2) || (x >= x1 && x <= x2)) {
      const t = (x - x1) / (x2 - x1 || 1);
      return y1 + (y2 - y1) * t;
    }
  }
  return 0;
}

export function districtNeighbors(id: string, within = 6): string[] {
  const d = DISTRICT_BY_ID[id];
  return DISTRICTS.filter((o) => o.id !== id && Math.hypot(o.x - d.x, o.y - d.y) < within + (o.w + d.w) / 2).map((o) => o.id);
}

/** Hills of the theater: [x, y, radius] in km. Shared by the painter and the flight model. */
export const HILLS: [number, number, number][] = [[10, -35, 14], [-38, -24, 16], [18, 34, 15], [-55, 25, 18], [50, -45, 20], [45, 40, 18], [-20, 40, 12], [80, -10, 14], [105, 55, 16], [110, -60, 18]];

/** Ground elevation in km (the city and river valley sit near zero). */
export function terrainHeight(x: number, y: number): number {
  let h = 0;
  for (const [hx, hy, r] of HILLS) {
    const d2 = ((x - hx) ** 2 + (y - hy) ** 2) / (r * r);
    if (d2 < 2.2) h += (0.12 + r * 0.03) * Math.exp(-2.5 * d2);
  }
  return h;
}