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