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.
- package/LICENSE +21 -0
- package/README.md +507 -0
- package/dist/cli/format.d.ts +13 -0
- package/dist/cli/format.js +319 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +1121 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +143 -0
- package/dist/core/async-engine.d.ts +411 -0
- package/dist/core/async-engine.js +2274 -0
- package/dist/core/async-worldgen.d.ts +19 -0
- package/dist/core/async-worldgen.js +221 -0
- package/dist/core/engine.d.ts +154 -0
- package/dist/core/engine.js +1104 -0
- package/dist/core/pathfinding.d.ts +38 -0
- package/dist/core/pathfinding.js +146 -0
- package/dist/core/types.d.ts +489 -0
- package/dist/core/types.js +359 -0
- package/dist/core/worldgen.d.ts +22 -0
- package/dist/core/worldgen.js +292 -0
- package/dist/db/database.d.ts +83 -0
- package/dist/db/database.js +829 -0
- package/dist/db/turso-database.d.ts +177 -0
- package/dist/db/turso-database.js +1586 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +1877 -0
- package/dist/server/api.d.ts +8 -0
- package/dist/server/api.js +1234 -0
- package/dist/server/async-tick-server.d.ts +5 -0
- package/dist/server/async-tick-server.js +63 -0
- package/dist/server/errors.d.ts +78 -0
- package/dist/server/errors.js +156 -0
- package/dist/server/rate-limit.d.ts +22 -0
- package/dist/server/rate-limit.js +134 -0
- package/dist/server/tick-server.d.ts +9 -0
- package/dist/server/tick-server.js +114 -0
- package/dist/server/validation.d.ts +194 -0
- package/dist/server/validation.js +114 -0
- 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
|
+
}
|