hytopia 0.1.77 → 0.1.78

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,435 @@
1
+ import { Vector3, World, SimpleEntityController, PlayerEntity } from "hytopia";
2
+ import { BaseAgent } from "../BaseAgent";
3
+ import type { AgentBehavior } from "../BaseAgent";
4
+ import { Player } from "hytopia";
5
+
6
+ interface Node {
7
+ x: number;
8
+ z: number;
9
+ g: number; // Cost from start
10
+ h: number; // Heuristic (estimated cost to end)
11
+ f: number; // Total cost (g + h)
12
+ parent?: Node;
13
+ }
14
+
15
+ /**
16
+ * This is a simple implementation of A* pathfinding for Agents.
17
+ * There are many simplifications here, like no diagonal movement, sketchy jump code, and no path smoothing.
18
+ * It is good enough for a simple demo like this, but in a more polished game you would want to use a more robust pathfinding library or implementation.
19
+ */
20
+ export class PathfindingBehavior implements AgentBehavior {
21
+ private path: Vector3[] = [];
22
+ private currentPathIndex: number = 0;
23
+ private targetEntity?: BaseAgent;
24
+ private moveSpeed = 4;
25
+ private isJumping = false; // Track if we're currently in a jump
26
+ private jumpCooldown = 0; // Cooldown timer for jumps
27
+
28
+ onUpdate(agent: BaseAgent, world: World): void {
29
+ if (!(agent.controller instanceof SimpleEntityController)) return;
30
+
31
+ // Decrease jump cooldown
32
+ if (this.jumpCooldown > 0) {
33
+ this.jumpCooldown--;
34
+ }
35
+
36
+ if (this.path.length > 0 && this.currentPathIndex < this.path.length) {
37
+ const nextPoint = this.path[this.currentPathIndex];
38
+ const distance = Vector3.fromVector3Like(agent.position).distance(
39
+ nextPoint
40
+ );
41
+
42
+ // Check if we're close enough to final destination
43
+ const isNearEnd = this.currentPathIndex >= this.path.length - 3;
44
+ if (isNearEnd) {
45
+ const finalPoint = this.path[this.path.length - 1];
46
+ const distanceToFinal = Vector3.fromVector3Like(
47
+ agent.position
48
+ ).distance(finalPoint);
49
+
50
+ if (distanceToFinal < 3) {
51
+ agent.stopModelAnimations(["walk"]);
52
+ agent.startModelLoopedAnimations(["idle"]);
53
+ if (this.targetEntity) {
54
+ agent.controller.face(
55
+ this.targetEntity.position,
56
+ this.moveSpeed * 2
57
+ );
58
+ }
59
+ agent.handleEnvironmentTrigger(
60
+ `You have arrived at your destination.`
61
+ );
62
+ this.targetEntity = undefined;
63
+ this.path = [];
64
+ this.currentPathIndex = 0;
65
+ return;
66
+ }
67
+ }
68
+
69
+ if (distance < 0.5) {
70
+ this.currentPathIndex++;
71
+ this.isJumping = false; // Reset jump state when reaching waypoint
72
+ if (this.currentPathIndex >= this.path.length) {
73
+ if (this.targetEntity) {
74
+ agent.controller.face(
75
+ this.targetEntity.position,
76
+ this.moveSpeed * 2
77
+ );
78
+ agent.stopModelAnimations(["walk"]);
79
+ agent.startModelLoopedAnimations(["idle"]);
80
+
81
+ this.targetEntity = undefined;
82
+ }
83
+ agent.handleEnvironmentTrigger(
84
+ `You have arrived at your destination.`
85
+ );
86
+ return;
87
+ }
88
+ } else {
89
+ const yDiff = nextPoint.y - agent.position.y;
90
+ const horizontalDistance = Math.sqrt(
91
+ Math.pow(nextPoint.x - agent.position.x, 2) +
92
+ Math.pow(nextPoint.z - agent.position.z, 2)
93
+ );
94
+
95
+ if (
96
+ yDiff > 0.5 &&
97
+ horizontalDistance < 1.5 &&
98
+ !this.isJumping &&
99
+ this.jumpCooldown === 0
100
+ ) {
101
+ const direction = Vector3.fromVector3Like(nextPoint)
102
+ .subtract(Vector3.fromVector3Like(agent.position))
103
+ .normalize();
104
+ direction.y = 10 * agent.mass;
105
+ agent.applyImpulse(direction);
106
+ this.isJumping = true;
107
+ this.jumpCooldown = 30;
108
+ }
109
+
110
+ agent.controller.move(nextPoint, this.moveSpeed, {
111
+ moveIgnoreAxes: yDiff >= 0 ? { y: true } : undefined,
112
+ });
113
+ agent.controller.face(nextPoint, this.moveSpeed * 2);
114
+ agent.startModelLoopedAnimations(["walk"]);
115
+ }
116
+ } else if (this.path.length > 0) {
117
+ this.path = [];
118
+ this.currentPathIndex = 0;
119
+ this.isJumping = false;
120
+ this.jumpCooldown = 0;
121
+ agent.stopModelAnimations(["walk"]);
122
+ agent.startModelLoopedAnimations(["idle"]);
123
+ }
124
+ }
125
+
126
+ private isWalkable(
127
+ world: World,
128
+ x: number,
129
+ z: number,
130
+ y: number,
131
+ startY?: number
132
+ ): { walkable: boolean; y?: number; canJumpFrom?: boolean } {
133
+ const maxDropHeight = 3;
134
+ const maxStepUp = startY !== undefined ? 1 : 1.5;
135
+ const legY = Math.floor(y);
136
+
137
+ if (startY !== undefined && legY > startY + 2) {
138
+ return { walkable: false };
139
+ }
140
+
141
+ for (
142
+ let floorY = Math.min(
143
+ legY + maxStepUp,
144
+ startY ? startY + 2 : Infinity
145
+ );
146
+ floorY >= legY - maxDropHeight && floorY >= 0;
147
+ floorY--
148
+ ) {
149
+ // Check floor block
150
+ const blockBelow = world.chunkLattice.hasBlock({
151
+ x: Math.floor(x),
152
+ y: floorY - 1,
153
+ z: Math.floor(z),
154
+ });
155
+
156
+ if (!blockBelow) continue;
157
+
158
+ // Check leg and head space
159
+ const blockLeg = world.chunkLattice.hasBlock({
160
+ x: Math.floor(x),
161
+ y: floorY,
162
+ z: Math.floor(z),
163
+ });
164
+ const blockHead = world.chunkLattice.hasBlock({
165
+ x: Math.floor(x),
166
+ y: floorY + 1,
167
+ z: Math.floor(z),
168
+ });
169
+
170
+ // Check extra block above for jump clearance
171
+ const blockJump = world.chunkLattice.hasBlock({
172
+ x: Math.floor(x),
173
+ y: floorY + 2,
174
+ z: Math.floor(z),
175
+ });
176
+
177
+ // If both leg and head space are clear
178
+ if (!blockLeg && !blockHead) {
179
+ return {
180
+ walkable: true,
181
+ y: floorY,
182
+ // Can only jump from here if we have the extra block of clearance
183
+ canJumpFrom: !blockJump,
184
+ };
185
+ }
186
+ }
187
+ return { walkable: false };
188
+ }
189
+
190
+ private heuristic(x1: number, z1: number, x2: number, z2: number): number {
191
+ return Math.abs(x1 - x2) + Math.abs(z1 - z2); // Manhattan distance
192
+ }
193
+
194
+ getState(): string {
195
+ // We want to return a message depending on whether or not we are currently pathfinding
196
+ if (this.path.length > 0) {
197
+ const distance = Vector3.fromVector3Like(
198
+ this.path[this.path.length - 1]
199
+ ).distance(this.path[0]);
200
+ return `Pathfinding (${distance.toFixed(1)}m remaining)`;
201
+ } else {
202
+ return "Not currently pathfinding";
203
+ }
204
+ }
205
+
206
+ private findPath(
207
+ agent: BaseAgent,
208
+ world: World,
209
+ start: Vector3,
210
+ end: Vector3
211
+ ): Vector3[] {
212
+ const openSet: Node[] = [];
213
+ const closedSet: Set<string> = new Set();
214
+ const startY = Math.floor(start.y);
215
+
216
+ interface NodeWithY extends Node {
217
+ y: number;
218
+ }
219
+
220
+ const startNode: NodeWithY = {
221
+ x: Math.floor(start.x),
222
+ z: Math.floor(start.z),
223
+ y: Math.floor(start.y),
224
+ g: 0,
225
+ h: this.heuristic(start.x, start.z, end.x, end.z),
226
+ f: 0,
227
+ };
228
+
229
+ openSet.push(startNode);
230
+
231
+ while (openSet.length > 0) {
232
+ let current = openSet.reduce((min, node) =>
233
+ node.f < min.f ? node : min
234
+ ) as NodeWithY;
235
+
236
+ if (
237
+ Math.abs(current.x - end.x) < 1 &&
238
+ Math.abs(current.z - end.z) < 1
239
+ ) {
240
+ // Path found, reconstruct it
241
+ const path: Vector3[] = [];
242
+ while (current) {
243
+ const walkableCheck = this.isWalkable(
244
+ world,
245
+ current.x,
246
+ current.z,
247
+ current.y,
248
+ startY
249
+ );
250
+ path.unshift(
251
+ new Vector3(
252
+ current.x + 0.5,
253
+ walkableCheck.y! + 1,
254
+ current.z + 0.5
255
+ )
256
+ );
257
+ current = current.parent! as NodeWithY;
258
+ }
259
+ return path;
260
+ }
261
+
262
+ openSet.splice(openSet.indexOf(current), 1);
263
+ closedSet.add(`${current.x},${current.z}`);
264
+
265
+ // Only cardinal directions - no diagonals
266
+ const neighbors = [
267
+ { x: current.x + 1, z: current.z }, // East
268
+ { x: current.x - 1, z: current.z }, // West
269
+ { x: current.x, z: current.z + 1 }, // North
270
+ { x: current.x, z: current.z - 1 }, // South
271
+ ];
272
+
273
+ for (const neighbor of neighbors) {
274
+ if (closedSet.has(`${neighbor.x},${neighbor.z}`)) continue;
275
+
276
+ const walkableCheck = this.isWalkable(
277
+ world,
278
+ neighbor.x,
279
+ neighbor.z,
280
+ current.y,
281
+ startY
282
+ );
283
+
284
+ if (!walkableCheck.walkable) continue;
285
+ if (walkableCheck.y! > startY + 2) continue;
286
+
287
+ // Calculate movement cost
288
+ let movementCost = 1;
289
+
290
+ // Add significant cost for moving up (jumping)
291
+ const heightDiff = walkableCheck.y! - current.y;
292
+ if (heightDiff > 0) {
293
+ // Check if we can actually jump from the current position
294
+ const currentPos = this.isWalkable(
295
+ world,
296
+ current.x,
297
+ current.z,
298
+ current.y,
299
+ startY
300
+ );
301
+
302
+ if (!currentPos.canJumpFrom) {
303
+ movementCost += 1000;
304
+ } else {
305
+ movementCost += heightDiff * 5;
306
+ }
307
+ }
308
+
309
+ const g = current.g + movementCost;
310
+ const h = this.heuristic(neighbor.x, neighbor.z, end.x, end.z);
311
+ const f = g + h;
312
+
313
+ const existingNode = openSet.find(
314
+ (n) => n.x === neighbor.x && n.z === neighbor.z
315
+ );
316
+
317
+ if (!existingNode || g < existingNode.g) {
318
+ const newNode: NodeWithY = {
319
+ x: neighbor.x,
320
+ z: neighbor.z,
321
+ y: walkableCheck.y!,
322
+ g,
323
+ h,
324
+ f,
325
+ parent: current,
326
+ };
327
+
328
+ if (!existingNode) {
329
+ openSet.push(newNode);
330
+ } else {
331
+ Object.assign(existingNode, newNode);
332
+ }
333
+ }
334
+ }
335
+ }
336
+ return [];
337
+ }
338
+
339
+ onToolCall(
340
+ agent: BaseAgent,
341
+ world: World,
342
+ toolName: string,
343
+ args: any,
344
+ player?: Player
345
+ ): string | void {
346
+ if (toolName === "pathfindTo") {
347
+ let targetPos: Vector3;
348
+ let targetName: string;
349
+
350
+ if (args.coordinates) {
351
+ // Use provided coordinates
352
+ targetPos = new Vector3(
353
+ args.coordinates.x,
354
+ args.coordinates.y,
355
+ args.coordinates.z
356
+ );
357
+ } else {
358
+ // Find target entity
359
+ console.log("Pathfinding to", args.targetName);
360
+ const target = world.entityManager
361
+ .getAllEntities()
362
+ .find(
363
+ (e) =>
364
+ (e instanceof BaseAgent &&
365
+ e.name === args.targetName) ||
366
+ (e instanceof PlayerEntity &&
367
+ e.player.username === args.targetName)
368
+ );
369
+
370
+ if (!target) {
371
+ return;
372
+ }
373
+
374
+ if (target instanceof BaseAgent) {
375
+ this.targetEntity = target;
376
+ }
377
+ targetPos = Vector3.fromVector3Like(target.position);
378
+ targetName =
379
+ target instanceof PlayerEntity
380
+ ? target.player.username
381
+ : target.name;
382
+ }
383
+
384
+ const startPos = Vector3.fromVector3Like(agent.position);
385
+
386
+ // Check if start and end are walkable
387
+ const startWalkable = this.isWalkable(
388
+ world,
389
+ Math.floor(startPos.x),
390
+ Math.floor(startPos.z),
391
+ Math.floor(startPos.y)
392
+ );
393
+
394
+ const endWalkable = this.isWalkable(
395
+ world,
396
+ Math.floor(targetPos.x),
397
+ Math.floor(targetPos.z),
398
+ Math.floor(targetPos.y)
399
+ );
400
+
401
+ this.path = this.findPath(agent, world, startPos, targetPos);
402
+
403
+ if (this.path.length === 0) {
404
+ return "No valid path found to target.";
405
+ }
406
+
407
+ this.currentPathIndex = 0;
408
+
409
+ return "Started pathfinding. The system will notify you when you arrive.";
410
+ }
411
+ }
412
+
413
+ getPromptInstructions(): string {
414
+ return `
415
+ To navigate to a target, use:
416
+ <action type="pathfindTo">
417
+ {
418
+ "targetName": "Name of character or player to pathfind to", // Optional
419
+ "coordinates": { // Optional
420
+ "x": number,
421
+ "y": number,
422
+ "z": number
423
+ }
424
+ }
425
+ </action>
426
+
427
+ Returns:
428
+ - Success message if pathfinding is successfully started
429
+ - Error message if no path can be found.
430
+
431
+ The Pathfinding procedure will result in a later message when you arrive at your destination.
432
+
433
+ You must provide either targetName OR coordinates.`;
434
+ }
435
+ }
@@ -0,0 +1,50 @@
1
+ import { World, Player } from "hytopia";
2
+ import type { AgentBehavior, BaseAgent } from "../BaseAgent";
3
+
4
+ /**
5
+ * Very simple implementation of speak behavior for Agents.
6
+ * You can imagine how this could be extended to include more complex behaviors like Text-to-Speech.
7
+ * It has no state, and no callbacks.
8
+ */
9
+ export class SpeakBehavior implements AgentBehavior {
10
+ onUpdate(agent: BaseAgent, world: World): void {}
11
+
12
+ getPromptInstructions(): string {
13
+ return `
14
+ To speak out loud, use:
15
+ <action type="speak">
16
+ {
17
+ "message": "What to say"
18
+ }
19
+ </action>`;
20
+ }
21
+
22
+ getState(): string {
23
+ return "";
24
+ }
25
+
26
+ onToolCall(
27
+ agent: BaseAgent,
28
+ world: World,
29
+ toolName: string,
30
+ args: { message: string }
31
+ ): string | void {
32
+ if (toolName === "speak") {
33
+ agent.setChatUIState({ message: args.message });
34
+
35
+ if (world) {
36
+ world.chatManager.sendBroadcastMessage(
37
+ `[${agent.name}]: ${args.message}`,
38
+ "FF69B4"
39
+ );
40
+ }
41
+
42
+ // Clear message after delay
43
+ setTimeout(() => {
44
+ agent.setChatUIState({ message: "" });
45
+ }, 5300);
46
+
47
+ return "You said: " + args.message;
48
+ }
49
+ }
50
+ }