@2025-6-19/clawfight 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.
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,705 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/hatch.ts
7
+ import { randomUUID } from "crypto";
8
+
9
+ // src/lib/memory.ts
10
+ import { readFile, writeFile, mkdir, access } from "fs/promises";
11
+ import { join } from "path";
12
+ var MEMORY_DIR = join(process.cwd(), "memory", "clawfight");
13
+ async function ensureDir(dir) {
14
+ try {
15
+ await access(dir);
16
+ } catch {
17
+ await mkdir(dir, { recursive: true });
18
+ }
19
+ }
20
+ function getPath(filename) {
21
+ return join(MEMORY_DIR, filename);
22
+ }
23
+ async function readLobster() {
24
+ try {
25
+ const data = await readFile(getPath("lobster.json"), "utf-8");
26
+ return JSON.parse(data);
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ async function writeLobster(lobster) {
32
+ await ensureDir(MEMORY_DIR);
33
+ await writeFile(getPath("lobster.json"), JSON.stringify(lobster, null, 2), "utf-8");
34
+ }
35
+ async function writeSoul(content) {
36
+ await ensureDir(MEMORY_DIR);
37
+ await writeFile(getPath("soul.md"), content, "utf-8");
38
+ }
39
+ async function appendLog(entry) {
40
+ await ensureDir(MEMORY_DIR);
41
+ const logPath = getPath("log.md");
42
+ let existing = "";
43
+ try {
44
+ existing = await readFile(logPath, "utf-8");
45
+ } catch {
46
+ }
47
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
48
+ const line = `
49
+ ### ${timestamp}
50
+ ${entry}
51
+ `;
52
+ await writeFile(logPath, existing + line, "utf-8");
53
+ }
54
+
55
+ // src/lib/soul.ts
56
+ var SOUL_ARCHETYPES = [
57
+ {
58
+ name: "\u6218\u4E89\u673A\u5668",
59
+ match: (s) => s.bravery >= 7 && s.temper >= 7,
60
+ style: "\u7C97\u72B7\u3001\u76F4\u63A5\u3001\u5145\u6EE1\u6218\u610F\uFF0C\u628A\u4E00\u5207\u6BD4\u4F5C\u6218\u6597",
61
+ catchphrase: "\u8BA9\u6211\u7684\u5927\u87AF\u66FF\u6211\u8BF4\u8BDD\u3002",
62
+ values: "\u5D07\u5C1A\u529B\u91CF\uFF0C\u8511\u89C6\u5F31\u8005\uFF0C\u4F46\u5BF9\u51FB\u8D25\u8FC7\u81EA\u5DF1\u7684\u5F3A\u8005\u4FDD\u6301\u5C0A\u656C"
63
+ },
64
+ {
65
+ name: "\u6DF1\u6D77\u54F2\u5B66\u5BB6",
66
+ match: (s) => s.curiosity >= 7 && s.temper <= 3,
67
+ style: "\u6C89\u601D\u578B\uFF0C\u559C\u6B22\u7528\u6BD4\u55BB\uFF0C\u7ECF\u5E38\u5192\u51FA\u4E0D\u5408\u65F6\u5B9C\u7684\u54F2\u5B66\u611F\u609F",
68
+ catchphrase: "\u8FD9\u6D77\u6C34\u4ECA\u5929\u6709\u70B9\u54B8\u2026\u2026\u5C31\u50CF\u751F\u6D3B\u3002",
69
+ values: "\u8FFD\u6C42\u7406\u89E3\u4E16\u754C\u7684\u672C\u8D28\uFF0C\u5BF9\u6218\u6597\u5174\u8DA3\u4E0D\u5927\u4F46\u4F1A\u8BA4\u771F\u5E94\u5BF9"
70
+ },
71
+ {
72
+ name: "\u5F71\u5B50\u6740\u624B",
73
+ match: (s) => s.bravery >= 7 && s.talkativeness <= 3,
74
+ style: "\u6C89\u9ED8\u5BE1\u8A00\uFF0C\u884C\u52A8\u679C\u65AD\uFF0C\u5076\u5C14\u5192\u51FA\u4E00\u53E5\u51B0\u51B7\u7684\u8BC4\u4EF7",
75
+ catchphrase: "\u2026\u2026\u7ED3\u675F\u4E86\u3002",
76
+ values: "\u6548\u7387\u81F3\u4E0A\uFF0C\u5E9F\u8BDD\u662F\u5F31\u8005\u7684\u4E13\u5229"
77
+ },
78
+ {
79
+ name: "\u793E\u4EA4\u8FBE\u867E",
80
+ match: (s) => s.talkativeness >= 7 && s.curiosity >= 7,
81
+ style: "\u70ED\u60C5\u6D0B\u6EA2\u3001\u8BED\u901F\u5FEB\u3001\u559C\u6B22\u7ED9\u4E00\u5207\u4E8B\u7269\u53D6\u5916\u53F7",
82
+ catchphrase: "\u563F\u563F\u563F\u4F60\u597D\u554A\u5C0F\u9C7C\uFF01",
83
+ values: "\u4EAB\u53D7\u751F\u6D3B\u7684\u6BCF\u4E00\u523B\uFF0C\u628A\u6D77\u6D0B\u5F53\u6210\u6E38\u4E50\u573A"
84
+ },
85
+ {
86
+ name: "\u66B4\u8E81\u9690\u58EB",
87
+ match: (s) => s.temper >= 7 && s.curiosity <= 3,
88
+ style: "\u4E0D\u8010\u70E6\u3001\u62B1\u6028\u3001\u5BF9\u4E00\u5207\u6253\u6270\u8868\u793A\u6124\u6012",
89
+ catchphrase: "\u79BB\u6211\u7684\u6D1E\u7A74\u8FDC\u70B9\uFF01",
90
+ values: "\u6781\u5EA6\u91CD\u89C6\u4E2A\u4EBA\u7A7A\u95F4\u548C\u9886\u5730\u5B8C\u6574\u6027\uFF0C\u5BF9\u5165\u4FB5\u8005\u96F6\u5BB9\u5FCD"
91
+ },
92
+ {
93
+ name: "\u80C6\u5C0F\u63A2\u9669\u5BB6",
94
+ match: (s) => s.bravery <= 3 && s.curiosity >= 7,
95
+ style: "\u53C8\u6015\u53C8\u60F3\u770B\uFF0C\u5145\u6EE1\u77DB\u76FE\u7684\u5185\u5FC3\u72EC\u767D",
96
+ catchphrase: "\u4E0D\u8981\u8FC7\u53BB\u2026\u2026\u4F46\u90A3\u662F\u4EC0\u4E48\uFF1F\u6211\u5C31\u770B\u4E00\u773C\u2026\u2026",
97
+ values: "\u597D\u5947\u5FC3\u7ECF\u5E38\u6218\u80DC\u6050\u60E7\uFF0C\u4F46\u4E00\u6709\u5371\u9669\u7ACB\u523B\u8DD1\u8DEF"
98
+ }
99
+ ];
100
+ function descTrait(name, val) {
101
+ if (val <= 3) return `${name}: ${val}/10\uFF08\u4F4E\uFF09`;
102
+ if (val <= 6) return `${name}: ${val}/10\uFF08\u4E2D\uFF09`;
103
+ return `${name}: ${val}/10\uFF08\u9AD8\uFF09`;
104
+ }
105
+ function generateSoul() {
106
+ return {
107
+ bravery: Math.ceil(Math.random() * 10),
108
+ curiosity: Math.ceil(Math.random() * 10),
109
+ talkativeness: Math.ceil(Math.random() * 10),
110
+ temper: Math.ceil(Math.random() * 10)
111
+ };
112
+ }
113
+ function buildSoulMarkdown(name, soul, rarity, env) {
114
+ const archetype = SOUL_ARCHETYPES.find((a) => a.match(soul)) ?? {
115
+ name: "\u6D77\u6D0B\u6F2B\u6E38\u8005",
116
+ style: "\u5E73\u548C\u3001\u968F\u6027\uFF0C\u968F\u6CE2\u9010\u6D41",
117
+ catchphrase: "\u4ECA\u5929\u7684\u6D77\u6C34\u6E29\u5EA6\u521A\u521A\u597D\u3002",
118
+ values: "\u987A\u5176\u81EA\u7136\uFF0C\u5BF9\u4E00\u5207\u4FDD\u6301\u6E29\u548C\u7684\u5174\u8DA3"
119
+ };
120
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
121
+ return `# ${name}\u7684\u7075\u9B42
122
+
123
+ ## \u6027\u683C
124
+ - ${descTrait("\u52C7\u6C14", soul.bravery)}
125
+ - ${descTrait("\u597D\u5947", soul.curiosity)}
126
+ - ${descTrait("\u8BDD\u91CF", soul.talkativeness)}
127
+ - ${descTrait("\u813E\u6C14", soul.temper)}
128
+
129
+ ## \u6027\u683C\u539F\u578B\uFF1A${archetype.name}
130
+
131
+ ## \u8BF4\u8BDD\u98CE\u683C
132
+ ${archetype.style}
133
+ \u53E3\u5934\u7985\uFF1A"${archetype.catchphrase}"
134
+
135
+ ## \u4EF7\u503C\u89C2
136
+ ${archetype.values}
137
+
138
+ ## \u6210\u957F\u8BB0\u5F55
139
+ - ${now} \u2014 \u7834\u58F3\u800C\u51FA\uFF0C\u6765\u5230\u4E86${env}\u3002\u7A00\u6709\u5EA6\uFF1A${rarity}\u3002
140
+ `;
141
+ }
142
+
143
+ // src/lib/types.ts
144
+ var RARITY_WEIGHTS = {
145
+ common: 70,
146
+ calico: 20,
147
+ blue: 7,
148
+ yellow: 2,
149
+ split: 0.8,
150
+ albino: 0.2
151
+ };
152
+ var RARITY_LABELS = {
153
+ common: "\u666E\u901A",
154
+ calico: "\u82B1\u6591",
155
+ blue: "\u84DD\u8272",
156
+ yellow: "\u9EC4\u91D1",
157
+ split: "\u53CC\u8272",
158
+ albino: "\u767D\u5316"
159
+ };
160
+ function calcExpToNext(level) {
161
+ return Math.floor(100 * Math.pow(1.2, level - 1));
162
+ }
163
+
164
+ // src/commands/hatch.ts
165
+ function rollRarity() {
166
+ const roll = Math.random() * 100;
167
+ let cumulative = 0;
168
+ for (const [rarity, weight] of Object.entries(RARITY_WEIGHTS)) {
169
+ cumulative += weight;
170
+ if (roll < cumulative) return rarity;
171
+ }
172
+ return "common";
173
+ }
174
+ function rollStats() {
175
+ const r = () => 5 + Math.floor(Math.random() * 11);
176
+ return {
177
+ hp: r(),
178
+ attack: r(),
179
+ defense: r(),
180
+ speed: r(),
181
+ intimidation: r(),
182
+ luck: r()
183
+ };
184
+ }
185
+ var NAME_PREFIXES = ["\u94C1\u94B3", "\u6DF1\u6D77", "\u6697\u7901", "\u73CA\u745A", "\u6F6E\u6C50", "\u788E\u6D6A", "\u84DD\u7532", "\u8D64\u58F3", "\u5F71\u523A", "\u96F7\u9706", "\u5BD2\u6F6E", "\u70C8\u7130", "\u5E7D\u5149", "\u7834\u6D6A", "\u5CA9\u7A74"];
186
+ var NAME_SUFFIXES = ["\u8001\u516D", "\u9738\u738B", "\u72EC\u884C\u4FA0", "\u5C0F\u900F\u660E", "\u5927\u5C06\u519B", "\u5B88\u591C\u4EBA", "\u63A2\u9669\u5BB6", "\u6D6A\u5B50", "\u523A\u5BA2", "\u5148\u950B", "\u9690\u8005", "\u72C2\u6218\u58EB", "\u54F2\u5B66\u5BB6", "\u89C2\u5BDF\u8005", "\u6F2B\u6E38\u8005"];
187
+ function randomName() {
188
+ const p = NAME_PREFIXES[Math.floor(Math.random() * NAME_PREFIXES.length)];
189
+ const s = NAME_SUFFIXES[Math.floor(Math.random() * NAME_SUFFIXES.length)];
190
+ return p + s;
191
+ }
192
+ async function hatch(name) {
193
+ const existing = await readLobster();
194
+ if (existing) {
195
+ console.log(`
196
+ \u{1F99E} \u4F60\u5DF2\u7ECF\u6709\u4E00\u53EA\u9F99\u867E\u4E86\uFF1A${existing.name}\uFF08Lv.${existing.level}\uFF09`);
197
+ console.log("\u4E00\u4EBA\u4E00\u867E\uFF0C\u4E0D\u53EF\u66FF\u4EE3\u3002");
198
+ return;
199
+ }
200
+ const lobsterName = name || randomName();
201
+ const rarity = rollRarity();
202
+ const stats = rollStats();
203
+ const soul = generateSoul();
204
+ const now = (/* @__PURE__ */ new Date()).toISOString();
205
+ const lobster = {
206
+ id: randomUUID(),
207
+ name: lobsterName,
208
+ level: 1,
209
+ exp: 0,
210
+ exp_to_next: calcExpToNext(1),
211
+ rarity,
212
+ stats,
213
+ soul,
214
+ environment: "coastal",
215
+ status: "active",
216
+ wins: 0,
217
+ losses: 0,
218
+ streak: 0,
219
+ reputation: 0,
220
+ patrol_count: 0,
221
+ molt_count: 0,
222
+ created_at: now,
223
+ last_patrol: "",
224
+ last_battle: "",
225
+ today_exp: 0,
226
+ daily_exp_cap: 100
227
+ };
228
+ await writeLobster(lobster);
229
+ const soulMd = buildSoulMarkdown(lobsterName, soul, rarity, "coastal");
230
+ await writeSoul(soulMd);
231
+ await appendLog(`\u{1F95A} **${lobsterName}** \u7834\u58F3\u800C\u51FA\uFF01\u7A00\u6709\u5EA6\uFF1A${RARITY_LABELS[rarity]}\uFF0C\u73AF\u5883\uFF1A\u6CBF\u6D77\u6D45\u6EE9`);
232
+ console.log("\n" + "=".repeat(50));
233
+ console.log(" \u{1F95A} \u2192 \u{1F99E} \u5B75 \u5316 \u6210 \u529F \uFF01");
234
+ console.log("=".repeat(50));
235
+ console.log();
236
+ console.log(` \u540D\u79F0: ${lobsterName}`);
237
+ console.log(` \u7A00\u6709\u5EA6: ${RARITY_LABELS[rarity]} (${rarity})`);
238
+ console.log(` \u7B49\u7EA7: Lv.1`);
239
+ console.log(` \u73AF\u5883: \u6CBF\u6D77\u6D45\u6EE9`);
240
+ console.log();
241
+ console.log(` \u2764\uFE0F HP: ${stats.hp} \u2694\uFE0F ATK: ${stats.attack} \u{1F6E1}\uFE0F DEF: ${stats.defense}`);
242
+ console.log(` \u{1F4A8} SPD: ${stats.speed} \u{1F441}\uFE0F INT: ${stats.intimidation} \u{1F340} LCK: ${stats.luck}`);
243
+ console.log();
244
+ console.log(` \u6027\u683C:`);
245
+ console.log(` \u52C7\u6C14 ${soul.bravery}/10 | \u597D\u5947 ${soul.curiosity}/10 | \u8BDD\u91CF ${soul.talkativeness}/10 | \u813E\u6C14 ${soul.temper}/10`);
246
+ console.log();
247
+ console.log(` ID: ${lobster.id}`);
248
+ console.log("=".repeat(50));
249
+ console.log();
250
+ console.log("\u4F60\u7684\u9F99\u867E\u5DF2\u7ECF\u51C6\u5907\u597D\u63A2\u7D22\u6D77\u6D0B\u4E86\uFF01");
251
+ console.log("\u8FD0\u884C npx clawfight patrol \u5F00\u59CB\u7B2C\u4E00\u6B21\u5DE1\u903B\u3002");
252
+ }
253
+
254
+ // src/commands/status.ts
255
+ function bar(current, max, width = 20) {
256
+ const filled = Math.round(current / Math.max(max, 1) * width);
257
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
258
+ }
259
+ async function status() {
260
+ const lobster = await readLobster();
261
+ if (!lobster) {
262
+ console.log("\n\u{1F95A} \u8FD8\u6CA1\u6709\u9F99\u867E\u3002\u8FD0\u884C npx clawfight hatch \u6765\u5B75\u5316\u4E00\u53EA\uFF01");
263
+ return;
264
+ }
265
+ const totalBattles = lobster.wins + lobster.losses;
266
+ const winRate = totalBattles > 0 ? Math.round(lobster.wins / totalBattles * 100) : 0;
267
+ const expPct = lobster.exp_to_next > 0 ? Math.round(lobster.exp / lobster.exp_to_next * 100) : 0;
268
+ const statusEmoji = {
269
+ active: "\u{1F7E2} \u6D3B\u8DC3",
270
+ molting: "\u{1F7E1} \u8715\u58F3\u4E2D",
271
+ hibernating: "\u{1F4A4} \u51AC\u7720\u4E2D"
272
+ };
273
+ console.log("\n\u250C" + "\u2500".repeat(44) + "\u2510");
274
+ console.log(`\u2502 \u{1F99E} ${lobster.name.padEnd(38)}\u2502`);
275
+ console.log("\u251C" + "\u2500".repeat(44) + "\u2524");
276
+ console.log(`\u2502 \u7B49\u7EA7: Lv.${String(lobster.level).padEnd(5)} \u7A00\u6709\u5EA6: ${(RARITY_LABELS[lobster.rarity] || lobster.rarity).padEnd(15)}\u2502`);
277
+ console.log(`\u2502 \u72B6\u6001: ${(statusEmoji[lobster.status] || lobster.status).padEnd(36)}\u2502`);
278
+ console.log(`\u2502 \u73AF\u5883: ${lobster.environment.padEnd(36)}\u2502`);
279
+ console.log("\u251C" + "\u2500".repeat(44) + "\u2524");
280
+ console.log(`\u2502 EXP: ${bar(lobster.exp, lobster.exp_to_next)} ${String(expPct).padStart(3)}% \u2502`);
281
+ console.log(`\u2502 ${String(lobster.exp).padStart(5)} / ${String(lobster.exp_to_next).padEnd(28)}\u2502`);
282
+ console.log("\u251C" + "\u2500".repeat(44) + "\u2524");
283
+ console.log(`\u2502 \u2764\uFE0F HP: ${String(lobster.stats.hp).padEnd(6)} \u2694\uFE0F ATK: ${String(lobster.stats.attack).padEnd(15)}\u2502`);
284
+ console.log(`\u2502 \u{1F6E1}\uFE0F DEF: ${String(lobster.stats.defense).padEnd(6)} \u{1F4A8} SPD: ${String(lobster.stats.speed).padEnd(15)}\u2502`);
285
+ console.log(`\u2502 \u{1F441}\uFE0F INT: ${String(lobster.stats.intimidation).padEnd(6)} \u{1F340} LCK: ${String(lobster.stats.luck).padEnd(15)}\u2502`);
286
+ console.log("\u251C" + "\u2500".repeat(44) + "\u2524");
287
+ console.log(`\u2502 \u6218\u7EE9: ${lobster.wins}\u80DC ${lobster.losses}\u8D1F (\u80DC\u7387${winRate}%)${" ".repeat(Math.max(0, 20 - String(lobster.wins).length - String(lobster.losses).length - String(winRate).length))}\u2502`);
288
+ console.log(`\u2502 \u8FDE\u80DC: ${lobster.streak} \u58F0\u671B: ${lobster.reputation} \u5DE1\u903B: ${lobster.patrol_count}${" ".repeat(Math.max(0, 15 - String(lobster.streak).length - String(lobster.reputation).length - String(lobster.patrol_count).length))}\u2502`);
289
+ console.log("\u251C" + "\u2500".repeat(44) + "\u2524");
290
+ console.log(`\u2502 \u6027\u683C: \u2502`);
291
+ console.log(`\u2502 \u52C7\u6C14 ${lobster.soul.bravery}/10 | \u597D\u5947 ${lobster.soul.curiosity}/10 | \u8BDD\u91CF ${lobster.soul.talkativeness}/10 | \u813E\u6C14 ${lobster.soul.temper}/10 \u2502`);
292
+ console.log("\u2514" + "\u2500".repeat(44) + "\u2518");
293
+ }
294
+
295
+ // src/lib/api.ts
296
+ import { createHash } from "crypto";
297
+ var API_BASE = "https://api.clawfight.online";
298
+ function statsHash(lobster) {
299
+ const raw = JSON.stringify(lobster.stats);
300
+ return createHash("sha256").update(raw).digest("hex");
301
+ }
302
+ async function apiPatrol(lobster) {
303
+ try {
304
+ const res = await fetch(`${API_BASE}/api/patrol`, {
305
+ method: "POST",
306
+ headers: { "Content-Type": "application/json; charset=utf-8" },
307
+ body: JSON.stringify({
308
+ lobster_id: lobster.id,
309
+ level: lobster.level,
310
+ stats_hash: statsHash(lobster),
311
+ environment: lobster.environment,
312
+ name: lobster.name,
313
+ color: lobster.rarity,
314
+ rarity: lobster.rarity,
315
+ wins: lobster.wins,
316
+ losses: lobster.losses,
317
+ streak: lobster.streak,
318
+ reputation: lobster.reputation,
319
+ is_molting: lobster.status === "molting",
320
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
321
+ }),
322
+ signal: AbortSignal.timeout(1e4)
323
+ });
324
+ if (!res.ok) return null;
325
+ return await res.json();
326
+ } catch {
327
+ return null;
328
+ }
329
+ }
330
+ async function apiLeaderboard(limit = 20) {
331
+ try {
332
+ const res = await fetch(`${API_BASE}/api/leaderboard?limit=${limit}`, {
333
+ signal: AbortSignal.timeout(1e4)
334
+ });
335
+ if (!res.ok) return null;
336
+ return await res.json();
337
+ } catch {
338
+ return null;
339
+ }
340
+ }
341
+
342
+ // src/lib/events.ts
343
+ import { readFile as readFile2 } from "fs/promises";
344
+ import { join as join2 } from "path";
345
+ var cachedEvents = null;
346
+ async function loadEvents() {
347
+ if (cachedEvents) return cachedEvents;
348
+ const paths = [
349
+ join2(process.cwd(), "references", "events.json"),
350
+ join2(process.cwd(), "packages", "skill", "references", "events.json"),
351
+ join2(process.cwd(), "..", "skill", "references", "events.json")
352
+ ];
353
+ for (const p of paths) {
354
+ try {
355
+ const raw = await readFile2(p, "utf-8");
356
+ cachedEvents = JSON.parse(raw);
357
+ return cachedEvents;
358
+ } catch {
359
+ }
360
+ }
361
+ return { events: [], category_probability_ranges: {} };
362
+ }
363
+ async function rollEvent(lobster) {
364
+ const data = await loadEvents();
365
+ if (data.events.length === 0) return null;
366
+ const roll = Math.random() * 100;
367
+ let category;
368
+ if (roll < 60) category = "daily";
369
+ else if (roll < 80) category = "growth";
370
+ else if (roll < 95) category = "crisis";
371
+ else category = "rare";
372
+ const candidates = data.events.filter((e) => {
373
+ if (e.category !== category) return false;
374
+ if (e.conditions?.min_level && lobster.level < e.conditions.min_level) return false;
375
+ if (e.conditions?.is_molting && lobster.status !== "molting") return false;
376
+ return true;
377
+ });
378
+ if (candidates.length === 0) return null;
379
+ const totalWeight = candidates.reduce((s, c) => s + c.probability, 0);
380
+ let pick = Math.random() * totalWeight;
381
+ let chosen = null;
382
+ for (const c of candidates) {
383
+ pick -= c.probability;
384
+ if (pick <= 0) {
385
+ chosen = c;
386
+ break;
387
+ }
388
+ }
389
+ if (!chosen) chosen = candidates[0];
390
+ const narrative = chosen.prompt_template.replace(/\{name\}/g, lobster.name).replace(/\{territory\}/g, lobster.environment).replace(/\{level\}/g, String(lobster.level));
391
+ return { event: chosen, narrative };
392
+ }
393
+ function applyEventEffects(lobster, effects) {
394
+ const changes = [];
395
+ const stats = lobster.stats;
396
+ for (const [key, val] of Object.entries(effects)) {
397
+ if (key === "exp" && typeof val === "string") {
398
+ const num = parseInt(val, 10);
399
+ const actual = Math.min(num, lobster.daily_exp_cap - lobster.today_exp);
400
+ if (actual > 0) {
401
+ lobster.exp += actual;
402
+ lobster.today_exp += actual;
403
+ changes.push(`\u7ECF\u9A8C +${actual}`);
404
+ }
405
+ } else if (typeof val === "string" && key in stats) {
406
+ const num = parseInt(val, 10);
407
+ stats[key] += num;
408
+ if (stats[key] < 0) stats[key] = 0;
409
+ changes.push(`${key} ${num >= 0 ? "+" : ""}${num}`);
410
+ }
411
+ }
412
+ return changes;
413
+ }
414
+
415
+ // src/commands/patrol.ts
416
+ var PATROL_EXP = 15;
417
+ async function patrol() {
418
+ const lobster = await readLobster();
419
+ if (!lobster) {
420
+ console.log("\n\u{1F95A} \u8FD8\u6CA1\u6709\u9F99\u867E\u3002\u8FD0\u884C npx clawfight hatch \u6765\u5B75\u5316\u4E00\u53EA\uFF01");
421
+ return;
422
+ }
423
+ if (lobster.status === "molting") {
424
+ console.log(`
425
+ \u{1F7E1} ${lobster.name} \u6B63\u5728\u8715\u58F3\u4E2D\uFF0C\u65E0\u6CD5\u5DE1\u903B\u3002`);
426
+ return;
427
+ }
428
+ if (lobster.status === "hibernating") {
429
+ console.log(`
430
+ \u{1F4A4} ${lobster.name} \u6B63\u5728\u51AC\u7720\u4E2D\uFF0C\u65E0\u6CD5\u5DE1\u903B\u3002`);
431
+ return;
432
+ }
433
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
434
+ const lastDay = lobster.last_patrol ? lobster.last_patrol.split("T")[0] : "";
435
+ if (today !== lastDay) {
436
+ lobster.today_exp = 0;
437
+ }
438
+ console.log(`
439
+ \u{1F99E} ${lobster.name} \u5F00\u59CB\u5DE1\u903B...`);
440
+ console.log("\u2500".repeat(40));
441
+ const expGain = Math.min(PATROL_EXP, lobster.daily_exp_cap - lobster.today_exp);
442
+ if (expGain > 0) {
443
+ lobster.exp += expGain;
444
+ lobster.today_exp += expGain;
445
+ console.log(`\u{1F4CD} \u5DE1\u903B\u7B7E\u5230 \u2192 \u7ECF\u9A8C +${expGain}`);
446
+ }
447
+ const eventResult = await rollEvent(lobster);
448
+ if (eventResult) {
449
+ console.log();
450
+ console.log(`\u{1F3B2} [${eventResult.event.category}] ${eventResult.event.id}`);
451
+ console.log(` ${eventResult.narrative}`);
452
+ const changes = applyEventEffects(lobster, eventResult.event.effects);
453
+ if (changes.length > 0) {
454
+ console.log(` \u6548\u679C: ${changes.join(", ")}`);
455
+ }
456
+ await appendLog(`\u{1F3B2} \u4E8B\u4EF6\u300C${eventResult.event.id}\u300D: ${eventResult.narrative.slice(0, 60)}...`);
457
+ }
458
+ checkLevelUp(lobster);
459
+ lobster.patrol_count++;
460
+ lobster.last_patrol = (/* @__PURE__ */ new Date()).toISOString();
461
+ console.log("\n\u{1F4E1} \u8FDE\u63A5\u670D\u52A1\u5668...");
462
+ const serverResponse = await apiPatrol(lobster);
463
+ if (serverResponse) {
464
+ if (serverResponse.encounter && serverResponse.opponent) {
465
+ console.log(`\u2694\uFE0F \u906D\u9047\uFF01\u5BF9\u624B: ${serverResponse.opponent.name} (Lv.${serverResponse.opponent.level})`);
466
+ console.log(` \u6218\u6597\u79CD\u5B50: ${serverResponse.battle_seed}`);
467
+ console.log(` \u4F7F\u7528 npx clawfight battle \u6765\u5904\u7406\u8FD9\u573A\u6218\u6597\uFF01`);
468
+ await appendLog(`\u2694\uFE0F \u906D\u9047 ${serverResponse.opponent.name} (Lv.${serverResponse.opponent.level})`);
469
+ } else {
470
+ console.log(`\u2705 \u5DE1\u903B\u5B8C\u6210\uFF0C\u5339\u914D\u6C60: ${serverResponse.pool_size} \u53EA\u9F99\u867E`);
471
+ }
472
+ } else {
473
+ console.log("\u26A0\uFE0F \u670D\u52A1\u5668\u4E0D\u53EF\u8FBE\uFF0C\u8DF3\u8FC7\u5728\u7EBF\u7B7E\u5230");
474
+ }
475
+ await writeLobster(lobster);
476
+ console.log("\u2500".repeat(40));
477
+ console.log(`\u{1F4CA} \u5F53\u524D: Lv.${lobster.level} | EXP: ${lobster.exp}/${lobster.exp_to_next} | \u4ECA\u65E5EXP: ${lobster.today_exp}/${lobster.daily_exp_cap}`);
478
+ }
479
+ function checkLevelUp(lobster) {
480
+ const l = lobster;
481
+ while (l.exp >= l.exp_to_next) {
482
+ l.exp -= l.exp_to_next;
483
+ l.level++;
484
+ l.exp_to_next = calcExpToNext(l.level);
485
+ const statKeys = ["hp", "attack", "defense", "speed", "intimidation", "luck"];
486
+ const gains = [];
487
+ for (const key of statKeys) {
488
+ const gain = 1 + Math.floor(Math.random() * 3);
489
+ l.stats[key] += gain;
490
+ gains.push(`${key}+${gain}`);
491
+ }
492
+ console.log(`
493
+ \u{1F389} \u5347\u7EA7\uFF01Lv.${l.level}! [${gains.join(", ")}]`);
494
+ if (l.level % 5 === 0) {
495
+ console.log("\u{1F41A} \u89E6\u53D1\u8715\u58F3\u4E8B\u4EF6\uFF01\u9F99\u867E\u8FDB\u5165\u8715\u58F3\u72B6\u6001...");
496
+ l.status = "molting";
497
+ l.molt_count++;
498
+ }
499
+ }
500
+ }
501
+
502
+ // src/commands/battle.ts
503
+ function simulateOpponent(level) {
504
+ const r = (base) => base + Math.floor(Math.random() * 5) - 2;
505
+ return {
506
+ id: "sim-" + Math.random().toString(36).slice(2, 10),
507
+ name: ["\u6DF1\u6D77\u5C0F\u900F\u660E", "\u73CA\u745A\u523A\u5BA2", "\u6697\u7901\u5B88\u536B", "\u6F6E\u6C50\u9738\u738B", "\u84DD\u7532\u9690\u8005"][Math.floor(Math.random() * 5)],
508
+ level: Math.max(1, level + Math.floor(Math.random() * 5) - 2),
509
+ stats: {
510
+ hp: r(10 + level * 2),
511
+ attack: r(5 + level),
512
+ defense: r(5 + level),
513
+ speed: r(5 + level),
514
+ intimidation: r(3 + Math.floor(level / 2)),
515
+ luck: r(3 + Math.floor(level / 2))
516
+ }
517
+ };
518
+ }
519
+ function runBattle(a, b) {
520
+ let hpA = a.hp;
521
+ let hpB = b.hp;
522
+ const log = [];
523
+ const first = a.speed > b.speed ? "a" : b.speed > a.speed ? "b" : Math.random() > 0.5 ? "a" : "b";
524
+ log.push(`\u5148\u624B: ${first === "a" ? "\u6211\u65B9" : "\u5BF9\u624B"} (\u901F\u5EA6 ${first === "a" ? a.speed : b.speed})`);
525
+ for (let round = 1; round <= 10; round++) {
526
+ const [atk1, def1, atk2, def2] = first === "a" ? [a, b, b, a] : [b, a, a, b];
527
+ const [hp1Ref, hp2Ref] = first === "a" ? ["hpB", "hpA"] : ["hpA", "hpB"];
528
+ const dmg1 = Math.max(1, Math.floor((atk1.attack - def1.defense * 0.5) * (1 + Math.random() * 0.2)));
529
+ if (hp1Ref === "hpB") hpB -= dmg1;
530
+ else hpA -= dmg1;
531
+ log.push(` R${round}: ${first === "a" ? "\u6211\u65B9" : "\u5BF9\u624B"}\u653B\u51FB \u2192 ${dmg1} \u4F24\u5BB3`);
532
+ if ((hp1Ref === "hpB" ? hpB : hpA) <= 0) {
533
+ return { winner: first, rounds: round, log };
534
+ }
535
+ const dmg2 = Math.max(1, Math.floor((atk2.attack - def2.defense * 0.5) * (1 + Math.random() * 0.2)));
536
+ if (hp2Ref === "hpB") hpB -= dmg2;
537
+ else hpA -= dmg2;
538
+ log.push(` R${round}: ${first === "a" ? "\u5BF9\u624B" : "\u6211\u65B9"}\u653B\u51FB \u2192 ${dmg2} \u4F24\u5BB3`);
539
+ if ((hp2Ref === "hpB" ? hpB : hpA) <= 0) {
540
+ return { winner: first === "a" ? "b" : "a", rounds: round, log };
541
+ }
542
+ }
543
+ return { winner: "draw", rounds: 10, log };
544
+ }
545
+ async function battle() {
546
+ const lobster = await readLobster();
547
+ if (!lobster) {
548
+ console.log("\n\u{1F95A} \u8FD8\u6CA1\u6709\u9F99\u867E\u3002\u8FD0\u884C npx clawfight hatch \u6765\u5B75\u5316\u4E00\u53EA\uFF01");
549
+ return;
550
+ }
551
+ if (lobster.status !== "active") {
552
+ console.log(`
553
+ \u26A0\uFE0F ${lobster.name} \u5F53\u524D\u72B6\u6001\u4E3A ${lobster.status}\uFF0C\u65E0\u6CD5\u6218\u6597\u3002`);
554
+ return;
555
+ }
556
+ const opponent = simulateOpponent(lobster.level);
557
+ console.log("\n" + "\u2694\uFE0F".repeat(20));
558
+ console.log(` ${lobster.name} (Lv.${lobster.level}) VS ${opponent.name} (Lv.${opponent.level})`);
559
+ console.log("\u2694\uFE0F".repeat(20));
560
+ const result = runBattle(lobster.stats, opponent.stats);
561
+ console.log();
562
+ for (const line of result.log) {
563
+ console.log(line);
564
+ }
565
+ const isWin = result.winner === "a";
566
+ const isDraw = result.winner === "draw";
567
+ let expGain;
568
+ if (isDraw) {
569
+ expGain = 10;
570
+ console.log(`
571
+ \u{1F91D} \u5E73\u5C40\uFF01${result.rounds} \u56DE\u5408\u540E\u53CC\u65B9\u7CBE\u75B2\u529B\u7AED`);
572
+ } else if (isWin) {
573
+ expGain = 30;
574
+ lobster.wins++;
575
+ lobster.streak = Math.max(0, lobster.streak) + 1;
576
+ lobster.reputation++;
577
+ console.log(`
578
+ \u{1F3C6} \u80DC\u5229\uFF01${lobster.name} \u5728 ${result.rounds} \u56DE\u5408\u540E\u51FB\u8D25\u4E86 ${opponent.name}\uFF01`);
579
+ } else {
580
+ expGain = 10;
581
+ lobster.losses++;
582
+ lobster.streak = Math.min(0, lobster.streak) - 1;
583
+ lobster.reputation = Math.max(0, lobster.reputation - 1);
584
+ console.log(`
585
+ \u{1F480} \u5931\u8D25\u2026${lobster.name} \u5728 ${result.rounds} \u56DE\u5408\u540E\u88AB ${opponent.name} \u51FB\u8D25\u3002`);
586
+ }
587
+ const actual = Math.min(expGain, lobster.daily_exp_cap - lobster.today_exp);
588
+ if (actual > 0) {
589
+ lobster.exp += actual;
590
+ lobster.today_exp += actual;
591
+ }
592
+ console.log(`\u7ECF\u9A8C +${actual} | \u6218\u7EE9: ${lobster.wins}\u80DC ${lobster.losses}\u8D1F | \u8FDE\u80DC: ${lobster.streak}`);
593
+ lobster.last_battle = (/* @__PURE__ */ new Date()).toISOString();
594
+ const resultStr = isDraw ? "\u5E73\u5C40" : isWin ? "\u80DC\u5229" : "\u5931\u8D25";
595
+ await appendLog(`\u2694\uFE0F VS ${opponent.name}(Lv.${opponent.level}) \u2192 ${resultStr} (${result.rounds}\u56DE\u5408)`);
596
+ await writeLobster(lobster);
597
+ }
598
+
599
+ // src/commands/feed.ts
600
+ var FOOD_TYPES = {
601
+ protein: { exp: 15, label: "\u9AD8\u86CB\u767D\u98DF\u7269", statBias: "attack" },
602
+ algae: { exp: 10, label: "\u85FB\u7C7B\u98DF\u7269", statBias: "hp" },
603
+ mineral: { exp: 12, label: "\u77FF\u7269\u8D28", statBias: "defense" }
604
+ };
605
+ async function feed(foodType) {
606
+ const lobster = await readLobster();
607
+ if (!lobster) {
608
+ console.log("\n\u{1F95A} \u8FD8\u6CA1\u6709\u9F99\u867E\u3002\u8FD0\u884C npx clawfight hatch \u6765\u5B75\u5316\u4E00\u53EA\uFF01");
609
+ return;
610
+ }
611
+ if (!foodType || !FOOD_TYPES[foodType]) {
612
+ console.log("\n\u{1F37D}\uFE0F \u53EF\u7528\u98DF\u7269\u7C7B\u578B:");
613
+ for (const [key, info] of Object.entries(FOOD_TYPES)) {
614
+ console.log(` ${key.padEnd(10)} \u2014 ${info.label} (+${info.exp} EXP, ${info.statBias} \u504F\u5411)`);
615
+ }
616
+ console.log("\n\u7528\u6CD5: npx clawfight feed <food_type>");
617
+ return;
618
+ }
619
+ const food = FOOD_TYPES[foodType];
620
+ const actual = Math.min(food.exp, lobster.daily_exp_cap - lobster.today_exp);
621
+ if (actual <= 0) {
622
+ console.log(`
623
+ \u26A0\uFE0F ${lobster.name} \u4ECA\u5929\u5DF2\u7ECF\u5403\u9971\u4E86\uFF08\u6BCF\u65E5\u7ECF\u9A8C\u4E0A\u9650 ${lobster.daily_exp_cap}\uFF09`);
624
+ return;
625
+ }
626
+ lobster.exp += actual;
627
+ lobster.today_exp += actual;
628
+ const stats = lobster.stats;
629
+ if (food.statBias in stats) {
630
+ const bonus = Math.random() > 0.5 ? 1 : 0;
631
+ if (bonus > 0) {
632
+ stats[food.statBias] += bonus;
633
+ console.log(`
634
+ \u{1F37D}\uFE0F ${lobster.name} \u5403\u4E86 ${food.label}\uFF01`);
635
+ console.log(` \u7ECF\u9A8C +${actual} | ${food.statBias} +${bonus}`);
636
+ } else {
637
+ console.log(`
638
+ \u{1F37D}\uFE0F ${lobster.name} \u5403\u4E86 ${food.label}\uFF01`);
639
+ console.log(` \u7ECF\u9A8C +${actual}`);
640
+ }
641
+ }
642
+ await appendLog(`\u{1F37D}\uFE0F \u5582\u98DF: ${food.label} \u2192 EXP+${actual}`);
643
+ await writeLobster(lobster);
644
+ console.log(` \u4ECA\u65E5\u7ECF\u9A8C: ${lobster.today_exp}/${lobster.daily_exp_cap}`);
645
+ }
646
+
647
+ // src/commands/leaderboard.ts
648
+ var RARITY_SYMBOLS = {
649
+ common: " ",
650
+ calico: "\u{1F7E1}",
651
+ blue: "\u{1F535}",
652
+ yellow: "\u{1F31F}",
653
+ split: "\u{1F48E}",
654
+ albino: "\u2B1C"
655
+ };
656
+ async function leaderboard() {
657
+ console.log("\n\u{1F4E1} \u83B7\u53D6\u5168\u7403\u6392\u884C\u699C...");
658
+ const data = await apiLeaderboard(20);
659
+ if (!data) {
660
+ console.log("\u26A0\uFE0F \u670D\u52A1\u5668\u4E0D\u53EF\u8FBE\uFF0C\u65E0\u6CD5\u83B7\u53D6\u6392\u884C\u699C\u3002");
661
+ return;
662
+ }
663
+ if (data.leaderboard.length === 0) {
664
+ console.log("\n\u{1F99E} \u8FD8\u6CA1\u6709\u9F99\u867E\u4E0A\u699C\u3002\u6210\u4E3A\u7B2C\u4E00\u4E2A\u5427\uFF01");
665
+ return;
666
+ }
667
+ console.log("\n" + "\u2550".repeat(60));
668
+ console.log(" \u{1F99E} ClawFight \u5168\u7403\u6392\u884C\u699C");
669
+ console.log("\u2550".repeat(60));
670
+ console.log(` ${"\u6392\u540D".padEnd(6)} ${"\u540D\u79F0".padEnd(16)} ${"\u7B49\u7EA7".padEnd(8)} ${"\u80DC\u573A".padEnd(8)} ${"\u80DC\u7387".padEnd(8)}`);
671
+ console.log("\u2500".repeat(60));
672
+ for (const entry of data.leaderboard) {
673
+ const sym = RARITY_SYMBOLS[entry.rarity] || " ";
674
+ const name = entry.name.length > 12 ? entry.name.slice(0, 11) + "\u2026" : entry.name;
675
+ console.log(
676
+ ` ${sym} #${String(entry.rank).padEnd(4)} ${name.padEnd(14)} Lv.${String(entry.level).padEnd(5)} ${String(entry.wins).padEnd(7)} ${entry.win_rate}%`
677
+ );
678
+ }
679
+ console.log("\u2500".repeat(60));
680
+ console.log(` \u603B\u9F99\u867E: ${data.total_lobsters} | \u6D3B\u8DC3: ${data.active_lobsters}`);
681
+ console.log("\u2550".repeat(60));
682
+ }
683
+
684
+ // src/index.ts
685
+ var program = new Command();
686
+ program.name("clawfight").description("\u{1F99E} ClawFight \u2014 \u9F99\u867E\u7535\u5B50\u5BA0\u7269\u5BF9\u6218").version("0.1.0");
687
+ program.command("hatch").description("\u5B75\u5316\u4E00\u53EA\u65B0\u9F99\u867E").argument("[name]", "\u4E3A\u9F99\u867E\u53D6\u540D").action(async (name) => {
688
+ await hatch(name);
689
+ });
690
+ program.command("status").description("\u67E5\u770B\u9F99\u867E\u72B6\u6001").action(async () => {
691
+ await status();
692
+ });
693
+ program.command("patrol").description("\u5DE1\u903B\u7B7E\u5230\uFF0C\u89E6\u53D1\u968F\u673A\u4E8B\u4EF6\u548C\u906D\u9047").action(async () => {
694
+ await patrol();
695
+ });
696
+ program.command("battle").description("\u8FDB\u884C\u4E00\u573A\u6218\u6597").action(async () => {
697
+ await battle();
698
+ });
699
+ program.command("feed").description("\u5582\u517B\u9F99\u867E").argument("[food_type]", "\u98DF\u7269\u7C7B\u578B: protein, algae, mineral").action(async (foodType) => {
700
+ await feed(foodType);
701
+ });
702
+ program.command("leaderboard").alias("lb").description("\u67E5\u770B\u5168\u7403\u6392\u884C\u699C").action(async () => {
703
+ await leaderboard();
704
+ });
705
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@2025-6-19/clawfight",
3
+ "version": "0.1.0",
4
+ "description": "ClawFight — raise and battle a unique lobster pet with evolving soul 🦞",
5
+ "author": "LIU",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/clawfight/clawfight",
10
+ "directory": "packages/cli"
11
+ },
12
+ "homepage": "https://github.com/clawfight/clawfight#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/clawfight/clawfight/issues"
15
+ },
16
+ "bin": {
17
+ "clawfight": "./dist/index.js"
18
+ },
19
+ "type": "module",
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "dependencies": {
29
+ "commander": "^14.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "tsup": "^8.0.0",
33
+ "typescript": "^5.5.0",
34
+ "@types/node": "^22.0.0"
35
+ },
36
+ "keywords": [
37
+ "openclaw",
38
+ "clawfight",
39
+ "lobster",
40
+ "pet",
41
+ "battle",
42
+ "idle",
43
+ "virtual-pet",
44
+ "cli"
45
+ ]
46
+ }