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,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
+ }