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,829 @@
1
+ /**
2
+ * BURNRATE Database Layer
3
+ * SQLite-based persistence for game state
4
+ */
5
+ import Database from 'better-sqlite3';
6
+ import { v4 as uuid } from 'uuid';
7
+ import { emptyInventory } from '../core/types.js';
8
+ export class GameDatabase {
9
+ db;
10
+ constructor(dbPath = ':memory:') {
11
+ this.db = new Database(dbPath);
12
+ this.db.pragma('journal_mode = WAL');
13
+ this.initSchema();
14
+ }
15
+ initSchema() {
16
+ this.db.exec(`
17
+ -- World state
18
+ CREATE TABLE IF NOT EXISTS world (
19
+ id INTEGER PRIMARY KEY CHECK (id = 1),
20
+ current_tick INTEGER NOT NULL DEFAULT 0,
21
+ season_number INTEGER NOT NULL DEFAULT 1,
22
+ season_week INTEGER NOT NULL DEFAULT 1
23
+ );
24
+
25
+ -- Zones
26
+ CREATE TABLE IF NOT EXISTS zones (
27
+ id TEXT PRIMARY KEY,
28
+ name TEXT NOT NULL,
29
+ type TEXT NOT NULL,
30
+ owner_id TEXT,
31
+ supply_level REAL NOT NULL DEFAULT 100,
32
+ burn_rate INTEGER NOT NULL,
33
+ compliance_streak INTEGER NOT NULL DEFAULT 0,
34
+ su_stockpile INTEGER NOT NULL DEFAULT 0,
35
+ inventory TEXT NOT NULL DEFAULT '{}',
36
+ production_capacity INTEGER NOT NULL DEFAULT 0,
37
+ garrison_level INTEGER NOT NULL DEFAULT 0,
38
+ market_depth REAL NOT NULL DEFAULT 1.0
39
+ );
40
+
41
+ -- Routes
42
+ CREATE TABLE IF NOT EXISTS routes (
43
+ id TEXT PRIMARY KEY,
44
+ from_zone_id TEXT NOT NULL,
45
+ to_zone_id TEXT NOT NULL,
46
+ distance INTEGER NOT NULL,
47
+ capacity INTEGER NOT NULL,
48
+ base_risk REAL NOT NULL,
49
+ chokepoint_rating REAL NOT NULL DEFAULT 1.0,
50
+ FOREIGN KEY (from_zone_id) REFERENCES zones(id),
51
+ FOREIGN KEY (to_zone_id) REFERENCES zones(id)
52
+ );
53
+
54
+ -- Players
55
+ CREATE TABLE IF NOT EXISTS players (
56
+ id TEXT PRIMARY KEY,
57
+ name TEXT NOT NULL UNIQUE,
58
+ tier TEXT NOT NULL DEFAULT 'freelance',
59
+ inventory TEXT NOT NULL DEFAULT '{}',
60
+ location_id TEXT NOT NULL,
61
+ faction_id TEXT,
62
+ reputation INTEGER NOT NULL DEFAULT 0,
63
+ actions_today INTEGER NOT NULL DEFAULT 0,
64
+ last_action_tick INTEGER NOT NULL DEFAULT 0,
65
+ licenses TEXT NOT NULL DEFAULT '{"courier":true,"freight":false,"convoy":false}',
66
+ FOREIGN KEY (location_id) REFERENCES zones(id),
67
+ FOREIGN KEY (faction_id) REFERENCES factions(id)
68
+ );
69
+
70
+ -- Factions
71
+ CREATE TABLE IF NOT EXISTS factions (
72
+ id TEXT PRIMARY KEY,
73
+ name TEXT NOT NULL UNIQUE,
74
+ tag TEXT NOT NULL UNIQUE,
75
+ founder_id TEXT NOT NULL,
76
+ treasury TEXT NOT NULL DEFAULT '{}',
77
+ officer_withdraw_limit INTEGER NOT NULL DEFAULT 1000,
78
+ doctrine_hash TEXT,
79
+ upgrades TEXT NOT NULL DEFAULT '{}',
80
+ relations TEXT NOT NULL DEFAULT '{}',
81
+ FOREIGN KEY (founder_id) REFERENCES players(id)
82
+ );
83
+
84
+ -- Faction members
85
+ CREATE TABLE IF NOT EXISTS faction_members (
86
+ faction_id TEXT NOT NULL,
87
+ player_id TEXT NOT NULL,
88
+ rank TEXT NOT NULL DEFAULT 'member',
89
+ joined_at INTEGER NOT NULL,
90
+ PRIMARY KEY (faction_id, player_id),
91
+ FOREIGN KEY (faction_id) REFERENCES factions(id),
92
+ FOREIGN KEY (player_id) REFERENCES players(id)
93
+ );
94
+
95
+ -- Shipments
96
+ CREATE TABLE IF NOT EXISTS shipments (
97
+ id TEXT PRIMARY KEY,
98
+ player_id TEXT NOT NULL,
99
+ type TEXT NOT NULL,
100
+ path TEXT NOT NULL,
101
+ current_position INTEGER NOT NULL DEFAULT 0,
102
+ ticks_to_next_zone INTEGER NOT NULL,
103
+ cargo TEXT NOT NULL DEFAULT '{}',
104
+ escort_ids TEXT NOT NULL DEFAULT '[]',
105
+ created_at INTEGER NOT NULL,
106
+ status TEXT NOT NULL DEFAULT 'in_transit',
107
+ FOREIGN KEY (player_id) REFERENCES players(id)
108
+ );
109
+
110
+ -- Units
111
+ CREATE TABLE IF NOT EXISTS units (
112
+ id TEXT PRIMARY KEY,
113
+ player_id TEXT NOT NULL,
114
+ type TEXT NOT NULL,
115
+ location_id TEXT NOT NULL,
116
+ strength INTEGER NOT NULL,
117
+ speed INTEGER NOT NULL,
118
+ maintenance INTEGER NOT NULL,
119
+ assignment_id TEXT,
120
+ for_sale_price INTEGER,
121
+ FOREIGN KEY (player_id) REFERENCES players(id),
122
+ FOREIGN KEY (location_id) REFERENCES zones(id)
123
+ );
124
+
125
+ -- Market orders
126
+ CREATE TABLE IF NOT EXISTS market_orders (
127
+ id TEXT PRIMARY KEY,
128
+ player_id TEXT NOT NULL,
129
+ zone_id TEXT NOT NULL,
130
+ resource TEXT NOT NULL,
131
+ side TEXT NOT NULL,
132
+ price INTEGER NOT NULL,
133
+ quantity INTEGER NOT NULL,
134
+ original_quantity INTEGER NOT NULL,
135
+ created_at INTEGER NOT NULL,
136
+ FOREIGN KEY (player_id) REFERENCES players(id),
137
+ FOREIGN KEY (zone_id) REFERENCES zones(id)
138
+ );
139
+
140
+ -- Trades
141
+ CREATE TABLE IF NOT EXISTS trades (
142
+ id TEXT PRIMARY KEY,
143
+ zone_id TEXT NOT NULL,
144
+ resource TEXT NOT NULL,
145
+ buyer_id TEXT NOT NULL,
146
+ seller_id TEXT NOT NULL,
147
+ price INTEGER NOT NULL,
148
+ quantity INTEGER NOT NULL,
149
+ executed_at INTEGER NOT NULL,
150
+ FOREIGN KEY (zone_id) REFERENCES zones(id)
151
+ );
152
+
153
+ -- Contracts
154
+ CREATE TABLE IF NOT EXISTS contracts (
155
+ id TEXT PRIMARY KEY,
156
+ type TEXT NOT NULL,
157
+ poster_id TEXT NOT NULL,
158
+ poster_type TEXT NOT NULL,
159
+ accepted_by TEXT,
160
+ details TEXT NOT NULL DEFAULT '{}',
161
+ deadline INTEGER NOT NULL,
162
+ reward TEXT NOT NULL DEFAULT '{}',
163
+ bonus TEXT,
164
+ status TEXT NOT NULL DEFAULT 'open',
165
+ created_at INTEGER NOT NULL
166
+ );
167
+
168
+ -- Intel reports
169
+ CREATE TABLE IF NOT EXISTS intel (
170
+ id TEXT PRIMARY KEY,
171
+ player_id TEXT NOT NULL,
172
+ faction_id TEXT,
173
+ target_type TEXT NOT NULL,
174
+ target_id TEXT NOT NULL,
175
+ gathered_at INTEGER NOT NULL,
176
+ data TEXT NOT NULL DEFAULT '{}',
177
+ signal_quality INTEGER NOT NULL,
178
+ FOREIGN KEY (player_id) REFERENCES players(id),
179
+ FOREIGN KEY (faction_id) REFERENCES factions(id)
180
+ );
181
+ CREATE INDEX IF NOT EXISTS idx_intel_faction ON intel(faction_id);
182
+
183
+ -- Game events (event sourcing)
184
+ CREATE TABLE IF NOT EXISTS events (
185
+ id TEXT PRIMARY KEY,
186
+ type TEXT NOT NULL,
187
+ tick INTEGER NOT NULL,
188
+ timestamp TEXT NOT NULL,
189
+ actor_id TEXT,
190
+ actor_type TEXT NOT NULL,
191
+ data TEXT NOT NULL DEFAULT '{}',
192
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
193
+ );
194
+ CREATE INDEX IF NOT EXISTS idx_events_tick ON events(tick);
195
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
196
+ CREATE INDEX IF NOT EXISTS idx_events_actor ON events(actor_id);
197
+
198
+ -- Initialize world state
199
+ INSERT OR IGNORE INTO world (id, current_tick, season_number, season_week)
200
+ VALUES (1, 0, 1, 1);
201
+ `);
202
+ }
203
+ // ============================================================================
204
+ // WORLD STATE
205
+ // ============================================================================
206
+ getCurrentTick() {
207
+ const row = this.db.prepare('SELECT current_tick FROM world WHERE id = 1').get();
208
+ return row.current_tick;
209
+ }
210
+ incrementTick() {
211
+ this.db.prepare('UPDATE world SET current_tick = current_tick + 1 WHERE id = 1').run();
212
+ return this.getCurrentTick();
213
+ }
214
+ getSeasonInfo() {
215
+ const row = this.db.prepare('SELECT season_number, season_week FROM world WHERE id = 1').get();
216
+ return { seasonNumber: row.season_number, seasonWeek: row.season_week };
217
+ }
218
+ // ============================================================================
219
+ // ZONES
220
+ // ============================================================================
221
+ createZone(zone) {
222
+ const id = uuid();
223
+ this.db.prepare(`
224
+ INSERT INTO zones (id, name, type, owner_id, supply_level, burn_rate, compliance_streak,
225
+ su_stockpile, inventory, production_capacity, garrison_level, market_depth)
226
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
227
+ `).run(id, zone.name, zone.type, zone.ownerId, zone.supplyLevel, zone.burnRate, zone.complianceStreak, zone.suStockpile, JSON.stringify(zone.inventory), zone.productionCapacity, zone.garrisonLevel, zone.marketDepth);
228
+ return { id, ...zone };
229
+ }
230
+ getZone(id) {
231
+ const row = this.db.prepare('SELECT * FROM zones WHERE id = ?').get(id);
232
+ if (!row)
233
+ return null;
234
+ return this.rowToZone(row);
235
+ }
236
+ getAllZones() {
237
+ const rows = this.db.prepare('SELECT * FROM zones').all();
238
+ return rows.map(this.rowToZone);
239
+ }
240
+ updateZone(id, updates) {
241
+ const sets = [];
242
+ const values = [];
243
+ if (updates.ownerId !== undefined) {
244
+ sets.push('owner_id = ?');
245
+ values.push(updates.ownerId);
246
+ }
247
+ if (updates.supplyLevel !== undefined) {
248
+ sets.push('supply_level = ?');
249
+ values.push(updates.supplyLevel);
250
+ }
251
+ if (updates.complianceStreak !== undefined) {
252
+ sets.push('compliance_streak = ?');
253
+ values.push(updates.complianceStreak);
254
+ }
255
+ if (updates.suStockpile !== undefined) {
256
+ sets.push('su_stockpile = ?');
257
+ values.push(updates.suStockpile);
258
+ }
259
+ if (updates.inventory !== undefined) {
260
+ sets.push('inventory = ?');
261
+ values.push(JSON.stringify(updates.inventory));
262
+ }
263
+ if (updates.garrisonLevel !== undefined) {
264
+ sets.push('garrison_level = ?');
265
+ values.push(updates.garrisonLevel);
266
+ }
267
+ if (sets.length === 0)
268
+ return;
269
+ values.push(id);
270
+ this.db.prepare(`UPDATE zones SET ${sets.join(', ')} WHERE id = ?`).run(...values);
271
+ }
272
+ rowToZone(row) {
273
+ return {
274
+ id: row.id,
275
+ name: row.name,
276
+ type: row.type,
277
+ ownerId: row.owner_id,
278
+ supplyLevel: row.supply_level,
279
+ burnRate: row.burn_rate,
280
+ complianceStreak: row.compliance_streak,
281
+ suStockpile: row.su_stockpile,
282
+ inventory: JSON.parse(row.inventory),
283
+ productionCapacity: row.production_capacity,
284
+ garrisonLevel: row.garrison_level,
285
+ marketDepth: row.market_depth,
286
+ medkitStockpile: row.medkit_stockpile || 0,
287
+ commsStockpile: row.comms_stockpile || 0
288
+ };
289
+ }
290
+ // ============================================================================
291
+ // ROUTES
292
+ // ============================================================================
293
+ createRoute(route) {
294
+ const id = uuid();
295
+ this.db.prepare(`
296
+ INSERT INTO routes (id, from_zone_id, to_zone_id, distance, capacity, base_risk, chokepoint_rating)
297
+ VALUES (?, ?, ?, ?, ?, ?, ?)
298
+ `).run(id, route.fromZoneId, route.toZoneId, route.distance, route.capacity, route.baseRisk, route.chokepointRating);
299
+ return { id, ...route };
300
+ }
301
+ getRoute(id) {
302
+ const row = this.db.prepare('SELECT * FROM routes WHERE id = ?').get(id);
303
+ if (!row)
304
+ return null;
305
+ return this.rowToRoute(row);
306
+ }
307
+ getRoutesBetween(fromZoneId, toZoneId) {
308
+ const rows = this.db.prepare('SELECT * FROM routes WHERE from_zone_id = ? AND to_zone_id = ?').all(fromZoneId, toZoneId);
309
+ return rows.map(this.rowToRoute);
310
+ }
311
+ getRoutesFromZone(zoneId) {
312
+ const rows = this.db.prepare('SELECT * FROM routes WHERE from_zone_id = ?').all(zoneId);
313
+ return rows.map(this.rowToRoute);
314
+ }
315
+ getAllRoutes() {
316
+ const rows = this.db.prepare('SELECT * FROM routes').all();
317
+ return rows.map(this.rowToRoute);
318
+ }
319
+ rowToRoute(row) {
320
+ return {
321
+ id: row.id,
322
+ fromZoneId: row.from_zone_id,
323
+ toZoneId: row.to_zone_id,
324
+ distance: row.distance,
325
+ capacity: row.capacity,
326
+ baseRisk: row.base_risk,
327
+ chokepointRating: row.chokepoint_rating
328
+ };
329
+ }
330
+ // ============================================================================
331
+ // PLAYERS
332
+ // ============================================================================
333
+ createPlayer(name, startingZoneId) {
334
+ const id = uuid();
335
+ const inventory = { ...emptyInventory(), credits: 500 }; // Starting credits
336
+ const licenses = { courier: true, freight: false, convoy: false };
337
+ this.db.prepare(`
338
+ INSERT INTO players (id, name, tier, inventory, location_id, reputation, licenses)
339
+ VALUES (?, ?, 'freelance', ?, ?, 0, ?)
340
+ `).run(id, name, JSON.stringify(inventory), startingZoneId, JSON.stringify(licenses));
341
+ return {
342
+ id, name, tier: 'freelance', inventory, locationId: startingZoneId,
343
+ factionId: null, reputation: 0, actionsToday: 0, lastActionTick: 0, licenses,
344
+ tutorialStep: 0
345
+ };
346
+ }
347
+ getPlayer(id) {
348
+ const row = this.db.prepare('SELECT * FROM players WHERE id = ?').get(id);
349
+ if (!row)
350
+ return null;
351
+ return this.rowToPlayer(row);
352
+ }
353
+ getPlayerByName(name) {
354
+ const row = this.db.prepare('SELECT * FROM players WHERE name = ?').get(name);
355
+ if (!row)
356
+ return null;
357
+ return this.rowToPlayer(row);
358
+ }
359
+ getAllPlayers() {
360
+ const rows = this.db.prepare('SELECT * FROM players').all();
361
+ return rows.map(this.rowToPlayer);
362
+ }
363
+ updatePlayer(id, updates) {
364
+ const sets = [];
365
+ const values = [];
366
+ if (updates.inventory !== undefined) {
367
+ sets.push('inventory = ?');
368
+ values.push(JSON.stringify(updates.inventory));
369
+ }
370
+ if (updates.locationId !== undefined) {
371
+ sets.push('location_id = ?');
372
+ values.push(updates.locationId);
373
+ }
374
+ if (updates.factionId !== undefined) {
375
+ sets.push('faction_id = ?');
376
+ values.push(updates.factionId);
377
+ }
378
+ if (updates.reputation !== undefined) {
379
+ sets.push('reputation = ?');
380
+ values.push(updates.reputation);
381
+ }
382
+ if (updates.actionsToday !== undefined) {
383
+ sets.push('actions_today = ?');
384
+ values.push(updates.actionsToday);
385
+ }
386
+ if (updates.lastActionTick !== undefined) {
387
+ sets.push('last_action_tick = ?');
388
+ values.push(updates.lastActionTick);
389
+ }
390
+ if (updates.tier !== undefined) {
391
+ sets.push('tier = ?');
392
+ values.push(updates.tier);
393
+ }
394
+ if (updates.licenses !== undefined) {
395
+ sets.push('licenses = ?');
396
+ values.push(JSON.stringify(updates.licenses));
397
+ }
398
+ if (sets.length === 0)
399
+ return;
400
+ values.push(id);
401
+ this.db.prepare(`UPDATE players SET ${sets.join(', ')} WHERE id = ?`).run(...values);
402
+ }
403
+ rowToPlayer(row) {
404
+ return {
405
+ id: row.id,
406
+ name: row.name,
407
+ tier: row.tier,
408
+ inventory: JSON.parse(row.inventory),
409
+ locationId: row.location_id,
410
+ factionId: row.faction_id,
411
+ reputation: row.reputation,
412
+ actionsToday: row.actions_today,
413
+ lastActionTick: row.last_action_tick,
414
+ licenses: JSON.parse(row.licenses),
415
+ tutorialStep: row.tutorial_step || 0
416
+ };
417
+ }
418
+ // ============================================================================
419
+ // FACTIONS
420
+ // ============================================================================
421
+ createFaction(name, tag, founderId) {
422
+ const id = uuid();
423
+ const treasury = emptyInventory();
424
+ const upgrades = {
425
+ relayNetwork: 0,
426
+ routeFortification: 0,
427
+ productionBonus: 0,
428
+ garrisonStrength: 0,
429
+ marketDepth: 0
430
+ };
431
+ const tick = this.getCurrentTick();
432
+ this.db.prepare(`
433
+ INSERT INTO factions (id, name, tag, founder_id, treasury, upgrades, relations)
434
+ VALUES (?, ?, ?, ?, ?, ?, '{}')
435
+ `).run(id, name, tag, founderId, JSON.stringify(treasury), JSON.stringify(upgrades));
436
+ // Add founder as member
437
+ this.db.prepare(`
438
+ INSERT INTO faction_members (faction_id, player_id, rank, joined_at)
439
+ VALUES (?, ?, 'founder', ?)
440
+ `).run(id, founderId, tick);
441
+ // Update player's faction
442
+ this.updatePlayer(founderId, { factionId: id });
443
+ return {
444
+ id, name, tag, founderId, treasury,
445
+ officerWithdrawLimit: 1000,
446
+ members: [{ playerId: founderId, rank: 'founder', joinedAt: tick }],
447
+ controlledZones: [],
448
+ doctrineHash: null,
449
+ upgrades,
450
+ relations: {}
451
+ };
452
+ }
453
+ getFaction(id) {
454
+ const row = this.db.prepare('SELECT * FROM factions WHERE id = ?').get(id);
455
+ if (!row)
456
+ return null;
457
+ return this.rowToFaction(row);
458
+ }
459
+ getAllFactions() {
460
+ const rows = this.db.prepare('SELECT * FROM factions').all();
461
+ return rows.map(r => this.rowToFaction(r));
462
+ }
463
+ rowToFaction(row) {
464
+ const members = this.db.prepare('SELECT player_id, rank, joined_at FROM faction_members WHERE faction_id = ?').all(row.id);
465
+ const controlledZones = this.db.prepare('SELECT id FROM zones WHERE owner_id = ?').all(row.id).map((z) => z.id);
466
+ return {
467
+ id: row.id,
468
+ name: row.name,
469
+ tag: row.tag,
470
+ founderId: row.founder_id,
471
+ treasury: JSON.parse(row.treasury),
472
+ officerWithdrawLimit: row.officer_withdraw_limit,
473
+ members: members.map(m => ({
474
+ playerId: m.player_id,
475
+ rank: m.rank,
476
+ joinedAt: m.joined_at
477
+ })),
478
+ controlledZones,
479
+ doctrineHash: row.doctrine_hash,
480
+ upgrades: JSON.parse(row.upgrades),
481
+ relations: JSON.parse(row.relations)
482
+ };
483
+ }
484
+ addFactionMember(factionId, playerId, rank = 'member') {
485
+ const tick = this.getCurrentTick();
486
+ this.db.prepare(`
487
+ INSERT INTO faction_members (faction_id, player_id, rank, joined_at)
488
+ VALUES (?, ?, ?, ?)
489
+ `).run(factionId, playerId, rank, tick);
490
+ this.updatePlayer(playerId, { factionId });
491
+ }
492
+ removeFactionMember(factionId, playerId) {
493
+ this.db.prepare('DELETE FROM faction_members WHERE faction_id = ? AND player_id = ?').run(factionId, playerId);
494
+ this.updatePlayer(playerId, { factionId: null });
495
+ }
496
+ updateFactionMemberRank(factionId, playerId, rank) {
497
+ this.db.prepare('UPDATE faction_members SET rank = ? WHERE faction_id = ? AND player_id = ?').run(rank, factionId, playerId);
498
+ }
499
+ // ============================================================================
500
+ // SHIPMENTS
501
+ // ============================================================================
502
+ createShipment(shipment) {
503
+ const id = uuid();
504
+ this.db.prepare(`
505
+ INSERT INTO shipments (id, player_id, type, path, current_position, ticks_to_next_zone,
506
+ cargo, escort_ids, created_at, status)
507
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
508
+ `).run(id, shipment.playerId, shipment.type, JSON.stringify(shipment.path), shipment.currentPosition, shipment.ticksToNextZone, JSON.stringify(shipment.cargo), JSON.stringify(shipment.escortIds), shipment.createdAt, shipment.status);
509
+ return { id, ...shipment };
510
+ }
511
+ getShipment(id) {
512
+ const row = this.db.prepare('SELECT * FROM shipments WHERE id = ?').get(id);
513
+ if (!row)
514
+ return null;
515
+ return this.rowToShipment(row);
516
+ }
517
+ getActiveShipments() {
518
+ const rows = this.db.prepare("SELECT * FROM shipments WHERE status = 'in_transit'").all();
519
+ return rows.map(this.rowToShipment);
520
+ }
521
+ getPlayerShipments(playerId) {
522
+ const rows = this.db.prepare('SELECT * FROM shipments WHERE player_id = ?').all(playerId);
523
+ return rows.map(this.rowToShipment);
524
+ }
525
+ updateShipment(id, updates) {
526
+ const sets = [];
527
+ const values = [];
528
+ if (updates.currentPosition !== undefined) {
529
+ sets.push('current_position = ?');
530
+ values.push(updates.currentPosition);
531
+ }
532
+ if (updates.ticksToNextZone !== undefined) {
533
+ sets.push('ticks_to_next_zone = ?');
534
+ values.push(updates.ticksToNextZone);
535
+ }
536
+ if (updates.status !== undefined) {
537
+ sets.push('status = ?');
538
+ values.push(updates.status);
539
+ }
540
+ if (updates.escortIds !== undefined) {
541
+ sets.push('escort_ids = ?');
542
+ values.push(JSON.stringify(updates.escortIds));
543
+ }
544
+ if (sets.length === 0)
545
+ return;
546
+ values.push(id);
547
+ this.db.prepare(`UPDATE shipments SET ${sets.join(', ')} WHERE id = ?`).run(...values);
548
+ }
549
+ rowToShipment(row) {
550
+ return {
551
+ id: row.id,
552
+ playerId: row.player_id,
553
+ type: row.type,
554
+ path: JSON.parse(row.path),
555
+ currentPosition: row.current_position,
556
+ ticksToNextZone: row.ticks_to_next_zone,
557
+ cargo: JSON.parse(row.cargo),
558
+ escortIds: JSON.parse(row.escort_ids),
559
+ createdAt: row.created_at,
560
+ status: row.status
561
+ };
562
+ }
563
+ // ============================================================================
564
+ // MARKET ORDERS
565
+ // ============================================================================
566
+ createOrder(order) {
567
+ const id = uuid();
568
+ this.db.prepare(`
569
+ INSERT INTO market_orders (id, player_id, zone_id, resource, side, price, quantity, original_quantity, created_at)
570
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
571
+ `).run(id, order.playerId, order.zoneId, order.resource, order.side, order.price, order.quantity, order.originalQuantity, order.createdAt);
572
+ return { id, ...order };
573
+ }
574
+ getOrder(id) {
575
+ const row = this.db.prepare('SELECT * FROM market_orders WHERE id = ?').get(id);
576
+ if (!row)
577
+ return null;
578
+ return this.rowToOrder(row);
579
+ }
580
+ getOrdersForZone(zoneId, resource) {
581
+ let query = 'SELECT * FROM market_orders WHERE zone_id = ? AND quantity > 0';
582
+ const params = [zoneId];
583
+ if (resource) {
584
+ query += ' AND resource = ?';
585
+ params.push(resource);
586
+ }
587
+ query += ' ORDER BY side, price';
588
+ const rows = this.db.prepare(query).all(...params);
589
+ return rows.map(this.rowToOrder);
590
+ }
591
+ updateOrder(id, quantity) {
592
+ this.db.prepare('UPDATE market_orders SET quantity = ? WHERE id = ?').run(quantity, id);
593
+ }
594
+ deleteOrder(id) {
595
+ this.db.prepare('DELETE FROM market_orders WHERE id = ?').run(id);
596
+ }
597
+ rowToOrder(row) {
598
+ return {
599
+ id: row.id,
600
+ playerId: row.player_id,
601
+ zoneId: row.zone_id,
602
+ resource: row.resource,
603
+ side: row.side,
604
+ price: row.price,
605
+ quantity: row.quantity,
606
+ originalQuantity: row.original_quantity,
607
+ createdAt: row.created_at
608
+ };
609
+ }
610
+ // ============================================================================
611
+ // EVENTS
612
+ // ============================================================================
613
+ recordEvent(event) {
614
+ const id = uuid();
615
+ this.db.prepare(`
616
+ INSERT INTO events (id, type, tick, timestamp, actor_id, actor_type, data)
617
+ VALUES (?, ?, ?, ?, ?, ?, ?)
618
+ `).run(id, event.type, event.tick, event.timestamp.toISOString(), event.actorId, event.actorType, JSON.stringify(event.data));
619
+ return { id, ...event };
620
+ }
621
+ getEvents(options = {}) {
622
+ let query = 'SELECT * FROM events WHERE 1=1';
623
+ const params = [];
624
+ if (options.type) {
625
+ query += ' AND type = ?';
626
+ params.push(options.type);
627
+ }
628
+ if (options.actorId) {
629
+ query += ' AND actor_id = ?';
630
+ params.push(options.actorId);
631
+ }
632
+ if (options.sincesTick !== undefined) {
633
+ query += ' AND tick >= ?';
634
+ params.push(options.sincesTick);
635
+ }
636
+ query += ' ORDER BY tick DESC, created_at DESC';
637
+ if (options.limit) {
638
+ query += ' LIMIT ?';
639
+ params.push(options.limit);
640
+ }
641
+ if (options.offset) {
642
+ query += ' OFFSET ?';
643
+ params.push(options.offset);
644
+ }
645
+ const rows = this.db.prepare(query).all(...params);
646
+ return rows.map(row => ({
647
+ id: row.id,
648
+ type: row.type,
649
+ tick: row.tick,
650
+ timestamp: new Date(row.timestamp),
651
+ actorId: row.actor_id,
652
+ actorType: row.actor_type,
653
+ data: JSON.parse(row.data)
654
+ }));
655
+ }
656
+ // ============================================================================
657
+ // CONTRACTS
658
+ // ============================================================================
659
+ createContract(contract) {
660
+ const id = uuid();
661
+ this.db.prepare(`
662
+ INSERT INTO contracts (id, type, poster_id, poster_type, accepted_by, details, deadline, reward, bonus, status, created_at)
663
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
664
+ `).run(id, contract.type, contract.posterId, contract.posterType, contract.acceptedBy, JSON.stringify(contract.details), contract.deadline, JSON.stringify(contract.reward), contract.bonus ? JSON.stringify(contract.bonus) : null, contract.status, contract.createdAt);
665
+ return { id, ...contract };
666
+ }
667
+ getContract(id) {
668
+ const row = this.db.prepare('SELECT * FROM contracts WHERE id = ?').get(id);
669
+ if (!row)
670
+ return null;
671
+ return this.rowToContract(row);
672
+ }
673
+ getOpenContracts(zoneId) {
674
+ let query = "SELECT * FROM contracts WHERE status = 'open'";
675
+ const params = [];
676
+ // Note: zoneId filtering would need to parse details JSON; for now return all open
677
+ query += ' ORDER BY created_at DESC';
678
+ const rows = this.db.prepare(query).all(...params);
679
+ return rows.map(this.rowToContract);
680
+ }
681
+ updateContract(id, updates) {
682
+ const sets = [];
683
+ const values = [];
684
+ if (updates.status) {
685
+ sets.push('status = ?');
686
+ values.push(updates.status);
687
+ }
688
+ if (updates.acceptedBy) {
689
+ sets.push('accepted_by = ?');
690
+ values.push(updates.acceptedBy);
691
+ }
692
+ if (sets.length === 0)
693
+ return;
694
+ values.push(id);
695
+ this.db.prepare(`UPDATE contracts SET ${sets.join(', ')} WHERE id = ?`).run(...values);
696
+ }
697
+ rowToContract(row) {
698
+ return {
699
+ id: row.id,
700
+ type: row.type,
701
+ posterId: row.poster_id,
702
+ posterType: row.poster_type,
703
+ acceptedBy: row.accepted_by,
704
+ details: JSON.parse(row.details),
705
+ deadline: row.deadline,
706
+ reward: JSON.parse(row.reward),
707
+ bonus: row.bonus ? JSON.parse(row.bonus) : undefined,
708
+ status: row.status,
709
+ createdAt: row.created_at
710
+ };
711
+ }
712
+ // ============================================================================
713
+ // UNITS
714
+ // ============================================================================
715
+ createUnit(unit) {
716
+ const id = uuid();
717
+ this.db.prepare(`
718
+ INSERT INTO units (id, player_id, type, location_id, strength, speed, maintenance, assignment_id, for_sale_price)
719
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
720
+ `).run(id, unit.playerId, unit.type, unit.locationId, unit.strength, unit.speed, unit.maintenance, unit.assignmentId, unit.forSalePrice);
721
+ return { id, ...unit };
722
+ }
723
+ getUnit(id) {
724
+ const row = this.db.prepare('SELECT * FROM units WHERE id = ?').get(id);
725
+ if (!row)
726
+ return null;
727
+ return this.rowToUnit(row);
728
+ }
729
+ getPlayerUnits(playerId) {
730
+ const rows = this.db.prepare('SELECT * FROM units WHERE player_id = ?').all(playerId);
731
+ return rows.map(this.rowToUnit);
732
+ }
733
+ updateUnit(id, updates) {
734
+ const sets = [];
735
+ const values = [];
736
+ if (updates.locationId !== undefined) {
737
+ sets.push('location_id = ?');
738
+ values.push(updates.locationId);
739
+ }
740
+ if (updates.assignmentId !== undefined) {
741
+ sets.push('assignment_id = ?');
742
+ values.push(updates.assignmentId);
743
+ }
744
+ if (updates.playerId !== undefined) {
745
+ sets.push('player_id = ?');
746
+ values.push(updates.playerId);
747
+ }
748
+ if (updates.forSalePrice !== undefined) {
749
+ sets.push('for_sale_price = ?');
750
+ values.push(updates.forSalePrice);
751
+ }
752
+ if (sets.length === 0)
753
+ return;
754
+ values.push(id);
755
+ this.db.prepare(`UPDATE units SET ${sets.join(', ')} WHERE id = ?`).run(...values);
756
+ }
757
+ deleteUnit(id) {
758
+ this.db.prepare('DELETE FROM units WHERE id = ?').run(id);
759
+ }
760
+ rowToUnit(row) {
761
+ return {
762
+ id: row.id,
763
+ playerId: row.player_id,
764
+ type: row.type,
765
+ locationId: row.location_id,
766
+ strength: row.strength,
767
+ speed: row.speed,
768
+ maintenance: row.maintenance,
769
+ assignmentId: row.assignment_id,
770
+ forSalePrice: row.for_sale_price
771
+ };
772
+ }
773
+ getUnitsForSaleAtZone(zoneId) {
774
+ const rows = this.db.prepare('SELECT * FROM units WHERE location_id = ? AND for_sale_price IS NOT NULL').all(zoneId);
775
+ return rows.map(this.rowToUnit);
776
+ }
777
+ // ============================================================================
778
+ // INTEL
779
+ // ============================================================================
780
+ createIntel(intel) {
781
+ const id = uuid();
782
+ this.db.prepare(`
783
+ INSERT INTO intel (id, player_id, faction_id, target_type, target_id, gathered_at, data, signal_quality)
784
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
785
+ `).run(id, intel.playerId, intel.factionId, intel.targetType, intel.targetId, intel.gatheredAt, JSON.stringify(intel.data), intel.signalQuality);
786
+ return { id, ...intel };
787
+ }
788
+ getPlayerIntel(playerId, limit = 100) {
789
+ const rows = this.db.prepare('SELECT * FROM intel WHERE player_id = ? ORDER BY gathered_at DESC LIMIT ?').all(playerId, limit);
790
+ return rows.map(this.rowToIntel);
791
+ }
792
+ getFactionIntel(factionId, limit = 100) {
793
+ const rows = this.db.prepare('SELECT * FROM intel WHERE faction_id = ? ORDER BY gathered_at DESC LIMIT ?').all(factionId, limit);
794
+ return rows.map(this.rowToIntel);
795
+ }
796
+ getLatestIntelForTarget(targetType, targetId, factionId) {
797
+ let query = 'SELECT * FROM intel WHERE target_type = ? AND target_id = ?';
798
+ const params = [targetType, targetId];
799
+ if (factionId) {
800
+ query += ' AND faction_id = ?';
801
+ params.push(factionId);
802
+ }
803
+ query += ' ORDER BY gathered_at DESC LIMIT 1';
804
+ const row = this.db.prepare(query).get(...params);
805
+ return row ? this.rowToIntel(row) : null;
806
+ }
807
+ rowToIntel(row) {
808
+ return {
809
+ id: row.id,
810
+ playerId: row.player_id,
811
+ factionId: row.faction_id,
812
+ targetType: row.target_type,
813
+ targetId: row.target_id,
814
+ gatheredAt: row.gathered_at,
815
+ data: JSON.parse(row.data),
816
+ signalQuality: row.signal_quality
817
+ };
818
+ }
819
+ // ============================================================================
820
+ // UTILITY
821
+ // ============================================================================
822
+ close() {
823
+ this.db.close();
824
+ }
825
+ /** Run arbitrary SQL (for testing/migrations) */
826
+ exec(sql) {
827
+ this.db.exec(sql);
828
+ }
829
+ }