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,1121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BURNRATE CLI
|
|
4
|
+
* A logistics war MMO for Claude Code
|
|
5
|
+
* The front doesn't feed itself.
|
|
6
|
+
*/
|
|
7
|
+
// Route 'setup' command to the setup script before loading heavy dependencies
|
|
8
|
+
if (process.argv[2] === 'setup') {
|
|
9
|
+
await import('./setup.js');
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { GameDatabase } from '../db/database.js';
|
|
14
|
+
import { GameEngine } from '../core/engine.js';
|
|
15
|
+
import { generateWorld, seedMarkets } from '../core/worldgen.js';
|
|
16
|
+
import { formatView, formatZone, formatRoutes, formatMarket, formatShipments, formatEvents, formatHelp } from './format.js';
|
|
17
|
+
import { TIER_LIMITS } from '../core/types.js';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import os from 'os';
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
// Database path
|
|
22
|
+
const DB_DIR = path.join(os.homedir(), '.burnrate');
|
|
23
|
+
const DB_PATH = path.join(DB_DIR, 'game.db');
|
|
24
|
+
// Ensure directory exists
|
|
25
|
+
if (!fs.existsSync(DB_DIR)) {
|
|
26
|
+
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
// Initialize
|
|
29
|
+
const db = new GameDatabase(DB_PATH);
|
|
30
|
+
const engine = new GameEngine(db);
|
|
31
|
+
// Check if world needs initialization
|
|
32
|
+
const zones = db.getAllZones();
|
|
33
|
+
if (zones.length === 0) {
|
|
34
|
+
console.log('Initializing new world for Season 1...');
|
|
35
|
+
generateWorld(db);
|
|
36
|
+
seedMarkets(db);
|
|
37
|
+
console.log('World created. Welcome to BURNRATE.\n');
|
|
38
|
+
}
|
|
39
|
+
// Get or create player session
|
|
40
|
+
function getPlayer(name) {
|
|
41
|
+
const configPath = path.join(DB_DIR, 'player.json');
|
|
42
|
+
if (fs.existsSync(configPath)) {
|
|
43
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
44
|
+
return db.getPlayer(config.playerId);
|
|
45
|
+
}
|
|
46
|
+
if (!name) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// Create new player
|
|
50
|
+
const hubs = db.getAllZones().filter(z => z.type === 'hub');
|
|
51
|
+
const startZone = hubs[0];
|
|
52
|
+
const player = db.createPlayer(name, startZone.id);
|
|
53
|
+
fs.writeFileSync(configPath, JSON.stringify({ playerId: player.id }));
|
|
54
|
+
return player;
|
|
55
|
+
}
|
|
56
|
+
const program = new Command();
|
|
57
|
+
program
|
|
58
|
+
.name('burnrate')
|
|
59
|
+
.description('A logistics war MMO for Claude Code. The front doesn\'t feed itself.')
|
|
60
|
+
.version('0.1.0');
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// VIEW COMMANDS
|
|
63
|
+
// ============================================================================
|
|
64
|
+
program
|
|
65
|
+
.command('view')
|
|
66
|
+
.description('View the world state and your status')
|
|
67
|
+
.argument('[target]', 'What to view: zone, routes, market, shipments, events')
|
|
68
|
+
.argument('[id]', 'Target ID (zone name, resource, etc.)')
|
|
69
|
+
.action((target, id) => {
|
|
70
|
+
const player = getPlayer();
|
|
71
|
+
if (!target) {
|
|
72
|
+
// Main dashboard
|
|
73
|
+
if (!player) {
|
|
74
|
+
console.log('No player found. Run: burnrate join <name>');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(formatView(db, player));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
switch (target) {
|
|
81
|
+
case 'zone': {
|
|
82
|
+
const zoneName = id;
|
|
83
|
+
const allZones = db.getAllZones();
|
|
84
|
+
const zone = zoneName
|
|
85
|
+
? allZones.find(z => z.name.toLowerCase() === zoneName.toLowerCase())
|
|
86
|
+
: player ? db.getZone(player.locationId) : allZones[0];
|
|
87
|
+
if (!zone) {
|
|
88
|
+
console.log(`Zone not found: ${zoneName}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(formatZone(db, zone));
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'routes': {
|
|
95
|
+
const fromZone = player ? db.getZone(player.locationId) : null;
|
|
96
|
+
if (!fromZone) {
|
|
97
|
+
console.log('Join the game first: burnrate join <name>');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
console.log(formatRoutes(db, fromZone.id));
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'market': {
|
|
104
|
+
const resource = id;
|
|
105
|
+
const zoneId = player?.locationId;
|
|
106
|
+
if (!zoneId) {
|
|
107
|
+
console.log('Join the game first: burnrate join <name>');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log(formatMarket(db, zoneId, resource));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case 'shipments': {
|
|
114
|
+
if (!player) {
|
|
115
|
+
console.log('Join the game first: burnrate join <name>');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const shipments = db.getPlayerShipments(player.id);
|
|
119
|
+
console.log(formatShipments(shipments));
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case 'events': {
|
|
123
|
+
const limit = id ? parseInt(id) : 20;
|
|
124
|
+
const events = db.getEvents({ limit, actorId: player?.id });
|
|
125
|
+
console.log(formatEvents(events));
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
console.log(`Unknown view target: ${target}`);
|
|
130
|
+
console.log('Try: zone, routes, market, shipments, events');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// JOIN COMMAND
|
|
135
|
+
// ============================================================================
|
|
136
|
+
program
|
|
137
|
+
.command('join')
|
|
138
|
+
.description('Join the game with a new character')
|
|
139
|
+
.argument('<name>', 'Your player name')
|
|
140
|
+
.action((name) => {
|
|
141
|
+
const existing = db.getPlayerByName(name);
|
|
142
|
+
if (existing) {
|
|
143
|
+
console.log(`Name "${name}" is taken. Choose another.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const player = getPlayer(name);
|
|
147
|
+
if (player) {
|
|
148
|
+
console.log(`\nWelcome to BURNRATE, ${player.name}!`);
|
|
149
|
+
console.log(`\nYou start at ${db.getZone(player.locationId)?.name} with:`);
|
|
150
|
+
console.log(` ${player.inventory.credits} credits`);
|
|
151
|
+
console.log(` Courier license (can ship up to 10 units)`);
|
|
152
|
+
console.log(`\nRun 'burnrate view' to see the world.`);
|
|
153
|
+
console.log(`Run 'burnrate help' for commands.\n`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// ROUTE COMMAND
|
|
158
|
+
// ============================================================================
|
|
159
|
+
program
|
|
160
|
+
.command('route')
|
|
161
|
+
.description('View direct routes from a zone')
|
|
162
|
+
.argument('[from]', 'Zone name (defaults to your location)')
|
|
163
|
+
.action((from) => {
|
|
164
|
+
const player = getPlayer();
|
|
165
|
+
const allZones = db.getAllZones();
|
|
166
|
+
let fromZone;
|
|
167
|
+
if (from) {
|
|
168
|
+
fromZone = allZones.find(z => z.name.toLowerCase() === from.toLowerCase());
|
|
169
|
+
if (!fromZone) {
|
|
170
|
+
console.log(`Zone not found: ${from}`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (player) {
|
|
175
|
+
fromZone = db.getZone(player.locationId);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.log('Specify a zone or join the game first');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!fromZone)
|
|
182
|
+
return;
|
|
183
|
+
const routes = db.getRoutesFromZone(fromZone.id);
|
|
184
|
+
if (routes.length === 0) {
|
|
185
|
+
console.log(`\nNo routes from ${fromZone.name}\n`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
console.log(`\n┌─ ROUTES FROM: ${fromZone.name} ─────────────────────────────────┐`);
|
|
189
|
+
console.log(`│ Destination │ Dist │ Risk │ Choke │`);
|
|
190
|
+
console.log(`├──────────────────────┼──────┼───────┼───────┤`);
|
|
191
|
+
for (const route of routes) {
|
|
192
|
+
const toZone = db.getZone(route.toZoneId);
|
|
193
|
+
const name = (toZone?.name || 'Unknown').padEnd(20).slice(0, 20);
|
|
194
|
+
const dist = route.distance.toString().padStart(4);
|
|
195
|
+
const risk = (Math.round(route.baseRisk * 100) + '%').padStart(5);
|
|
196
|
+
const choke = route.chokepointRating.toFixed(1).padStart(5);
|
|
197
|
+
console.log(`│ ${name} │ ${dist} │ ${risk} │ ${choke} │`);
|
|
198
|
+
}
|
|
199
|
+
console.log(`└──────────────────────┴──────┴───────┴───────┘`);
|
|
200
|
+
console.log(`\nMulti-hop routing? Build your own pathfinder.\n`);
|
|
201
|
+
});
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// SHIP COMMAND
|
|
204
|
+
// ============================================================================
|
|
205
|
+
program
|
|
206
|
+
.command('ship')
|
|
207
|
+
.description('Create a shipment to move cargo')
|
|
208
|
+
.requiredOption('--to <zone>', 'Destination zone')
|
|
209
|
+
.requiredOption('--cargo <cargo>', 'Cargo in format "resource:amount,resource:amount"')
|
|
210
|
+
.option('--type <type>', 'Shipment type: courier, freight, convoy', 'courier')
|
|
211
|
+
.option('--via <zones>', 'Intermediate zones for multi-hop (comma-separated)')
|
|
212
|
+
.action((options) => {
|
|
213
|
+
const player = getPlayer();
|
|
214
|
+
if (!player) {
|
|
215
|
+
console.log('Join the game first: burnrate join <name>');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Parse cargo
|
|
219
|
+
const cargo = {};
|
|
220
|
+
for (const part of options.cargo.split(',')) {
|
|
221
|
+
const [resource, amount] = part.split(':');
|
|
222
|
+
cargo[resource.trim()] = parseInt(amount);
|
|
223
|
+
}
|
|
224
|
+
// Find zones
|
|
225
|
+
const allZones = db.getAllZones();
|
|
226
|
+
const fromZone = db.getZone(player.locationId);
|
|
227
|
+
const toZone = allZones.find(z => z.name.toLowerCase() === options.to.toLowerCase());
|
|
228
|
+
if (!fromZone) {
|
|
229
|
+
console.log('Could not determine your location');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!toZone) {
|
|
233
|
+
console.log(`Zone not found: ${options.to}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Build path
|
|
237
|
+
const path = [fromZone.id];
|
|
238
|
+
if (options.via) {
|
|
239
|
+
// Multi-hop: player specified waypoints
|
|
240
|
+
for (const waypoint of options.via.split(',')) {
|
|
241
|
+
const zone = allZones.find(z => z.name.toLowerCase() === waypoint.trim().toLowerCase());
|
|
242
|
+
if (!zone) {
|
|
243
|
+
console.log(`Waypoint not found: ${waypoint}`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
path.push(zone.id);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
path.push(toZone.id);
|
|
250
|
+
// Validate all legs have direct routes
|
|
251
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
252
|
+
const routes = db.getRoutesBetween(path[i], path[i + 1]);
|
|
253
|
+
if (routes.length === 0) {
|
|
254
|
+
const fromName = db.getZone(path[i])?.name || path[i];
|
|
255
|
+
const toName = db.getZone(path[i + 1])?.name || path[i + 1];
|
|
256
|
+
console.log(`\n✗ No direct route: ${fromName} → ${toName}`);
|
|
257
|
+
console.log(` Use 'burnrate route ${fromName}' to see connections\n`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const result = engine.createShipmentWithPath(player.id, options.type, path, cargo);
|
|
262
|
+
if (result.success && result.shipment) {
|
|
263
|
+
const pathNames = path.map(id => db.getZone(id)?.name.split('.').pop() || id.slice(0, 6));
|
|
264
|
+
console.log(`\nShipment #${result.shipment.id.slice(0, 8)} created`);
|
|
265
|
+
console.log(`├─ Type: ${result.shipment.type}`);
|
|
266
|
+
console.log(`├─ Route: ${pathNames.join(' → ')}`);
|
|
267
|
+
console.log(`├─ Hops: ${path.length - 1}`);
|
|
268
|
+
console.log(`└─ Status: In transit\n`);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// TRADE COMMANDS
|
|
276
|
+
// ============================================================================
|
|
277
|
+
program
|
|
278
|
+
.command('buy')
|
|
279
|
+
.description('Place a buy order')
|
|
280
|
+
.argument('<resource>', 'Resource to buy')
|
|
281
|
+
.argument('<quantity>', 'Amount to buy')
|
|
282
|
+
.option('--limit <price>', 'Max price per unit')
|
|
283
|
+
.action((resource, quantity, options) => {
|
|
284
|
+
const player = getPlayer();
|
|
285
|
+
if (!player) {
|
|
286
|
+
console.log('Join the game first: burnrate join <name>');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const price = options.limit ? parseInt(options.limit) : 100; // Default high limit
|
|
290
|
+
const qty = parseInt(quantity);
|
|
291
|
+
const result = engine.placeOrder(player.id, player.locationId, resource, 'buy', price, qty);
|
|
292
|
+
if (result.success) {
|
|
293
|
+
console.log(`\n✓ Buy order placed: ${qty} ${resource} at ${price} cr/unit\n`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
program
|
|
300
|
+
.command('sell')
|
|
301
|
+
.description('Place a sell order')
|
|
302
|
+
.argument('<resource>', 'Resource to sell')
|
|
303
|
+
.argument('<quantity>', 'Amount to sell')
|
|
304
|
+
.option('--limit <price>', 'Min price per unit')
|
|
305
|
+
.action((resource, quantity, options) => {
|
|
306
|
+
const player = getPlayer();
|
|
307
|
+
if (!player) {
|
|
308
|
+
console.log('Join the game first: burnrate join <name>');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const price = options.limit ? parseInt(options.limit) : 1; // Default low limit
|
|
312
|
+
const qty = parseInt(quantity);
|
|
313
|
+
const result = engine.placeOrder(player.id, player.locationId, resource, 'sell', price, qty);
|
|
314
|
+
if (result.success) {
|
|
315
|
+
console.log(`\n✓ Sell order placed: ${qty} ${resource} at ${price} cr/unit\n`);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// SCAN COMMAND
|
|
323
|
+
// ============================================================================
|
|
324
|
+
program
|
|
325
|
+
.command('scan')
|
|
326
|
+
.description('Gather intel on a zone or route')
|
|
327
|
+
.argument('<target>', 'Zone name or route ID')
|
|
328
|
+
.action((target) => {
|
|
329
|
+
const player = getPlayer();
|
|
330
|
+
if (!player) {
|
|
331
|
+
console.log('Join the game first: burnrate join <name>');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Try to find as zone first
|
|
335
|
+
const allZones = db.getAllZones();
|
|
336
|
+
const zone = allZones.find(z => z.name.toLowerCase() === target.toLowerCase());
|
|
337
|
+
if (zone) {
|
|
338
|
+
const result = engine.scan(player.id, 'zone', zone.id);
|
|
339
|
+
if (result.success && result.intel) {
|
|
340
|
+
console.log(`\n┌─ INTEL: ${zone.name} ───────────────────────────┐`);
|
|
341
|
+
console.log(`│ Type: ${result.intel.type}`);
|
|
342
|
+
console.log(`│ Owner: ${result.intel.owner || 'Neutral'}`);
|
|
343
|
+
console.log(`│ Supply: ${result.intel.supplyState} (${Math.round(result.intel.supplyLevel)}%)`);
|
|
344
|
+
console.log(`│ Market Activity: ${result.intel.marketActivity} orders`);
|
|
345
|
+
console.log(`└───────────────────────────────────────────────────┘\n`);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
console.log(`Zone not found: ${target}`);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
// ============================================================================
|
|
356
|
+
// SUPPLY COMMAND
|
|
357
|
+
// ============================================================================
|
|
358
|
+
program
|
|
359
|
+
.command('supply')
|
|
360
|
+
.description('Deposit Supply Units to a zone')
|
|
361
|
+
.argument('<amount>', 'Amount of SU to deposit')
|
|
362
|
+
.action((amount) => {
|
|
363
|
+
const player = getPlayer();
|
|
364
|
+
if (!player) {
|
|
365
|
+
console.log('Join the game first: burnrate join <name>');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const result = engine.depositSU(player.id, player.locationId, parseInt(amount));
|
|
369
|
+
if (result.success) {
|
|
370
|
+
const zone = db.getZone(player.locationId);
|
|
371
|
+
console.log(`\n✓ Deposited ${amount} SU to ${zone?.name}`);
|
|
372
|
+
console.log(` New stockpile: ${zone?.suStockpile} SU\n`);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// TICK COMMAND (for testing/development)
|
|
380
|
+
// ============================================================================
|
|
381
|
+
program
|
|
382
|
+
.command('tick')
|
|
383
|
+
.description('Process a game tick (dev/testing)')
|
|
384
|
+
.option('--count <n>', 'Number of ticks to process', '1')
|
|
385
|
+
.action((options) => {
|
|
386
|
+
const count = parseInt(options.count);
|
|
387
|
+
for (let i = 0; i < count; i++) {
|
|
388
|
+
const result = engine.processTick();
|
|
389
|
+
if (i === count - 1) {
|
|
390
|
+
console.log(`\nTick ${result.tick} processed. ${result.events.length} events.\n`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
// ============================================================================
|
|
395
|
+
// DEV COMMANDS
|
|
396
|
+
// ============================================================================
|
|
397
|
+
program
|
|
398
|
+
.command('dev:grant')
|
|
399
|
+
.description('Grant resources to yourself (dev/testing)')
|
|
400
|
+
.argument('<resource>', 'Resource to grant (or "starter" for a starter pack)')
|
|
401
|
+
.argument('[amount]', 'Amount to grant', '100')
|
|
402
|
+
.action((resource, amount) => {
|
|
403
|
+
const player = getPlayer();
|
|
404
|
+
if (!player) {
|
|
405
|
+
console.log('Join the game first: burnrate join <name>');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const newInventory = { ...player.inventory };
|
|
409
|
+
if (resource === 'starter') {
|
|
410
|
+
// Starter pack for testing
|
|
411
|
+
newInventory.credits += 5000;
|
|
412
|
+
newInventory.ore += 100;
|
|
413
|
+
newInventory.fuel += 100;
|
|
414
|
+
newInventory.grain += 100;
|
|
415
|
+
newInventory.fiber += 100;
|
|
416
|
+
newInventory.rations += 50;
|
|
417
|
+
newInventory.parts += 50;
|
|
418
|
+
newInventory.ammo += 50;
|
|
419
|
+
console.log('\n✓ Starter pack granted: 5000 cr + raw materials + supplies\n');
|
|
420
|
+
}
|
|
421
|
+
else if (resource === 'credits') {
|
|
422
|
+
newInventory.credits += parseInt(amount);
|
|
423
|
+
console.log(`\n✓ Granted ${amount} credits\n`);
|
|
424
|
+
}
|
|
425
|
+
else if (resource in newInventory) {
|
|
426
|
+
newInventory[resource] += parseInt(amount);
|
|
427
|
+
console.log(`\n✓ Granted ${amount} ${resource}\n`);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
console.log(`Unknown resource: ${resource}`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
db.updatePlayer(player.id, { inventory: newInventory });
|
|
434
|
+
});
|
|
435
|
+
program
|
|
436
|
+
.command('dev:move')
|
|
437
|
+
.description('Teleport to a zone (dev/testing)')
|
|
438
|
+
.argument('<zone>', 'Zone name to move to')
|
|
439
|
+
.action((zoneName) => {
|
|
440
|
+
const player = getPlayer();
|
|
441
|
+
if (!player) {
|
|
442
|
+
console.log('Join the game first: burnrate join <name>');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const allZones = db.getAllZones();
|
|
446
|
+
const zone = allZones.find(z => z.name.toLowerCase() === zoneName.toLowerCase());
|
|
447
|
+
if (!zone) {
|
|
448
|
+
console.log(`Zone not found: ${zoneName}`);
|
|
449
|
+
console.log('Available zones:', allZones.map(z => z.name).join(', '));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
db.updatePlayer(player.id, { locationId: zone.id });
|
|
453
|
+
console.log(`\n✓ Teleported to ${zone.name}\n`);
|
|
454
|
+
});
|
|
455
|
+
// ============================================================================
|
|
456
|
+
// FACTION COMMANDS
|
|
457
|
+
// ============================================================================
|
|
458
|
+
program
|
|
459
|
+
.command('faction')
|
|
460
|
+
.description('Faction management')
|
|
461
|
+
.argument('<action>', 'Action: create, info, join, leave, deposit, members')
|
|
462
|
+
.argument('[args...]', 'Action arguments')
|
|
463
|
+
.action((action, args) => {
|
|
464
|
+
const player = getPlayer();
|
|
465
|
+
if (!player) {
|
|
466
|
+
console.log('Join the game first: burnrate join <name>');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
switch (action) {
|
|
470
|
+
case 'create': {
|
|
471
|
+
if (args.length < 2) {
|
|
472
|
+
console.log('Usage: burnrate faction create <name> <tag>');
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const [name, tag] = args;
|
|
476
|
+
if (player.factionId) {
|
|
477
|
+
console.log('You are already in a faction. Leave first.');
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const faction = db.createFaction(name, tag.toUpperCase(), player.id);
|
|
481
|
+
console.log(`\n✓ Faction "${faction.name}" [${faction.tag}] created!`);
|
|
482
|
+
console.log(` You are the Founder.\n`);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
case 'info': {
|
|
486
|
+
const factionId = player.factionId || args[0];
|
|
487
|
+
if (!factionId) {
|
|
488
|
+
console.log('You are not in a faction. Specify faction ID or join one.');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const faction = db.getFaction(factionId);
|
|
492
|
+
if (!faction) {
|
|
493
|
+
console.log('Faction not found.');
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
console.log(`\n┌─ FACTION: ${faction.name} [${faction.tag}] ───────────────────────┐`);
|
|
497
|
+
console.log(`│ Members: ${faction.members.length}/50`);
|
|
498
|
+
console.log(`│ Controlled Zones: ${faction.controlledZones.length}`);
|
|
499
|
+
console.log(`│ Treasury: ${faction.treasury.credits} cr`);
|
|
500
|
+
console.log(`│ Upgrades:`);
|
|
501
|
+
console.log(`│ Relay Network: ${faction.upgrades.relayNetwork}`);
|
|
502
|
+
console.log(`│ Route Fortification: ${faction.upgrades.routeFortification}`);
|
|
503
|
+
console.log(`│ Production Bonus: ${faction.upgrades.productionBonus}`);
|
|
504
|
+
console.log(`└────────────────────────────────────────────────────────────┘\n`);
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case 'members': {
|
|
508
|
+
if (!player.factionId) {
|
|
509
|
+
console.log('You are not in a faction.');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const faction = db.getFaction(player.factionId);
|
|
513
|
+
if (!faction)
|
|
514
|
+
return;
|
|
515
|
+
console.log(`\n┌─ MEMBERS: ${faction.name} ────────────────────────────────────┐`);
|
|
516
|
+
for (const member of faction.members) {
|
|
517
|
+
const memberPlayer = db.getPlayer(member.playerId);
|
|
518
|
+
const rankIcon = member.rank === 'founder' ? '★' : member.rank === 'officer' ? '◆' : '○';
|
|
519
|
+
console.log(`│ ${rankIcon} ${memberPlayer?.name.padEnd(20)} ${member.rank.padEnd(10)} │`);
|
|
520
|
+
}
|
|
521
|
+
console.log(`└────────────────────────────────────────────────────────────┘\n`);
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case 'join': {
|
|
525
|
+
// For now, just join by faction name (in real game, would need invite)
|
|
526
|
+
if (args.length < 1) {
|
|
527
|
+
console.log('Usage: burnrate faction join <faction-name>');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (player.factionId) {
|
|
531
|
+
console.log('You are already in a faction. Leave first.');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const factions = db.getAllFactions();
|
|
535
|
+
const faction = factions.find(f => f.name.toLowerCase() === args[0].toLowerCase());
|
|
536
|
+
if (!faction) {
|
|
537
|
+
console.log('Faction not found.');
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (faction.members.length >= 50) {
|
|
541
|
+
console.log('Faction is full (50 members max).');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
db.addFactionMember(faction.id, player.id, 'member');
|
|
545
|
+
console.log(`\n✓ Joined ${faction.name} [${faction.tag}] as Member\n`);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case 'leave': {
|
|
549
|
+
if (!player.factionId) {
|
|
550
|
+
console.log('You are not in a faction.');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const faction = db.getFaction(player.factionId);
|
|
554
|
+
if (!faction)
|
|
555
|
+
return;
|
|
556
|
+
if (faction.founderId === player.id) {
|
|
557
|
+
console.log('Founders cannot leave. Transfer ownership first or disband.');
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
db.removeFactionMember(faction.id, player.id);
|
|
561
|
+
console.log(`\n✓ Left ${faction.name}\n`);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
case 'deposit': {
|
|
565
|
+
if (!player.factionId) {
|
|
566
|
+
console.log('You are not in a faction.');
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (args.length < 1) {
|
|
570
|
+
console.log('Usage: burnrate faction deposit <amount>');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const amount = parseInt(args[0]);
|
|
574
|
+
if (player.inventory.credits < amount) {
|
|
575
|
+
console.log('Insufficient credits.');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const faction = db.getFaction(player.factionId);
|
|
579
|
+
if (!faction)
|
|
580
|
+
return;
|
|
581
|
+
// Update player
|
|
582
|
+
const newPlayerInv = { ...player.inventory };
|
|
583
|
+
newPlayerInv.credits -= amount;
|
|
584
|
+
db.updatePlayer(player.id, { inventory: newPlayerInv });
|
|
585
|
+
// Update faction treasury (need to add this to db)
|
|
586
|
+
console.log(`\n✓ Deposited ${amount} cr to ${faction.name} treasury\n`);
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
case 'intel': {
|
|
590
|
+
if (!player.factionId) {
|
|
591
|
+
console.log('You are not in a faction.');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const faction = db.getFaction(player.factionId);
|
|
595
|
+
if (!faction)
|
|
596
|
+
return;
|
|
597
|
+
const limit = args[0] ? parseInt(args[0]) : 20;
|
|
598
|
+
const intelReports = db.getFactionIntel(player.factionId, limit);
|
|
599
|
+
const tick = db.getCurrentTick();
|
|
600
|
+
console.log(`\n┌─ FACTION INTEL: ${faction.name} [${faction.tag}] ─────────────────────┐`);
|
|
601
|
+
if (intelReports.length === 0) {
|
|
602
|
+
console.log(`│ No intel gathered. Use 'burnrate scan <zone>' to gather. │`);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
console.log(`│ Target │ Age │ Quality │ Key Info │`);
|
|
606
|
+
console.log(`├──────────────────────┼──────┼─────────┼─────────────────────┤`);
|
|
607
|
+
for (const report of intelReports) {
|
|
608
|
+
const age = tick - report.gatheredAt;
|
|
609
|
+
const ageStr = `${age}t`.padStart(4);
|
|
610
|
+
const qualityStr = `${report.signalQuality}%`.padStart(4);
|
|
611
|
+
let targetName = report.targetId.slice(0, 18);
|
|
612
|
+
let keyInfo = '';
|
|
613
|
+
if (report.targetType === 'zone') {
|
|
614
|
+
const zone = db.getZone(report.targetId);
|
|
615
|
+
targetName = (zone?.name || report.targetId).slice(0, 18).padEnd(18);
|
|
616
|
+
const data = report.data;
|
|
617
|
+
keyInfo = `${data.supplyState || '?'} ${data.suStockpile || 0}SU`;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
targetName = 'Route'.padEnd(18);
|
|
621
|
+
const data = report.data;
|
|
622
|
+
keyInfo = data.raiderPresence ? `Raiders! str:${data.raiderStrength}` : 'Clear';
|
|
623
|
+
}
|
|
624
|
+
console.log(`│ ${targetName.padEnd(20)} │ ${ageStr} │ ${qualityStr} │ ${keyInfo.padEnd(19)} │`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
console.log(`└──────────────────────┴──────┴─────────┴─────────────────────┘`);
|
|
628
|
+
console.log(`\nIntel decays over time. Fresh scans yield better data.\n`);
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
default:
|
|
632
|
+
console.log('Unknown faction action. Try: create, info, join, leave, deposit, members, intel');
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
// ============================================================================
|
|
636
|
+
// CONTRACT COMMANDS
|
|
637
|
+
// ============================================================================
|
|
638
|
+
program
|
|
639
|
+
.command('contracts')
|
|
640
|
+
.description('View and manage contracts')
|
|
641
|
+
.argument('[action]', 'Action: list, post, accept, complete')
|
|
642
|
+
.argument('[args...]', 'Action arguments')
|
|
643
|
+
.action((action, args) => {
|
|
644
|
+
const player = getPlayer();
|
|
645
|
+
if (!player) {
|
|
646
|
+
console.log('Join the game first: burnrate join <name>');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const tick = db.getCurrentTick();
|
|
650
|
+
if (!action || action === 'list') {
|
|
651
|
+
// List available contracts
|
|
652
|
+
const contracts = db.getOpenContracts();
|
|
653
|
+
console.log(`\n┌─ AVAILABLE CONTRACTS ────────────────────────────────────────┐`);
|
|
654
|
+
if (contracts.length === 0) {
|
|
655
|
+
console.log(`│ No open contracts │`);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
for (const c of contracts.slice(0, 10)) {
|
|
659
|
+
const deadline = c.deadline - tick;
|
|
660
|
+
const typeStr = c.type.toUpperCase().padEnd(8);
|
|
661
|
+
const rewardStr = `${c.reward.credits} cr`.padEnd(10);
|
|
662
|
+
console.log(`│ ${c.id.slice(0, 8)} ${typeStr} ${rewardStr} ${deadline}t left │`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
console.log(`└──────────────────────────────────────────────────────────────┘\n`);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
switch (action) {
|
|
669
|
+
case 'post': {
|
|
670
|
+
// Post a new haul contract
|
|
671
|
+
if (args.length < 5) {
|
|
672
|
+
console.log('Usage: burnrate contracts post <type> <from> <to> <resource:qty> <reward>');
|
|
673
|
+
console.log('Example: burnrate contracts post haul Hub.Central Factory.North fuel:20 500');
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const [type, from, to, cargoStr, rewardStr] = args;
|
|
677
|
+
if (type !== 'haul') {
|
|
678
|
+
console.log('Currently only "haul" contracts are supported.');
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const allZones = db.getAllZones();
|
|
682
|
+
const fromZone = allZones.find(z => z.name.toLowerCase() === from.toLowerCase());
|
|
683
|
+
const toZone = allZones.find(z => z.name.toLowerCase() === to.toLowerCase());
|
|
684
|
+
if (!fromZone || !toZone) {
|
|
685
|
+
console.log('Zone not found.');
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const [resource, qtyStr] = cargoStr.split(':');
|
|
689
|
+
const qty = parseInt(qtyStr);
|
|
690
|
+
const reward = parseInt(rewardStr);
|
|
691
|
+
if (player.inventory.credits < reward) {
|
|
692
|
+
console.log('Insufficient credits to fund contract reward.');
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
// Deduct reward from player
|
|
696
|
+
const newInv = { ...player.inventory };
|
|
697
|
+
newInv.credits -= reward;
|
|
698
|
+
db.updatePlayer(player.id, { inventory: newInv });
|
|
699
|
+
const contract = db.createContract({
|
|
700
|
+
type: 'haul',
|
|
701
|
+
posterId: player.id,
|
|
702
|
+
posterType: 'player',
|
|
703
|
+
acceptedBy: null,
|
|
704
|
+
details: {
|
|
705
|
+
fromZoneId: fromZone.id,
|
|
706
|
+
toZoneId: toZone.id,
|
|
707
|
+
resource: resource,
|
|
708
|
+
quantity: qty
|
|
709
|
+
},
|
|
710
|
+
deadline: tick + 100,
|
|
711
|
+
reward: { credits: reward, reputation: 10 },
|
|
712
|
+
status: 'open',
|
|
713
|
+
createdAt: tick
|
|
714
|
+
});
|
|
715
|
+
console.log(`\n✓ Contract posted: ${contract.id.slice(0, 8)}`);
|
|
716
|
+
console.log(` Haul ${qty} ${resource} from ${fromZone.name} to ${toZone.name}`);
|
|
717
|
+
console.log(` Reward: ${reward} cr Deadline: 100 ticks\n`);
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
case 'accept': {
|
|
721
|
+
if (args.length < 1) {
|
|
722
|
+
console.log('Usage: burnrate contracts accept <contract-id>');
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const contracts = db.getOpenContracts();
|
|
726
|
+
const contract = contracts.find(c => c.id.startsWith(args[0]));
|
|
727
|
+
if (!contract) {
|
|
728
|
+
console.log('Contract not found or already taken.');
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
db.updateContract(contract.id, { status: 'active', acceptedBy: player.id });
|
|
732
|
+
console.log(`\n✓ Contract ${contract.id.slice(0, 8)} accepted\n`);
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
default:
|
|
736
|
+
console.log('Unknown action. Try: list, post, accept');
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
// ============================================================================
|
|
740
|
+
// PRODUCE COMMAND
|
|
741
|
+
// ============================================================================
|
|
742
|
+
program
|
|
743
|
+
.command('produce')
|
|
744
|
+
.description('Convert resources at a Factory')
|
|
745
|
+
.argument('<output>', 'What to produce: metal, chemicals, rations, textiles, ammo, medkits, parts, comms, escort, raider')
|
|
746
|
+
.argument('<quantity>', 'How many units to produce')
|
|
747
|
+
.action((output, quantity) => {
|
|
748
|
+
const player = getPlayer();
|
|
749
|
+
if (!player) {
|
|
750
|
+
console.log('Join the game first: burnrate join <name>');
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const result = engine.produce(player.id, output, parseInt(quantity));
|
|
754
|
+
if (result.success) {
|
|
755
|
+
if (result.units && result.units.length > 0) {
|
|
756
|
+
// Unit production
|
|
757
|
+
const unit = result.units[0];
|
|
758
|
+
console.log(`\n✓ Produced ${result.produced} ${output} unit(s)`);
|
|
759
|
+
console.log(` Strength: ${unit.strength} Speed: ${unit.speed} Maintenance: ${unit.maintenance} cr/tick`);
|
|
760
|
+
console.log(` Use 'burnrate units' to view your units\n`);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
// Resource production
|
|
764
|
+
console.log(`\n✓ Produced ${result.produced} ${output}\n`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
// ============================================================================
|
|
772
|
+
// EXTRACT COMMAND
|
|
773
|
+
// ============================================================================
|
|
774
|
+
program
|
|
775
|
+
.command('extract')
|
|
776
|
+
.description('Extract raw resources from a Field')
|
|
777
|
+
.argument('<quantity>', 'How many units to extract')
|
|
778
|
+
.action((quantity) => {
|
|
779
|
+
const player = getPlayer();
|
|
780
|
+
if (!player) {
|
|
781
|
+
console.log('Join the game first: burnrate join <name>');
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const result = engine.extract(player.id, parseInt(quantity));
|
|
785
|
+
if (result.success && result.extracted) {
|
|
786
|
+
console.log(`\n✓ Extracted ${result.extracted.amount} ${result.extracted.resource}\n`);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
// ============================================================================
|
|
793
|
+
// CAPTURE COMMAND
|
|
794
|
+
// ============================================================================
|
|
795
|
+
program
|
|
796
|
+
.command('capture')
|
|
797
|
+
.description('Capture a neutral or collapsed zone for your faction')
|
|
798
|
+
.action(() => {
|
|
799
|
+
const player = getPlayer();
|
|
800
|
+
if (!player) {
|
|
801
|
+
console.log('Join the game first: burnrate join <name>');
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const zone = db.getZone(player.locationId);
|
|
805
|
+
if (!zone) {
|
|
806
|
+
console.log('Zone not found');
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const result = engine.captureZone(player.id, player.locationId);
|
|
810
|
+
if (result.success) {
|
|
811
|
+
const faction = player.factionId ? db.getFaction(player.factionId) : null;
|
|
812
|
+
console.log(`\n✓ ${zone.name} captured for ${faction?.name || 'your faction'}!\n`);
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
// ============================================================================
|
|
819
|
+
// UNITS COMMAND
|
|
820
|
+
// ============================================================================
|
|
821
|
+
program
|
|
822
|
+
.command('units')
|
|
823
|
+
.description('View and manage your combat units')
|
|
824
|
+
.argument('[action]', 'Action: list, escort, raider')
|
|
825
|
+
.argument('[args...]', 'Action arguments')
|
|
826
|
+
.action((action, args) => {
|
|
827
|
+
const player = getPlayer();
|
|
828
|
+
if (!player) {
|
|
829
|
+
console.log('Join the game first: burnrate join <name>');
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const units = db.getPlayerUnits(player.id);
|
|
833
|
+
if (!action || action === 'list') {
|
|
834
|
+
console.log(`\n┌─ YOUR UNITS ─────────────────────────────────────────────┐`);
|
|
835
|
+
if (units.length === 0) {
|
|
836
|
+
console.log(`│ No units. Hire at Hubs: burnrate hire escort │`);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
for (const u of units) {
|
|
840
|
+
const location = db.getZone(u.locationId);
|
|
841
|
+
const assignStr = u.assignmentId ? `→ ${u.assignmentId.slice(0, 8)}` : 'idle';
|
|
842
|
+
console.log(`│ ${u.id.slice(0, 8)} ${u.type.padEnd(8)} str:${u.strength.toString().padEnd(3)} ${assignStr.padEnd(14)} ${location?.name.slice(0, 15) || ''} │`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
console.log(`└───────────────────────────────────────────────────────────┘\n`);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
switch (action) {
|
|
849
|
+
case 'escort': {
|
|
850
|
+
// Assign escort to shipment
|
|
851
|
+
if (args.length < 2) {
|
|
852
|
+
console.log('Usage: burnrate units escort <unit-id> <shipment-id>');
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const [unitId, shipmentId] = args;
|
|
856
|
+
const unit = units.find(u => u.id.startsWith(unitId));
|
|
857
|
+
if (!unit) {
|
|
858
|
+
console.log('Unit not found. Use "burnrate units" to list your units.');
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const shipments = db.getPlayerShipments(player.id);
|
|
862
|
+
const shipment = shipments.find(s => s.id.startsWith(shipmentId));
|
|
863
|
+
if (!shipment) {
|
|
864
|
+
console.log('Shipment not found. Use "burnrate view shipments" to list.');
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const result = engine.assignEscort(player.id, unit.id, shipment.id);
|
|
868
|
+
if (result.success) {
|
|
869
|
+
console.log(`\n✓ Escort ${unit.id.slice(0, 8)} assigned to shipment ${shipment.id.slice(0, 8)}\n`);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
873
|
+
}
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
case 'raider': {
|
|
877
|
+
// Deploy raider on route
|
|
878
|
+
if (args.length < 2) {
|
|
879
|
+
console.log('Usage: burnrate units raider <unit-id> <from-zone> <to-zone>');
|
|
880
|
+
console.log('Example: burnrate units raider abc123 Hub.Central Factory.North');
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const [unitId, ...routeArgs] = args;
|
|
884
|
+
const unit = units.find(u => u.id.startsWith(unitId));
|
|
885
|
+
if (!unit) {
|
|
886
|
+
console.log('Unit not found. Use "burnrate units" to list your units.');
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
// Find route between zones
|
|
890
|
+
if (routeArgs.length < 2) {
|
|
891
|
+
console.log('Specify both from and to zones.');
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const allZones = db.getAllZones();
|
|
895
|
+
const fromZone = allZones.find(z => z.name.toLowerCase() === routeArgs[0].toLowerCase());
|
|
896
|
+
const toZone = allZones.find(z => z.name.toLowerCase() === routeArgs[1].toLowerCase());
|
|
897
|
+
if (!fromZone || !toZone) {
|
|
898
|
+
console.log('Zone not found.');
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const routes = db.getRoutesBetween(fromZone.id, toZone.id);
|
|
902
|
+
if (routes.length === 0) {
|
|
903
|
+
console.log('No route between those zones.');
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const result = engine.deployRaider(player.id, unit.id, routes[0].id);
|
|
907
|
+
if (result.success) {
|
|
908
|
+
console.log(`\n✓ Raider ${unit.id.slice(0, 8)} deployed on ${fromZone.name} → ${toZone.name}\n`);
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
912
|
+
}
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
case 'sell': {
|
|
916
|
+
// List unit for sale
|
|
917
|
+
if (args.length < 2) {
|
|
918
|
+
console.log('Usage: burnrate units sell <unit-id> <price>');
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const [sellUnitId, priceStr] = args;
|
|
922
|
+
const unitToSell = units.find(u => u.id.startsWith(sellUnitId));
|
|
923
|
+
if (!unitToSell) {
|
|
924
|
+
console.log('Unit not found. Use "burnrate units" to list your units.');
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const price = parseInt(priceStr);
|
|
928
|
+
if (isNaN(price) || price < 1) {
|
|
929
|
+
console.log('Price must be a positive number.');
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const sellResult = engine.listUnitForSale(player.id, unitToSell.id, price);
|
|
933
|
+
if (sellResult.success) {
|
|
934
|
+
console.log(`\n✓ ${unitToSell.type} ${unitToSell.id.slice(0, 8)} listed for ${price} cr`);
|
|
935
|
+
console.log(` Location: ${db.getZone(unitToSell.locationId)?.name}\n`);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
console.log(`\n✗ ${sellResult.error}\n`);
|
|
939
|
+
}
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
case 'unsell': {
|
|
943
|
+
// Remove unit from sale
|
|
944
|
+
if (args.length < 1) {
|
|
945
|
+
console.log('Usage: burnrate units unsell <unit-id>');
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const unitToUnsell = units.find(u => u.id.startsWith(args[0]));
|
|
949
|
+
if (!unitToUnsell) {
|
|
950
|
+
console.log('Unit not found.');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const unsellResult = engine.unlistUnit(player.id, unitToUnsell.id);
|
|
954
|
+
if (unsellResult.success) {
|
|
955
|
+
console.log(`\n✓ ${unitToUnsell.type} ${unitToUnsell.id.slice(0, 8)} removed from sale\n`);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
console.log(`\n✗ ${unsellResult.error}\n`);
|
|
959
|
+
}
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
default:
|
|
963
|
+
console.log('Unknown action. Try: list, escort, raider, sell, unsell');
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
// ============================================================================
|
|
967
|
+
// HIRE COMMAND (buy units from marketplace)
|
|
968
|
+
// ============================================================================
|
|
969
|
+
program
|
|
970
|
+
.command('hire')
|
|
971
|
+
.description('Hire (buy) a unit from the marketplace at your current Hub')
|
|
972
|
+
.argument('[unit-id]', 'Unit ID to hire (omit to list available units)')
|
|
973
|
+
.action((unitId) => {
|
|
974
|
+
const player = getPlayer();
|
|
975
|
+
if (!player) {
|
|
976
|
+
console.log('Join the game first: burnrate join <name>');
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const zone = db.getZone(player.locationId);
|
|
980
|
+
if (!zone || zone.type !== 'hub') {
|
|
981
|
+
console.log('\nUnits can only be hired at Hubs. Travel to a Hub first.\n');
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const unitsForSale = db.getUnitsForSaleAtZone(player.locationId);
|
|
985
|
+
if (!unitId) {
|
|
986
|
+
// List available units
|
|
987
|
+
console.log(`\n┌─ UNITS FOR HIRE: ${zone.name} ────────────────────────────────┐`);
|
|
988
|
+
if (unitsForSale.length === 0) {
|
|
989
|
+
console.log(`│ No units for sale. Players must produce and list units. │`);
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
console.log(`│ ID │ Type │ Str │ Spd │ Maint │ Price │`);
|
|
993
|
+
console.log(`├───────────┼─────────┼─────┼─────┼───────┼───────────┤`);
|
|
994
|
+
for (const u of unitsForSale) {
|
|
995
|
+
const seller = db.getPlayer(u.playerId);
|
|
996
|
+
const sellerName = seller?.name.slice(0, 10) || 'Unknown';
|
|
997
|
+
console.log(`│ ${u.id.slice(0, 8)} │ ${u.type.padEnd(7)} │ ${u.strength.toString().padStart(3)} │ ${u.speed.toString().padStart(3)} │ ${(u.maintenance + '/t').padStart(5)} │ ${(u.forSalePrice + ' cr').padStart(9)} │`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
console.log(`└───────────┴─────────┴─────┴─────┴───────┴───────────┘`);
|
|
1001
|
+
console.log(`\nTo hire: burnrate hire <unit-id>\n`);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
// Find and hire unit
|
|
1005
|
+
const unit = unitsForSale.find(u => u.id.startsWith(unitId));
|
|
1006
|
+
if (!unit) {
|
|
1007
|
+
console.log(`\nUnit not found or not for sale. Use 'burnrate hire' to list available units.\n`);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const result = engine.hireUnit(player.id, unit.id);
|
|
1011
|
+
if (result.success && result.unit) {
|
|
1012
|
+
console.log(`\n✓ Hired ${result.unit.type} for ${unit.forSalePrice} cr`);
|
|
1013
|
+
console.log(` Strength: ${result.unit.strength} Maintenance: ${result.unit.maintenance} cr/tick`);
|
|
1014
|
+
console.log(` Use 'burnrate units' to view your units\n`);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
console.log(`\n✗ ${result.error}\n`);
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
// ============================================================================
|
|
1021
|
+
// STATUS COMMAND
|
|
1022
|
+
// ============================================================================
|
|
1023
|
+
program
|
|
1024
|
+
.command('status')
|
|
1025
|
+
.description('Show your current status')
|
|
1026
|
+
.action(() => {
|
|
1027
|
+
const player = getPlayer();
|
|
1028
|
+
if (!player) {
|
|
1029
|
+
console.log('Join the game first: burnrate join <name>');
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const zone = db.getZone(player.locationId);
|
|
1033
|
+
const tick = db.getCurrentTick();
|
|
1034
|
+
const limits = TIER_LIMITS[player.tier];
|
|
1035
|
+
console.log(`\n╔═══════════════════════════════════════════════════════╗`);
|
|
1036
|
+
console.log(`║ ${player.name.padEnd(20)} │ ${player.tier.toUpperCase().padEnd(10)} │ Tick ${tick.toString().padStart(6)} ║`);
|
|
1037
|
+
console.log(`╠═══════════════════════════════════════════════════════╣`);
|
|
1038
|
+
console.log(`║ Location: ${(zone?.name || 'Unknown').padEnd(43)} ║`);
|
|
1039
|
+
console.log(`║ Credits: ${player.inventory.credits.toString().padEnd(44)} ║`);
|
|
1040
|
+
console.log(`║ Reputation: ${player.reputation.toString().padEnd(41)} ║`);
|
|
1041
|
+
console.log(`║ Actions: ${player.actionsToday}/${limits.dailyActions} today`.padEnd(56) + `║`);
|
|
1042
|
+
console.log(`╠═══════════════════════════════════════════════════════╣`);
|
|
1043
|
+
console.log(`║ INVENTORY ║`);
|
|
1044
|
+
const inv = player.inventory;
|
|
1045
|
+
console.log(`║ ore: ${inv.ore.toString().padEnd(6)} fuel: ${inv.fuel.toString().padEnd(6)} grain: ${inv.grain.toString().padEnd(6)} fiber: ${inv.fiber.toString().padEnd(4)} ║`);
|
|
1046
|
+
console.log(`║ metal: ${inv.metal.toString().padEnd(4)} chem: ${inv.chemicals.toString().padEnd(6)} rations: ${inv.rations.toString().padEnd(4)} text: ${inv.textiles.toString().padEnd(5)} ║`);
|
|
1047
|
+
console.log(`║ ammo: ${inv.ammo.toString().padEnd(5)} med: ${inv.medkits.toString().padEnd(7)} parts: ${inv.parts.toString().padEnd(6)} comms: ${inv.comms.toString().padEnd(4)} ║`);
|
|
1048
|
+
console.log(`╚═══════════════════════════════════════════════════════╝\n`);
|
|
1049
|
+
});
|
|
1050
|
+
// ============================================================================
|
|
1051
|
+
// SERVER COMMAND
|
|
1052
|
+
// ============================================================================
|
|
1053
|
+
program
|
|
1054
|
+
.command('server')
|
|
1055
|
+
.description('Manage the tick server')
|
|
1056
|
+
.argument('<action>', 'Action: status, start, stop')
|
|
1057
|
+
.option('--interval <ms>', 'Tick interval in milliseconds (start only)', '600000')
|
|
1058
|
+
.action((action, options) => {
|
|
1059
|
+
const statusPath = path.join(DB_DIR, 'server-status.json');
|
|
1060
|
+
const pidPath = path.join(DB_DIR, 'server.pid');
|
|
1061
|
+
switch (action) {
|
|
1062
|
+
case 'status': {
|
|
1063
|
+
if (!fs.existsSync(statusPath)) {
|
|
1064
|
+
console.log('\nServer status: Not running\n');
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
const status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
|
|
1069
|
+
const lastTick = new Date(status.lastTickTime);
|
|
1070
|
+
const elapsed = Math.floor((Date.now() - lastTick.getTime()) / 1000);
|
|
1071
|
+
console.log(`\n┌─ TICK SERVER STATUS ────────────────────────────────────┐`);
|
|
1072
|
+
console.log(`│ Status: ${status.running ? 'RUNNING' : 'STOPPED'}`.padEnd(58) + `│`);
|
|
1073
|
+
console.log(`│ PID: ${status.pid}`.padEnd(58) + `│`);
|
|
1074
|
+
console.log(`│ Current Tick: ${status.currentTick}`.padEnd(58) + `│`);
|
|
1075
|
+
console.log(`│ Ticks Processed: ${status.tickCount}`.padEnd(58) + `│`);
|
|
1076
|
+
console.log(`│ Tick Interval: ${status.intervalMs / 60000} minutes`.padEnd(58) + `│`);
|
|
1077
|
+
console.log(`│ Last Tick: ${elapsed}s ago`.padEnd(58) + `│`);
|
|
1078
|
+
console.log(`└──────────────────────────────────────────────────────────┘\n`);
|
|
1079
|
+
}
|
|
1080
|
+
catch {
|
|
1081
|
+
console.log('\nServer status: Unknown (corrupted status file)\n');
|
|
1082
|
+
}
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
case 'start': {
|
|
1086
|
+
console.log('\nTo start the tick server, run in a separate terminal:');
|
|
1087
|
+
console.log(` BURNRATE_TICK_INTERVAL=${options.interval} node dist/server/tick-server.js`);
|
|
1088
|
+
console.log('\nOr for testing (1 tick per second):');
|
|
1089
|
+
console.log(' BURNRATE_TICK_INTERVAL=1000 node dist/server/tick-server.js\n');
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
case 'stop': {
|
|
1093
|
+
if (!fs.existsSync(statusPath)) {
|
|
1094
|
+
console.log('\nServer is not running\n');
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
const status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
|
|
1099
|
+
console.log(`\nTo stop the server, kill process ${status.pid}:`);
|
|
1100
|
+
console.log(` kill ${status.pid}\n`);
|
|
1101
|
+
}
|
|
1102
|
+
catch {
|
|
1103
|
+
console.log('\nCould not read server status\n');
|
|
1104
|
+
}
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
default:
|
|
1108
|
+
console.log('Unknown action. Try: status, start, stop');
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
// ============================================================================
|
|
1112
|
+
// HELP COMMAND
|
|
1113
|
+
// ============================================================================
|
|
1114
|
+
program
|
|
1115
|
+
.command('help')
|
|
1116
|
+
.description('Show detailed help')
|
|
1117
|
+
.action(() => {
|
|
1118
|
+
console.log(formatHelp());
|
|
1119
|
+
});
|
|
1120
|
+
// Parse and execute
|
|
1121
|
+
program.parse();
|