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.
- package/docs/server.gameserver.md +2 -2
- package/docs/{server.gameserver.modelmanager.md → server.gameserver.modelregistry.md} +3 -3
- package/docs/server.md +1 -1
- package/docs/{server.modelmanager.getboundingbox.md → server.modelregistry.getboundingbox.md} +2 -2
- package/docs/server.modelregistry.instance.md +13 -0
- package/docs/{server.modelmanager.md → server.modelregistry.md} +12 -12
- package/examples/ai-agents/README.md +47 -0
- package/examples/ai-agents/assets/map.json +25828 -0
- package/examples/ai-agents/assets/ui/index.html +215 -0
- package/examples/ai-agents/index.ts +350 -0
- package/examples/ai-agents/package.json +16 -0
- package/examples/ai-agents/src/BaseAgent.ts +482 -0
- package/examples/ai-agents/src/behaviors/FishingBehavior.ts +181 -0
- package/examples/ai-agents/src/behaviors/FollowBehavior.ts +171 -0
- package/examples/ai-agents/src/behaviors/MiningBehavior.ts +226 -0
- package/examples/ai-agents/src/behaviors/PathfindingBehavior.ts +435 -0
- package/examples/ai-agents/src/behaviors/SpeakBehavior.ts +50 -0
- package/examples/ai-agents/src/behaviors/TradeBehavior.ts +254 -0
- package/examples/entity-controller/MyEntityController.ts +1 -1
- package/package.json +1 -1
- package/server.api.json +15 -15
- package/server.d.ts +11 -19
- package/server.js +1 -1
- package/docs/server.modelmanager.instance.md +0 -13
@@ -0,0 +1,171 @@
|
|
1
|
+
import {
|
2
|
+
Player,
|
3
|
+
PlayerEntity,
|
4
|
+
SimpleEntityController,
|
5
|
+
Vector3,
|
6
|
+
World,
|
7
|
+
} from "hytopia";
|
8
|
+
import type { AgentBehavior, BaseAgent } from "../BaseAgent";
|
9
|
+
import { PlayerManager } from "hytopia";
|
10
|
+
|
11
|
+
/**
|
12
|
+
* This is a simple implementation of a follow behavior for Agents. It includes basic animation handling, jump detection, and running vs walking.
|
13
|
+
* Agents can call actions like `follow` to start following a player.
|
14
|
+
*
|
15
|
+
* This provides a simple example of how behaviours have State, which get passed to the LLM.
|
16
|
+
*/
|
17
|
+
export class FollowBehavior implements AgentBehavior {
|
18
|
+
private playerToFollow: Player | null = null;
|
19
|
+
private followDistance = 2;
|
20
|
+
private speed = 3;
|
21
|
+
private runSpeed = 6;
|
22
|
+
private runThreshold = 5;
|
23
|
+
private isJumping = false;
|
24
|
+
private jumpCooldown = 0;
|
25
|
+
|
26
|
+
private needsToJump(
|
27
|
+
world: World,
|
28
|
+
currentPos: Vector3,
|
29
|
+
targetPos: Vector3
|
30
|
+
): boolean {
|
31
|
+
// Check block directly in front of us
|
32
|
+
const direction = new Vector3(
|
33
|
+
targetPos.x - currentPos.x,
|
34
|
+
0,
|
35
|
+
targetPos.z - currentPos.z
|
36
|
+
).normalize();
|
37
|
+
|
38
|
+
// Check one block ahead
|
39
|
+
const checkPos = {
|
40
|
+
x: Math.floor(currentPos.x + direction.x),
|
41
|
+
y: Math.floor(currentPos.y),
|
42
|
+
z: Math.floor(currentPos.z + direction.z),
|
43
|
+
};
|
44
|
+
|
45
|
+
// Check if there's a block at head or leg level
|
46
|
+
const blockAtLegs = world.chunkLattice.hasBlock(checkPos);
|
47
|
+
const blockAtHead = world.chunkLattice.hasBlock({
|
48
|
+
...checkPos,
|
49
|
+
y: checkPos.y + 1,
|
50
|
+
});
|
51
|
+
|
52
|
+
// Check if there's space to jump into
|
53
|
+
const blockAboveHead = world.chunkLattice.hasBlock({
|
54
|
+
...checkPos,
|
55
|
+
y: checkPos.y + 2,
|
56
|
+
});
|
57
|
+
|
58
|
+
return (blockAtLegs || blockAtHead) && !blockAboveHead;
|
59
|
+
}
|
60
|
+
|
61
|
+
onUpdate(agent: BaseAgent, world: World): void {
|
62
|
+
if (!this.playerToFollow) return;
|
63
|
+
if (!(agent.controller instanceof SimpleEntityController)) return;
|
64
|
+
|
65
|
+
// Decrease jump cooldown
|
66
|
+
if (this.jumpCooldown > 0) {
|
67
|
+
this.jumpCooldown--;
|
68
|
+
}
|
69
|
+
|
70
|
+
const targetEntity = world.entityManager
|
71
|
+
.getPlayerEntitiesByPlayer(this.playerToFollow)
|
72
|
+
.at(0);
|
73
|
+
|
74
|
+
if (!targetEntity) return;
|
75
|
+
|
76
|
+
const dx = targetEntity.position.x - agent.position.x;
|
77
|
+
const dz = targetEntity.position.z - agent.position.z;
|
78
|
+
const distance = Math.sqrt(dx * dx + dz * dz);
|
79
|
+
const yDiff = targetEntity.position.y - agent.position.y;
|
80
|
+
|
81
|
+
if (Math.abs(distance - this.followDistance) > 0.5) {
|
82
|
+
const isRunning = distance > this.runThreshold;
|
83
|
+
agent.startModelLoopedAnimations([isRunning ? "run" : "walk"]);
|
84
|
+
|
85
|
+
const angle = Math.atan2(dz, dx);
|
86
|
+
const targetPos = new Vector3(
|
87
|
+
targetEntity.position.x - Math.cos(angle) * this.followDistance,
|
88
|
+
targetEntity.position.y,
|
89
|
+
targetEntity.position.z - Math.sin(angle) * this.followDistance
|
90
|
+
);
|
91
|
+
|
92
|
+
// Check if we need to jump
|
93
|
+
if (
|
94
|
+
!this.isJumping &&
|
95
|
+
this.jumpCooldown === 0 &&
|
96
|
+
this.needsToJump(
|
97
|
+
world,
|
98
|
+
Vector3.fromVector3Like(agent.position),
|
99
|
+
targetPos
|
100
|
+
)
|
101
|
+
) {
|
102
|
+
const direction = Vector3.fromVector3Like(targetPos)
|
103
|
+
.subtract(Vector3.fromVector3Like(agent.position))
|
104
|
+
.normalize();
|
105
|
+
direction.y = 10 * agent.mass;
|
106
|
+
agent.applyImpulse(direction);
|
107
|
+
this.isJumping = true;
|
108
|
+
this.jumpCooldown = 30;
|
109
|
+
}
|
110
|
+
|
111
|
+
(agent.controller as SimpleEntityController).move(
|
112
|
+
targetPos,
|
113
|
+
isRunning ? this.runSpeed : this.speed,
|
114
|
+
{ moveIgnoreAxes: yDiff >= 0 ? { y: true } : undefined }
|
115
|
+
);
|
116
|
+
agent.controller.face(targetEntity.position, this.speed * 2);
|
117
|
+
} else {
|
118
|
+
agent.stopModelAnimations(["walk", "run"]);
|
119
|
+
agent.startModelLoopedAnimations(["idle"]);
|
120
|
+
this.isJumping = false; // Reset jump state when we're close enough
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
getState(): string {
|
125
|
+
if (this.playerToFollow) {
|
126
|
+
return "You are following " + this.playerToFollow.username;
|
127
|
+
}
|
128
|
+
return "You are not following anyone";
|
129
|
+
}
|
130
|
+
|
131
|
+
onToolCall(
|
132
|
+
agent: BaseAgent,
|
133
|
+
world: World,
|
134
|
+
toolName: string,
|
135
|
+
args: any
|
136
|
+
): void {
|
137
|
+
if (toolName === "follow") {
|
138
|
+
if (args.following) {
|
139
|
+
const allPlayers = PlayerManager.instance.getConnectedPlayers();
|
140
|
+
|
141
|
+
console.log(
|
142
|
+
"All player usernames:",
|
143
|
+
allPlayers.map((p) => p.username)
|
144
|
+
);
|
145
|
+
const player = allPlayers.find(
|
146
|
+
(p) => p.username === args.targetPlayer
|
147
|
+
);
|
148
|
+
if (player) {
|
149
|
+
this.playerToFollow = player;
|
150
|
+
console.log("Following player:", player.username);
|
151
|
+
} else {
|
152
|
+
console.log("Could not find player:", args.targetPlayer);
|
153
|
+
}
|
154
|
+
} else {
|
155
|
+
this.playerToFollow = null;
|
156
|
+
console.log("Stopped following all players");
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
getPromptInstructions(): string {
|
162
|
+
return `
|
163
|
+
To follow a player, use:
|
164
|
+
<action type="follow">
|
165
|
+
{
|
166
|
+
"targetPlayer": "Username of player to follow",
|
167
|
+
"following": true // true to start following, false to stop
|
168
|
+
}
|
169
|
+
</action>`;
|
170
|
+
}
|
171
|
+
}
|
@@ -0,0 +1,226 @@
|
|
1
|
+
import { Vector3, World } from "hytopia";
|
2
|
+
import { BaseAgent, type AgentBehavior } from "../BaseAgent";
|
3
|
+
|
4
|
+
interface MiningResult {
|
5
|
+
success: boolean;
|
6
|
+
type?:
|
7
|
+
| "coal"
|
8
|
+
| "iron"
|
9
|
+
| "silver"
|
10
|
+
| "gold"
|
11
|
+
| "diamond"
|
12
|
+
| "mysterious_crystal";
|
13
|
+
quantity?: number;
|
14
|
+
}
|
15
|
+
|
16
|
+
interface MineralInfo {
|
17
|
+
chance: number;
|
18
|
+
quantity: [number, number]; // [min, max]
|
19
|
+
value: number; // For trading purposes
|
20
|
+
}
|
21
|
+
|
22
|
+
/**
|
23
|
+
* This is a simple implementation of a mining behavior for Agents.
|
24
|
+
* It does not include animations or any other fancy features.
|
25
|
+
* Agents can call actions like `mine` to start mining, and the environment will trigger a callback when the mining is complete.
|
26
|
+
* This is a simple example of ENVIRONMENT type messages for Agents.
|
27
|
+
*/
|
28
|
+
export class MiningBehavior implements AgentBehavior {
|
29
|
+
private isMining: boolean = false;
|
30
|
+
private readonly CAVE_LOCATION = new Vector3(-30, 1, 15);
|
31
|
+
private readonly MINING_RANGE = 15; // meters from cave entrance
|
32
|
+
private inventory: Record<string, number> = {
|
33
|
+
coal: 0,
|
34
|
+
iron: 0,
|
35
|
+
silver: 0,
|
36
|
+
gold: 0,
|
37
|
+
diamond: 0,
|
38
|
+
mysterious_crystal: 0,
|
39
|
+
};
|
40
|
+
|
41
|
+
private readonly MINERALS: Record<string, MineralInfo> = {
|
42
|
+
coal: { chance: 0.4, quantity: [1, 5], value: 1 },
|
43
|
+
iron: { chance: 0.3, quantity: [1, 3], value: 2 },
|
44
|
+
silver: { chance: 0.15, quantity: [1, 2], value: 5 },
|
45
|
+
gold: { chance: 0.1, quantity: [1, 1], value: 10 },
|
46
|
+
diamond: { chance: 0.04, quantity: [1, 1], value: 25 },
|
47
|
+
mysterious_crystal: { chance: 0.01, quantity: [1, 1], value: 50 },
|
48
|
+
};
|
49
|
+
|
50
|
+
onUpdate(agent: BaseAgent, world: World): void {
|
51
|
+
// Could add ambient mining animations here
|
52
|
+
}
|
53
|
+
|
54
|
+
private isNearCave(agent: BaseAgent): boolean {
|
55
|
+
const distance = Vector3.fromVector3Like(agent.position).distance(
|
56
|
+
this.CAVE_LOCATION
|
57
|
+
);
|
58
|
+
return distance <= this.MINING_RANGE;
|
59
|
+
}
|
60
|
+
|
61
|
+
private rollForMinerals(): MiningResult {
|
62
|
+
const roll = Math.random();
|
63
|
+
let cumulative = 0;
|
64
|
+
|
65
|
+
// First check for no find
|
66
|
+
const noFindChance = 0.3; // 30% chance of finding nothing
|
67
|
+
if (roll < noFindChance) {
|
68
|
+
return { success: false };
|
69
|
+
}
|
70
|
+
|
71
|
+
// Roll for each mineral type
|
72
|
+
for (const [type, info] of Object.entries(this.MINERALS)) {
|
73
|
+
cumulative += info.chance;
|
74
|
+
if (roll < cumulative) {
|
75
|
+
const quantity = Math.floor(
|
76
|
+
Math.random() * (info.quantity[1] - info.quantity[0] + 1) +
|
77
|
+
info.quantity[0]
|
78
|
+
);
|
79
|
+
return {
|
80
|
+
success: true,
|
81
|
+
type: type as MiningResult["type"],
|
82
|
+
quantity,
|
83
|
+
};
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
return { success: false };
|
88
|
+
}
|
89
|
+
|
90
|
+
onToolCall(
|
91
|
+
agent: BaseAgent,
|
92
|
+
world: World,
|
93
|
+
toolName: string,
|
94
|
+
args: any
|
95
|
+
): string | void {
|
96
|
+
if (toolName === "mine") {
|
97
|
+
console.log("Mining tool called");
|
98
|
+
|
99
|
+
if (!this.isNearCave(agent)) {
|
100
|
+
return "You need to be closer to the cave to mine!";
|
101
|
+
}
|
102
|
+
|
103
|
+
if (this.isMining) {
|
104
|
+
return "You're already mining!";
|
105
|
+
}
|
106
|
+
|
107
|
+
this.isMining = true;
|
108
|
+
|
109
|
+
// Start mining animation if available
|
110
|
+
agent.stopModelAnimations(["walk", "run"]);
|
111
|
+
agent.startModelLoopedAnimations(["idle"]); // Could be replaced with mining animation
|
112
|
+
|
113
|
+
// Simulate mining time
|
114
|
+
setTimeout(() => {
|
115
|
+
this.isMining = false;
|
116
|
+
const result = this.rollForMinerals();
|
117
|
+
|
118
|
+
if (!result.success) {
|
119
|
+
agent.handleEnvironmentTrigger(
|
120
|
+
"Just found some worthless rocks..."
|
121
|
+
);
|
122
|
+
return;
|
123
|
+
}
|
124
|
+
|
125
|
+
if (result.type && result.quantity) {
|
126
|
+
const mineralName = result.type.replace("_", " ");
|
127
|
+
agent.addToInventory({
|
128
|
+
name: mineralName,
|
129
|
+
quantity: result.quantity,
|
130
|
+
metadata: {
|
131
|
+
value: this.MINERALS[result.type].value,
|
132
|
+
},
|
133
|
+
});
|
134
|
+
|
135
|
+
let message = `Found ${result.quantity} ${mineralName}!`;
|
136
|
+
if (result.type === "mysterious_crystal") {
|
137
|
+
message +=
|
138
|
+
" *mutters* These crystals... there's something strange about them...";
|
139
|
+
}
|
140
|
+
|
141
|
+
agent.handleEnvironmentTrigger(message);
|
142
|
+
}
|
143
|
+
}, 30000); // 30 second mining time
|
144
|
+
|
145
|
+
return "Mining away...";
|
146
|
+
} else if (toolName === "check_minerals") {
|
147
|
+
const inventory = Object.entries(this.inventory)
|
148
|
+
.filter(([_, amount]) => amount > 0)
|
149
|
+
.map(
|
150
|
+
([mineral, amount]) =>
|
151
|
+
`${mineral.replace("_", " ")}: ${amount}`
|
152
|
+
)
|
153
|
+
.join(", ");
|
154
|
+
|
155
|
+
return inventory || "No minerals in inventory";
|
156
|
+
} else if (toolName === "give_minerals") {
|
157
|
+
const { mineral, quantity, target } = args;
|
158
|
+
const mineralName = mineral.replace("_", " ");
|
159
|
+
|
160
|
+
if (!agent.removeFromInventory(mineralName, quantity)) {
|
161
|
+
return `Not enough ${mineralName} in inventory`;
|
162
|
+
}
|
163
|
+
|
164
|
+
const nearbyEntities = agent.getNearbyEntities(5);
|
165
|
+
const targetEntity = nearbyEntities.find((e) => e.name === target);
|
166
|
+
|
167
|
+
if (!targetEntity) {
|
168
|
+
return `Cannot find ${target} nearby. Try getting closer to them.`;
|
169
|
+
}
|
170
|
+
|
171
|
+
// Add to target's inventory if it's an agent
|
172
|
+
if (targetEntity.type === "Agent") {
|
173
|
+
const targetAgent = world.entityManager
|
174
|
+
.getAllEntities()
|
175
|
+
.find(
|
176
|
+
(e) => e instanceof BaseAgent && e.name === target
|
177
|
+
) as BaseAgent;
|
178
|
+
|
179
|
+
if (targetAgent) {
|
180
|
+
targetAgent.addToInventory({
|
181
|
+
name: mineralName,
|
182
|
+
quantity,
|
183
|
+
metadata: {
|
184
|
+
value: this.MINERALS[mineral].value,
|
185
|
+
},
|
186
|
+
});
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
return `Successfully gave ${quantity} ${mineralName} to ${target}`;
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
getPromptInstructions(): string {
|
195
|
+
return `
|
196
|
+
To mine in the cave, use:
|
197
|
+
<action type="mine"></action>
|
198
|
+
|
199
|
+
To check your mineral inventory, use:
|
200
|
+
<action type="check_minerals"></action>
|
201
|
+
|
202
|
+
To give minerals to another agent, use:
|
203
|
+
<action type="give_minerals">
|
204
|
+
{
|
205
|
+
mineral: "coal" | "iron" | "silver" | "gold" | "diamond" | "mysterious_crystal",
|
206
|
+
quantity: number
|
207
|
+
target: "name of the player or agent to give the minerals to"
|
208
|
+
}
|
209
|
+
</action>
|
210
|
+
|
211
|
+
You must be within 15 meters of the cave to mine.
|
212
|
+
Each mining attempt takes 30 seconds and has a chance to find various minerals.
|
213
|
+
You can only mine one spot at a time.`;
|
214
|
+
}
|
215
|
+
|
216
|
+
getState(): string {
|
217
|
+
const minerals = Object.entries(this.inventory)
|
218
|
+
.filter(([_, amount]) => amount > 0)
|
219
|
+
.map(([mineral, amount]) => `${mineral}: ${amount}`)
|
220
|
+
.join(", ");
|
221
|
+
|
222
|
+
return this.isMining
|
223
|
+
? "Currently mining"
|
224
|
+
: `Not mining. Inventory: ${minerals || "empty"}`;
|
225
|
+
}
|
226
|
+
}
|