kill-switch-mcp 1.0.1 → 1.1.2

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,263 @@
1
+ // formatter.ts - State formatting for CLI and agent consumption
2
+ // Adapted from webclient/src/bot/formatters.ts for SDK use
3
+
4
+ import type { BotWorldState, PrayerName } from './types';
5
+ import { PRAYER_NAMES } from './types';
6
+
7
+ /**
8
+ * Format a duration in ms to human readable string
9
+ */
10
+ function formatAge(ms: number): string {
11
+ if (ms < 1000) return 'just now';
12
+ const seconds = Math.floor(ms / 1000);
13
+ if (seconds < 60) return `${seconds}s ago`;
14
+ const minutes = Math.floor(seconds / 60);
15
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s ago`;
16
+ const hours = Math.floor(minutes / 60);
17
+ return `${hours}h ${minutes % 60}m ago`;
18
+ }
19
+
20
+ /**
21
+ * Format world state as readable plaintext/markdown
22
+ */
23
+ export function formatWorldState(state: BotWorldState, stateAgeMs?: number): string {
24
+ const lines: string[] = [];
25
+
26
+ lines.push('# World State');
27
+ const ageStr = stateAgeMs !== undefined ? ` | Updated: ${formatAge(stateAgeMs)}` : '';
28
+ lines.push(`Tick: ${state.tick} | In Game: ${state.inGame}${ageStr}`);
29
+
30
+ // Player info
31
+ if (state.player) {
32
+ const p = state.player;
33
+ lines.push('');
34
+ lines.push('## Player');
35
+ lines.push(`Name: ${p.name} (Combat ${p.combatLevel})`);
36
+ lines.push(`Position: (${p.worldX}, ${p.worldZ}) Level ${p.level}`);
37
+
38
+ // Combat status
39
+ if (p.combat.inCombat) {
40
+ const target = state.nearbyNpcs.find(n => n.index === p.combat.targetIndex);
41
+ if (target) {
42
+ const hpStr = target.maxHp > 0 ? ` HP: ${target.hp}/${target.maxHp}` : '';
43
+ lines.push(`In Combat: ${target.name}${hpStr}`);
44
+ } else {
45
+ lines.push(`In Combat: target index ${p.combat.targetIndex}`);
46
+ }
47
+ }
48
+
49
+ // Recent combat events
50
+ if (state.combatEvents && state.combatEvents.length > 0) {
51
+ const recentEvents = state.combatEvents.slice(-5);
52
+ for (const evt of recentEvents) {
53
+ const ticksAgo = state.tick - evt.tick;
54
+ const ago = ticksAgo > 0 ? ` (${ticksAgo} ticks ago)` : '';
55
+ if (evt.type === 'damage_taken') {
56
+ lines.push(` <- Took ${evt.damage} damage${ago}`);
57
+ } else if (evt.type === 'damage_dealt') {
58
+ lines.push(` -> Dealt ${evt.damage} damage${ago}`);
59
+ } else if (evt.type === 'kill') {
60
+ lines.push(` ** Kill${ago}`);
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ // Modal/dialog state (important for understanding game state)
67
+ if (state.modalOpen) {
68
+ lines.push('');
69
+ lines.push(`## Modal Open (interface: ${state.modalInterface})`);
70
+ if (state.modalInterface === 269) {
71
+ lines.push('(Character design screen - use acceptCharacterDesign to continue)');
72
+ }
73
+ }
74
+
75
+ if (state.dialog.isOpen) {
76
+ lines.push('');
77
+ lines.push('## Dialog');
78
+ if (state.dialog.isWaiting) {
79
+ lines.push('(Waiting for server response...)');
80
+ } else if (state.dialog.options.length > 0) {
81
+ lines.push('Options:');
82
+ for (const opt of state.dialog.options) {
83
+ lines.push(` ${opt.index}. ${opt.text}`);
84
+ }
85
+ } else {
86
+ lines.push('(Click to continue - use optionIndex: 0)');
87
+ }
88
+ }
89
+
90
+ // Interface state (crafting menus, etc.)
91
+ if (state.interface && state.interface.isOpen) {
92
+ lines.push('');
93
+ lines.push(`## Interface (id: ${state.interface.interfaceId})`);
94
+ if (state.interface.options.length > 0) {
95
+ lines.push('Options:');
96
+ for (const opt of state.interface.options) {
97
+ lines.push(` ${opt.index}. ${opt.text}`);
98
+ }
99
+ }
100
+ }
101
+
102
+ // Shop state
103
+ if (state.shop && state.shop.isOpen) {
104
+ lines.push('');
105
+ lines.push('## Shop');
106
+ lines.push(`Title: ${state.shop.title}`);
107
+ lines.push('');
108
+ lines.push('**Items for sale:**');
109
+ if (state.shop.shopItems.length === 0) {
110
+ lines.push(' (Empty)');
111
+ } else {
112
+ for (const item of state.shop.shopItems) {
113
+ lines.push(` [${item.slot}] ${item.name} x${item.count} - buy: ${item.buyPrice}gp`);
114
+ }
115
+ }
116
+ lines.push('');
117
+ lines.push('**Your items (to sell):**');
118
+ if (state.shop.playerItems.length === 0) {
119
+ lines.push(' (Empty)');
120
+ } else {
121
+ for (const item of state.shop.playerItems) {
122
+ lines.push(` [${item.slot}] ${item.name} x${item.count} - sell: ${item.sellPrice}gp`);
123
+ }
124
+ }
125
+ }
126
+
127
+ // Skills
128
+ lines.push('');
129
+ lines.push('## Skills');
130
+ for (const skill of state.skills) {
131
+ const boosted = skill.level !== skill.baseLevel ? `${skill.level}/` : '';
132
+ lines.push(`${skill.name}: ${boosted}${skill.baseLevel} (${skill.experience.toLocaleString()} xp)`);
133
+ }
134
+
135
+ // Inventory
136
+ lines.push('');
137
+ const usedSlots = state.inventory.length;
138
+ const maxSlots = 28;
139
+ const emptySlots = maxSlots - usedSlots;
140
+ lines.push(`## Inventory (${emptySlots} empty slots)`);
141
+ if (state.inventory.length === 0) {
142
+ lines.push('(Empty)');
143
+ } else {
144
+ const itemCounts = new Map<string, { count: number; options: string[] }>();
145
+ for (const item of state.inventory) {
146
+ const existing = itemCounts.get(item.name);
147
+ if (existing) {
148
+ existing.count += item.count;
149
+ } else {
150
+ itemCounts.set(item.name, {
151
+ count: item.count,
152
+ options: item.optionsWithIndex?.map(o => o.text) ?? []
153
+ });
154
+ }
155
+ }
156
+ for (const [name, data] of itemCounts) {
157
+ const opts = data.options.length > 0 ? ` [${data.options.join(', ')}]` : '';
158
+ lines.push(`- ${name} x${data.count}${opts}`);
159
+ }
160
+ }
161
+
162
+ // Equipment
163
+ if (state.equipment.length > 0) {
164
+ lines.push('');
165
+ lines.push('## Equipment');
166
+ for (const item of state.equipment) {
167
+ lines.push(`- ${item.name}`);
168
+ }
169
+ }
170
+
171
+ // Combat style
172
+ if (state.combatStyle) {
173
+ lines.push('');
174
+ lines.push('## Combat Style');
175
+ lines.push(`Weapon: ${state.combatStyle.weaponName}`);
176
+ const current = state.combatStyle.styles[state.combatStyle.currentStyle];
177
+ if (current) {
178
+ lines.push(`Style: ${current.name} (${current.type}) - trains ${current.trainedSkill}`);
179
+ }
180
+ }
181
+
182
+ // Prayers
183
+ if (state.prayers) {
184
+ const activePrayers = state.prayers.activePrayers
185
+ .map((active, i) => active ? PRAYER_NAMES[i] : null)
186
+ .filter((name): name is PrayerName => name !== null);
187
+ if (activePrayers.length > 0) {
188
+ lines.push('');
189
+ lines.push('## Active Prayers');
190
+ lines.push(`Points: ${state.prayers.prayerPoints}/${state.prayers.prayerLevel}`);
191
+ lines.push(`Active: ${activePrayers.join(', ')}`);
192
+ }
193
+ }
194
+
195
+ // Nearby NPCs
196
+ if (state.nearbyNpcs.length > 0) {
197
+ lines.push('');
198
+ lines.push('## Nearby NPCs');
199
+ for (const npc of state.nearbyNpcs.slice(0, 10)) {
200
+ const lvl = npc.combatLevel > 0 ? ` (Lvl ${npc.combatLevel})` : '';
201
+ const hp = npc.maxHp > 0 ? ` HP: ${npc.hp}/${npc.maxHp}` : '';
202
+ const combat = npc.inCombat ? ' [in combat]' : '';
203
+ const opts = npc.options?.length > 0 ? ` [${npc.options.join(', ')}]` : '';
204
+ lines.push(`- ${npc.name}${lvl}${hp}${combat} - ${npc.distance} tiles (idx: ${npc.index})${opts}`);
205
+ }
206
+ if (state.nearbyNpcs.length > 10) {
207
+ lines.push(` ... and ${state.nearbyNpcs.length - 10} more`);
208
+ }
209
+ }
210
+
211
+ // Nearby Players
212
+ if (state.nearbyPlayers.length > 0) {
213
+ lines.push('');
214
+ lines.push('## Nearby Players');
215
+ for (const pl of state.nearbyPlayers.slice(0, 5)) {
216
+ lines.push(`- ${pl.name} (Combat ${pl.combatLevel}) - ${pl.distance} tiles`);
217
+ }
218
+ if (state.nearbyPlayers.length > 5) {
219
+ lines.push(` ... and ${state.nearbyPlayers.length - 5} more`);
220
+ }
221
+ }
222
+
223
+ // Nearby Locs
224
+ if (state.nearbyLocs.length > 0) {
225
+ lines.push('');
226
+ lines.push('## Nearby Objects');
227
+ for (const loc of state.nearbyLocs.slice(0, 10)) {
228
+ const opts = loc.options?.length > 0 ? ` [${loc.options.join(', ')}]` : '';
229
+ lines.push(`- ${loc.name} at (${loc.x}, ${loc.z}) - ${loc.distance} tiles${opts}`);
230
+ }
231
+ if (state.nearbyLocs.length > 10) {
232
+ lines.push(` ... and ${state.nearbyLocs.length - 10} more`);
233
+ }
234
+ }
235
+
236
+ // Ground Items
237
+ if (state.groundItems.length > 0) {
238
+ lines.push('');
239
+ lines.push('## Ground Items');
240
+ for (const item of state.groundItems.slice(0, 10)) {
241
+ lines.push(`- ${item.name} x${item.count} at (${item.x}, ${item.z}) - ${item.distance} tiles`);
242
+ }
243
+ if (state.groundItems.length > 10) {
244
+ lines.push(` ... and ${state.groundItems.length - 10} more`);
245
+ }
246
+ }
247
+
248
+ // Recent messages
249
+ if (state.gameMessages && state.gameMessages.length > 0) {
250
+ lines.push('');
251
+ lines.push('## Recent Messages');
252
+ for (const msg of state.gameMessages.slice(-5)) {
253
+ const cleanText = msg.text.replace(/@\w+@/g, '');
254
+ if (msg.sender) {
255
+ lines.push(`- ${msg.sender}: ${cleanText}`);
256
+ } else {
257
+ lines.push(`- ${cleanText}`);
258
+ }
259
+ }
260
+ }
261
+
262
+ return lines.join('\n');
263
+ }