burnrate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +507 -0
  3. package/dist/cli/format.d.ts +13 -0
  4. package/dist/cli/format.js +319 -0
  5. package/dist/cli/index.d.ts +7 -0
  6. package/dist/cli/index.js +1121 -0
  7. package/dist/cli/setup.d.ts +8 -0
  8. package/dist/cli/setup.js +143 -0
  9. package/dist/core/async-engine.d.ts +411 -0
  10. package/dist/core/async-engine.js +2274 -0
  11. package/dist/core/async-worldgen.d.ts +19 -0
  12. package/dist/core/async-worldgen.js +221 -0
  13. package/dist/core/engine.d.ts +154 -0
  14. package/dist/core/engine.js +1104 -0
  15. package/dist/core/pathfinding.d.ts +38 -0
  16. package/dist/core/pathfinding.js +146 -0
  17. package/dist/core/types.d.ts +489 -0
  18. package/dist/core/types.js +359 -0
  19. package/dist/core/worldgen.d.ts +22 -0
  20. package/dist/core/worldgen.js +292 -0
  21. package/dist/db/database.d.ts +83 -0
  22. package/dist/db/database.js +829 -0
  23. package/dist/db/turso-database.d.ts +177 -0
  24. package/dist/db/turso-database.js +1586 -0
  25. package/dist/mcp/server.d.ts +7 -0
  26. package/dist/mcp/server.js +1877 -0
  27. package/dist/server/api.d.ts +8 -0
  28. package/dist/server/api.js +1234 -0
  29. package/dist/server/async-tick-server.d.ts +5 -0
  30. package/dist/server/async-tick-server.js +63 -0
  31. package/dist/server/errors.d.ts +78 -0
  32. package/dist/server/errors.js +156 -0
  33. package/dist/server/rate-limit.d.ts +22 -0
  34. package/dist/server/rate-limit.js +134 -0
  35. package/dist/server/tick-server.d.ts +9 -0
  36. package/dist/server/tick-server.js +114 -0
  37. package/dist/server/validation.d.ts +194 -0
  38. package/dist/server/validation.js +114 -0
  39. package/package.json +65 -0
