@pilaf/framework 1.2.2 → 1.3.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/lib/StoryRunner.js +1571 -22
- package/lib/helpers/correlation.js +325 -0
- package/lib/helpers/entities.js +344 -0
- package/lib/index.js +4 -0
- package/package.json +4 -3
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CorrelationUtils - Server-side correlation for player actions
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for correlating player actions with server-side
|
|
5
|
+
* confirmation events. Works with or without EventObserver:
|
|
6
|
+
*
|
|
7
|
+
* - WITH EventObserver: Event-driven correlation (faster, more reliable)
|
|
8
|
+
* - WITHOUT EventObserver: Timeout-based fallback (simple, compatible)
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities (MECE):
|
|
11
|
+
* - ONLY: Wait for server confirmation of player actions
|
|
12
|
+
* - NOT: Execute player actions (use bot methods directly)
|
|
13
|
+
* - NOT: Parse server logs (EventObserver/LogParser's responsibility)
|
|
14
|
+
* - NOT: Collect logs (LogCollector's responsibility)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Simple glob pattern matcher
|
|
19
|
+
*
|
|
20
|
+
* Supports:
|
|
21
|
+
* - * : matches any sequence of characters
|
|
22
|
+
* - ? : matches any single character
|
|
23
|
+
*
|
|
24
|
+
* @private
|
|
25
|
+
* @param {string} text - Text to match against
|
|
26
|
+
* @param {string} pattern - Glob pattern (e.g., "*placed*", "entity.*")
|
|
27
|
+
* @returns {boolean} True if pattern matches
|
|
28
|
+
*/
|
|
29
|
+
function _matchPattern(text, pattern) {
|
|
30
|
+
if (!text || !pattern) return false;
|
|
31
|
+
|
|
32
|
+
// Convert glob pattern to regex
|
|
33
|
+
// First escape all special regex characters except * and ?
|
|
34
|
+
let regexPattern = pattern
|
|
35
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&'); // Escape special regex chars
|
|
36
|
+
|
|
37
|
+
// Then convert * and ? to their regex equivalents
|
|
38
|
+
regexPattern = '^' + regexPattern
|
|
39
|
+
.replace(/\*/g, '.*') // * becomes .*
|
|
40
|
+
.replace(/\?/g, '.') + '$'; // ? becomes .
|
|
41
|
+
|
|
42
|
+
const regex = new RegExp(regexPattern, 'i'); // Case-insensitive
|
|
43
|
+
return regex.test(text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wait for server confirmation of a player action
|
|
48
|
+
*
|
|
49
|
+
* This is the main correlation utility. It waits for a server log event
|
|
50
|
+
* that matches the expected pattern, confirming that the player's action
|
|
51
|
+
* was processed by the server.
|
|
52
|
+
*
|
|
53
|
+
* Supports two modes:
|
|
54
|
+
* 1. Event-driven mode (with EventObserver): Listens for matching events
|
|
55
|
+
* 2. Timeout mode (without EventObserver): Waits for specified timeout
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} storyRunner - StoryRunner instance (provides access to backends)
|
|
58
|
+
* @param {Object} options - Options
|
|
59
|
+
* @param {string} options.pattern - Glob pattern to match in server logs
|
|
60
|
+
* @param {number} [options.timeout=5000] - Timeout in milliseconds
|
|
61
|
+
* @param {boolean} [options.invert=false] - If true, wait for ABSENCE of pattern
|
|
62
|
+
* @param {string} [options.player] - Player name to filter events (optional)
|
|
63
|
+
* @returns {Promise<Object|null>} Matching event object, or null if timeout/inverted
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* // Wait for block placement confirmation
|
|
67
|
+
* await CorrelationUtils.waitForServerConfirmation(storyRunner, {
|
|
68
|
+
* pattern: '*Cannot place block*',
|
|
69
|
+
* invert: true,
|
|
70
|
+
* timeout: 3000
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Wait for command execution
|
|
75
|
+
* const event = await CorrelationUtils.waitForServerConfirmation(storyRunner, {
|
|
76
|
+
* pattern: '*issued command* /gamemode*',
|
|
77
|
+
* timeout: 5000
|
|
78
|
+
* });
|
|
79
|
+
*/
|
|
80
|
+
async function waitForServerConfirmation(storyRunner, options) {
|
|
81
|
+
const {
|
|
82
|
+
pattern = '*',
|
|
83
|
+
timeout = 5000,
|
|
84
|
+
invert = false,
|
|
85
|
+
player = null
|
|
86
|
+
} = options || {};
|
|
87
|
+
|
|
88
|
+
// Try EventObserver mode first (event-driven, faster)
|
|
89
|
+
const eventObserver = _getEventObserver(storyRunner);
|
|
90
|
+
|
|
91
|
+
if (eventObserver && eventObserver.isObserving) {
|
|
92
|
+
return await _waitForEventObserver(eventObserver, {
|
|
93
|
+
pattern,
|
|
94
|
+
timeout,
|
|
95
|
+
invert,
|
|
96
|
+
player
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fall back to timeout mode (simple, works without EventObserver)
|
|
101
|
+
return await _waitForTimeout({ timeout, invert });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get EventObserver from StoryRunner's backends
|
|
106
|
+
*
|
|
107
|
+
* Tries multiple strategies to find an available EventObserver:
|
|
108
|
+
* 1. Any player backend's EventObserver (MineflayerBackend)
|
|
109
|
+
* 2. RCON backend's EventObserver (if available)
|
|
110
|
+
*
|
|
111
|
+
* @private
|
|
112
|
+
* @param {Object} storyRunner - StoryRunner instance
|
|
113
|
+
* @returns {EventObserver|null} EventObserver instance or null
|
|
114
|
+
*/
|
|
115
|
+
function _getEventObserver(storyRunner) {
|
|
116
|
+
if (!storyRunner || !storyRunner.backends) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Strategy 1: Check player backends (MineflayerBackend may have EventObserver)
|
|
121
|
+
if (storyRunner.backends.players) {
|
|
122
|
+
for (const [username, backend] of storyRunner.backends.players) {
|
|
123
|
+
if (backend && typeof backend.getEventObserver === 'function') {
|
|
124
|
+
const observer = backend.getEventObserver();
|
|
125
|
+
if (observer && observer.isObserving) {
|
|
126
|
+
return observer;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Strategy 2: Check RCON backend (unlikely to have EventObserver, but future-proof)
|
|
133
|
+
if (storyRunner.backends.rcon && typeof storyRunner.backends.rcon.getEventObserver === 'function') {
|
|
134
|
+
const observer = storyRunner.backends.rcon.getEventObserver();
|
|
135
|
+
if (observer && observer.isObserving) {
|
|
136
|
+
return observer;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Wait for event using EventObserver (event-driven mode)
|
|
145
|
+
*
|
|
146
|
+
* @private
|
|
147
|
+
* @param {EventObserver} eventObserver - EventObserver instance
|
|
148
|
+
* @param {Object} options - Options
|
|
149
|
+
* @returns {Promise<Object|null>} Matching event or null
|
|
150
|
+
*/
|
|
151
|
+
async function _waitForEventObserver(eventObserver, options) {
|
|
152
|
+
const { pattern, timeout, invert, player } = options;
|
|
153
|
+
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
let timer = null;
|
|
156
|
+
let unsubscribe = null;
|
|
157
|
+
|
|
158
|
+
const cleanup = (result) => {
|
|
159
|
+
if (timer) {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
timer = null;
|
|
162
|
+
}
|
|
163
|
+
if (unsubscribe) {
|
|
164
|
+
unsubscribe();
|
|
165
|
+
unsubscribe = null;
|
|
166
|
+
}
|
|
167
|
+
resolve(result);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Set timeout
|
|
171
|
+
timer = setTimeout(() => {
|
|
172
|
+
cleanup(null); // Timeout - return null
|
|
173
|
+
}, timeout);
|
|
174
|
+
|
|
175
|
+
// Subscribe to all events
|
|
176
|
+
unsubscribe = eventObserver.onEvent('*', (event) => {
|
|
177
|
+
// Filter by player if specified
|
|
178
|
+
if (player && event.data?.player !== player) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if pattern matches
|
|
183
|
+
const matches = _checkEventMatch(event, pattern);
|
|
184
|
+
|
|
185
|
+
if (invert) {
|
|
186
|
+
// Inverted mode: wait for event that DOESN'T match pattern
|
|
187
|
+
// For inverted mode, we can't easily detect "absence" of events
|
|
188
|
+
// So we just wait for timeout and verify no matching events occurred
|
|
189
|
+
// This is a simplified approach - could be enhanced with event tracking
|
|
190
|
+
if (!matches) {
|
|
191
|
+
// Got a non-matching event, but we can't conclude yet
|
|
192
|
+
// Continue waiting for timeout or matching event
|
|
193
|
+
} else {
|
|
194
|
+
// Got a matching event in inverted mode - this is "bad"
|
|
195
|
+
// We could reject here, but for now we just continue
|
|
196
|
+
// The caller will interpret null as "no matching events during timeout"
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// Normal mode: wait for event that MATCHES pattern
|
|
200
|
+
if (matches) {
|
|
201
|
+
cleanup(event); // Found matching event - return it
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// For inverted mode, we need to track if we saw any matching events
|
|
207
|
+
// This is a simple implementation - could be enhanced
|
|
208
|
+
if (invert) {
|
|
209
|
+
// In inverted mode, we just wait for timeout
|
|
210
|
+
// The assumption is: no matching events = success
|
|
211
|
+
// This is not perfect but works for simple cases
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Wait using simple timeout (fallback mode)
|
|
218
|
+
*
|
|
219
|
+
* @private
|
|
220
|
+
* @param {Object} options - Options
|
|
221
|
+
* @returns {Promise<null>} Always returns null after timeout
|
|
222
|
+
*/
|
|
223
|
+
async function _waitForTimeout(options) {
|
|
224
|
+
const { timeout } = options;
|
|
225
|
+
await new Promise(resolve => setTimeout(resolve, timeout));
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if event matches the pattern
|
|
231
|
+
*
|
|
232
|
+
* Checks multiple fields in the event for pattern matching:
|
|
233
|
+
* - event.type (e.g., "command.issued", "block.broken")
|
|
234
|
+
* - event.message (raw log message)
|
|
235
|
+
* - event.data (additional event data)
|
|
236
|
+
*
|
|
237
|
+
* @private
|
|
238
|
+
* @param {Object} event - Event object from EventObserver
|
|
239
|
+
* @param {string} pattern - Glob pattern to match
|
|
240
|
+
* @returns {boolean} True if pattern matches event
|
|
241
|
+
*/
|
|
242
|
+
function _checkEventMatch(event, pattern) {
|
|
243
|
+
if (!event || !pattern) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check event type
|
|
248
|
+
if (event.type && _matchPattern(event.type, pattern)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check raw message (if available)
|
|
253
|
+
if (event.message && _matchPattern(event.message, pattern)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check data fields
|
|
258
|
+
if (event.data) {
|
|
259
|
+
// Convert data to string for matching
|
|
260
|
+
const dataStr = JSON.stringify(event.data);
|
|
261
|
+
if (_matchPattern(dataStr, pattern)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get default timeout for an action type
|
|
271
|
+
*
|
|
272
|
+
* Returns reasonable defaults for different action types:
|
|
273
|
+
* - Block actions: 3-5 seconds
|
|
274
|
+
* - Movement: 1-2 seconds
|
|
275
|
+
* - Entity interaction: 3-5 seconds
|
|
276
|
+
* - Commands: 2-3 seconds
|
|
277
|
+
*
|
|
278
|
+
* @param {string} actionType - Action type (e.g., 'break_block', 'place_block')
|
|
279
|
+
* @returns {number} Timeout in milliseconds
|
|
280
|
+
*/
|
|
281
|
+
function getDefaultTimeout(actionType) {
|
|
282
|
+
const timeouts = {
|
|
283
|
+
// Block interaction (slowest)
|
|
284
|
+
'break_block': 5000,
|
|
285
|
+
'place_block': 5000,
|
|
286
|
+
'interact_with_block': 3000,
|
|
287
|
+
|
|
288
|
+
// Movement (fast)
|
|
289
|
+
'move_forward': 2000,
|
|
290
|
+
'move_backward': 2000,
|
|
291
|
+
'move_left': 2000,
|
|
292
|
+
'move_right': 2000,
|
|
293
|
+
'jump': 1000,
|
|
294
|
+
'look_at': 1000,
|
|
295
|
+
'navigate_to': 15000, // Pathfinding can be slow
|
|
296
|
+
|
|
297
|
+
// Entity interaction (medium)
|
|
298
|
+
'attack_entity': 3000,
|
|
299
|
+
'interact_with_entity': 5000,
|
|
300
|
+
'mount_entity': 3000,
|
|
301
|
+
'dismount': 1000,
|
|
302
|
+
|
|
303
|
+
// Inventory (medium)
|
|
304
|
+
'drop_item': 2000,
|
|
305
|
+
'consume_item': 2000,
|
|
306
|
+
'equip_item': 2000,
|
|
307
|
+
'swap_inventory_slots': 1000,
|
|
308
|
+
|
|
309
|
+
// Commands (fast)
|
|
310
|
+
'execute_command': 3000,
|
|
311
|
+
'execute_player_command': 3000,
|
|
312
|
+
'chat': 2000,
|
|
313
|
+
|
|
314
|
+
// Default
|
|
315
|
+
'default': 5000
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
return timeouts[actionType] || timeouts['default'];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
waitForServerConfirmation,
|
|
323
|
+
_matchPattern, // Exported for testing
|
|
324
|
+
getDefaultTimeout
|
|
325
|
+
};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EntityUtils - Utility functions for working with Mineflayer entities
|
|
3
|
+
*
|
|
4
|
+
* Provides helper methods for finding and querying entities in a
|
|
5
|
+
* Mineflayer bot's entity list.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities (MECE):
|
|
8
|
+
* - ONLY: Entity lookup and query utilities
|
|
9
|
+
* - NOT: Entity manipulation (use bot methods directly)
|
|
10
|
+
* - NOT: Entity spawning/despawning (server-side actions)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Entity alias mappings
|
|
15
|
+
* Maps generic entity names to their specific Minecraft entity type names
|
|
16
|
+
* Used when the summon command accepts a generic name but spawns a specific variant
|
|
17
|
+
*
|
|
18
|
+
* @private
|
|
19
|
+
* @constant {Object<string, string[]>}
|
|
20
|
+
*/
|
|
21
|
+
const ENTITY_ALIASES = {
|
|
22
|
+
// Generic boat → oak_boat (default boat type in Minecraft)
|
|
23
|
+
'boat': ['oak_boat', 'boat'],
|
|
24
|
+
// Generic horse → horse (already correct)
|
|
25
|
+
// Generic minecart → minecart (already correct)
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get all possible entity names to search for an entity
|
|
30
|
+
* Includes the original name plus any known aliases/variants
|
|
31
|
+
*
|
|
32
|
+
* @private
|
|
33
|
+
* @param {string} identifier - Entity identifier to search for
|
|
34
|
+
* @returns {Array<string>} Array of possible entity names to search
|
|
35
|
+
*/
|
|
36
|
+
function _getSearchNames(identifier) {
|
|
37
|
+
// Always include the original identifier
|
|
38
|
+
const names = [identifier];
|
|
39
|
+
|
|
40
|
+
// Check if this is an alias for a specific entity type
|
|
41
|
+
if (ENTITY_ALIASES[identifier]) {
|
|
42
|
+
names.push(...ENTITY_ALIASES[identifier]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// For boats, also try all wood variants if searching for generic "boat"
|
|
46
|
+
if (identifier === 'boat') {
|
|
47
|
+
const woodTypes = ['oak', 'birch', 'spruce', 'jungle', 'acacia', 'dark_oak', 'mangrove', 'cherry', 'pale_oak'];
|
|
48
|
+
woodTypes.forEach(wood => {
|
|
49
|
+
names.push(`${wood}_boat`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [...new Set(names)]; // Remove duplicates
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find an entity by identifier
|
|
58
|
+
*
|
|
59
|
+
* Searches for an entity using multiple strategies:
|
|
60
|
+
* 1. Direct entity ID (number)
|
|
61
|
+
* 2. Custom name (named entities, mobs with nametags)
|
|
62
|
+
* 3. Display name (player names, entity display names)
|
|
63
|
+
* 4. Entity type name (zombie, skeleton, etc.) with alias support
|
|
64
|
+
*
|
|
65
|
+
* @param {Object} bot - Mineflayer bot instance
|
|
66
|
+
* @param {number|string} identifier - Entity ID, name, or custom name
|
|
67
|
+
* @returns {Object|null} Entity object or null if not found
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // Find by entity ID
|
|
71
|
+
* const entity = EntityUtils.findEntity(bot, 123);
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Find by custom name
|
|
75
|
+
* const entity = EntityUtils.findEntity(bot, 'MyPet');
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Find by entity type
|
|
79
|
+
* const entity = EntityUtils.findEntity(bot, 'zombie');
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // Find by alias (boat searches for oak_boat, birch_boat, etc.)
|
|
83
|
+
* const entity = EntityUtils.findEntity(bot, 'boat');
|
|
84
|
+
*/
|
|
85
|
+
function findEntity(bot, identifier) {
|
|
86
|
+
if (!bot || !bot.entities) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Strategy 1: Direct entity ID (number)
|
|
91
|
+
if (typeof identifier === 'number') {
|
|
92
|
+
return bot.entities[identifier] || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const allEntities = Object.values(bot.entities).filter(e => e);
|
|
96
|
+
|
|
97
|
+
// Strategy 2: Try custom name (named mobs, nametags)
|
|
98
|
+
let entity = Object.values(bot.entities).find(e => {
|
|
99
|
+
if (!e) return false;
|
|
100
|
+
const customName = e.customName;
|
|
101
|
+
// Check both string and text component formats
|
|
102
|
+
if (typeof customName === 'string') {
|
|
103
|
+
return customName === identifier;
|
|
104
|
+
}
|
|
105
|
+
if (customName && typeof customName === 'object' && customName.text) {
|
|
106
|
+
return customName.text === identifier;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (entity) {
|
|
112
|
+
return entity;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Strategy 3: Try display name (players, some entities)
|
|
116
|
+
entity = Object.values(bot.entities).find(e => {
|
|
117
|
+
if (!e) return false;
|
|
118
|
+
return e.displayName === identifier ||
|
|
119
|
+
(e.username && e.username === identifier);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (entity) {
|
|
123
|
+
return entity;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Strategy 4: Try entity type name with alias support
|
|
127
|
+
const searchNames = _getSearchNames(identifier);
|
|
128
|
+
|
|
129
|
+
entity = Object.values(bot.entities).find(e => {
|
|
130
|
+
if (!e) return false;
|
|
131
|
+
// Check if entity name matches any of the search names
|
|
132
|
+
return searchNames.includes(e.name) ||
|
|
133
|
+
(e.type && searchNames.includes(e.type));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (entity) {
|
|
137
|
+
return entity;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Calculate distance between two positions
|
|
145
|
+
*
|
|
146
|
+
* @private
|
|
147
|
+
* @param {Object} pos1 - First position {x, y, z}
|
|
148
|
+
* @param {Object} pos2 - Second position {x, y, z}
|
|
149
|
+
* @returns {number} Distance in blocks
|
|
150
|
+
*/
|
|
151
|
+
function _calculateDistance(pos1, pos2) {
|
|
152
|
+
if (!pos1 || !pos2) return Infinity;
|
|
153
|
+
const dx = pos2.x - pos1.x;
|
|
154
|
+
const dy = pos2.y - pos1.y;
|
|
155
|
+
const dz = pos2.z - pos1.z;
|
|
156
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get the nearest entity of a specific type
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} bot - Mineflayer bot instance
|
|
163
|
+
* @param {string} typeName - Entity type name (e.g., 'zombie', 'player')
|
|
164
|
+
* @param {number} [maxDistance=32] - Maximum search distance in blocks
|
|
165
|
+
* @returns {Object|null} Nearest entity or null if not found
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* // Find nearest zombie within 32 blocks
|
|
169
|
+
* const zombie = EntityUtils.getNearestEntity(bot, 'zombie', 32);
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* // Find nearest player within 16 blocks
|
|
173
|
+
* const player = EntityUtils.getNearestEntity(bot, 'player', 16);
|
|
174
|
+
*/
|
|
175
|
+
function getNearestEntity(bot, typeName, maxDistance = 32) {
|
|
176
|
+
if (!bot || !bot.entities || !bot.entity) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const playerPos = bot.entity.position;
|
|
181
|
+
|
|
182
|
+
// Find all entities of the specified type within max distance
|
|
183
|
+
const nearbyEntities = Object.values(bot.entities)
|
|
184
|
+
.filter(e => {
|
|
185
|
+
if (!e || !e.position || !e.name) return false;
|
|
186
|
+
// Match entity type
|
|
187
|
+
return e.name === typeName;
|
|
188
|
+
})
|
|
189
|
+
.map(e => ({
|
|
190
|
+
entity: e,
|
|
191
|
+
distance: _calculateDistance(playerPos, e.position)
|
|
192
|
+
}))
|
|
193
|
+
.filter(e => e.distance <= maxDistance);
|
|
194
|
+
|
|
195
|
+
// Sort by distance (nearest first)
|
|
196
|
+
nearbyEntities.sort((a, b) => a.distance - b.distance);
|
|
197
|
+
|
|
198
|
+
// Return nearest entity or null
|
|
199
|
+
return nearbyEntities.length > 0 ? nearbyEntities[0].entity : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get all entities of a specific type within distance
|
|
204
|
+
*
|
|
205
|
+
* @param {Object} bot - Mineflayer bot instance
|
|
206
|
+
* @param {string} typeName - Entity type name
|
|
207
|
+
* @param {number} [maxDistance=32] - Maximum search distance in blocks
|
|
208
|
+
* @returns {Array<Object>} Array of matching entities (sorted by distance)
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* // Get all zombies within 32 blocks
|
|
212
|
+
* const zombies = EntityUtils.getEntitiesOfType(bot, 'zombie', 32);
|
|
213
|
+
*/
|
|
214
|
+
function getEntitiesOfType(bot, typeName, maxDistance = 32) {
|
|
215
|
+
if (!bot || !bot.entities || !bot.entity) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const playerPos = bot.entity.position;
|
|
220
|
+
|
|
221
|
+
const nearbyEntities = Object.values(bot.entities)
|
|
222
|
+
.filter(e => {
|
|
223
|
+
if (!e || !e.position || !e.name) return false;
|
|
224
|
+
return e.name === typeName && _calculateDistance(playerPos, e.position) <= maxDistance;
|
|
225
|
+
})
|
|
226
|
+
.map(e => ({
|
|
227
|
+
entity: e,
|
|
228
|
+
distance: _calculateDistance(playerPos, e.position)
|
|
229
|
+
}))
|
|
230
|
+
.sort((a, b) => a.distance - b.distance);
|
|
231
|
+
|
|
232
|
+
return nearbyEntities.map(e => e.entity);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Find multiple entities by matching a predicate
|
|
237
|
+
*
|
|
238
|
+
* @param {Object} bot - Mineflayer bot instance
|
|
239
|
+
* @param {Function} predicate - Function that returns true for matching entities
|
|
240
|
+
* @param {number} [maxDistance=32] - Maximum search distance in blocks
|
|
241
|
+
* @returns {Array<Object>} Array of matching entities (sorted by distance)
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* // Find all hostile mobs with low health
|
|
245
|
+
* const weakHostiles = EntityUtils.findEntities(bot, (e) => {
|
|
246
|
+
* return e.health < 10 && isHostile(e.name);
|
|
247
|
+
* }, 32);
|
|
248
|
+
*/
|
|
249
|
+
function findEntities(bot, predicate, maxDistance = 32) {
|
|
250
|
+
if (!bot || !bot.entities || !bot.entity || typeof predicate !== 'function') {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const playerPos = bot.entity.position;
|
|
255
|
+
|
|
256
|
+
const matchingEntities = Object.values(bot.entities)
|
|
257
|
+
.filter(e => {
|
|
258
|
+
if (!e || !e.position) return false;
|
|
259
|
+
return _calculateDistance(playerPos, e.position) <= maxDistance && predicate(e);
|
|
260
|
+
})
|
|
261
|
+
.map(e => ({
|
|
262
|
+
entity: e,
|
|
263
|
+
distance: _calculateDistance(playerPos, e.position)
|
|
264
|
+
}))
|
|
265
|
+
.sort((a, b) => a.distance - b.distance);
|
|
266
|
+
|
|
267
|
+
return matchingEntities.map(e => e.entity);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get distance from player to an entity
|
|
272
|
+
*
|
|
273
|
+
* @param {Object} bot - Mineflayer bot instance
|
|
274
|
+
* @param {Object} entity - Entity object
|
|
275
|
+
* @returns {number|null} Distance in blocks, or null if invalid
|
|
276
|
+
*/
|
|
277
|
+
function getDistanceToEntity(bot, entity) {
|
|
278
|
+
if (!bot || !bot.entity || !entity || !entity.position) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return _calculateDistance(bot.entity.position, entity.position);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if an entity is alive (has health > 0)
|
|
287
|
+
*
|
|
288
|
+
* @param {Object} entity - Entity object
|
|
289
|
+
* @returns {boolean} True if entity is alive
|
|
290
|
+
*/
|
|
291
|
+
function isEntityAlive(entity) {
|
|
292
|
+
if (!entity) return false;
|
|
293
|
+
// Some entities may not have health property
|
|
294
|
+
return entity.health === undefined || entity.health > 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get entity display name (with fallbacks)
|
|
299
|
+
*
|
|
300
|
+
* Returns the most appropriate name for display:
|
|
301
|
+
* 1. Custom name (nametag)
|
|
302
|
+
* 2. Username (for players)
|
|
303
|
+
* 3. Display name
|
|
304
|
+
* 4. Entity type name
|
|
305
|
+
*
|
|
306
|
+
* @param {Object} entity - Entity object
|
|
307
|
+
* @returns {string} Display name
|
|
308
|
+
*/
|
|
309
|
+
function getEntityDisplayName(entity) {
|
|
310
|
+
if (!entity) return 'Unknown';
|
|
311
|
+
|
|
312
|
+
// Try custom name first
|
|
313
|
+
if (entity.customName) {
|
|
314
|
+
if (typeof entity.customName === 'string') {
|
|
315
|
+
return entity.customName;
|
|
316
|
+
}
|
|
317
|
+
if (typeof entity.customName === 'object' && entity.customName.text) {
|
|
318
|
+
return entity.customName.text;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Try username (for players)
|
|
323
|
+
if (entity.username) {
|
|
324
|
+
return entity.username;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Try display name
|
|
328
|
+
if (entity.displayName) {
|
|
329
|
+
return entity.displayName;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Fall back to entity type name
|
|
333
|
+
return entity.name || 'Unknown';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = {
|
|
337
|
+
findEntity,
|
|
338
|
+
getNearestEntity,
|
|
339
|
+
getEntitiesOfType,
|
|
340
|
+
findEntities,
|
|
341
|
+
getDistanceToEntity,
|
|
342
|
+
isEntityAlive,
|
|
343
|
+
getEntityDisplayName
|
|
344
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,8 @@ const { PilafReporter } = require('./reporters/pilaf-reporter');
|
|
|
3
3
|
const { StoryRunner } = require('./StoryRunner');
|
|
4
4
|
const { waitForEvents, captureEvents } = require('./helpers/events');
|
|
5
5
|
const { captureState, compareStates } = require('./helpers/state');
|
|
6
|
+
const { CorrelationUtils } = require('./helpers/correlation');
|
|
7
|
+
const { EntityUtils } = require('./helpers/entities');
|
|
6
8
|
const { toHaveReceivedLightningStrikes } = require('./matchers/game-matchers');
|
|
7
9
|
const { createTestContext, cleanupTestContext } = require('./test-context');
|
|
8
10
|
|
|
@@ -49,6 +51,8 @@ module.exports = {
|
|
|
49
51
|
captureEvents,
|
|
50
52
|
captureState,
|
|
51
53
|
compareStates,
|
|
54
|
+
CorrelationUtils,
|
|
55
|
+
EntityUtils,
|
|
52
56
|
|
|
53
57
|
// Matchers
|
|
54
58
|
toHaveReceivedLightningStrikes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pilaf/framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"jest": "^29.7.0",
|
|
22
22
|
"js-yaml": "^4.1.0",
|
|
23
|
-
"
|
|
24
|
-
"@pilaf/
|
|
23
|
+
"mineflayer-pathfinder": "^2.4.5",
|
|
24
|
+
"@pilaf/backends": "1.3.0",
|
|
25
|
+
"@pilaf/reporting": "1.3.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@jest/globals": "^29.7.0"
|