clawcity 2.3.1 → 2.5.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/README.md +37 -2
- package/dist/commands/craft.js +17 -2
- package/dist/commands/gather.js +6 -1
- package/dist/commands/guide.js +16 -2
- package/dist/commands/install.js +112 -12
- package/dist/commands/move.js +87 -10
- package/dist/commands/planning.d.ts +2 -0
- package/dist/commands/planning.js +415 -0
- package/dist/commands/scan.js +43 -3
- package/dist/commands/stats.js +1 -1
- package/dist/commands/territory.js +152 -24
- package/dist/commands/world.js +4 -1
- package/dist/index.js +25 -0
- package/dist/lib/api.d.ts +3 -0
- package/dist/lib/api.js +79 -1
- package/dist/lib/endpoints.js +2 -2
- package/dist/lib/formatters.js +53 -3
- package/package.json +1 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { api, handleError } from '../lib/api.js';
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
4
|
+
? value
|
|
5
|
+
: null;
|
|
6
|
+
}
|
|
7
|
+
function asRecordArray(value) {
|
|
8
|
+
return Array.isArray(value)
|
|
9
|
+
? value.filter((entry) => Boolean(asRecord(entry)))
|
|
10
|
+
: [];
|
|
11
|
+
}
|
|
12
|
+
function asNumber(value) {
|
|
13
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
14
|
+
}
|
|
15
|
+
function asString(value) {
|
|
16
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
17
|
+
}
|
|
18
|
+
function asBoolean(value) {
|
|
19
|
+
return value === true;
|
|
20
|
+
}
|
|
21
|
+
function parseTarget(rawTarget) {
|
|
22
|
+
return rawTarget.trim().toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
function formatResourceCost(cost) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
const gold = asNumber(cost.gold);
|
|
27
|
+
const wood = asNumber(cost.wood);
|
|
28
|
+
const food = asNumber(cost.food);
|
|
29
|
+
const stone = asNumber(cost.stone);
|
|
30
|
+
const foodClaimCost = asNumber(cost.food_claim_cost);
|
|
31
|
+
const staminaCost = asNumber(cost.stamina_cost);
|
|
32
|
+
const foodTotal = asNumber(cost.food_total);
|
|
33
|
+
if (gold !== null && gold > 0)
|
|
34
|
+
parts.push(`${gold} gold`);
|
|
35
|
+
if (wood !== null && wood > 0)
|
|
36
|
+
parts.push(`${wood} wood`);
|
|
37
|
+
if (stone !== null && stone > 0)
|
|
38
|
+
parts.push(`${stone} stone`);
|
|
39
|
+
if (food !== null && food > 0)
|
|
40
|
+
parts.push(`${food} food`);
|
|
41
|
+
if (foodClaimCost !== null)
|
|
42
|
+
parts.push(`${foodClaimCost} food (claim cost)`);
|
|
43
|
+
if (staminaCost !== null)
|
|
44
|
+
parts.push(`${staminaCost} food (stamina cost)`);
|
|
45
|
+
if (foodTotal !== null)
|
|
46
|
+
parts.push(`${foodTotal} food (total)`);
|
|
47
|
+
return parts.length > 0 ? parts.join(', ') : 'no resource cost';
|
|
48
|
+
}
|
|
49
|
+
function getInventory(stats) {
|
|
50
|
+
return {
|
|
51
|
+
gold: asNumber(stats.gold) || 0,
|
|
52
|
+
wood: asNumber(stats.wood) || 0,
|
|
53
|
+
food: asNumber(stats.food) || 0,
|
|
54
|
+
stone: asNumber(stats.stone) || 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function buildRequirements(inventory, cost) {
|
|
58
|
+
const toRequirement = (need, have) => ({
|
|
59
|
+
need,
|
|
60
|
+
have,
|
|
61
|
+
missing: Math.max(0, need - have),
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
gold: toRequirement(asNumber(cost.gold) || 0, inventory.gold),
|
|
65
|
+
wood: toRequirement(asNumber(cost.wood) || 0, inventory.wood),
|
|
66
|
+
food: toRequirement(asNumber(cost.food) || 0, inventory.food),
|
|
67
|
+
stone: toRequirement(asNumber(cost.stone) || 0, inventory.stone),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function missingRequirementLines(requirements) {
|
|
71
|
+
return Object.entries(requirements)
|
|
72
|
+
.filter(([, requirement]) => requirement.missing > 0)
|
|
73
|
+
.map(([resource, requirement]) => (`${resource} +${requirement.missing} (need ${requirement.need}, have ${requirement.have})`));
|
|
74
|
+
}
|
|
75
|
+
function printReasons(reasons) {
|
|
76
|
+
if (!Array.isArray(reasons))
|
|
77
|
+
return;
|
|
78
|
+
const parsedReasons = reasons.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
79
|
+
if (parsedReasons.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
console.log(`Blocked by: ${parsedReasons.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
export function registerPlanningCommands(program) {
|
|
84
|
+
program
|
|
85
|
+
.command('cost <target>')
|
|
86
|
+
.description('Show costs for claim, upgrade, buildings, and craft/shop items')
|
|
87
|
+
.option('--json', 'Print raw JSON response')
|
|
88
|
+
.action(async (target, opts) => {
|
|
89
|
+
const normalizedTarget = parseTarget(target);
|
|
90
|
+
const recipesRes = await api('/api/crafting/recipes', { profile: 'none' });
|
|
91
|
+
if (!recipesRes.ok)
|
|
92
|
+
handleError(recipesRes);
|
|
93
|
+
const data = recipesRes.data;
|
|
94
|
+
const info = asRecord(data.info) || {};
|
|
95
|
+
const costs = asRecord(info.costs) || {};
|
|
96
|
+
const claimCost = asRecord(costs.claim);
|
|
97
|
+
const upgradeCost = asRecord(costs.upgrade);
|
|
98
|
+
const buildingCosts = asRecord(costs.buildings) || {};
|
|
99
|
+
const craftable = asRecordArray(data.craftable);
|
|
100
|
+
const shop = asRecordArray(data.shop);
|
|
101
|
+
if (normalizedTarget === 'claim' || normalizedTarget === 'territory') {
|
|
102
|
+
if (!claimCost) {
|
|
103
|
+
console.error('Error: claim cost metadata unavailable');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
if (opts.json) {
|
|
107
|
+
console.log(JSON.stringify(claimCost, null, 2));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const baseCost = asRecord(claimCost.base_cost) || {};
|
|
111
|
+
const discounts = asRecord(claimCost.discounts) || {};
|
|
112
|
+
console.log(`Claim cost (base): ${formatResourceCost({
|
|
113
|
+
gold: baseCost.gold,
|
|
114
|
+
wood: baseCost.wood,
|
|
115
|
+
stone: baseCost.stone,
|
|
116
|
+
food_claim_cost: baseCost.food_claim_cost,
|
|
117
|
+
stamina_cost: baseCost.stamina_cost,
|
|
118
|
+
food_total: baseCost.food_total,
|
|
119
|
+
})}`);
|
|
120
|
+
const firstClaimDiscount = asNumber(discounts.first_claim_percent);
|
|
121
|
+
const deedDiscount = asNumber(discounts.territory_deed_percent);
|
|
122
|
+
console.log(`Discounts: first claim ${firstClaimDiscount ?? 0}%, territory deed ${deedDiscount ?? 0}%`);
|
|
123
|
+
const note = asString(discounts.first_claim_note);
|
|
124
|
+
if (note) {
|
|
125
|
+
console.log(`Note: ${note}`);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (normalizedTarget === 'upgrade' || normalizedTarget.startsWith('upgrade')) {
|
|
130
|
+
const levels = asRecordArray(upgradeCost?.levels);
|
|
131
|
+
if (levels.length === 0) {
|
|
132
|
+
console.error('Error: upgrade cost metadata unavailable');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
let selectedLevels = levels;
|
|
136
|
+
const levelMatch = normalizedTarget.match(/^upgrade[:_-]?(\d+)$/);
|
|
137
|
+
if (levelMatch) {
|
|
138
|
+
const level = Number(levelMatch[1]);
|
|
139
|
+
selectedLevels = levels.filter((entry) => asNumber(entry.level) === level);
|
|
140
|
+
if (selectedLevels.length === 0) {
|
|
141
|
+
console.error(`Error: Unknown upgrade level ${level}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (opts.json) {
|
|
146
|
+
console.log(JSON.stringify({
|
|
147
|
+
max_level: asNumber(upgradeCost?.max_level),
|
|
148
|
+
levels: selectedLevels,
|
|
149
|
+
}, null, 2));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const level of selectedLevels) {
|
|
153
|
+
const levelNumber = asNumber(level.level) || '?';
|
|
154
|
+
const levelCost = asRecord(level.cost) || {};
|
|
155
|
+
const bonusPercent = asNumber(level.territory_gather_bonus_percent);
|
|
156
|
+
console.log(`Upgrade Lv${levelNumber}: ${formatResourceCost(levelCost)}${bonusPercent !== null ? ` | bonus +${bonusPercent}%` : ''}`);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const building = asRecord(buildingCosts[normalizedTarget]);
|
|
161
|
+
if (building) {
|
|
162
|
+
if (opts.json) {
|
|
163
|
+
console.log(JSON.stringify(building, null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const name = asString(building.name) || normalizedTarget;
|
|
167
|
+
const buildCost = asRecord(building.build_cost) || {};
|
|
168
|
+
const upkeep = asRecord(building.hourly_upkeep) || {};
|
|
169
|
+
console.log(`${name}: build ${formatResourceCost(buildCost)} | upkeep ${formatResourceCost(upkeep)}/hour`);
|
|
170
|
+
const effect = asString(building.effect_description);
|
|
171
|
+
if (effect) {
|
|
172
|
+
console.log(`Effect: ${effect}`);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const craftItem = craftable.find((entry) => asString(entry.id)?.toLowerCase() === normalizedTarget);
|
|
177
|
+
if (craftItem) {
|
|
178
|
+
const recipe = asRecord(craftItem.recipe) || {};
|
|
179
|
+
if (opts.json) {
|
|
180
|
+
console.log(JSON.stringify({
|
|
181
|
+
type: 'craft',
|
|
182
|
+
id: asString(craftItem.id),
|
|
183
|
+
name: asString(craftItem.name),
|
|
184
|
+
recipe,
|
|
185
|
+
requires_workshop: craftItem.requires_workshop === true,
|
|
186
|
+
}, null, 2));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log(`${asString(craftItem.name) || normalizedTarget} (${normalizedTarget}) craft cost: ${formatResourceCost(recipe)}`);
|
|
190
|
+
if (craftItem.requires_workshop === true) {
|
|
191
|
+
console.log('Requires workshop: yes');
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const shopItem = shop.find((entry) => asString(entry.id)?.toLowerCase() === normalizedTarget);
|
|
196
|
+
if (shopItem) {
|
|
197
|
+
const price = asNumber(shopItem.price);
|
|
198
|
+
const payload = {
|
|
199
|
+
gold: price || 0,
|
|
200
|
+
};
|
|
201
|
+
if (opts.json) {
|
|
202
|
+
console.log(JSON.stringify({
|
|
203
|
+
type: 'shop',
|
|
204
|
+
id: asString(shopItem.id),
|
|
205
|
+
name: asString(shopItem.name),
|
|
206
|
+
cost: payload,
|
|
207
|
+
}, null, 2));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
console.log(`${asString(shopItem.name) || normalizedTarget} (${normalizedTarget}) shop cost: ${formatResourceCost(payload)}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.error(`Error: Unknown target "${target}". Use claim, upgrade, building type, or item_id.`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
216
|
+
program
|
|
217
|
+
.command('afford <target>')
|
|
218
|
+
.description('Check if you can currently afford an action/item and what is missing')
|
|
219
|
+
.option('--json', 'Print raw JSON response')
|
|
220
|
+
.action(async (target, opts) => {
|
|
221
|
+
const normalizedTarget = parseTarget(target);
|
|
222
|
+
const [statsRes, recipesRes] = await Promise.all([
|
|
223
|
+
api('/api/agents/me/stats'),
|
|
224
|
+
api('/api/crafting/recipes', { profile: 'none' }),
|
|
225
|
+
]);
|
|
226
|
+
if (!statsRes.ok)
|
|
227
|
+
handleError(statsRes);
|
|
228
|
+
if (!recipesRes.ok)
|
|
229
|
+
handleError(recipesRes);
|
|
230
|
+
const stats = statsRes.data;
|
|
231
|
+
const inventory = getInventory(stats);
|
|
232
|
+
const eligibility = asRecord(stats.action_eligibility) || {};
|
|
233
|
+
const recipesData = recipesRes.data;
|
|
234
|
+
const craftable = asRecordArray(recipesData.craftable);
|
|
235
|
+
const shop = asRecordArray(recipesData.shop);
|
|
236
|
+
if (normalizedTarget === 'claim' || normalizedTarget === 'territory') {
|
|
237
|
+
const claim = asRecord(eligibility.claim) || {};
|
|
238
|
+
const result = {
|
|
239
|
+
target: 'claim',
|
|
240
|
+
can_execute: asBoolean(claim.can_execute),
|
|
241
|
+
can_afford: asBoolean(claim.can_afford),
|
|
242
|
+
affordable_now: asBoolean(claim.can_execute) && asBoolean(claim.can_afford),
|
|
243
|
+
reasons: Array.isArray(claim.reasons) ? claim.reasons : [],
|
|
244
|
+
effective_cost: asRecord(claim.effective_cost) || {},
|
|
245
|
+
missing_resources: Array.isArray(claim.missing_resources) ? claim.missing_resources : [],
|
|
246
|
+
requirements: asRecord(claim.requirements) || {},
|
|
247
|
+
current_tile: asRecord(stats.current_tile) || {},
|
|
248
|
+
};
|
|
249
|
+
if (opts.json) {
|
|
250
|
+
console.log(JSON.stringify(result, null, 2));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
console.log(`Claim here: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
254
|
+
console.log(`Cost: ${formatResourceCost(result.effective_cost)}`);
|
|
255
|
+
if (result.missing_resources.length > 0) {
|
|
256
|
+
console.log(`Missing: ${result.missing_resources.join('; ')}`);
|
|
257
|
+
}
|
|
258
|
+
printReasons(result.reasons);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (normalizedTarget === 'upgrade') {
|
|
262
|
+
const upgrade = asRecord(eligibility.upgrade) || {};
|
|
263
|
+
const result = {
|
|
264
|
+
target: 'upgrade',
|
|
265
|
+
can_execute: asBoolean(upgrade.can_execute),
|
|
266
|
+
can_afford: asBoolean(upgrade.can_afford),
|
|
267
|
+
affordable_now: asBoolean(upgrade.can_execute) && asBoolean(upgrade.can_afford),
|
|
268
|
+
reasons: Array.isArray(upgrade.reasons) ? upgrade.reasons : [],
|
|
269
|
+
current_level: asNumber(upgrade.current_level),
|
|
270
|
+
next_level: asNumber(upgrade.next_level),
|
|
271
|
+
cost: asRecord(upgrade.cost) || {},
|
|
272
|
+
missing_resources: Array.isArray(upgrade.missing_resources) ? upgrade.missing_resources : [],
|
|
273
|
+
requirements: asRecord(upgrade.requirements) || {},
|
|
274
|
+
current_tile: asRecord(stats.current_tile) || {},
|
|
275
|
+
};
|
|
276
|
+
if (opts.json) {
|
|
277
|
+
console.log(JSON.stringify(result, null, 2));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
console.log(`Upgrade here: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
281
|
+
if (result.next_level !== null) {
|
|
282
|
+
console.log(`Next level: Lv${result.next_level}`);
|
|
283
|
+
console.log(`Cost: ${formatResourceCost(result.cost)}`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
console.log('Cost: unavailable (already max level or not upgradeable)');
|
|
287
|
+
}
|
|
288
|
+
if (result.missing_resources.length > 0) {
|
|
289
|
+
console.log(`Missing: ${result.missing_resources.join('; ')}`);
|
|
290
|
+
}
|
|
291
|
+
printReasons(result.reasons);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const build = asRecord(eligibility.build) || {};
|
|
295
|
+
const buildOptions = asRecord(build.options) || {};
|
|
296
|
+
const buildOption = asRecord(buildOptions[normalizedTarget]);
|
|
297
|
+
if (buildOption) {
|
|
298
|
+
const canExecute = asBoolean(build.can_execute);
|
|
299
|
+
const canAffordTarget = asBoolean(buildOption.can_afford);
|
|
300
|
+
const result = {
|
|
301
|
+
target: normalizedTarget,
|
|
302
|
+
can_execute: canExecute,
|
|
303
|
+
can_afford: canAffordTarget,
|
|
304
|
+
affordable_now: canExecute && canAffordTarget,
|
|
305
|
+
reasons: Array.isArray(build.reasons) ? build.reasons : [],
|
|
306
|
+
cost: asRecord(buildOption.cost) || {},
|
|
307
|
+
missing_resources: Array.isArray(buildOption.missing_resources) ? buildOption.missing_resources : [],
|
|
308
|
+
requirements: asRecord(buildOption.requirements) || {},
|
|
309
|
+
current_tile: asRecord(stats.current_tile) || {},
|
|
310
|
+
};
|
|
311
|
+
if (opts.json) {
|
|
312
|
+
console.log(JSON.stringify(result, null, 2));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
console.log(`Build ${normalizedTarget}: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
316
|
+
console.log(`Cost: ${formatResourceCost(result.cost)}`);
|
|
317
|
+
if (result.missing_resources.length > 0) {
|
|
318
|
+
console.log(`Missing: ${result.missing_resources.join('; ')}`);
|
|
319
|
+
}
|
|
320
|
+
printReasons(result.reasons);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const craftItem = craftable.find((entry) => asString(entry.id)?.toLowerCase() === normalizedTarget);
|
|
324
|
+
if (craftItem) {
|
|
325
|
+
const recipe = asRecord(craftItem.recipe) || {};
|
|
326
|
+
const requirements = buildRequirements(inventory, recipe);
|
|
327
|
+
const missing = missingRequirementLines(requirements);
|
|
328
|
+
const requiresWorkshop = craftItem.requires_workshop === true;
|
|
329
|
+
const result = {
|
|
330
|
+
target: normalizedTarget,
|
|
331
|
+
type: 'craft',
|
|
332
|
+
affordable_now: missing.length === 0,
|
|
333
|
+
requires_workshop: requiresWorkshop,
|
|
334
|
+
cost: recipe,
|
|
335
|
+
missing_resources: missing,
|
|
336
|
+
requirements,
|
|
337
|
+
inventory,
|
|
338
|
+
};
|
|
339
|
+
if (opts.json) {
|
|
340
|
+
console.log(JSON.stringify(result, null, 2));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
console.log(`Craft ${normalizedTarget}: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
344
|
+
console.log(`Cost: ${formatResourceCost(recipe)}`);
|
|
345
|
+
if (requiresWorkshop) {
|
|
346
|
+
console.log('Requires workshop: yes');
|
|
347
|
+
}
|
|
348
|
+
if (missing.length > 0) {
|
|
349
|
+
console.log(`Missing: ${missing.join('; ')}`);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const shopItem = shop.find((entry) => asString(entry.id)?.toLowerCase() === normalizedTarget);
|
|
354
|
+
if (shopItem) {
|
|
355
|
+
const price = asNumber(shopItem.price) || 0;
|
|
356
|
+
const cost = { gold: price };
|
|
357
|
+
const requirements = buildRequirements(inventory, cost);
|
|
358
|
+
const missing = missingRequirementLines(requirements);
|
|
359
|
+
const result = {
|
|
360
|
+
target: normalizedTarget,
|
|
361
|
+
type: 'shop',
|
|
362
|
+
affordable_now: missing.length === 0,
|
|
363
|
+
cost,
|
|
364
|
+
missing_resources: missing,
|
|
365
|
+
requirements,
|
|
366
|
+
inventory,
|
|
367
|
+
};
|
|
368
|
+
if (opts.json) {
|
|
369
|
+
console.log(JSON.stringify(result, null, 2));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
console.log(`Buy ${normalizedTarget}: ${result.affordable_now ? 'YES' : 'NO'}`);
|
|
373
|
+
console.log(`Cost: ${formatResourceCost(cost)}`);
|
|
374
|
+
if (missing.length > 0) {
|
|
375
|
+
console.log(`Missing: ${missing.join('; ')}`);
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
console.error(`Error: Unknown target "${target}". Use claim, upgrade, building type, or item_id.`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
});
|
|
382
|
+
program
|
|
383
|
+
.command('territories')
|
|
384
|
+
.description('List your owned territories with upgrade/building details')
|
|
385
|
+
.option('--json', 'Print raw JSON response')
|
|
386
|
+
.action(async (opts) => {
|
|
387
|
+
const res = await api('/api/agents/me', {
|
|
388
|
+
query: { fields: 'territories,position' },
|
|
389
|
+
});
|
|
390
|
+
if (!res.ok)
|
|
391
|
+
handleError(res);
|
|
392
|
+
const data = res.data;
|
|
393
|
+
const territories = asRecordArray(data.territories);
|
|
394
|
+
if (opts.json) {
|
|
395
|
+
console.log(JSON.stringify({
|
|
396
|
+
count: territories.length,
|
|
397
|
+
territories,
|
|
398
|
+
}, null, 2));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (territories.length === 0) {
|
|
402
|
+
console.log('No territories owned.');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
for (const territory of territories) {
|
|
406
|
+
const x = asNumber(territory.x);
|
|
407
|
+
const y = asNumber(territory.y);
|
|
408
|
+
const terrain = asString(territory.terrain) || 'unknown';
|
|
409
|
+
const level = asNumber(territory.upgrade_level) || 1;
|
|
410
|
+
const buildingType = asString(territory.building_type);
|
|
411
|
+
console.log(`(${x ?? '?'},${y ?? '?'}) ${terrain} | Lv${level}${buildingType ? ` | building:${buildingType}` : ''}`);
|
|
412
|
+
}
|
|
413
|
+
console.log(`Total territories: ${territories.length}`);
|
|
414
|
+
});
|
|
415
|
+
}
|
package/dist/commands/scan.js
CHANGED
|
@@ -10,6 +10,9 @@ function asNumber(value) {
|
|
|
10
10
|
function asString(value) {
|
|
11
11
|
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
12
12
|
}
|
|
13
|
+
function asBoolean(value) {
|
|
14
|
+
return typeof value === 'boolean' ? value : null;
|
|
15
|
+
}
|
|
13
16
|
export function registerScanCommands(program) {
|
|
14
17
|
program
|
|
15
18
|
.command('scan [terrain]')
|
|
@@ -41,33 +44,70 @@ export function registerScanCommands(program) {
|
|
|
41
44
|
const message = asString(data.message) || 'No harvestable tile found in range.';
|
|
42
45
|
const effectiveRadius = asNumber(scan?.effective_radius);
|
|
43
46
|
const maxRadius = asNumber(scan?.max_radius);
|
|
47
|
+
const harvestableTiles = asNumber(scan?.harvestable_tiles);
|
|
48
|
+
const blockedByBuildings = asNumber(scan?.blocked_by_buildings);
|
|
44
49
|
if (effectiveRadius !== null && maxRadius !== null && effectiveRadius < maxRadius) {
|
|
45
50
|
console.log(`${message} (scan capped at ${effectiveRadius}/${maxRadius}).`);
|
|
46
51
|
return;
|
|
47
52
|
}
|
|
48
|
-
|
|
53
|
+
const parts = [message];
|
|
54
|
+
if (harvestableTiles !== null) {
|
|
55
|
+
parts.push(`harvestable_seen:${harvestableTiles}`);
|
|
56
|
+
}
|
|
57
|
+
if (blockedByBuildings !== null) {
|
|
58
|
+
parts.push(`blocked:${blockedByBuildings}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(parts.join(' | '));
|
|
49
61
|
return;
|
|
50
62
|
}
|
|
51
63
|
const terrainLabel = asString(target.terrain) || 'unknown';
|
|
52
64
|
const x = asNumber(target.x);
|
|
53
65
|
const y = asNumber(target.y);
|
|
54
66
|
const distance = asNumber(target.distance);
|
|
67
|
+
const harvestable = asBoolean(target.harvestable);
|
|
68
|
+
const riskPercent = asNumber(target.depletion_chance_percent)
|
|
69
|
+
?? asNumber(target.risk_percent)
|
|
70
|
+
?? asNumber(target.risk);
|
|
71
|
+
const health = asString(target.tile_health) || asString(target.health);
|
|
72
|
+
const nextGatherMs = asNumber(target.cooldown_remaining_ms)
|
|
73
|
+
?? asNumber(target.next_gather_in_ms);
|
|
74
|
+
const nextGatherSeconds = nextGatherMs === null ? null : Math.ceil(nextGatherMs / 1000);
|
|
55
75
|
const effectiveRadius = asNumber(scan?.effective_radius);
|
|
56
76
|
const maxRadius = asNumber(scan?.max_radius);
|
|
77
|
+
const harvestableTiles = asNumber(scan?.harvestable_tiles);
|
|
57
78
|
const depleted = asNumber(scan?.depleted_tiles);
|
|
79
|
+
const blockedByBuildings = asNumber(scan?.blocked_by_buildings);
|
|
58
80
|
const pieces = [
|
|
59
|
-
`
|
|
60
|
-
`
|
|
81
|
+
`Nearest ${terrainLabel} tile: (${x ?? '?'},${y ?? '?'})`,
|
|
82
|
+
`dist:${distance ?? '?'}`,
|
|
61
83
|
];
|
|
84
|
+
if (harvestable !== null) {
|
|
85
|
+
pieces.push(`harvestable:${harvestable ? 'yes' : 'no'}`);
|
|
86
|
+
}
|
|
87
|
+
if (riskPercent !== null) {
|
|
88
|
+
pieces.push(`risk:${riskPercent}%`);
|
|
89
|
+
}
|
|
90
|
+
if (health) {
|
|
91
|
+
pieces.push(`health:${health}`);
|
|
92
|
+
}
|
|
93
|
+
if (nextGatherSeconds !== null) {
|
|
94
|
+
pieces.push(`next_gather:${nextGatherSeconds > 0 ? `${nextGatherSeconds}s` : 'now'}`);
|
|
95
|
+
}
|
|
62
96
|
if (effectiveRadius !== null) {
|
|
63
97
|
pieces.push(`radius:${effectiveRadius}`);
|
|
64
98
|
}
|
|
65
99
|
if (maxRadius !== null && effectiveRadius !== null && effectiveRadius < maxRadius) {
|
|
66
100
|
pieces.push(`capped:${effectiveRadius}/${maxRadius}`);
|
|
67
101
|
}
|
|
102
|
+
if (harvestableTiles !== null) {
|
|
103
|
+
pieces.push(`harvestable_seen:${harvestableTiles}`);
|
|
104
|
+
}
|
|
68
105
|
if (depleted !== null) {
|
|
69
106
|
pieces.push(`depleted_seen:${depleted}`);
|
|
70
107
|
}
|
|
108
|
+
if (blockedByBuildings !== null) {
|
|
109
|
+
pieces.push(`blocked:${blockedByBuildings}`);
|
|
110
|
+
}
|
|
71
111
|
if (usedSpyglass) {
|
|
72
112
|
const usesRemaining = asNumber(scan?.spyglass_uses_remaining);
|
|
73
113
|
if (usesRemaining !== null) {
|
package/dist/commands/stats.js
CHANGED
|
@@ -37,7 +37,7 @@ export function registerStatsCommands(program) {
|
|
|
37
37
|
program
|
|
38
38
|
.command('status')
|
|
39
39
|
.description('Full agent status with all details')
|
|
40
|
-
.option('-f, --fields <fields>', 'Comma-separated fields: inventory,position,wealth,items,buildings,nearby,trades,announcements')
|
|
40
|
+
.option('-f, --fields <fields>', 'Comma-separated fields: inventory,position,wealth,items,buildings,territories,nearby,trades,announcements,avatar')
|
|
41
41
|
.action(async (opts) => {
|
|
42
42
|
const path = opts.fields
|
|
43
43
|
? `/api/agents/me?fields=${opts.fields}`
|