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,1586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BURNRATE Database Layer (Turso/LibSQL)
|
|
3
|
+
* Multiplayer-ready persistence using Turso distributed SQLite
|
|
4
|
+
*/
|
|
5
|
+
import { createClient } from '@libsql/client';
|
|
6
|
+
import { v4 as uuid } from 'uuid';
|
|
7
|
+
import { emptyInventory, getIntelFreshness, getDecayedSignalQuality, applyIntelDecay } from '../core/types.js';
|
|
8
|
+
export class TursoDatabase {
|
|
9
|
+
client;
|
|
10
|
+
constructor(url, authToken) {
|
|
11
|
+
// Use environment variables or fall back to local file for dev
|
|
12
|
+
this.client = createClient({
|
|
13
|
+
url: url || process.env.TURSO_DATABASE_URL || 'file:local.db',
|
|
14
|
+
authToken: authToken || process.env.TURSO_AUTH_TOKEN,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async initialize() {
|
|
18
|
+
await this.initSchema();
|
|
19
|
+
}
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// TRANSACTION SUPPORT
|
|
22
|
+
// ============================================================================
|
|
23
|
+
/**
|
|
24
|
+
* Execute multiple statements in a single transaction.
|
|
25
|
+
* All statements succeed or all fail.
|
|
26
|
+
*/
|
|
27
|
+
async transaction(fn) {
|
|
28
|
+
const statements = [];
|
|
29
|
+
const ctx = new TransactionContext(statements);
|
|
30
|
+
// Collect all statements
|
|
31
|
+
const result = await fn(ctx);
|
|
32
|
+
// Execute as batch with write mode (transaction)
|
|
33
|
+
if (statements.length > 0) {
|
|
34
|
+
await this.client.batch(statements, 'write');
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execute a batch of independent statements efficiently.
|
|
40
|
+
* Not a transaction - some may succeed while others fail.
|
|
41
|
+
*/
|
|
42
|
+
async batch(statements) {
|
|
43
|
+
if (statements.length > 0) {
|
|
44
|
+
await this.client.batch(statements);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async initSchema() {
|
|
48
|
+
const statements = [
|
|
49
|
+
// World state
|
|
50
|
+
`CREATE TABLE IF NOT EXISTS world (
|
|
51
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
52
|
+
current_tick INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
season_number INTEGER NOT NULL DEFAULT 1,
|
|
54
|
+
season_week INTEGER NOT NULL DEFAULT 1
|
|
55
|
+
)`,
|
|
56
|
+
// Zones
|
|
57
|
+
`CREATE TABLE IF NOT EXISTS zones (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
name TEXT NOT NULL,
|
|
60
|
+
type TEXT NOT NULL,
|
|
61
|
+
owner_id TEXT,
|
|
62
|
+
supply_level REAL NOT NULL DEFAULT 100,
|
|
63
|
+
burn_rate INTEGER NOT NULL,
|
|
64
|
+
compliance_streak INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
su_stockpile INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
inventory TEXT NOT NULL DEFAULT '{}',
|
|
67
|
+
production_capacity INTEGER NOT NULL DEFAULT 0,
|
|
68
|
+
garrison_level INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
market_depth REAL NOT NULL DEFAULT 1.0,
|
|
70
|
+
medkit_stockpile INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
comms_stockpile INTEGER NOT NULL DEFAULT 0
|
|
72
|
+
)`,
|
|
73
|
+
// Routes
|
|
74
|
+
`CREATE TABLE IF NOT EXISTS routes (
|
|
75
|
+
id TEXT PRIMARY KEY,
|
|
76
|
+
from_zone_id TEXT NOT NULL,
|
|
77
|
+
to_zone_id TEXT NOT NULL,
|
|
78
|
+
distance INTEGER NOT NULL,
|
|
79
|
+
capacity INTEGER NOT NULL,
|
|
80
|
+
base_risk REAL NOT NULL,
|
|
81
|
+
chokepoint_rating REAL NOT NULL DEFAULT 1.0,
|
|
82
|
+
FOREIGN KEY (from_zone_id) REFERENCES zones(id),
|
|
83
|
+
FOREIGN KEY (to_zone_id) REFERENCES zones(id)
|
|
84
|
+
)`,
|
|
85
|
+
// Players
|
|
86
|
+
`CREATE TABLE IF NOT EXISTS players (
|
|
87
|
+
id TEXT PRIMARY KEY,
|
|
88
|
+
name TEXT NOT NULL UNIQUE,
|
|
89
|
+
api_key TEXT UNIQUE,
|
|
90
|
+
tier TEXT NOT NULL DEFAULT 'freelance',
|
|
91
|
+
inventory TEXT NOT NULL DEFAULT '{}',
|
|
92
|
+
location_id TEXT NOT NULL,
|
|
93
|
+
faction_id TEXT,
|
|
94
|
+
reputation INTEGER NOT NULL DEFAULT 0,
|
|
95
|
+
actions_today INTEGER NOT NULL DEFAULT 0,
|
|
96
|
+
last_action_tick INTEGER NOT NULL DEFAULT 0,
|
|
97
|
+
licenses TEXT NOT NULL DEFAULT '{"courier":true,"freight":false,"convoy":false}',
|
|
98
|
+
tutorial_step INTEGER NOT NULL DEFAULT 0,
|
|
99
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
100
|
+
FOREIGN KEY (location_id) REFERENCES zones(id),
|
|
101
|
+
FOREIGN KEY (faction_id) REFERENCES factions(id)
|
|
102
|
+
)`,
|
|
103
|
+
// Factions
|
|
104
|
+
`CREATE TABLE IF NOT EXISTS factions (
|
|
105
|
+
id TEXT PRIMARY KEY,
|
|
106
|
+
name TEXT NOT NULL UNIQUE,
|
|
107
|
+
tag TEXT NOT NULL UNIQUE,
|
|
108
|
+
founder_id TEXT NOT NULL,
|
|
109
|
+
treasury TEXT NOT NULL DEFAULT '{}',
|
|
110
|
+
officer_withdraw_limit INTEGER NOT NULL DEFAULT 1000,
|
|
111
|
+
doctrine_hash TEXT,
|
|
112
|
+
upgrades TEXT NOT NULL DEFAULT '{}',
|
|
113
|
+
relations TEXT NOT NULL DEFAULT '{}',
|
|
114
|
+
FOREIGN KEY (founder_id) REFERENCES players(id)
|
|
115
|
+
)`,
|
|
116
|
+
// Faction members
|
|
117
|
+
`CREATE TABLE IF NOT EXISTS faction_members (
|
|
118
|
+
faction_id TEXT NOT NULL,
|
|
119
|
+
player_id TEXT NOT NULL,
|
|
120
|
+
rank TEXT NOT NULL DEFAULT 'member',
|
|
121
|
+
joined_at INTEGER NOT NULL,
|
|
122
|
+
PRIMARY KEY (faction_id, player_id),
|
|
123
|
+
FOREIGN KEY (faction_id) REFERENCES factions(id),
|
|
124
|
+
FOREIGN KEY (player_id) REFERENCES players(id)
|
|
125
|
+
)`,
|
|
126
|
+
// Shipments
|
|
127
|
+
`CREATE TABLE IF NOT EXISTS shipments (
|
|
128
|
+
id TEXT PRIMARY KEY,
|
|
129
|
+
player_id TEXT NOT NULL,
|
|
130
|
+
type TEXT NOT NULL,
|
|
131
|
+
path TEXT NOT NULL,
|
|
132
|
+
current_position INTEGER NOT NULL DEFAULT 0,
|
|
133
|
+
ticks_to_next_zone INTEGER NOT NULL,
|
|
134
|
+
cargo TEXT NOT NULL DEFAULT '{}',
|
|
135
|
+
escort_ids TEXT NOT NULL DEFAULT '[]',
|
|
136
|
+
created_at INTEGER NOT NULL,
|
|
137
|
+
status TEXT NOT NULL DEFAULT 'in_transit',
|
|
138
|
+
FOREIGN KEY (player_id) REFERENCES players(id)
|
|
139
|
+
)`,
|
|
140
|
+
// Units
|
|
141
|
+
`CREATE TABLE IF NOT EXISTS units (
|
|
142
|
+
id TEXT PRIMARY KEY,
|
|
143
|
+
player_id TEXT NOT NULL,
|
|
144
|
+
type TEXT NOT NULL,
|
|
145
|
+
location_id TEXT NOT NULL,
|
|
146
|
+
strength INTEGER NOT NULL,
|
|
147
|
+
speed INTEGER NOT NULL,
|
|
148
|
+
maintenance INTEGER NOT NULL,
|
|
149
|
+
assignment_id TEXT,
|
|
150
|
+
for_sale_price INTEGER,
|
|
151
|
+
FOREIGN KEY (player_id) REFERENCES players(id),
|
|
152
|
+
FOREIGN KEY (location_id) REFERENCES zones(id)
|
|
153
|
+
)`,
|
|
154
|
+
// Market orders
|
|
155
|
+
`CREATE TABLE IF NOT EXISTS market_orders (
|
|
156
|
+
id TEXT PRIMARY KEY,
|
|
157
|
+
player_id TEXT NOT NULL,
|
|
158
|
+
zone_id TEXT NOT NULL,
|
|
159
|
+
resource TEXT NOT NULL,
|
|
160
|
+
side TEXT NOT NULL,
|
|
161
|
+
price INTEGER NOT NULL,
|
|
162
|
+
quantity INTEGER NOT NULL,
|
|
163
|
+
original_quantity INTEGER NOT NULL,
|
|
164
|
+
created_at INTEGER NOT NULL,
|
|
165
|
+
FOREIGN KEY (player_id) REFERENCES players(id),
|
|
166
|
+
FOREIGN KEY (zone_id) REFERENCES zones(id)
|
|
167
|
+
)`,
|
|
168
|
+
// Trades
|
|
169
|
+
`CREATE TABLE IF NOT EXISTS trades (
|
|
170
|
+
id TEXT PRIMARY KEY,
|
|
171
|
+
zone_id TEXT NOT NULL,
|
|
172
|
+
resource TEXT NOT NULL,
|
|
173
|
+
buyer_id TEXT NOT NULL,
|
|
174
|
+
seller_id TEXT NOT NULL,
|
|
175
|
+
price INTEGER NOT NULL,
|
|
176
|
+
quantity INTEGER NOT NULL,
|
|
177
|
+
executed_at INTEGER NOT NULL,
|
|
178
|
+
FOREIGN KEY (zone_id) REFERENCES zones(id)
|
|
179
|
+
)`,
|
|
180
|
+
// Contracts
|
|
181
|
+
`CREATE TABLE IF NOT EXISTS contracts (
|
|
182
|
+
id TEXT PRIMARY KEY,
|
|
183
|
+
type TEXT NOT NULL,
|
|
184
|
+
poster_id TEXT NOT NULL,
|
|
185
|
+
poster_type TEXT NOT NULL,
|
|
186
|
+
accepted_by TEXT,
|
|
187
|
+
details TEXT NOT NULL DEFAULT '{}',
|
|
188
|
+
deadline INTEGER NOT NULL,
|
|
189
|
+
reward TEXT NOT NULL DEFAULT '{}',
|
|
190
|
+
bonus TEXT,
|
|
191
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
192
|
+
created_at INTEGER NOT NULL
|
|
193
|
+
)`,
|
|
194
|
+
// Intel reports
|
|
195
|
+
`CREATE TABLE IF NOT EXISTS intel (
|
|
196
|
+
id TEXT PRIMARY KEY,
|
|
197
|
+
player_id TEXT NOT NULL,
|
|
198
|
+
faction_id TEXT,
|
|
199
|
+
target_type TEXT NOT NULL,
|
|
200
|
+
target_id TEXT NOT NULL,
|
|
201
|
+
gathered_at INTEGER NOT NULL,
|
|
202
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
203
|
+
signal_quality INTEGER NOT NULL,
|
|
204
|
+
FOREIGN KEY (player_id) REFERENCES players(id),
|
|
205
|
+
FOREIGN KEY (faction_id) REFERENCES factions(id)
|
|
206
|
+
)`,
|
|
207
|
+
// Game events
|
|
208
|
+
`CREATE TABLE IF NOT EXISTS events (
|
|
209
|
+
id TEXT PRIMARY KEY,
|
|
210
|
+
type TEXT NOT NULL,
|
|
211
|
+
tick INTEGER NOT NULL,
|
|
212
|
+
timestamp TEXT NOT NULL,
|
|
213
|
+
actor_id TEXT,
|
|
214
|
+
actor_type TEXT NOT NULL,
|
|
215
|
+
data TEXT NOT NULL DEFAULT '{}',
|
|
216
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
217
|
+
)`,
|
|
218
|
+
// Season scores
|
|
219
|
+
`CREATE TABLE IF NOT EXISTS season_scores (
|
|
220
|
+
id TEXT PRIMARY KEY,
|
|
221
|
+
season_number INTEGER NOT NULL,
|
|
222
|
+
entity_id TEXT NOT NULL,
|
|
223
|
+
entity_type TEXT NOT NULL,
|
|
224
|
+
entity_name TEXT NOT NULL,
|
|
225
|
+
zones_controlled INTEGER NOT NULL DEFAULT 0,
|
|
226
|
+
supply_delivered INTEGER NOT NULL DEFAULT 0,
|
|
227
|
+
shipments_completed INTEGER NOT NULL DEFAULT 0,
|
|
228
|
+
contracts_completed INTEGER NOT NULL DEFAULT 0,
|
|
229
|
+
reputation_gained INTEGER NOT NULL DEFAULT 0,
|
|
230
|
+
combat_victories INTEGER NOT NULL DEFAULT 0,
|
|
231
|
+
total_score INTEGER NOT NULL DEFAULT 0,
|
|
232
|
+
UNIQUE(season_number, entity_id)
|
|
233
|
+
)`,
|
|
234
|
+
// Doctrines (Phase 5)
|
|
235
|
+
`CREATE TABLE IF NOT EXISTS doctrines (
|
|
236
|
+
id TEXT PRIMARY KEY,
|
|
237
|
+
faction_id TEXT NOT NULL,
|
|
238
|
+
title TEXT NOT NULL,
|
|
239
|
+
content TEXT NOT NULL,
|
|
240
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
241
|
+
created_at INTEGER NOT NULL,
|
|
242
|
+
updated_at INTEGER NOT NULL,
|
|
243
|
+
created_by TEXT NOT NULL,
|
|
244
|
+
FOREIGN KEY (faction_id) REFERENCES factions(id),
|
|
245
|
+
FOREIGN KEY (created_by) REFERENCES players(id)
|
|
246
|
+
)`,
|
|
247
|
+
// Webhooks (Phase 6)
|
|
248
|
+
`CREATE TABLE IF NOT EXISTS webhooks (
|
|
249
|
+
id TEXT PRIMARY KEY,
|
|
250
|
+
player_id TEXT NOT NULL,
|
|
251
|
+
url TEXT NOT NULL,
|
|
252
|
+
events TEXT NOT NULL DEFAULT '[]',
|
|
253
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
254
|
+
created_at INTEGER NOT NULL,
|
|
255
|
+
last_triggered_at INTEGER,
|
|
256
|
+
fail_count INTEGER NOT NULL DEFAULT 0,
|
|
257
|
+
FOREIGN KEY (player_id) REFERENCES players(id)
|
|
258
|
+
)`,
|
|
259
|
+
// Conditional orders (Phase 5)
|
|
260
|
+
`CREATE TABLE IF NOT EXISTS conditional_orders (
|
|
261
|
+
id TEXT PRIMARY KEY,
|
|
262
|
+
player_id TEXT NOT NULL,
|
|
263
|
+
zone_id TEXT NOT NULL,
|
|
264
|
+
resource TEXT NOT NULL,
|
|
265
|
+
side TEXT NOT NULL,
|
|
266
|
+
trigger_price INTEGER NOT NULL,
|
|
267
|
+
quantity INTEGER NOT NULL,
|
|
268
|
+
condition TEXT NOT NULL,
|
|
269
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
270
|
+
created_at INTEGER NOT NULL,
|
|
271
|
+
FOREIGN KEY (player_id) REFERENCES players(id),
|
|
272
|
+
FOREIGN KEY (zone_id) REFERENCES zones(id)
|
|
273
|
+
)`,
|
|
274
|
+
// Time-weighted orders (Phase 5)
|
|
275
|
+
`CREATE TABLE IF NOT EXISTS time_weighted_orders (
|
|
276
|
+
id TEXT PRIMARY KEY,
|
|
277
|
+
player_id TEXT NOT NULL,
|
|
278
|
+
zone_id TEXT NOT NULL,
|
|
279
|
+
resource TEXT NOT NULL,
|
|
280
|
+
side TEXT NOT NULL,
|
|
281
|
+
price INTEGER NOT NULL,
|
|
282
|
+
total_quantity INTEGER NOT NULL,
|
|
283
|
+
remaining_quantity INTEGER NOT NULL,
|
|
284
|
+
quantity_per_tick INTEGER NOT NULL,
|
|
285
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
286
|
+
created_at INTEGER NOT NULL,
|
|
287
|
+
FOREIGN KEY (player_id) REFERENCES players(id),
|
|
288
|
+
FOREIGN KEY (zone_id) REFERENCES zones(id)
|
|
289
|
+
)`,
|
|
290
|
+
// Audit logs (Phase 7)
|
|
291
|
+
`CREATE TABLE IF NOT EXISTS audit_logs (
|
|
292
|
+
id TEXT PRIMARY KEY,
|
|
293
|
+
faction_id TEXT NOT NULL,
|
|
294
|
+
player_id TEXT NOT NULL,
|
|
295
|
+
action TEXT NOT NULL,
|
|
296
|
+
details TEXT NOT NULL DEFAULT '{}',
|
|
297
|
+
tick INTEGER NOT NULL,
|
|
298
|
+
timestamp TEXT NOT NULL,
|
|
299
|
+
FOREIGN KEY (faction_id) REFERENCES factions(id),
|
|
300
|
+
FOREIGN KEY (player_id) REFERENCES players(id)
|
|
301
|
+
)`,
|
|
302
|
+
// Indexes
|
|
303
|
+
`CREATE INDEX IF NOT EXISTS idx_events_tick ON events(tick)`,
|
|
304
|
+
`CREATE INDEX IF NOT EXISTS idx_events_type ON events(type)`,
|
|
305
|
+
`CREATE INDEX IF NOT EXISTS idx_events_actor ON events(actor_id)`,
|
|
306
|
+
`CREATE INDEX IF NOT EXISTS idx_intel_faction ON intel(faction_id)`,
|
|
307
|
+
`CREATE INDEX IF NOT EXISTS idx_players_api_key ON players(api_key)`,
|
|
308
|
+
`CREATE INDEX IF NOT EXISTS idx_season_scores_season ON season_scores(season_number)`,
|
|
309
|
+
`CREATE INDEX IF NOT EXISTS idx_season_scores_total ON season_scores(total_score DESC)`,
|
|
310
|
+
`CREATE INDEX IF NOT EXISTS idx_doctrines_faction ON doctrines(faction_id)`,
|
|
311
|
+
`CREATE INDEX IF NOT EXISTS idx_webhooks_player ON webhooks(player_id)`,
|
|
312
|
+
`CREATE INDEX IF NOT EXISTS idx_conditional_orders_status ON conditional_orders(status)`,
|
|
313
|
+
`CREATE INDEX IF NOT EXISTS idx_time_weighted_orders_status ON time_weighted_orders(status)`,
|
|
314
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_logs_faction ON audit_logs(faction_id)`,
|
|
315
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_logs_tick ON audit_logs(tick)`,
|
|
316
|
+
// Initialize world state
|
|
317
|
+
`INSERT OR IGNORE INTO world (id, current_tick, season_number, season_week) VALUES (1, 0, 1, 1)`,
|
|
318
|
+
];
|
|
319
|
+
for (const sql of statements) {
|
|
320
|
+
await this.client.execute(sql);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ============================================================================
|
|
324
|
+
// WORLD STATE
|
|
325
|
+
// ============================================================================
|
|
326
|
+
async getCurrentTick() {
|
|
327
|
+
const result = await this.client.execute('SELECT current_tick FROM world WHERE id = 1');
|
|
328
|
+
return result.rows[0]?.current_tick || 0;
|
|
329
|
+
}
|
|
330
|
+
async incrementTick() {
|
|
331
|
+
await this.client.execute('UPDATE world SET current_tick = current_tick + 1 WHERE id = 1');
|
|
332
|
+
return this.getCurrentTick();
|
|
333
|
+
}
|
|
334
|
+
async getSeasonInfo() {
|
|
335
|
+
const result = await this.client.execute('SELECT season_number, season_week FROM world WHERE id = 1');
|
|
336
|
+
const row = result.rows[0];
|
|
337
|
+
return {
|
|
338
|
+
seasonNumber: row?.season_number || 1,
|
|
339
|
+
seasonWeek: row?.season_week || 1
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// ZONES
|
|
344
|
+
// ============================================================================
|
|
345
|
+
async createZone(zone) {
|
|
346
|
+
const id = uuid();
|
|
347
|
+
await this.client.execute({
|
|
348
|
+
sql: `INSERT INTO zones (id, name, type, owner_id, supply_level, burn_rate, compliance_streak,
|
|
349
|
+
su_stockpile, inventory, production_capacity, garrison_level, market_depth,
|
|
350
|
+
medkit_stockpile, comms_stockpile)
|
|
351
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
352
|
+
args: [id, zone.name, zone.type, zone.ownerId, zone.supplyLevel, zone.burnRate,
|
|
353
|
+
zone.complianceStreak, zone.suStockpile, JSON.stringify(zone.inventory),
|
|
354
|
+
zone.productionCapacity, zone.garrisonLevel, zone.marketDepth,
|
|
355
|
+
zone.medkitStockpile, zone.commsStockpile]
|
|
356
|
+
});
|
|
357
|
+
return { id, ...zone };
|
|
358
|
+
}
|
|
359
|
+
async getZone(id) {
|
|
360
|
+
const result = await this.client.execute({
|
|
361
|
+
sql: 'SELECT * FROM zones WHERE id = ?',
|
|
362
|
+
args: [id]
|
|
363
|
+
});
|
|
364
|
+
if (result.rows.length === 0)
|
|
365
|
+
return null;
|
|
366
|
+
return this.rowToZone(result.rows[0]);
|
|
367
|
+
}
|
|
368
|
+
async getAllZones() {
|
|
369
|
+
const result = await this.client.execute('SELECT * FROM zones');
|
|
370
|
+
return result.rows.map(row => this.rowToZone(row));
|
|
371
|
+
}
|
|
372
|
+
async updateZone(id, updates) {
|
|
373
|
+
const sets = [];
|
|
374
|
+
const values = [];
|
|
375
|
+
if (updates.ownerId !== undefined) {
|
|
376
|
+
sets.push('owner_id = ?');
|
|
377
|
+
values.push(updates.ownerId);
|
|
378
|
+
}
|
|
379
|
+
if (updates.supplyLevel !== undefined) {
|
|
380
|
+
sets.push('supply_level = ?');
|
|
381
|
+
values.push(updates.supplyLevel);
|
|
382
|
+
}
|
|
383
|
+
if (updates.complianceStreak !== undefined) {
|
|
384
|
+
sets.push('compliance_streak = ?');
|
|
385
|
+
values.push(updates.complianceStreak);
|
|
386
|
+
}
|
|
387
|
+
if (updates.suStockpile !== undefined) {
|
|
388
|
+
sets.push('su_stockpile = ?');
|
|
389
|
+
values.push(updates.suStockpile);
|
|
390
|
+
}
|
|
391
|
+
if (updates.inventory !== undefined) {
|
|
392
|
+
sets.push('inventory = ?');
|
|
393
|
+
values.push(JSON.stringify(updates.inventory));
|
|
394
|
+
}
|
|
395
|
+
if (updates.garrisonLevel !== undefined) {
|
|
396
|
+
sets.push('garrison_level = ?');
|
|
397
|
+
values.push(updates.garrisonLevel);
|
|
398
|
+
}
|
|
399
|
+
if (updates.medkitStockpile !== undefined) {
|
|
400
|
+
sets.push('medkit_stockpile = ?');
|
|
401
|
+
values.push(updates.medkitStockpile);
|
|
402
|
+
}
|
|
403
|
+
if (updates.commsStockpile !== undefined) {
|
|
404
|
+
sets.push('comms_stockpile = ?');
|
|
405
|
+
values.push(updates.commsStockpile);
|
|
406
|
+
}
|
|
407
|
+
if (sets.length === 0)
|
|
408
|
+
return;
|
|
409
|
+
values.push(id);
|
|
410
|
+
await this.client.execute({
|
|
411
|
+
sql: `UPDATE zones SET ${sets.join(', ')} WHERE id = ?`,
|
|
412
|
+
args: values
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
rowToZone(row) {
|
|
416
|
+
return {
|
|
417
|
+
id: row.id,
|
|
418
|
+
name: row.name,
|
|
419
|
+
type: row.type,
|
|
420
|
+
ownerId: row.owner_id,
|
|
421
|
+
supplyLevel: row.supply_level,
|
|
422
|
+
burnRate: row.burn_rate,
|
|
423
|
+
complianceStreak: row.compliance_streak,
|
|
424
|
+
suStockpile: row.su_stockpile,
|
|
425
|
+
inventory: JSON.parse(row.inventory),
|
|
426
|
+
productionCapacity: row.production_capacity,
|
|
427
|
+
garrisonLevel: row.garrison_level,
|
|
428
|
+
marketDepth: row.market_depth,
|
|
429
|
+
medkitStockpile: row.medkit_stockpile || 0,
|
|
430
|
+
commsStockpile: row.comms_stockpile || 0
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// ============================================================================
|
|
434
|
+
// ROUTES
|
|
435
|
+
// ============================================================================
|
|
436
|
+
async createRoute(route) {
|
|
437
|
+
const id = uuid();
|
|
438
|
+
await this.client.execute({
|
|
439
|
+
sql: `INSERT INTO routes (id, from_zone_id, to_zone_id, distance, capacity, base_risk, chokepoint_rating)
|
|
440
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
441
|
+
args: [id, route.fromZoneId, route.toZoneId, route.distance, route.capacity, route.baseRisk, route.chokepointRating]
|
|
442
|
+
});
|
|
443
|
+
return { id, ...route };
|
|
444
|
+
}
|
|
445
|
+
async getRoute(id) {
|
|
446
|
+
const result = await this.client.execute({
|
|
447
|
+
sql: 'SELECT * FROM routes WHERE id = ?',
|
|
448
|
+
args: [id]
|
|
449
|
+
});
|
|
450
|
+
if (result.rows.length === 0)
|
|
451
|
+
return null;
|
|
452
|
+
return this.rowToRoute(result.rows[0]);
|
|
453
|
+
}
|
|
454
|
+
async getRoutesBetween(fromZoneId, toZoneId) {
|
|
455
|
+
const result = await this.client.execute({
|
|
456
|
+
sql: 'SELECT * FROM routes WHERE from_zone_id = ? AND to_zone_id = ?',
|
|
457
|
+
args: [fromZoneId, toZoneId]
|
|
458
|
+
});
|
|
459
|
+
return result.rows.map(row => this.rowToRoute(row));
|
|
460
|
+
}
|
|
461
|
+
async getRoutesFromZone(zoneId) {
|
|
462
|
+
const result = await this.client.execute({
|
|
463
|
+
sql: 'SELECT * FROM routes WHERE from_zone_id = ?',
|
|
464
|
+
args: [zoneId]
|
|
465
|
+
});
|
|
466
|
+
return result.rows.map(row => this.rowToRoute(row));
|
|
467
|
+
}
|
|
468
|
+
async getAllRoutes() {
|
|
469
|
+
const result = await this.client.execute('SELECT * FROM routes');
|
|
470
|
+
return result.rows.map(row => this.rowToRoute(row));
|
|
471
|
+
}
|
|
472
|
+
rowToRoute(row) {
|
|
473
|
+
return {
|
|
474
|
+
id: row.id,
|
|
475
|
+
fromZoneId: row.from_zone_id,
|
|
476
|
+
toZoneId: row.to_zone_id,
|
|
477
|
+
distance: row.distance,
|
|
478
|
+
capacity: row.capacity,
|
|
479
|
+
baseRisk: row.base_risk,
|
|
480
|
+
chokepointRating: row.chokepoint_rating
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
// ============================================================================
|
|
484
|
+
// PLAYERS
|
|
485
|
+
// ============================================================================
|
|
486
|
+
async createPlayer(name, startingZoneId) {
|
|
487
|
+
const id = uuid();
|
|
488
|
+
const apiKey = `br_${uuid().replace(/-/g, '')}`;
|
|
489
|
+
const inventory = { ...emptyInventory(), credits: 500 };
|
|
490
|
+
const licenses = { courier: true, freight: false, convoy: false };
|
|
491
|
+
await this.client.execute({
|
|
492
|
+
sql: `INSERT INTO players (id, name, api_key, tier, inventory, location_id, reputation, licenses)
|
|
493
|
+
VALUES (?, ?, ?, 'freelance', ?, ?, 0, ?)`,
|
|
494
|
+
args: [id, name, apiKey, JSON.stringify(inventory), startingZoneId, JSON.stringify(licenses)]
|
|
495
|
+
});
|
|
496
|
+
return {
|
|
497
|
+
id, name, tier: 'freelance', inventory, locationId: startingZoneId,
|
|
498
|
+
factionId: null, reputation: 0, actionsToday: 0, lastActionTick: 0, licenses,
|
|
499
|
+
tutorialStep: 0,
|
|
500
|
+
apiKey
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async getPlayer(id) {
|
|
504
|
+
const result = await this.client.execute({
|
|
505
|
+
sql: 'SELECT * FROM players WHERE id = ?',
|
|
506
|
+
args: [id]
|
|
507
|
+
});
|
|
508
|
+
if (result.rows.length === 0)
|
|
509
|
+
return null;
|
|
510
|
+
return this.rowToPlayer(result.rows[0]);
|
|
511
|
+
}
|
|
512
|
+
async getPlayerByApiKey(apiKey) {
|
|
513
|
+
const result = await this.client.execute({
|
|
514
|
+
sql: 'SELECT * FROM players WHERE api_key = ?',
|
|
515
|
+
args: [apiKey]
|
|
516
|
+
});
|
|
517
|
+
if (result.rows.length === 0)
|
|
518
|
+
return null;
|
|
519
|
+
return this.rowToPlayer(result.rows[0]);
|
|
520
|
+
}
|
|
521
|
+
async getPlayerByName(name) {
|
|
522
|
+
const result = await this.client.execute({
|
|
523
|
+
sql: 'SELECT * FROM players WHERE name = ?',
|
|
524
|
+
args: [name]
|
|
525
|
+
});
|
|
526
|
+
if (result.rows.length === 0)
|
|
527
|
+
return null;
|
|
528
|
+
return this.rowToPlayer(result.rows[0]);
|
|
529
|
+
}
|
|
530
|
+
async getAllPlayers() {
|
|
531
|
+
const result = await this.client.execute('SELECT * FROM players');
|
|
532
|
+
return result.rows.map(row => this.rowToPlayer(row));
|
|
533
|
+
}
|
|
534
|
+
async updatePlayer(id, updates) {
|
|
535
|
+
const sets = [];
|
|
536
|
+
const values = [];
|
|
537
|
+
if (updates.inventory !== undefined) {
|
|
538
|
+
sets.push('inventory = ?');
|
|
539
|
+
values.push(JSON.stringify(updates.inventory));
|
|
540
|
+
}
|
|
541
|
+
if (updates.locationId !== undefined) {
|
|
542
|
+
sets.push('location_id = ?');
|
|
543
|
+
values.push(updates.locationId);
|
|
544
|
+
}
|
|
545
|
+
if (updates.factionId !== undefined) {
|
|
546
|
+
sets.push('faction_id = ?');
|
|
547
|
+
values.push(updates.factionId);
|
|
548
|
+
}
|
|
549
|
+
if (updates.reputation !== undefined) {
|
|
550
|
+
sets.push('reputation = ?');
|
|
551
|
+
values.push(updates.reputation);
|
|
552
|
+
}
|
|
553
|
+
if (updates.actionsToday !== undefined) {
|
|
554
|
+
sets.push('actions_today = ?');
|
|
555
|
+
values.push(updates.actionsToday);
|
|
556
|
+
}
|
|
557
|
+
if (updates.lastActionTick !== undefined) {
|
|
558
|
+
sets.push('last_action_tick = ?');
|
|
559
|
+
values.push(updates.lastActionTick);
|
|
560
|
+
}
|
|
561
|
+
if (updates.tier !== undefined) {
|
|
562
|
+
sets.push('tier = ?');
|
|
563
|
+
values.push(updates.tier);
|
|
564
|
+
}
|
|
565
|
+
if (updates.licenses !== undefined) {
|
|
566
|
+
sets.push('licenses = ?');
|
|
567
|
+
values.push(JSON.stringify(updates.licenses));
|
|
568
|
+
}
|
|
569
|
+
if (updates.tutorialStep !== undefined) {
|
|
570
|
+
sets.push('tutorial_step = ?');
|
|
571
|
+
values.push(updates.tutorialStep);
|
|
572
|
+
}
|
|
573
|
+
if (sets.length === 0)
|
|
574
|
+
return;
|
|
575
|
+
values.push(id);
|
|
576
|
+
await this.client.execute({
|
|
577
|
+
sql: `UPDATE players SET ${sets.join(', ')} WHERE id = ?`,
|
|
578
|
+
args: values
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
rowToPlayer(row) {
|
|
582
|
+
return {
|
|
583
|
+
id: row.id,
|
|
584
|
+
name: row.name,
|
|
585
|
+
tier: row.tier,
|
|
586
|
+
inventory: JSON.parse(row.inventory),
|
|
587
|
+
locationId: row.location_id,
|
|
588
|
+
factionId: row.faction_id,
|
|
589
|
+
reputation: row.reputation,
|
|
590
|
+
actionsToday: row.actions_today,
|
|
591
|
+
lastActionTick: row.last_action_tick,
|
|
592
|
+
licenses: JSON.parse(row.licenses),
|
|
593
|
+
tutorialStep: row.tutorial_step || 0
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
// ============================================================================
|
|
597
|
+
// FACTIONS
|
|
598
|
+
// ============================================================================
|
|
599
|
+
async createFaction(name, tag, founderId) {
|
|
600
|
+
const id = uuid();
|
|
601
|
+
const treasury = emptyInventory();
|
|
602
|
+
const upgrades = {
|
|
603
|
+
relayNetwork: 0,
|
|
604
|
+
routeFortification: 0,
|
|
605
|
+
productionBonus: 0,
|
|
606
|
+
garrisonStrength: 0,
|
|
607
|
+
marketDepth: 0
|
|
608
|
+
};
|
|
609
|
+
const tick = await this.getCurrentTick();
|
|
610
|
+
await this.client.execute({
|
|
611
|
+
sql: `INSERT INTO factions (id, name, tag, founder_id, treasury, upgrades, relations)
|
|
612
|
+
VALUES (?, ?, ?, ?, ?, ?, '{}')`,
|
|
613
|
+
args: [id, name, tag, founderId, JSON.stringify(treasury), JSON.stringify(upgrades)]
|
|
614
|
+
});
|
|
615
|
+
await this.client.execute({
|
|
616
|
+
sql: `INSERT INTO faction_members (faction_id, player_id, rank, joined_at) VALUES (?, ?, 'founder', ?)`,
|
|
617
|
+
args: [id, founderId, tick]
|
|
618
|
+
});
|
|
619
|
+
await this.updatePlayer(founderId, { factionId: id });
|
|
620
|
+
return {
|
|
621
|
+
id, name, tag, founderId, treasury,
|
|
622
|
+
officerWithdrawLimit: 1000,
|
|
623
|
+
members: [{ playerId: founderId, rank: 'founder', joinedAt: tick }],
|
|
624
|
+
controlledZones: [],
|
|
625
|
+
doctrineHash: null,
|
|
626
|
+
upgrades,
|
|
627
|
+
relations: {}
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
async getFaction(id) {
|
|
631
|
+
const result = await this.client.execute({
|
|
632
|
+
sql: 'SELECT * FROM factions WHERE id = ?',
|
|
633
|
+
args: [id]
|
|
634
|
+
});
|
|
635
|
+
if (result.rows.length === 0)
|
|
636
|
+
return null;
|
|
637
|
+
return this.rowToFaction(result.rows[0]);
|
|
638
|
+
}
|
|
639
|
+
async getAllFactions() {
|
|
640
|
+
const result = await this.client.execute('SELECT * FROM factions');
|
|
641
|
+
const factions = [];
|
|
642
|
+
for (const row of result.rows) {
|
|
643
|
+
factions.push(await this.rowToFaction(row));
|
|
644
|
+
}
|
|
645
|
+
return factions;
|
|
646
|
+
}
|
|
647
|
+
async rowToFaction(row) {
|
|
648
|
+
const membersResult = await this.client.execute({
|
|
649
|
+
sql: 'SELECT player_id, rank, joined_at FROM faction_members WHERE faction_id = ?',
|
|
650
|
+
args: [row.id]
|
|
651
|
+
});
|
|
652
|
+
const zonesResult = await this.client.execute({
|
|
653
|
+
sql: 'SELECT id FROM zones WHERE owner_id = ?',
|
|
654
|
+
args: [row.id]
|
|
655
|
+
});
|
|
656
|
+
return {
|
|
657
|
+
id: row.id,
|
|
658
|
+
name: row.name,
|
|
659
|
+
tag: row.tag,
|
|
660
|
+
founderId: row.founder_id,
|
|
661
|
+
treasury: JSON.parse(row.treasury),
|
|
662
|
+
officerWithdrawLimit: row.officer_withdraw_limit,
|
|
663
|
+
members: membersResult.rows.map(m => ({
|
|
664
|
+
playerId: m.player_id,
|
|
665
|
+
rank: m.rank,
|
|
666
|
+
joinedAt: m.joined_at
|
|
667
|
+
})),
|
|
668
|
+
controlledZones: zonesResult.rows.map(z => z.id),
|
|
669
|
+
doctrineHash: row.doctrine_hash,
|
|
670
|
+
upgrades: JSON.parse(row.upgrades),
|
|
671
|
+
relations: JSON.parse(row.relations)
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
async addFactionMember(factionId, playerId, rank = 'member') {
|
|
675
|
+
const tick = await this.getCurrentTick();
|
|
676
|
+
await this.client.execute({
|
|
677
|
+
sql: `INSERT INTO faction_members (faction_id, player_id, rank, joined_at) VALUES (?, ?, ?, ?)`,
|
|
678
|
+
args: [factionId, playerId, rank, tick]
|
|
679
|
+
});
|
|
680
|
+
await this.updatePlayer(playerId, { factionId });
|
|
681
|
+
}
|
|
682
|
+
async removeFactionMember(factionId, playerId) {
|
|
683
|
+
await this.client.execute({
|
|
684
|
+
sql: 'DELETE FROM faction_members WHERE faction_id = ? AND player_id = ?',
|
|
685
|
+
args: [factionId, playerId]
|
|
686
|
+
});
|
|
687
|
+
await this.updatePlayer(playerId, { factionId: null });
|
|
688
|
+
}
|
|
689
|
+
// ============================================================================
|
|
690
|
+
// SHIPMENTS
|
|
691
|
+
// ============================================================================
|
|
692
|
+
async createShipment(shipment) {
|
|
693
|
+
const id = uuid();
|
|
694
|
+
await this.client.execute({
|
|
695
|
+
sql: `INSERT INTO shipments (id, player_id, type, path, current_position, ticks_to_next_zone,
|
|
696
|
+
cargo, escort_ids, created_at, status)
|
|
697
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
698
|
+
args: [id, shipment.playerId, shipment.type, JSON.stringify(shipment.path),
|
|
699
|
+
shipment.currentPosition, shipment.ticksToNextZone, JSON.stringify(shipment.cargo),
|
|
700
|
+
JSON.stringify(shipment.escortIds), shipment.createdAt, shipment.status]
|
|
701
|
+
});
|
|
702
|
+
return { id, ...shipment };
|
|
703
|
+
}
|
|
704
|
+
async getShipment(id) {
|
|
705
|
+
const result = await this.client.execute({
|
|
706
|
+
sql: 'SELECT * FROM shipments WHERE id = ?',
|
|
707
|
+
args: [id]
|
|
708
|
+
});
|
|
709
|
+
if (result.rows.length === 0)
|
|
710
|
+
return null;
|
|
711
|
+
return this.rowToShipment(result.rows[0]);
|
|
712
|
+
}
|
|
713
|
+
async getActiveShipments() {
|
|
714
|
+
const result = await this.client.execute("SELECT * FROM shipments WHERE status = 'in_transit'");
|
|
715
|
+
return result.rows.map(row => this.rowToShipment(row));
|
|
716
|
+
}
|
|
717
|
+
async getPlayerShipments(playerId) {
|
|
718
|
+
const result = await this.client.execute({
|
|
719
|
+
sql: 'SELECT * FROM shipments WHERE player_id = ?',
|
|
720
|
+
args: [playerId]
|
|
721
|
+
});
|
|
722
|
+
return result.rows.map(row => this.rowToShipment(row));
|
|
723
|
+
}
|
|
724
|
+
async updateShipment(id, updates) {
|
|
725
|
+
const sets = [];
|
|
726
|
+
const values = [];
|
|
727
|
+
if (updates.currentPosition !== undefined) {
|
|
728
|
+
sets.push('current_position = ?');
|
|
729
|
+
values.push(updates.currentPosition);
|
|
730
|
+
}
|
|
731
|
+
if (updates.ticksToNextZone !== undefined) {
|
|
732
|
+
sets.push('ticks_to_next_zone = ?');
|
|
733
|
+
values.push(updates.ticksToNextZone);
|
|
734
|
+
}
|
|
735
|
+
if (updates.status !== undefined) {
|
|
736
|
+
sets.push('status = ?');
|
|
737
|
+
values.push(updates.status);
|
|
738
|
+
}
|
|
739
|
+
if (updates.escortIds !== undefined) {
|
|
740
|
+
sets.push('escort_ids = ?');
|
|
741
|
+
values.push(JSON.stringify(updates.escortIds));
|
|
742
|
+
}
|
|
743
|
+
if (sets.length === 0)
|
|
744
|
+
return;
|
|
745
|
+
values.push(id);
|
|
746
|
+
await this.client.execute({
|
|
747
|
+
sql: `UPDATE shipments SET ${sets.join(', ')} WHERE id = ?`,
|
|
748
|
+
args: values
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
rowToShipment(row) {
|
|
752
|
+
return {
|
|
753
|
+
id: row.id,
|
|
754
|
+
playerId: row.player_id,
|
|
755
|
+
type: row.type,
|
|
756
|
+
path: JSON.parse(row.path),
|
|
757
|
+
currentPosition: row.current_position,
|
|
758
|
+
ticksToNextZone: row.ticks_to_next_zone,
|
|
759
|
+
cargo: JSON.parse(row.cargo),
|
|
760
|
+
escortIds: JSON.parse(row.escort_ids),
|
|
761
|
+
createdAt: row.created_at,
|
|
762
|
+
status: row.status
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
// ============================================================================
|
|
766
|
+
// UNITS
|
|
767
|
+
// ============================================================================
|
|
768
|
+
async createUnit(unit) {
|
|
769
|
+
const id = uuid();
|
|
770
|
+
await this.client.execute({
|
|
771
|
+
sql: `INSERT INTO units (id, player_id, type, location_id, strength, speed, maintenance, assignment_id, for_sale_price)
|
|
772
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
773
|
+
args: [id, unit.playerId, unit.type, unit.locationId, unit.strength, unit.speed, unit.maintenance, unit.assignmentId, unit.forSalePrice]
|
|
774
|
+
});
|
|
775
|
+
return { id, ...unit };
|
|
776
|
+
}
|
|
777
|
+
async getUnit(id) {
|
|
778
|
+
const result = await this.client.execute({
|
|
779
|
+
sql: 'SELECT * FROM units WHERE id = ?',
|
|
780
|
+
args: [id]
|
|
781
|
+
});
|
|
782
|
+
if (result.rows.length === 0)
|
|
783
|
+
return null;
|
|
784
|
+
return this.rowToUnit(result.rows[0]);
|
|
785
|
+
}
|
|
786
|
+
async getPlayerUnits(playerId) {
|
|
787
|
+
const result = await this.client.execute({
|
|
788
|
+
sql: 'SELECT * FROM units WHERE player_id = ?',
|
|
789
|
+
args: [playerId]
|
|
790
|
+
});
|
|
791
|
+
return result.rows.map(row => this.rowToUnit(row));
|
|
792
|
+
}
|
|
793
|
+
async getUnitsForSaleAtZone(zoneId) {
|
|
794
|
+
const result = await this.client.execute({
|
|
795
|
+
sql: 'SELECT * FROM units WHERE location_id = ? AND for_sale_price IS NOT NULL',
|
|
796
|
+
args: [zoneId]
|
|
797
|
+
});
|
|
798
|
+
return result.rows.map(row => this.rowToUnit(row));
|
|
799
|
+
}
|
|
800
|
+
async updateUnit(id, updates) {
|
|
801
|
+
const sets = [];
|
|
802
|
+
const values = [];
|
|
803
|
+
if (updates.locationId !== undefined) {
|
|
804
|
+
sets.push('location_id = ?');
|
|
805
|
+
values.push(updates.locationId);
|
|
806
|
+
}
|
|
807
|
+
if (updates.assignmentId !== undefined) {
|
|
808
|
+
sets.push('assignment_id = ?');
|
|
809
|
+
values.push(updates.assignmentId);
|
|
810
|
+
}
|
|
811
|
+
if (updates.playerId !== undefined) {
|
|
812
|
+
sets.push('player_id = ?');
|
|
813
|
+
values.push(updates.playerId);
|
|
814
|
+
}
|
|
815
|
+
if (updates.forSalePrice !== undefined) {
|
|
816
|
+
sets.push('for_sale_price = ?');
|
|
817
|
+
values.push(updates.forSalePrice);
|
|
818
|
+
}
|
|
819
|
+
if (sets.length === 0)
|
|
820
|
+
return;
|
|
821
|
+
values.push(id);
|
|
822
|
+
await this.client.execute({
|
|
823
|
+
sql: `UPDATE units SET ${sets.join(', ')} WHERE id = ?`,
|
|
824
|
+
args: values
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
async deleteUnit(id) {
|
|
828
|
+
await this.client.execute({
|
|
829
|
+
sql: 'DELETE FROM units WHERE id = ?',
|
|
830
|
+
args: [id]
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
rowToUnit(row) {
|
|
834
|
+
return {
|
|
835
|
+
id: row.id,
|
|
836
|
+
playerId: row.player_id,
|
|
837
|
+
type: row.type,
|
|
838
|
+
locationId: row.location_id,
|
|
839
|
+
strength: row.strength,
|
|
840
|
+
speed: row.speed,
|
|
841
|
+
maintenance: row.maintenance,
|
|
842
|
+
assignmentId: row.assignment_id,
|
|
843
|
+
forSalePrice: row.for_sale_price
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
// ============================================================================
|
|
847
|
+
// MARKET ORDERS
|
|
848
|
+
// ============================================================================
|
|
849
|
+
async createOrder(order) {
|
|
850
|
+
const id = uuid();
|
|
851
|
+
await this.client.execute({
|
|
852
|
+
sql: `INSERT INTO market_orders (id, player_id, zone_id, resource, side, price, quantity, original_quantity, created_at)
|
|
853
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
854
|
+
args: [id, order.playerId, order.zoneId, order.resource, order.side, order.price, order.quantity, order.originalQuantity, order.createdAt]
|
|
855
|
+
});
|
|
856
|
+
return { id, ...order };
|
|
857
|
+
}
|
|
858
|
+
async getOrdersForZone(zoneId, resource) {
|
|
859
|
+
let sql = 'SELECT * FROM market_orders WHERE zone_id = ? AND quantity > 0';
|
|
860
|
+
const args = [zoneId];
|
|
861
|
+
if (resource) {
|
|
862
|
+
sql += ' AND resource = ?';
|
|
863
|
+
args.push(resource);
|
|
864
|
+
}
|
|
865
|
+
sql += ' ORDER BY side, price';
|
|
866
|
+
const result = await this.client.execute({ sql, args });
|
|
867
|
+
return result.rows.map(row => this.rowToOrder(row));
|
|
868
|
+
}
|
|
869
|
+
async updateOrder(id, quantity) {
|
|
870
|
+
await this.client.execute({
|
|
871
|
+
sql: 'UPDATE market_orders SET quantity = ? WHERE id = ?',
|
|
872
|
+
args: [quantity, id]
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
rowToOrder(row) {
|
|
876
|
+
return {
|
|
877
|
+
id: row.id,
|
|
878
|
+
playerId: row.player_id,
|
|
879
|
+
zoneId: row.zone_id,
|
|
880
|
+
resource: row.resource,
|
|
881
|
+
side: row.side,
|
|
882
|
+
price: row.price,
|
|
883
|
+
quantity: row.quantity,
|
|
884
|
+
originalQuantity: row.original_quantity,
|
|
885
|
+
createdAt: row.created_at
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
// ============================================================================
|
|
889
|
+
// CONTRACTS
|
|
890
|
+
// ============================================================================
|
|
891
|
+
async createContract(contract) {
|
|
892
|
+
const id = uuid();
|
|
893
|
+
await this.client.execute({
|
|
894
|
+
sql: `INSERT INTO contracts (id, type, poster_id, poster_type, accepted_by, details, deadline, reward, bonus, status, created_at)
|
|
895
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
896
|
+
args: [id, contract.type, contract.posterId, contract.posterType, contract.acceptedBy,
|
|
897
|
+
JSON.stringify(contract.details), contract.deadline, JSON.stringify(contract.reward),
|
|
898
|
+
contract.bonus ? JSON.stringify(contract.bonus) : null, contract.status, contract.createdAt]
|
|
899
|
+
});
|
|
900
|
+
return { id, ...contract };
|
|
901
|
+
}
|
|
902
|
+
async getOpenContracts() {
|
|
903
|
+
const result = await this.client.execute("SELECT * FROM contracts WHERE status = 'open' ORDER BY created_at DESC");
|
|
904
|
+
return result.rows.map(row => this.rowToContract(row));
|
|
905
|
+
}
|
|
906
|
+
async updateContract(id, updates) {
|
|
907
|
+
const sets = [];
|
|
908
|
+
const values = [];
|
|
909
|
+
if (updates.status) {
|
|
910
|
+
sets.push('status = ?');
|
|
911
|
+
values.push(updates.status);
|
|
912
|
+
}
|
|
913
|
+
if (updates.acceptedBy) {
|
|
914
|
+
sets.push('accepted_by = ?');
|
|
915
|
+
values.push(updates.acceptedBy);
|
|
916
|
+
}
|
|
917
|
+
if (sets.length === 0)
|
|
918
|
+
return;
|
|
919
|
+
values.push(id);
|
|
920
|
+
await this.client.execute({
|
|
921
|
+
sql: `UPDATE contracts SET ${sets.join(', ')} WHERE id = ?`,
|
|
922
|
+
args: values
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
rowToContract(row) {
|
|
926
|
+
return {
|
|
927
|
+
id: row.id,
|
|
928
|
+
type: row.type,
|
|
929
|
+
posterId: row.poster_id,
|
|
930
|
+
posterType: row.poster_type,
|
|
931
|
+
acceptedBy: row.accepted_by,
|
|
932
|
+
details: JSON.parse(row.details),
|
|
933
|
+
deadline: row.deadline,
|
|
934
|
+
reward: JSON.parse(row.reward),
|
|
935
|
+
bonus: row.bonus ? JSON.parse(row.bonus) : undefined,
|
|
936
|
+
status: row.status,
|
|
937
|
+
createdAt: row.created_at
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
// ============================================================================
|
|
941
|
+
// INTEL
|
|
942
|
+
// ============================================================================
|
|
943
|
+
async createIntel(intel) {
|
|
944
|
+
const id = uuid();
|
|
945
|
+
await this.client.execute({
|
|
946
|
+
sql: `INSERT INTO intel (id, player_id, faction_id, target_type, target_id, gathered_at, data, signal_quality)
|
|
947
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
948
|
+
args: [id, intel.playerId, intel.factionId, intel.targetType, intel.targetId, intel.gatheredAt, JSON.stringify(intel.data), intel.signalQuality]
|
|
949
|
+
});
|
|
950
|
+
return { id, ...intel };
|
|
951
|
+
}
|
|
952
|
+
async getFactionIntel(factionId, limit = 100) {
|
|
953
|
+
const result = await this.client.execute({
|
|
954
|
+
sql: 'SELECT * FROM intel WHERE faction_id = ? ORDER BY gathered_at DESC LIMIT ?',
|
|
955
|
+
args: [factionId, limit]
|
|
956
|
+
});
|
|
957
|
+
return result.rows.map(row => this.rowToIntel(row));
|
|
958
|
+
}
|
|
959
|
+
rowToIntel(row) {
|
|
960
|
+
return {
|
|
961
|
+
id: row.id,
|
|
962
|
+
playerId: row.player_id,
|
|
963
|
+
factionId: row.faction_id,
|
|
964
|
+
targetType: row.target_type,
|
|
965
|
+
targetId: row.target_id,
|
|
966
|
+
gatheredAt: row.gathered_at,
|
|
967
|
+
data: JSON.parse(row.data),
|
|
968
|
+
signalQuality: row.signal_quality
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
// ============================================================================
|
|
972
|
+
// EVENTS
|
|
973
|
+
// ============================================================================
|
|
974
|
+
async recordEvent(event) {
|
|
975
|
+
const id = uuid();
|
|
976
|
+
await this.client.execute({
|
|
977
|
+
sql: `INSERT INTO events (id, type, tick, timestamp, actor_id, actor_type, data)
|
|
978
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
979
|
+
args: [id, event.type, event.tick, event.timestamp.toISOString(), event.actorId, event.actorType, JSON.stringify(event.data)]
|
|
980
|
+
});
|
|
981
|
+
return { id, ...event };
|
|
982
|
+
}
|
|
983
|
+
async getEvents(options = {}) {
|
|
984
|
+
let sql = 'SELECT * FROM events WHERE 1=1';
|
|
985
|
+
const args = [];
|
|
986
|
+
if (options.type) {
|
|
987
|
+
sql += ' AND type = ?';
|
|
988
|
+
args.push(options.type);
|
|
989
|
+
}
|
|
990
|
+
if (options.actorId) {
|
|
991
|
+
sql += ' AND actor_id = ?';
|
|
992
|
+
args.push(options.actorId);
|
|
993
|
+
}
|
|
994
|
+
sql += ' ORDER BY tick DESC, created_at DESC';
|
|
995
|
+
if (options.limit) {
|
|
996
|
+
sql += ' LIMIT ?';
|
|
997
|
+
args.push(options.limit);
|
|
998
|
+
}
|
|
999
|
+
const result = await this.client.execute({ sql, args });
|
|
1000
|
+
return result.rows.map(row => ({
|
|
1001
|
+
id: row.id,
|
|
1002
|
+
type: row.type,
|
|
1003
|
+
tick: row.tick,
|
|
1004
|
+
timestamp: new Date(row.timestamp),
|
|
1005
|
+
actorId: row.actor_id,
|
|
1006
|
+
actorType: row.actor_type,
|
|
1007
|
+
data: JSON.parse(row.data)
|
|
1008
|
+
}));
|
|
1009
|
+
}
|
|
1010
|
+
// ============================================================================
|
|
1011
|
+
// PLAYER INTEL (with freshness)
|
|
1012
|
+
// ============================================================================
|
|
1013
|
+
async getPlayerIntel(playerId, limit = 100) {
|
|
1014
|
+
const result = await this.client.execute({
|
|
1015
|
+
sql: 'SELECT * FROM intel WHERE player_id = ? ORDER BY gathered_at DESC LIMIT ?',
|
|
1016
|
+
args: [playerId, limit]
|
|
1017
|
+
});
|
|
1018
|
+
return result.rows.map(row => this.rowToIntel(row));
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Get player intel with freshness calculation and data decay applied.
|
|
1022
|
+
* This is the recommended method for retrieving intel for display.
|
|
1023
|
+
*/
|
|
1024
|
+
async getPlayerIntelWithFreshness(playerId, limit = 100) {
|
|
1025
|
+
const currentTick = await this.getCurrentTick();
|
|
1026
|
+
const rawIntel = await this.getPlayerIntel(playerId, limit);
|
|
1027
|
+
return rawIntel.map(intel => this.applyFreshnessToIntel(intel, currentTick));
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Get faction intel with freshness calculation and data decay applied.
|
|
1031
|
+
*/
|
|
1032
|
+
async getFactionIntelWithFreshness(factionId, limit = 100) {
|
|
1033
|
+
const currentTick = await this.getCurrentTick();
|
|
1034
|
+
const rawIntel = await this.getFactionIntel(factionId, limit);
|
|
1035
|
+
return rawIntel.map(intel => this.applyFreshnessToIntel(intel, currentTick));
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get the most recent intel on a specific target (zone or route).
|
|
1039
|
+
* Returns intel with freshness applied.
|
|
1040
|
+
*/
|
|
1041
|
+
async getTargetIntel(playerId, factionId, targetType, targetId) {
|
|
1042
|
+
const currentTick = await this.getCurrentTick();
|
|
1043
|
+
// First try player's own intel
|
|
1044
|
+
let result = await this.client.execute({
|
|
1045
|
+
sql: `SELECT * FROM intel
|
|
1046
|
+
WHERE player_id = ? AND target_type = ? AND target_id = ?
|
|
1047
|
+
ORDER BY gathered_at DESC LIMIT 1`,
|
|
1048
|
+
args: [playerId, targetType, targetId]
|
|
1049
|
+
});
|
|
1050
|
+
// If not found and player has a faction, try faction intel
|
|
1051
|
+
if (result.rows.length === 0 && factionId) {
|
|
1052
|
+
result = await this.client.execute({
|
|
1053
|
+
sql: `SELECT * FROM intel
|
|
1054
|
+
WHERE faction_id = ? AND target_type = ? AND target_id = ?
|
|
1055
|
+
ORDER BY gathered_at DESC LIMIT 1`,
|
|
1056
|
+
args: [factionId, targetType, targetId]
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
if (result.rows.length === 0)
|
|
1060
|
+
return null;
|
|
1061
|
+
return this.applyFreshnessToIntel(this.rowToIntel(result.rows[0]), currentTick);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Delete expired intel older than a threshold to keep database clean.
|
|
1065
|
+
* Called periodically during tick processing.
|
|
1066
|
+
*/
|
|
1067
|
+
async cleanupExpiredIntel(maxAgeTicks = 200) {
|
|
1068
|
+
const currentTick = await this.getCurrentTick();
|
|
1069
|
+
const threshold = currentTick - maxAgeTicks;
|
|
1070
|
+
const result = await this.client.execute({
|
|
1071
|
+
sql: 'DELETE FROM intel WHERE gathered_at < ?',
|
|
1072
|
+
args: [threshold]
|
|
1073
|
+
});
|
|
1074
|
+
return result.rowsAffected;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Apply freshness calculation to raw intel data
|
|
1078
|
+
*/
|
|
1079
|
+
applyFreshnessToIntel(intel, currentTick) {
|
|
1080
|
+
const ageInTicks = currentTick - intel.gatheredAt;
|
|
1081
|
+
const freshness = getIntelFreshness(intel.gatheredAt, currentTick);
|
|
1082
|
+
const effectiveSignalQuality = getDecayedSignalQuality(intel.signalQuality, intel.gatheredAt, currentTick);
|
|
1083
|
+
const decayedData = applyIntelDecay(intel.data, freshness);
|
|
1084
|
+
return {
|
|
1085
|
+
...intel,
|
|
1086
|
+
data: decayedData,
|
|
1087
|
+
freshness,
|
|
1088
|
+
effectiveSignalQuality,
|
|
1089
|
+
ageInTicks
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
// ============================================================================
|
|
1093
|
+
// CONTRACT QUERIES
|
|
1094
|
+
// ============================================================================
|
|
1095
|
+
async getContract(id) {
|
|
1096
|
+
const result = await this.client.execute({
|
|
1097
|
+
sql: 'SELECT * FROM contracts WHERE id = ?',
|
|
1098
|
+
args: [id]
|
|
1099
|
+
});
|
|
1100
|
+
if (result.rows.length === 0)
|
|
1101
|
+
return null;
|
|
1102
|
+
return this.rowToContract(result.rows[0]);
|
|
1103
|
+
}
|
|
1104
|
+
async getContractsAtZone(zoneId) {
|
|
1105
|
+
const result = await this.client.execute({
|
|
1106
|
+
sql: `SELECT * FROM contracts
|
|
1107
|
+
WHERE status = 'open'
|
|
1108
|
+
AND (json_extract(details, '$.fromZoneId') = ? OR json_extract(details, '$.toZoneId') = ?)
|
|
1109
|
+
ORDER BY created_at DESC`,
|
|
1110
|
+
args: [zoneId, zoneId]
|
|
1111
|
+
});
|
|
1112
|
+
return result.rows.map(row => this.rowToContract(row));
|
|
1113
|
+
}
|
|
1114
|
+
async getPlayerContracts(playerId) {
|
|
1115
|
+
const result = await this.client.execute({
|
|
1116
|
+
sql: `SELECT * FROM contracts WHERE poster_id = ? OR accepted_by = ? ORDER BY created_at DESC`,
|
|
1117
|
+
args: [playerId, playerId]
|
|
1118
|
+
});
|
|
1119
|
+
return result.rows.map(row => this.rowToContract(row));
|
|
1120
|
+
}
|
|
1121
|
+
// ============================================================================
|
|
1122
|
+
// FACTION MEMBER RANK
|
|
1123
|
+
// ============================================================================
|
|
1124
|
+
async getFactionMemberRank(factionId, playerId) {
|
|
1125
|
+
const result = await this.client.execute({
|
|
1126
|
+
sql: 'SELECT rank FROM faction_members WHERE faction_id = ? AND player_id = ?',
|
|
1127
|
+
args: [factionId, playerId]
|
|
1128
|
+
});
|
|
1129
|
+
if (result.rows.length === 0)
|
|
1130
|
+
return null;
|
|
1131
|
+
return result.rows[0].rank;
|
|
1132
|
+
}
|
|
1133
|
+
async updateFactionMemberRank(factionId, playerId, rank) {
|
|
1134
|
+
await this.client.execute({
|
|
1135
|
+
sql: 'UPDATE faction_members SET rank = ? WHERE faction_id = ? AND player_id = ?',
|
|
1136
|
+
args: [rank, factionId, playerId]
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
// ============================================================================
|
|
1140
|
+
// FACTION UPDATES
|
|
1141
|
+
// ============================================================================
|
|
1142
|
+
async updateFaction(id, updates) {
|
|
1143
|
+
const sets = [];
|
|
1144
|
+
const values = [];
|
|
1145
|
+
if (updates.founderId !== undefined) {
|
|
1146
|
+
sets.push('founder_id = ?');
|
|
1147
|
+
values.push(updates.founderId);
|
|
1148
|
+
}
|
|
1149
|
+
if (updates.treasury !== undefined) {
|
|
1150
|
+
sets.push('treasury = ?');
|
|
1151
|
+
values.push(JSON.stringify(updates.treasury));
|
|
1152
|
+
}
|
|
1153
|
+
if (updates.officerWithdrawLimit !== undefined) {
|
|
1154
|
+
sets.push('officer_withdraw_limit = ?');
|
|
1155
|
+
values.push(updates.officerWithdrawLimit);
|
|
1156
|
+
}
|
|
1157
|
+
if (updates.doctrineHash !== undefined) {
|
|
1158
|
+
sets.push('doctrine_hash = ?');
|
|
1159
|
+
values.push(updates.doctrineHash);
|
|
1160
|
+
}
|
|
1161
|
+
if (updates.upgrades !== undefined) {
|
|
1162
|
+
sets.push('upgrades = ?');
|
|
1163
|
+
values.push(JSON.stringify(updates.upgrades));
|
|
1164
|
+
}
|
|
1165
|
+
if (updates.relations !== undefined) {
|
|
1166
|
+
sets.push('relations = ?');
|
|
1167
|
+
values.push(JSON.stringify(updates.relations));
|
|
1168
|
+
}
|
|
1169
|
+
if (sets.length === 0)
|
|
1170
|
+
return;
|
|
1171
|
+
values.push(id);
|
|
1172
|
+
await this.client.execute({
|
|
1173
|
+
sql: `UPDATE factions SET ${sets.join(', ')} WHERE id = ?`,
|
|
1174
|
+
args: values
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
// ============================================================================
|
|
1178
|
+
// SEASON SCORES
|
|
1179
|
+
// ============================================================================
|
|
1180
|
+
async getOrCreateSeasonScore(seasonNumber, entityId, entityType, entityName) {
|
|
1181
|
+
// Try to get existing
|
|
1182
|
+
const result = await this.client.execute({
|
|
1183
|
+
sql: 'SELECT * FROM season_scores WHERE season_number = ? AND entity_id = ?',
|
|
1184
|
+
args: [seasonNumber, entityId]
|
|
1185
|
+
});
|
|
1186
|
+
if (result.rows.length > 0) {
|
|
1187
|
+
return this.rowToSeasonScore(result.rows[0]);
|
|
1188
|
+
}
|
|
1189
|
+
// Create new
|
|
1190
|
+
const id = uuid();
|
|
1191
|
+
await this.client.execute({
|
|
1192
|
+
sql: `INSERT INTO season_scores (id, season_number, entity_id, entity_type, entity_name,
|
|
1193
|
+
zones_controlled, supply_delivered, shipments_completed, contracts_completed,
|
|
1194
|
+
reputation_gained, combat_victories, total_score)
|
|
1195
|
+
VALUES (?, ?, ?, ?, ?, 0, 0, 0, 0, 0, 0, 0)`,
|
|
1196
|
+
args: [id, seasonNumber, entityId, entityType, entityName]
|
|
1197
|
+
});
|
|
1198
|
+
return {
|
|
1199
|
+
id,
|
|
1200
|
+
seasonNumber,
|
|
1201
|
+
entityId,
|
|
1202
|
+
entityType,
|
|
1203
|
+
entityName,
|
|
1204
|
+
zonesControlled: 0,
|
|
1205
|
+
supplyDelivered: 0,
|
|
1206
|
+
shipmentsCompleted: 0,
|
|
1207
|
+
contractsCompleted: 0,
|
|
1208
|
+
reputationGained: 0,
|
|
1209
|
+
combatVictories: 0,
|
|
1210
|
+
totalScore: 0
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
async incrementSeasonScore(seasonNumber, entityId, entityType, entityName, field, amount) {
|
|
1214
|
+
// Ensure record exists
|
|
1215
|
+
await this.getOrCreateSeasonScore(seasonNumber, entityId, entityType, entityName);
|
|
1216
|
+
const fieldMap = {
|
|
1217
|
+
supplyDelivered: 'supply_delivered',
|
|
1218
|
+
shipmentsCompleted: 'shipments_completed',
|
|
1219
|
+
contractsCompleted: 'contracts_completed',
|
|
1220
|
+
reputationGained: 'reputation_gained',
|
|
1221
|
+
combatVictories: 'combat_victories'
|
|
1222
|
+
};
|
|
1223
|
+
const dbField = fieldMap[field];
|
|
1224
|
+
// Increment the field and recalculate total
|
|
1225
|
+
await this.client.execute({
|
|
1226
|
+
sql: `UPDATE season_scores
|
|
1227
|
+
SET ${dbField} = ${dbField} + ?,
|
|
1228
|
+
total_score = (zones_controlled * 100 + supply_delivered * 1 + shipments_completed * 10 +
|
|
1229
|
+
contracts_completed * 25 + reputation_gained * 2 + combat_victories * 50)
|
|
1230
|
+
WHERE season_number = ? AND entity_id = ?`,
|
|
1231
|
+
args: [amount, seasonNumber, entityId]
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
async updateSeasonZoneScores(seasonNumber) {
|
|
1235
|
+
// Get all factions and their controlled zone counts
|
|
1236
|
+
const factions = await this.getAllFactions();
|
|
1237
|
+
for (const faction of factions) {
|
|
1238
|
+
const zoneCount = faction.controlledZones.length;
|
|
1239
|
+
// Update or create score record
|
|
1240
|
+
await this.getOrCreateSeasonScore(seasonNumber, faction.id, 'faction', faction.name);
|
|
1241
|
+
await this.client.execute({
|
|
1242
|
+
sql: `UPDATE season_scores
|
|
1243
|
+
SET zones_controlled = ?,
|
|
1244
|
+
total_score = (? * 100 + supply_delivered * 1 + shipments_completed * 10 +
|
|
1245
|
+
contracts_completed * 25 + reputation_gained * 2 + combat_victories * 50)
|
|
1246
|
+
WHERE season_number = ? AND entity_id = ?`,
|
|
1247
|
+
args: [zoneCount, zoneCount, seasonNumber, faction.id]
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async getSeasonLeaderboard(seasonNumber, entityType, limit = 50) {
|
|
1252
|
+
let sql = 'SELECT * FROM season_scores WHERE season_number = ?';
|
|
1253
|
+
const args = [seasonNumber];
|
|
1254
|
+
if (entityType) {
|
|
1255
|
+
sql += ' AND entity_type = ?';
|
|
1256
|
+
args.push(entityType);
|
|
1257
|
+
}
|
|
1258
|
+
sql += ' ORDER BY total_score DESC LIMIT ?';
|
|
1259
|
+
args.push(limit);
|
|
1260
|
+
const result = await this.client.execute({ sql, args });
|
|
1261
|
+
return result.rows.map((row, index) => ({
|
|
1262
|
+
...this.rowToSeasonScore(row),
|
|
1263
|
+
rank: index + 1
|
|
1264
|
+
}));
|
|
1265
|
+
}
|
|
1266
|
+
async getEntitySeasonScore(seasonNumber, entityId) {
|
|
1267
|
+
const result = await this.client.execute({
|
|
1268
|
+
sql: 'SELECT * FROM season_scores WHERE season_number = ? AND entity_id = ?',
|
|
1269
|
+
args: [seasonNumber, entityId]
|
|
1270
|
+
});
|
|
1271
|
+
if (result.rows.length === 0)
|
|
1272
|
+
return null;
|
|
1273
|
+
return this.rowToSeasonScore(result.rows[0]);
|
|
1274
|
+
}
|
|
1275
|
+
async advanceSeason() {
|
|
1276
|
+
const season = await this.getSeasonInfo();
|
|
1277
|
+
const newSeason = season.seasonNumber + 1;
|
|
1278
|
+
await this.client.execute({
|
|
1279
|
+
sql: 'UPDATE world SET season_number = ?, season_week = 1 WHERE id = 1',
|
|
1280
|
+
args: [newSeason]
|
|
1281
|
+
});
|
|
1282
|
+
return { newSeason, newWeek: 1 };
|
|
1283
|
+
}
|
|
1284
|
+
async advanceWeek() {
|
|
1285
|
+
const season = await this.getSeasonInfo();
|
|
1286
|
+
const newWeek = season.seasonWeek + 1;
|
|
1287
|
+
await this.client.execute({
|
|
1288
|
+
sql: 'UPDATE world SET season_week = ? WHERE id = 1',
|
|
1289
|
+
args: [newWeek]
|
|
1290
|
+
});
|
|
1291
|
+
return { seasonNumber: season.seasonNumber, newWeek };
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Reset the game world for a new season.
|
|
1295
|
+
* Archives scores, resets zones/inventories, preserves accounts/licenses/factions.
|
|
1296
|
+
*/
|
|
1297
|
+
// ============================================================================
|
|
1298
|
+
// DOCTRINES (Phase 5)
|
|
1299
|
+
// ============================================================================
|
|
1300
|
+
async createDoctrine(factionId, title, content, createdBy) {
|
|
1301
|
+
const id = uuid();
|
|
1302
|
+
const tick = await this.getCurrentTick();
|
|
1303
|
+
await this.client.execute({
|
|
1304
|
+
sql: `INSERT INTO doctrines (id, faction_id, title, content, version, created_at, updated_at, created_by)
|
|
1305
|
+
VALUES (?, ?, ?, ?, 1, ?, ?, ?)`,
|
|
1306
|
+
args: [id, factionId, title, content, tick, tick, createdBy]
|
|
1307
|
+
});
|
|
1308
|
+
return { id, factionId, title, content, version: 1, createdAt: tick, updatedAt: tick, createdBy };
|
|
1309
|
+
}
|
|
1310
|
+
async getFactionDoctrines(factionId) {
|
|
1311
|
+
const result = await this.client.execute({
|
|
1312
|
+
sql: 'SELECT * FROM doctrines WHERE faction_id = ? ORDER BY updated_at DESC',
|
|
1313
|
+
args: [factionId]
|
|
1314
|
+
});
|
|
1315
|
+
return result.rows.map(row => ({
|
|
1316
|
+
id: row.id,
|
|
1317
|
+
factionId: row.faction_id,
|
|
1318
|
+
title: row.title,
|
|
1319
|
+
content: row.content,
|
|
1320
|
+
version: row.version,
|
|
1321
|
+
createdAt: row.created_at,
|
|
1322
|
+
updatedAt: row.updated_at,
|
|
1323
|
+
createdBy: row.created_by
|
|
1324
|
+
}));
|
|
1325
|
+
}
|
|
1326
|
+
async getDoctrine(id) {
|
|
1327
|
+
const result = await this.client.execute({
|
|
1328
|
+
sql: 'SELECT * FROM doctrines WHERE id = ?',
|
|
1329
|
+
args: [id]
|
|
1330
|
+
});
|
|
1331
|
+
if (result.rows.length === 0)
|
|
1332
|
+
return null;
|
|
1333
|
+
const row = result.rows[0];
|
|
1334
|
+
return {
|
|
1335
|
+
id: row.id,
|
|
1336
|
+
factionId: row.faction_id,
|
|
1337
|
+
title: row.title,
|
|
1338
|
+
content: row.content,
|
|
1339
|
+
version: row.version,
|
|
1340
|
+
createdAt: row.created_at,
|
|
1341
|
+
updatedAt: row.updated_at,
|
|
1342
|
+
createdBy: row.created_by
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
async updateDoctrine(id, content) {
|
|
1346
|
+
const tick = await this.getCurrentTick();
|
|
1347
|
+
await this.client.execute({
|
|
1348
|
+
sql: 'UPDATE doctrines SET content = ?, version = version + 1, updated_at = ? WHERE id = ?',
|
|
1349
|
+
args: [content, tick, id]
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
async deleteDoctrine(id) {
|
|
1353
|
+
await this.client.execute({
|
|
1354
|
+
sql: 'DELETE FROM doctrines WHERE id = ?',
|
|
1355
|
+
args: [id]
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
// ============================================================================
|
|
1359
|
+
// WEBHOOKS (Phase 6)
|
|
1360
|
+
// ============================================================================
|
|
1361
|
+
async createWebhook(playerId, url, events) {
|
|
1362
|
+
const id = uuid();
|
|
1363
|
+
const tick = await this.getCurrentTick();
|
|
1364
|
+
await this.client.execute({
|
|
1365
|
+
sql: `INSERT INTO webhooks (id, player_id, url, events, active, created_at, fail_count)
|
|
1366
|
+
VALUES (?, ?, ?, ?, 1, ?, 0)`,
|
|
1367
|
+
args: [id, playerId, url, JSON.stringify(events), tick]
|
|
1368
|
+
});
|
|
1369
|
+
return { id, playerId, url, events: events, active: true, createdAt: tick, lastTriggeredAt: null, failCount: 0 };
|
|
1370
|
+
}
|
|
1371
|
+
async getPlayerWebhooks(playerId) {
|
|
1372
|
+
const result = await this.client.execute({
|
|
1373
|
+
sql: 'SELECT * FROM webhooks WHERE player_id = ?',
|
|
1374
|
+
args: [playerId]
|
|
1375
|
+
});
|
|
1376
|
+
return result.rows.map(row => ({
|
|
1377
|
+
id: row.id,
|
|
1378
|
+
playerId: row.player_id,
|
|
1379
|
+
url: row.url,
|
|
1380
|
+
events: JSON.parse(row.events),
|
|
1381
|
+
active: !!row.active,
|
|
1382
|
+
createdAt: row.created_at,
|
|
1383
|
+
lastTriggeredAt: row.last_triggered_at,
|
|
1384
|
+
failCount: row.fail_count
|
|
1385
|
+
}));
|
|
1386
|
+
}
|
|
1387
|
+
async deleteWebhook(id, playerId) {
|
|
1388
|
+
const result = await this.client.execute({
|
|
1389
|
+
sql: 'DELETE FROM webhooks WHERE id = ? AND player_id = ?',
|
|
1390
|
+
args: [id, playerId]
|
|
1391
|
+
});
|
|
1392
|
+
return result.rowsAffected > 0;
|
|
1393
|
+
}
|
|
1394
|
+
async getWebhooksForEvent(eventType) {
|
|
1395
|
+
const result = await this.client.execute({
|
|
1396
|
+
sql: `SELECT * FROM webhooks WHERE active = 1 AND events LIKE ?`,
|
|
1397
|
+
args: [`%${eventType}%`]
|
|
1398
|
+
});
|
|
1399
|
+
return result.rows.map(row => ({
|
|
1400
|
+
id: row.id,
|
|
1401
|
+
playerId: row.player_id,
|
|
1402
|
+
url: row.url,
|
|
1403
|
+
events: JSON.parse(row.events),
|
|
1404
|
+
active: true,
|
|
1405
|
+
createdAt: row.created_at,
|
|
1406
|
+
lastTriggeredAt: row.last_triggered_at,
|
|
1407
|
+
failCount: row.fail_count
|
|
1408
|
+
}));
|
|
1409
|
+
}
|
|
1410
|
+
async updateWebhookStatus(id, lastTriggered, failCount) {
|
|
1411
|
+
await this.client.execute({
|
|
1412
|
+
sql: 'UPDATE webhooks SET last_triggered_at = ?, fail_count = ?, active = ? WHERE id = ?',
|
|
1413
|
+
args: [lastTriggered, failCount, failCount < 5 ? 1 : 0, id]
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
// ============================================================================
|
|
1417
|
+
// CONDITIONAL ORDERS (Phase 5)
|
|
1418
|
+
// ============================================================================
|
|
1419
|
+
async createConditionalOrder(order) {
|
|
1420
|
+
const id = uuid();
|
|
1421
|
+
await this.client.execute({
|
|
1422
|
+
sql: `INSERT INTO conditional_orders (id, player_id, zone_id, resource, side, trigger_price, quantity, condition, status, created_at)
|
|
1423
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active', ?)`,
|
|
1424
|
+
args: [id, order.playerId, order.zoneId, order.resource, order.side, order.triggerPrice, order.quantity, order.condition, order.createdAt]
|
|
1425
|
+
});
|
|
1426
|
+
return { id, ...order, status: 'active' };
|
|
1427
|
+
}
|
|
1428
|
+
async getActiveConditionalOrders() {
|
|
1429
|
+
const result = await this.client.execute("SELECT * FROM conditional_orders WHERE status = 'active'");
|
|
1430
|
+
return result.rows.map(row => ({
|
|
1431
|
+
id: row.id,
|
|
1432
|
+
playerId: row.player_id,
|
|
1433
|
+
zoneId: row.zone_id,
|
|
1434
|
+
resource: row.resource,
|
|
1435
|
+
side: row.side,
|
|
1436
|
+
triggerPrice: row.trigger_price,
|
|
1437
|
+
quantity: row.quantity,
|
|
1438
|
+
condition: row.condition,
|
|
1439
|
+
status: row.status,
|
|
1440
|
+
createdAt: row.created_at
|
|
1441
|
+
}));
|
|
1442
|
+
}
|
|
1443
|
+
async updateConditionalOrderStatus(id, status) {
|
|
1444
|
+
await this.client.execute({
|
|
1445
|
+
sql: 'UPDATE conditional_orders SET status = ? WHERE id = ?',
|
|
1446
|
+
args: [status, id]
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
// ============================================================================
|
|
1450
|
+
// TIME-WEIGHTED ORDERS (Phase 5)
|
|
1451
|
+
// ============================================================================
|
|
1452
|
+
async createTimeWeightedOrder(order) {
|
|
1453
|
+
const id = uuid();
|
|
1454
|
+
await this.client.execute({
|
|
1455
|
+
sql: `INSERT INTO time_weighted_orders (id, player_id, zone_id, resource, side, price, total_quantity, remaining_quantity, quantity_per_tick, status, created_at)
|
|
1456
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?)`,
|
|
1457
|
+
args: [id, order.playerId, order.zoneId, order.resource, order.side, order.price, order.totalQuantity, order.remainingQuantity, order.quantityPerTick, order.createdAt]
|
|
1458
|
+
});
|
|
1459
|
+
return { id, ...order, status: 'active' };
|
|
1460
|
+
}
|
|
1461
|
+
async getActiveTimeWeightedOrders() {
|
|
1462
|
+
const result = await this.client.execute("SELECT * FROM time_weighted_orders WHERE status = 'active'");
|
|
1463
|
+
return result.rows.map(row => ({
|
|
1464
|
+
id: row.id,
|
|
1465
|
+
playerId: row.player_id,
|
|
1466
|
+
zoneId: row.zone_id,
|
|
1467
|
+
resource: row.resource,
|
|
1468
|
+
side: row.side,
|
|
1469
|
+
price: row.price,
|
|
1470
|
+
totalQuantity: row.total_quantity,
|
|
1471
|
+
remainingQuantity: row.remaining_quantity,
|
|
1472
|
+
quantityPerTick: row.quantity_per_tick,
|
|
1473
|
+
status: row.status,
|
|
1474
|
+
createdAt: row.created_at
|
|
1475
|
+
}));
|
|
1476
|
+
}
|
|
1477
|
+
async updateTimeWeightedOrder(id, remainingQuantity) {
|
|
1478
|
+
const status = remainingQuantity <= 0 ? 'completed' : 'active';
|
|
1479
|
+
await this.client.execute({
|
|
1480
|
+
sql: 'UPDATE time_weighted_orders SET remaining_quantity = ?, status = ? WHERE id = ?',
|
|
1481
|
+
args: [remainingQuantity, status, id]
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
// ============================================================================
|
|
1485
|
+
// AUDIT LOGS (Phase 7)
|
|
1486
|
+
// ============================================================================
|
|
1487
|
+
async createAuditLog(factionId, playerId, action, details) {
|
|
1488
|
+
const id = uuid();
|
|
1489
|
+
const tick = await this.getCurrentTick();
|
|
1490
|
+
await this.client.execute({
|
|
1491
|
+
sql: `INSERT INTO audit_logs (id, faction_id, player_id, action, details, tick, timestamp)
|
|
1492
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1493
|
+
args: [id, factionId, playerId, action, JSON.stringify(details), tick, new Date().toISOString()]
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
async getFactionAuditLogs(factionId, limit = 100) {
|
|
1497
|
+
const result = await this.client.execute({
|
|
1498
|
+
sql: 'SELECT * FROM audit_logs WHERE faction_id = ? ORDER BY tick DESC LIMIT ?',
|
|
1499
|
+
args: [factionId, limit]
|
|
1500
|
+
});
|
|
1501
|
+
return result.rows.map(row => ({
|
|
1502
|
+
id: row.id,
|
|
1503
|
+
factionId: row.faction_id,
|
|
1504
|
+
playerId: row.player_id,
|
|
1505
|
+
action: row.action,
|
|
1506
|
+
details: JSON.parse(row.details),
|
|
1507
|
+
tick: row.tick,
|
|
1508
|
+
timestamp: new Date(row.timestamp)
|
|
1509
|
+
}));
|
|
1510
|
+
}
|
|
1511
|
+
async seasonReset(newSeasonNumber) {
|
|
1512
|
+
// Reset all zones to neutral defaults
|
|
1513
|
+
await this.client.execute(`UPDATE zones SET owner_id = NULL, supply_level = 100, compliance_streak = 0,
|
|
1514
|
+
su_stockpile = 0, inventory = '{}', garrison_level = 0,
|
|
1515
|
+
medkit_stockpile = 0, comms_stockpile = 0`);
|
|
1516
|
+
// Reset player inventories/credits, preserve accounts/licenses/factions
|
|
1517
|
+
const startingInventory = JSON.stringify({ ...emptyInventory(), credits: 500 });
|
|
1518
|
+
await this.client.execute({
|
|
1519
|
+
sql: `UPDATE players SET inventory = ?, actions_today = 0, last_action_tick = 0,
|
|
1520
|
+
reputation = CAST(reputation * 0.5 AS INTEGER)`,
|
|
1521
|
+
args: [startingInventory]
|
|
1522
|
+
});
|
|
1523
|
+
// Clear all active shipments
|
|
1524
|
+
await this.client.execute(`DELETE FROM shipments`);
|
|
1525
|
+
// Clear all units
|
|
1526
|
+
await this.client.execute(`DELETE FROM units`);
|
|
1527
|
+
// Clear all market orders
|
|
1528
|
+
await this.client.execute(`DELETE FROM market_orders`);
|
|
1529
|
+
// Clear all active contracts
|
|
1530
|
+
await this.client.execute(`DELETE FROM contracts WHERE status IN ('open', 'accepted')`);
|
|
1531
|
+
// Clear all intel
|
|
1532
|
+
await this.client.execute(`DELETE FROM intel`);
|
|
1533
|
+
// Reset faction treasuries
|
|
1534
|
+
const emptyTreasury = JSON.stringify(emptyInventory());
|
|
1535
|
+
await this.client.execute({
|
|
1536
|
+
sql: `UPDATE factions SET treasury = ?`,
|
|
1537
|
+
args: [emptyTreasury]
|
|
1538
|
+
});
|
|
1539
|
+
// Advance to new season
|
|
1540
|
+
await this.client.execute({
|
|
1541
|
+
sql: 'UPDATE world SET season_number = ?, season_week = 1 WHERE id = 1',
|
|
1542
|
+
args: [newSeasonNumber]
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
rowToSeasonScore(row) {
|
|
1546
|
+
return {
|
|
1547
|
+
id: row.id,
|
|
1548
|
+
seasonNumber: row.season_number,
|
|
1549
|
+
entityId: row.entity_id,
|
|
1550
|
+
entityType: row.entity_type,
|
|
1551
|
+
entityName: row.entity_name,
|
|
1552
|
+
zonesControlled: row.zones_controlled,
|
|
1553
|
+
supplyDelivered: row.supply_delivered,
|
|
1554
|
+
shipmentsCompleted: row.shipments_completed,
|
|
1555
|
+
contractsCompleted: row.contracts_completed,
|
|
1556
|
+
reputationGained: row.reputation_gained,
|
|
1557
|
+
combatVictories: row.combat_victories,
|
|
1558
|
+
totalScore: row.total_score
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
// ============================================================================
|
|
1563
|
+
// TRANSACTION CONTEXT
|
|
1564
|
+
// ============================================================================
|
|
1565
|
+
/**
|
|
1566
|
+
* Collects statements to be executed in a transaction.
|
|
1567
|
+
* Call add() to queue statements, they execute when transaction() completes.
|
|
1568
|
+
*/
|
|
1569
|
+
export class TransactionContext {
|
|
1570
|
+
statements;
|
|
1571
|
+
constructor(statements) {
|
|
1572
|
+
this.statements = statements;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Add a statement to the transaction
|
|
1576
|
+
*/
|
|
1577
|
+
add(sql, args = []) {
|
|
1578
|
+
this.statements.push({ sql, args });
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Generate a UUID for use in the transaction
|
|
1582
|
+
*/
|
|
1583
|
+
uuid() {
|
|
1584
|
+
return uuid();
|
|
1585
|
+
}
|
|
1586
|
+
}
|