@@ -0,0 +1,359 @@
1
+ /**
2
+ * BURNRATE Core Types
3
+ * The front doesn't feed itself.
4
+ */
5
+ /** Empty inventory factory */
6
+ export function emptyInventory() {
7
+ return {
8
+ ore: 0, fuel: 0, grain: 0, fiber: 0,
9
+ metal: 0, chemicals: 0, rations: 0, textiles: 0,
10
+ ammo: 0, medkits: 0, parts: 0, comms: 0,
11
+ credits: 0
12
+ };
13
+ }
14
+ /** SU recipe: what's needed to create 1 Supply Unit */
15
+ export const SU_RECIPE = {
16
+ rations: 2,
17
+ fuel: 1,
18
+ parts: 1,
19
+ ammo: 1
20
+ };
21
+ /** Production recipes: inputs required to produce 1 unit of output */
22
+ export const RECIPES = {
23
+ // T1 (Processed) - made from T0 (Raw)
24
+ metal: { inputs: { ore: 2, fuel: 1 }, tier: 1 },
25
+ chemicals: { inputs: { ore: 1, fuel: 2 }, tier: 1 },
26
+ rations: { inputs: { grain: 3, fuel: 1 }, tier: 1 },
27
+ textiles: { inputs: { fiber: 2, chemicals: 1 }, tier: 1 },
28
+ // T2 (Strategic) - made from T1 (Processed)
29
+ ammo: { inputs: { metal: 1, chemicals: 1 }, tier: 2 },
30
+ medkits: { inputs: { chemicals: 1, textiles: 1 }, tier: 2 },
31
+ parts: { inputs: { metal: 1, textiles: 1 }, tier: 2 },
32
+ comms: { inputs: { metal: 1, chemicals: 1, parts: 1 }, tier: 2 },
33
+ // T3 (Military Units) - made from T2 (Strategic)
34
+ escort: { inputs: { metal: 2, parts: 1, rations: 1 }, tier: 3, isUnit: true },
35
+ raider: { inputs: { metal: 2, parts: 2, comms: 1 }, tier: 3, isUnit: true },
36
+ };
37
+ /** What raw resource a Field produces based on its name */
38
+ export function getFieldResource(fieldName) {
39
+ if (fieldName.includes('Ore'))
40
+ return 'ore';
41
+ if (fieldName.includes('Fuel'))
42
+ return 'fuel';
43
+ if (fieldName.includes('Grain'))
44
+ return 'grain';
45
+ if (fieldName.includes('Fiber'))
46
+ return 'fiber';
47
+ return null;
48
+ }
49
+ export function getSupplyState(supplyLevel, complianceStreak = 0) {
50
+ if (supplyLevel >= 100 && complianceStreak >= 50)
51
+ return 'fortified';
52
+ if (supplyLevel >= 100)
53
+ return 'supplied';
54
+ if (supplyLevel >= 50)
55
+ return 'strained';
56
+ if (supplyLevel > 0)
57
+ return 'critical';
58
+ return 'collapsed';
59
+ }
60
+ /** Calculate zone efficiency from supply state, streak, and stockpiles */
61
+ export function getZoneEfficiency(supplyLevel, complianceStreak, medkitStockpile = 0, commsStockpile = 0) {
62
+ const state = getSupplyState(supplyLevel, complianceStreak);
63
+ // Base values by supply state
64
+ let raidResistance = 1.0;
65
+ let captureDefense = 1.0;
66
+ let productionBonus = 0.0;
67
+ switch (state) {
68
+ case 'fortified':
69
+ raidResistance = 1.5;
70
+ captureDefense = 1.5;
71
+ productionBonus = 0.1;
72
+ break;
73
+ case 'supplied':
74
+ raidResistance = 1.0;
75
+ captureDefense = 1.0;
76
+ productionBonus = 0.0;
77
+ break;
78
+ case 'strained':
79
+ raidResistance = 0.75;
80
+ captureDefense = 0.75;
81
+ productionBonus = 0.0;
82
+ break;
83
+ case 'critical':
84
+ raidResistance = 0.5;
85
+ captureDefense = 0.25;
86
+ productionBonus = 0.0;
87
+ break;
88
+ case 'collapsed':
89
+ raidResistance = 0.0;
90
+ captureDefense = 0.0;
91
+ productionBonus = 0.0;
92
+ break;
93
+ }
94
+ // Compliance streak bonuses (stacking on top of fortified)
95
+ if (complianceStreak >= 500) {
96
+ raidResistance += 0.5;
97
+ captureDefense += 0.5;
98
+ productionBonus += 0.2;
99
+ }
100
+ else if (complianceStreak >= 200) {
101
+ raidResistance += 0.25;
102
+ captureDefense += 0.25;
103
+ productionBonus += 0.1;
104
+ }
105
+ else if (complianceStreak >= 50) {
106
+ raidResistance += 0.1;
107
+ captureDefense += 0.1;
108
+ }
109
+ // Medkit stockpile → combat bonus (diminishing returns, cap at +50%)
110
+ const medkitBonus = Math.min(0.5, medkitStockpile * 0.02);
111
+ // Comms stockpile → intel defense (diminishing returns, cap at +50%)
112
+ const commsDefense = Math.min(0.5, commsStockpile * 0.025);
113
+ return { state, raidResistance, captureDefense, productionBonus, medkitBonus, commsDefense };
114
+ }
115
+ /** Burn rates by zone type */
116
+ export const BURN_RATES = {
117
+ hub: 0, // Hubs don't burn (safe zones)
118
+ factory: 5,
119
+ field: 3,
120
+ junction: 0, // Junctions don't burn (transit only)
121
+ front: 10,
122
+ stronghold: 20
123
+ };
124
+ export const SHIPMENT_SPECS = {
125
+ courier: { capacity: 10, speedModifier: 0.67, visibilityModifier: 0.5 },
126
+ freight: { capacity: 50, speedModifier: 1.0, visibilityModifier: 1.0 },
127
+ convoy: { capacity: 200, speedModifier: 1.33, visibilityModifier: 2.0 }
128
+ };
129
+ /** License unlock requirements */
130
+ export const LICENSE_REQUIREMENTS = {
131
+ courier: { reputationRequired: 0, creditsCost: 0, description: 'Basic small cargo transport' },
132
+ freight: { reputationRequired: 50, creditsCost: 500, description: 'Medium cargo transport' },
133
+ convoy: { reputationRequired: 200, creditsCost: 2000, description: 'Heavy armored transport' }
134
+ };
135
+ /** Reputation rewards for various actions */
136
+ export const REPUTATION_REWARDS = {
137
+ // Shipments
138
+ shipmentDelivered: 5, // per successful delivery
139
+ shipmentIntercepted: -10, // lost cargo
140
+ escortSuccess: 3, // escort protected a shipment
141
+ // Contracts
142
+ contractCompleted: 10, // base reward (+ contract-specific)
143
+ contractFailed: -20, // failed to complete
144
+ contractBonus: 5, // completed early
145
+ // Zone supply
146
+ supplyDelivered: 2, // per SU delivered
147
+ zoneCaptured: 25, // captured a zone
148
+ // Combat
149
+ raiderDestroyed: 5, // destroyed an enemy raider
150
+ escortDestroyed: -5, // lost an escort
151
+ // Market
152
+ tradeCompleted: 1, // completed a trade
153
+ // Daily decay (for inactive players)
154
+ dailyDecay: -1, // lose 1 rep per day of inactivity
155
+ // Maximum reputation
156
+ maxReputation: 1000
157
+ };
158
+ /** Reputation thresholds for titles */
159
+ export const REPUTATION_TITLES = [
160
+ { threshold: 0, title: 'Unknown' },
161
+ { threshold: 25, title: 'Runner' },
162
+ { threshold: 50, title: 'Trader' },
163
+ { threshold: 100, title: 'Hauler' },
164
+ { threshold: 200, title: 'Merchant' },
165
+ { threshold: 350, title: 'Supplier' },
166
+ { threshold: 500, title: 'Quartermaster' },
167
+ { threshold: 700, title: 'Logistics Chief' },
168
+ { threshold: 900, title: 'Supply Marshal' },
169
+ { threshold: 1000, title: 'Legend' }
170
+ ];
171
+ /** Get reputation title based on rep value */
172
+ export function getReputationTitle(reputation) {
173
+ for (let i = REPUTATION_TITLES.length - 1; i >= 0; i--) {
174
+ if (reputation >= REPUTATION_TITLES[i].threshold) {
175
+ return REPUTATION_TITLES[i].title;
176
+ }
177
+ }
178
+ return 'Unknown';
179
+ }
180
+ export const TIER_LIMITS = {
181
+ freelance: { dailyActions: 200, eventHistory: 200, concurrentContracts: 3, marketOrders: 5 },
182
+ operator: { dailyActions: 500, eventHistory: 10000, concurrentContracts: 10, marketOrders: 20 },
183
+ command: { dailyActions: 1000, eventHistory: 100000, concurrentContracts: 25, marketOrders: 50 }
184
+ };
185
+ /** Permissions for each rank */
186
+ export const FACTION_PERMISSIONS = {
187
+ founder: {
188
+ canInvite: true,
189
+ canKick: true,
190
+ canPromote: true,
191
+ canDemote: true,
192
+ canWithdraw: true,
193
+ canSetDoctrine: true,
194
+ canDeclareWar: true,
195
+ canUpgrade: true
196
+ },
197
+ officer: {
198
+ canInvite: true,
199
+ canKick: true, // can kick members only, not officers
200
+ canPromote: false,
201
+ canDemote: false,
202
+ canWithdraw: true, // up to officer limit
203
+ canSetDoctrine: false,
204
+ canDeclareWar: false,
205
+ canUpgrade: false
206
+ },
207
+ member: {
208
+ canInvite: false,
209
+ canKick: false,
210
+ canPromote: false,
211
+ canDemote: false,
212
+ canWithdraw: false,
213
+ canSetDoctrine: false,
214
+ canDeclareWar: false,
215
+ canUpgrade: false
216
+ }
217
+ };
218
+ /** Intel freshness thresholds (in ticks) */
219
+ export const INTEL_DECAY_THRESHOLDS = {
220
+ fresh: 10, // <10 ticks: full accuracy
221
+ stale: 50, // 10-50 ticks: degraded accuracy
222
+ // >50 ticks: expired, major degradation
223
+ };
224
+ /** Calculate intel freshness based on age */
225
+ export function getIntelFreshness(gatheredAt, currentTick) {
226
+ const age = currentTick - gatheredAt;
227
+ if (age < INTEL_DECAY_THRESHOLDS.fresh)
228
+ return 'fresh';
229
+ if (age < INTEL_DECAY_THRESHOLDS.stale)
230
+ return 'stale';
231
+ return 'expired';
232
+ }
233
+ /** Calculate effective signal quality with decay */
234
+ export function getDecayedSignalQuality(originalQuality, gatheredAt, currentTick) {
235
+ const freshness = getIntelFreshness(gatheredAt, currentTick);
236
+ switch (freshness) {
237
+ case 'fresh':
238
+ return originalQuality;
239
+ case 'stale':
240
+ // Lose 1% per tick after fresh threshold
241
+ const staleAge = currentTick - gatheredAt - INTEL_DECAY_THRESHOLDS.fresh;
242
+ return Math.max(50, originalQuality - staleAge);
243
+ case 'expired':
244
+ // Minimum 20% quality for expired intel
245
+ return Math.max(20, originalQuality * 0.3);
246
+ }
247
+ }
248
+ /** Apply decay to intel data based on freshness */
249
+ export function applyIntelDecay(data, freshness) {
250
+ if (freshness === 'fresh') {
251
+ return { ...data };
252
+ }
253
+ const decayed = { ...data };
254
+ if (freshness === 'stale') {
255
+ // For stale intel, add uncertainty to numeric values
256
+ for (const [key, value] of Object.entries(decayed)) {
257
+ if (typeof value === 'number' && key !== 'distance') {
258
+ // Add ±10% noise to numbers
259
+ const variance = Math.round(value * 0.1);
260
+ decayed[key] = value + (Math.random() > 0.5 ? variance : -variance);
261
+ }
262
+ }
263
+ decayed._stale = true;
264
+ decayed._warning = 'Intel is stale. Values may be inaccurate.';
265
+ }
266
+ if (freshness === 'expired') {
267
+ // For expired intel, heavily degrade or remove sensitive data
268
+ const sensitiveKeys = ['raiderStrength', 'raiderPresence', 'activeShipments',
269
+ 'suStockpile', 'supplyLevel', 'marketActivity'];
270
+ for (const key of sensitiveKeys) {
271
+ if (key in decayed) {
272
+ if (typeof decayed[key] === 'number') {
273
+ // Replace with "unknown" range
274
+ const originalValue = decayed[key];
275
+ decayed[key] = `~${Math.round(originalValue * 0.5)}-${Math.round(originalValue * 1.5)}`;
276
+ }
277
+ else if (typeof decayed[key] === 'boolean') {
278
+ decayed[key] = 'unknown';
279
+ }
280
+ }
281
+ }
282
+ decayed._expired = true;
283
+ decayed._warning = 'Intel is expired. Data is unreliable and should be refreshed.';
284
+ }
285
+ return decayed;
286
+ }
287
+ /** Tutorial contract definitions */
288
+ export const TUTORIAL_CONTRACTS = [
289
+ {
290
+ step: 1,
291
+ title: 'First Haul',
292
+ description: 'Buy 20 ore at the Hub market and deliver it to an adjacent Factory. This teaches market buying, inventory management, and basic shipping.',
293
+ type: 'tutorial',
294
+ reward: { credits: 100, reputation: 5 }
295
+ },
296
+ {
297
+ step: 2,
298
+ title: 'Factory Floor',
299
+ description: 'At a Factory, produce 10 metal from ore. This teaches production recipes and resource conversion.',
300
+ type: 'tutorial',
301
+ reward: { credits: 150, reputation: 5 }
302
+ },
303
+ {
304
+ step: 3,
305
+ title: 'Supply Run',
306
+ description: 'Craft and deposit 5 Supply Units to any Front zone. This teaches the SU recipe, supply mechanics, and multi-hop route planning.',
307
+ type: 'tutorial',
308
+ reward: { credits: 250, reputation: 10 }
309
+ },
310
+ {
311
+ step: 4,
312
+ title: 'Intel Sweep',
313
+ description: 'Scan 3 different zones to gather intel. This teaches the intel system, freshness decay, and faction intel sharing.',
314
+ type: 'tutorial',
315
+ reward: { credits: 200, reputation: 5 }
316
+ },
317
+ {
318
+ step: 5,
319
+ title: 'Join the Fight',
320
+ description: 'Join a faction and deposit any resource to the faction treasury. This teaches faction membership, treasury mechanics, and collaborative play.',
321
+ type: 'tutorial',
322
+ reward: { credits: 500, reputation: 15 }
323
+ }
324
+ ];
325
+ // ============================================================================
326
+ // SEASONS
327
+ // ============================================================================
328
+ /** Season configuration */
329
+ export const SEASON_CONFIG = {
330
+ ticksPerWeek: 1008, // 7 days * 144 ticks/day
331
+ weeksPerSeason: 4, // 4 weeks per season
332
+ ticksPerSeason: 4032, // 4 weeks * 1008 ticks
333
+ // Scoring weights
334
+ scoring: {
335
+ zonesControlled: 100, // per zone at season end
336
+ supplyDelivered: 1, // per SU delivered
337
+ shipmentsCompleted: 10, // per successful delivery
338
+ contractsCompleted: 25, // per contract completed
339
+ reputationGained: 2, // per reputation point gained
340
+ combatVictories: 50, // per successful defense/raid
341
+ }
342
+ };
343
+ /** Calculate total score from components */
344
+ export function calculateSeasonScore(score) {
345
+ return (score.zonesControlled * SEASON_CONFIG.scoring.zonesControlled +
346
+ score.supplyDelivered * SEASON_CONFIG.scoring.supplyDelivered +
347
+ score.shipmentsCompleted * SEASON_CONFIG.scoring.shipmentsCompleted +
348
+ score.contractsCompleted * SEASON_CONFIG.scoring.contractsCompleted +
349
+ score.reputationGained * SEASON_CONFIG.scoring.reputationGained +
350
+ score.combatVictories * SEASON_CONFIG.scoring.combatVictories);
351
+ }
352
+ export const GARRISON_LEVELS = [
353
+ { level: 0, defense: 0, raidResist: 0, maintenance: 0 },
354
+ { level: 1, defense: 0.1, raidResist: 0.1, maintenance: 10 },
355
+ { level: 2, defense: 0.2, raidResist: 0.2, maintenance: 25 },
356
+ { level: 3, defense: 0.35, raidResist: 0.35, maintenance: 50 },
357
+ { level: 4, defense: 0.5, raidResist: 0.5, maintenance: 100 },
358
+ { level: 5, defense: 0.75, raidResist: 0.75, maintenance: 200 },
359
+ ];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * BURNRATE World Generator
3
+ * Creates the initial map for a new season
4
+ */
5
+ import { GameDatabase } from '../db/database.js';
6
+ interface WorldGenConfig {
7
+ hubs: number;
8
+ factories: number;
9
+ fields: number;
10
+ junctions: number;
11
+ fronts: number;
12
+ strongholds: number;
13
+ }
14
+ /**
15
+ * Generate a new world for Season 1
16
+ */
17
+ export declare function generateWorld(db: GameDatabase, config?: WorldGenConfig): void;
18
+ /**
19
+ * Seed initial market prices in zones
20
+ */
21
+ export declare function seedMarkets(db: GameDatabase): void;
22
+ export {};
@@ -0,0 +1,292 @@
1
+ /**
2
+ * BURNRATE World Generator
3
+ * Creates the initial map for a new season
4
+ */
5
+ import { BURN_RATES, emptyInventory } from './types.js';
6
+ const DEFAULT_CONFIG = {
7
+ hubs: 3,
8
+ factories: 8,
9
+ fields: 12,
10
+ junctions: 10,
11
+ fronts: 6,
12
+ strongholds: 3
13
+ };
14
+ /**
15
+ * Generate a new world for Season 1
16
+ */
17
+ export function generateWorld(db, config = DEFAULT_CONFIG) {
18
+ const zones = [];
19
+ // Generate Hubs (safe starting areas)
20
+ const hubNames = ['Hub.Central', 'Hub.East', 'Hub.West'];
21
+ for (let i = 0; i < config.hubs; i++) {
22
+ const zone = db.createZone({
23
+ name: hubNames[i] || `Hub.${i + 1}`,
24
+ type: 'hub',
25
+ ownerId: null,
26
+ supplyLevel: 100,
27
+ burnRate: BURN_RATES.hub,
28
+ complianceStreak: 0,
29
+ suStockpile: 0,
30
+ inventory: { ...emptyInventory(), credits: 10000 }, // Hub has market liquidity
31
+ productionCapacity: 0,
32
+ garrisonLevel: 10, // Hubs are well defended
33
+ marketDepth: 2.0,
34
+ medkitStockpile: 0,
35
+ commsStockpile: 0
36
+ });
37
+ zones.push(zone);
38
+ }
39
+ // Generate Factories
40
+ const factoryNames = [
41
+ 'Factory.North', 'Factory.South', 'Factory.Iron', 'Factory.Chemical',
42
+ 'Factory.Munitions', 'Factory.Textile', 'Factory.Parts', 'Factory.Comms'
43
+ ];
44
+ for (let i = 0; i < config.factories; i++) {
45
+ const zone = db.createZone({
46
+ name: factoryNames[i] || `Factory.${i + 1}`,
47
+ type: 'factory',
48
+ ownerId: null,
49
+ supplyLevel: 100,
50
+ burnRate: BURN_RATES.factory,
51
+ complianceStreak: 0,
52
+ suStockpile: 50,
53
+ inventory: emptyInventory(),
54
+ productionCapacity: 100, // Can produce 100 units/tick
55
+ garrisonLevel: 2,
56
+ marketDepth: 1.0,
57
+ medkitStockpile: 0,
58
+ commsStockpile: 0
59
+ });
60
+ zones.push(zone);
61
+ }
62
+ // Generate Fields (resource extraction)
63
+ const fieldNames = [
64
+ 'Field.Ore.Alpha', 'Field.Ore.Beta', 'Field.Ore.Gamma',
65
+ 'Field.Fuel.1', 'Field.Fuel.2', 'Field.Fuel.3',
66
+ 'Field.Grain.North', 'Field.Grain.South', 'Field.Grain.Valley',
67
+ 'Field.Fiber.1', 'Field.Fiber.2', 'Field.Fiber.3'
68
+ ];
69
+ for (let i = 0; i < config.fields; i++) {
70
+ // Determine what this field produces based on name
71
+ const name = fieldNames[i] || `Field.${i + 1}`;
72
+ let inventory = emptyInventory();
73
+ if (name.includes('Ore'))
74
+ inventory.ore = 500;
75
+ else if (name.includes('Fuel'))
76
+ inventory.fuel = 500;
77
+ else if (name.includes('Grain'))
78
+ inventory.grain = 500;
79
+ else if (name.includes('Fiber'))
80
+ inventory.fiber = 500;
81
+ const zone = db.createZone({
82
+ name,
83
+ type: 'field',
84
+ ownerId: null,
85
+ supplyLevel: 100,
86
+ burnRate: BURN_RATES.field,
87
+ complianceStreak: 0,
88
+ suStockpile: 30,
89
+ inventory,
90
+ productionCapacity: 50, // Extraction rate
91
+ garrisonLevel: 1,
92
+ marketDepth: 0.5,
93
+ medkitStockpile: 0,
94
+ commsStockpile: 0
95
+ });
96
+ zones.push(zone);
97
+ }
98
+ // Generate Junctions (crossroads)
99
+ const junctionNames = [
100
+ 'Junction.1', 'Junction.2', 'Junction.3', 'Junction.4', 'Junction.5',
101
+ 'Junction.North', 'Junction.South', 'Junction.East', 'Junction.West', 'Junction.Center'
102
+ ];
103
+ for (let i = 0; i < config.junctions; i++) {
104
+ const zone = db.createZone({
105
+ name: junctionNames[i] || `Junction.${i + 1}`,
106
+ type: 'junction',
107
+ ownerId: null,
108
+ supplyLevel: 100,
109
+ burnRate: BURN_RATES.junction,
110
+ complianceStreak: 0,
111
+ suStockpile: 0,
112
+ inventory: emptyInventory(),
113
+ productionCapacity: 0,
114
+ garrisonLevel: 0,
115
+ marketDepth: 0.3,
116
+ medkitStockpile: 0,
117
+ commsStockpile: 0
118
+ });
119
+ zones.push(zone);
120
+ }
121
+ // Generate Fronts (contested zones)
122
+ const frontNames = [
123
+ 'Front.Kessel', 'Front.Ardenne', 'Front.Kursk',
124
+ 'Front.Marne', 'Front.Somme', 'Front.Verdun'
125
+ ];
126
+ for (let i = 0; i < config.fronts; i++) {
127
+ const zone = db.createZone({
128
+ name: frontNames[i] || `Front.${i + 1}`,
129
+ type: 'front',
130
+ ownerId: null,
131
+ supplyLevel: 0, // Starts unsupplied
132
+ burnRate: BURN_RATES.front,
133
+ complianceStreak: 0,
134
+ suStockpile: 0,
135
+ inventory: emptyInventory(),
136
+ productionCapacity: 0,
137
+ garrisonLevel: 0,
138
+ marketDepth: 0.2,
139
+ medkitStockpile: 0,
140
+ commsStockpile: 0
141
+ });
142
+ zones.push(zone);
143
+ }
144
+ // Generate Strongholds (victory points)
145
+ const strongholdNames = ['Stronghold.Prime', 'Stronghold.Alpha', 'Stronghold.Omega'];
146
+ for (let i = 0; i < config.strongholds; i++) {
147
+ const zone = db.createZone({
148
+ name: strongholdNames[i] || `Stronghold.${i + 1}`,
149
+ type: 'stronghold',
150
+ ownerId: null,
151
+ supplyLevel: 0,
152
+ burnRate: BURN_RATES.stronghold,
153
+ complianceStreak: 0,
154
+ suStockpile: 0,
155
+ inventory: emptyInventory(),
156
+ productionCapacity: 0,
157
+ garrisonLevel: 5,
158
+ marketDepth: 0.1,
159
+ medkitStockpile: 0,
160
+ commsStockpile: 0
161
+ });
162
+ zones.push(zone);
163
+ }
164
+ // Generate Routes
165
+ // Strategy: Create a connected graph with logical geography
166
+ const zoneMap = new Map(zones.map(z => [z.name, z]));
167
+ // Helper to create bidirectional routes
168
+ const connectZones = (from, to, distance, risk, chokepoint = 1.0) => {
169
+ const fromZone = zoneMap.get(from);
170
+ const toZone = zoneMap.get(to);
171
+ if (!fromZone || !toZone)
172
+ return;
173
+ // Create route in both directions
174
+ db.createRoute({
175
+ fromZoneId: fromZone.id,
176
+ toZoneId: toZone.id,
177
+ distance,
178
+ capacity: 500,
179
+ baseRisk: risk,
180
+ chokepointRating: chokepoint
181
+ });
182
+ db.createRoute({
183
+ fromZoneId: toZone.id,
184
+ toZoneId: fromZone.id,
185
+ distance,
186
+ capacity: 500,
187
+ baseRisk: risk,
188
+ chokepointRating: chokepoint
189
+ });
190
+ };
191
+ // Hub connections (safe routes between hubs)
192
+ connectZones('Hub.Central', 'Hub.East', 2, 0.02);
193
+ connectZones('Hub.Central', 'Hub.West', 2, 0.02);
194
+ connectZones('Hub.East', 'Hub.West', 3, 0.03);
195
+ // Hub to Factory routes
196
+ connectZones('Hub.Central', 'Factory.North', 2, 0.05);
197
+ connectZones('Hub.Central', 'Factory.South', 2, 0.05);
198
+ connectZones('Hub.East', 'Factory.Iron', 2, 0.05);
199
+ connectZones('Hub.East', 'Factory.Chemical', 2, 0.05);
200
+ connectZones('Hub.West', 'Factory.Munitions', 2, 0.05);
201
+ connectZones('Hub.West', 'Factory.Textile', 2, 0.05);
202
+ // Factory to Field routes
203
+ connectZones('Factory.Iron', 'Field.Ore.Alpha', 3, 0.08);
204
+ connectZones('Factory.Iron', 'Field.Ore.Beta', 3, 0.08);
205
+ connectZones('Factory.Chemical', 'Field.Fuel.1', 3, 0.08);
206
+ connectZones('Factory.Chemical', 'Field.Fuel.2', 3, 0.08);
207
+ connectZones('Factory.Textile', 'Field.Fiber.1', 3, 0.08);
208
+ connectZones('Factory.Textile', 'Field.Grain.North', 3, 0.08);
209
+ connectZones('Factory.North', 'Field.Ore.Gamma', 2, 0.06);
210
+ connectZones('Factory.South', 'Field.Fuel.3', 2, 0.06);
211
+ // Factory interconnections
212
+ connectZones('Factory.North', 'Factory.Iron', 2, 0.06);
213
+ connectZones('Factory.South', 'Factory.Chemical', 2, 0.06);
214
+ connectZones('Factory.Parts', 'Factory.Iron', 2, 0.06);
215
+ connectZones('Factory.Parts', 'Factory.Textile', 2, 0.06);
216
+ connectZones('Factory.Comms', 'Factory.Parts', 2, 0.06);
217
+ connectZones('Factory.Munitions', 'Factory.Chemical', 2, 0.06);
218
+ // Junction connections (crossroads)
219
+ connectZones('Junction.1', 'Factory.North', 2, 0.10);
220
+ connectZones('Junction.1', 'Factory.Parts', 2, 0.10);
221
+ connectZones('Junction.2', 'Factory.South', 2, 0.10);
222
+ connectZones('Junction.2', 'Factory.Munitions', 2, 0.10);
223
+ connectZones('Junction.3', 'Factory.Iron', 2, 0.10);
224
+ connectZones('Junction.4', 'Factory.Chemical', 2, 0.10);
225
+ connectZones('Junction.5', 'Factory.Textile', 2, 0.10);
226
+ // Junction to Junction (main arteries)
227
+ connectZones('Junction.1', 'Junction.North', 2, 0.12);
228
+ connectZones('Junction.2', 'Junction.South', 2, 0.12);
229
+ connectZones('Junction.3', 'Junction.East', 2, 0.12);
230
+ connectZones('Junction.4', 'Junction.West', 2, 0.12);
231
+ connectZones('Junction.5', 'Junction.Center', 2, 0.12);
232
+ connectZones('Junction.North', 'Junction.Center', 2, 0.15);
233
+ connectZones('Junction.South', 'Junction.Center', 2, 0.15);
234
+ connectZones('Junction.East', 'Junction.Center', 2, 0.15);
235
+ connectZones('Junction.West', 'Junction.Center', 2, 0.15);
236
+ // Front connections (dangerous routes)
237
+ connectZones('Junction.North', 'Front.Kessel', 3, 0.25, 1.5);
238
+ connectZones('Junction.East', 'Front.Ardenne', 3, 0.25, 1.5);
239
+ connectZones('Junction.South', 'Front.Kursk', 3, 0.25, 1.5);
240
+ connectZones('Junction.West', 'Front.Marne', 3, 0.25, 1.5);
241
+ connectZones('Junction.Center', 'Front.Somme', 3, 0.30, 2.0); // Major chokepoint
242
+ connectZones('Junction.Center', 'Front.Verdun', 3, 0.30, 2.0);
243
+ // Front interconnections
244
+ connectZones('Front.Kessel', 'Front.Ardenne', 4, 0.35, 1.8);
245
+ connectZones('Front.Ardenne', 'Front.Kursk', 4, 0.35, 1.8);
246
+ connectZones('Front.Kursk', 'Front.Marne', 4, 0.35, 1.8);
247
+ connectZones('Front.Marne', 'Front.Somme', 3, 0.30, 1.5);
248
+ connectZones('Front.Somme', 'Front.Verdun', 3, 0.30, 1.5);
249
+ connectZones('Front.Verdun', 'Front.Kessel', 4, 0.35, 1.8);
250
+ // Stronghold connections (hardest routes)
251
+ connectZones('Front.Kessel', 'Stronghold.Prime', 5, 0.40, 2.5);
252
+ connectZones('Front.Somme', 'Stronghold.Prime', 5, 0.40, 2.5);
253
+ connectZones('Front.Ardenne', 'Stronghold.Alpha', 5, 0.40, 2.5);
254
+ connectZones('Front.Kursk', 'Stronghold.Alpha', 5, 0.40, 2.5);
255
+ connectZones('Front.Marne', 'Stronghold.Omega', 5, 0.40, 2.5);
256
+ connectZones('Front.Verdun', 'Stronghold.Omega', 5, 0.40, 2.5);
257
+ // Some alternative routes (bypass options)
258
+ connectZones('Hub.Central', 'Junction.Center', 4, 0.15); // Direct but longer
259
+ connectZones('Hub.East', 'Junction.East', 3, 0.12);
260
+ connectZones('Hub.West', 'Junction.West', 3, 0.12);
261
+ // Field interconnections
262
+ connectZones('Field.Ore.Alpha', 'Field.Ore.Beta', 2, 0.08);
263
+ connectZones('Field.Fuel.1', 'Field.Fuel.2', 2, 0.08);
264
+ connectZones('Field.Grain.North', 'Field.Grain.South', 2, 0.08);
265
+ connectZones('Field.Fiber.1', 'Field.Fiber.2', 2, 0.08);
266
+ console.log(`Generated world with ${zones.length} zones`);
267
+ }
268
+ /**
269
+ * Seed initial market prices in zones
270
+ */
271
+ export function seedMarkets(db) {
272
+ const zones = db.getAllZones();
273
+ for (const zone of zones) {
274
+ if (zone.type === 'hub') {
275
+ // Add some initial inventory for trading
276
+ const inventory = { ...zone.inventory };
277
+ inventory.ore = 200;
278
+ inventory.fuel = 200;
279
+ inventory.grain = 200;
280
+ inventory.fiber = 200;
281
+ inventory.metal = 100;
282
+ inventory.chemicals = 100;
283
+ inventory.rations = 100;
284
+ inventory.textiles = 100;
285
+ inventory.ammo = 50;
286
+ inventory.medkits = 50;
287
+ inventory.parts = 50;
288
+ inventory.comms = 25;
289
+ db.updateZone(zone.id, { inventory });
290
+ }
291
+ }
292
+ }