hytopia 0.1.18 → 0.1.20

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a bug report. Use this for unexpected behaviors that we can reproduce so we can fix them in the SDK. If we find your issue is not an SDK bug, we'll provide clarity on a fix for your code.
4
+ title: <Concise title for this bug> [BUG]
5
+ labels: bug
6
+ assignees: iamarkdev
7
+
8
+ ---
9
+
10
+ **What were you trying to do, what did you expect to happen?**
11
+ Let us know what you were trying to do, and what you expected your code to do that it did not.
12
+
13
+ **What actually happened?**
14
+ What did your code actually do? Explain the bug/issue clearly.
15
+
16
+ **To Reproduce**
17
+ For SDK specific bugs, please provide the HYTOPIA SDK version you're using and a complete snippet of your code so we can reproduce this issue locally.
18
+
19
+ **Screenshots**
20
+ If applicable, add screenshots to help explain your problem.
21
+
22
+ **Additional context**
23
+ Add any other context about the problem here.
package/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # HYTOPIA SDK
2
2
 
3
3
  ## Quick Links
4
- [Quickstart](#quickstart) • [API Reference](./docs/server.md) • [Report Bugs or Request Features](https://github.com/hytopiagg/sdk/issues)
4
+ [Quickstart](#quickstart) • [API Reference](./docs/server.md) • [Examples](./examples) • [Join Our Developer Discord](https://discord.gg/hytopia-developers) • [Report Bugs or Request Features](https://github.com/hytopiagg/sdk/issues)
5
5
 
6
6
  ## What is HYTOPIA?
7
7
 
8
- ![HYTOPIA Banner](./readme/assets/banner.png)
8
+ ![HYTOPIA Demo](./readme/assets/demo.gif)
9
+
9
10
  HYTOPIA is a modern games platform inspired by Minecraft, Roblox, and Rec Room.
10
11
 
11
- HYTOPIA allows you to create your own highly-sharable, immersive, massively multiplayer games in a voxel-like style. All playable in a web browser on any device!
12
+ HYTOPIA allows you to create your own highly-sharable, immersive, massively multiplayer games in a voxel-like style by writing TypeScript or JavaScript. All playable in a web browser on any device!
12
13
 
13
14
  ## What is this SDK?
14
15
 
@@ -19,11 +20,11 @@ The HYTOPIA SDK makes it easy for developers to create multiplayer games on the
19
20
  Available as a simple NPM package, this SDK provides everything you need to get started:
20
21
 
21
22
  - Compiled HYTOPIA Server: The ready-to-use server software.
22
- - Game Client & Debugger: For rapidly testing and developing your games.
23
+ - Game Client & Debugger: Accessible at https://play.hytopia.com
23
24
  - TypeScript Definitions: For strong typing and code completion.
24
- - Documentation: Detailed guides and references.
25
- - Default Assets: Textures, models and audio you can use in your games.
26
- - Game Examples: Sample projects & scripts showing how to build different types of games.
25
+ - Documentation: Detailed guides and API reference.
26
+ - Default Assets: Textures, models, audio and more you can use in your games.
27
+ - Examples: Sample projects & scripts showing how to build different types of games.
27
28
 
28
29
  With these resources, you can quickly build and share immersive, voxel-style multiplayer games on HYTOPIA.
29
30
 
@@ -36,9 +37,13 @@ With these resources, you can quickly build and share immersive, voxel-style mul
36
37
  mkdir my-project-directory && cd my-project-directory
37
38
  ```
38
39
 
39
- 3. Initialize a hytopia project. Sets up package.json and all dependencies, copies assets and an index.ts game script into your project.
40
+ 3. Initialize a hytopia project from boilerplate or an existing example. Sets up package.json and all dependencies, copies assets and an index.ts game script into your project.
40
41
  ```bash
42
+ # Option 1: Initialize a boilerplate project
41
43
  bunx hytopia init
44
+
45
+ # Option 2: Initialize a project from any of the examples in the examples directory like so:
46
+ bunx hytopia init --template payload-game
42
47
  ```
43
48
 
44
49
  3. Start the server, use --watch for hot reloads as you make changes.
@@ -48,6 +53,8 @@ bun --watch index.ts
48
53
 
49
54
  4. Visit https://play.hytopia.com - when prompted, enter `localhost:8080` - this is the hostname of the local server you started in the previous step.
50
55
 
56
+ **Note: If you'd prefer to use JavaScript instead of TypeScript, simply change the file extension of index.ts to index.js - Your editor will highlight the TypeScript syntax errors, simple delete the type annotations and everything should work the same without any TypeScript usage.**
57
+
51
58
  Once you're up and running, here's some other resources to go further:
52
59
  - [Game Examples](./examples)
53
60
  - [API Reference](./docs/server.md)
package/bin/scripts.js CHANGED
@@ -43,7 +43,8 @@ const path = require('path');
43
43
  console.log('🔧 Initializing project');
44
44
  execSync('bun init --yes');
45
45
  execSync('bun add hytopia');
46
-
46
+
47
+ const srcDir = path.join(__dirname, '..', 'boilerplate');
47
48
  fs.cpSync(srcDir, destDir, { recursive: true });
48
49
  }
49
50
 
@@ -0,0 +1,22 @@
1
+ # HYTOPIA SDK Examples
2
+
3
+ HYTOPIA SDK examples are a collection of HYTOPIA SDK projects that demonstrate full games or specific features using the SDK.
4
+
5
+ In each folder, you'll find an index.ts file - this is the main entry point and where you'll find the code for each example.
6
+
7
+ Here's an overview of the examples available:
8
+ - [`payload-game`](./payload-game): A simple game where players must stay near a payload for it to move towards its destination while enemies spawn and swarm the players.
9
+ - [`character-controller`](./character-controller): A simple example of how to implement your own character controller class.
10
+ - [`entity-spawn`](./entity-spawn): A simple example of how to spawn an entity and set some of its properties.
11
+
12
+ ## Use Examples As Templates For Your Projects
13
+
14
+ If you want to init a new HYTOPIA project based on an example in this directory, you can use the `hytopia init` command with the `--template` flag.
15
+
16
+ For example, to create a new project based on the `payload-game` example, you can run:
17
+
18
+ ```bash
19
+ bunx hytopia init --template payload-game
20
+ ```
21
+
22
+ The value passed to the `--template` flag should be the name of the folder in the examples directory.
@@ -1,3 +1,21 @@
1
+ /**
2
+ * payload-game is a simple game that encompasses a number of core HYTOPIA SDK systems.
3
+ * This example utilizes entities, spawning, rigid body, colliders and sensors,
4
+ * collision groups, audio, character controller hooks, and more.
5
+ *
6
+ * This example is a quick and dirty implementation of an overwatch style push the payload
7
+ * in a multiplayer PvE style. Players start the game around the payload and must stay near it
8
+ * for it to move towards the next waypoint. Enemies spawn near the next arget waypoint of
9
+ * the payload and swarm towards the players. Players can left click to shoot spiders with
10
+ * bullets.
11
+ *
12
+ * This example is not meant to be a polished game, but rather a demonstration of how to use
13
+ * the SDK to build your own games.
14
+ *
15
+ * In a polished implementation, we'd be using multiple files and not just index.ts to
16
+ * break out and properly organize game behavior and mechanics.
17
+ */
18
+
1
19
  import {
2
20
  Audio,
3
21
  BlockType,
@@ -5,7 +23,6 @@ import {
5
23
  CollisionGroup,
6
24
  DefaultCharacterController,
7
25
  Entity,
8
- EventRouter,
9
26
  GameServer,
10
27
  PlayerEntity,
11
28
  RigidBodyType,
@@ -20,7 +37,7 @@ import type {
20
37
  Vector3,
21
38
  } from 'hytopia';
22
39
 
23
- import worldJson from './assets/map.json';
40
+ import map from './assets/map.json';
24
41
 
25
42
  // Constants
26
43
  const BULLET_SPEED = 50;
@@ -57,35 +74,32 @@ const PAYLOAD_WAYPOINT_ENEMY_SPAWNS = [
57
74
  ],
58
75
  ];
59
76
 
60
- // Globals, yuck
61
- const enemyHealth: Record<number, number> = {};
62
- const enemyPathfindAccumulators: Record<number, number> = {};
63
- const enemyPathfindingTargets: Record<number, Vector3> = {};
64
- const playerEntityHealth: Record<number, number> = {};
65
- let started = false;
66
- let payloadEntity: Entity | null = null;
67
- let payloadPlayerEntityCount = 0;
68
- let playerCount = 0;
69
- let targetWaypointCoordinateIndex = 0;
70
-
71
- // Configure Global Event Router
72
- EventRouter.serverInstance.logIgnoreEvents = [ 'CONNECTION.PACKET_SENT' ];
73
- EventRouter.serverInstance.logIgnoreEventPrefixes = [ 'CONNECTION.PACKET_SENT:' ];
74
- EventRouter.serverInstance.logAllEvents = false;
77
+ // Simple game state tracking via globals.
78
+ const enemyHealth: Record<number, number> = {}; // Entity id -> health
79
+ const enemyPathfindAccumulators: Record<number, number> = {}; // Entity id -> accumulator, so we don't pathfind each tick
80
+ const enemyPathfindingTargets: Record<number, Vector3> = {}; // Entity id -> target coordinate
81
+ const playerEntityHealth: Record<number, number> = {}; // Player entity id -> health
82
+ let started = false; // Game started flag
83
+ let payloadEntity: Entity | null = null; // Payload entity
84
+ let payloadPlayerEntityCount = 0; // Number of player entities within the payload sensor, minus number of enemies
85
+ let playerCount = 0; // Number of players in the game
86
+ let targetWaypointCoordinateIndex = 0; // Current waypoint coordinate index for the payload
75
87
 
76
88
  // Run
77
- void startServer(world => {
89
+ startServer(world => { // Perform our game setup logic in the startServer init callback here.
78
90
  const chatManager = world.chatManager;
79
91
 
80
- // Enable local ssl
92
+ // Enable local ssl, so we can connect to https://localhost:8080 from play.hytopia.com for testing
93
+ // If using NGROK or a reverse proxy that handles SSL, you need to comment this out to be able to
94
+ // connect to the server from the client using the reverse proxy URL.
81
95
  GameServer.instance.webServer.enableLocalSSL();
82
96
 
83
97
  // Load Map
84
- world.loadMap(worldJson);
98
+ world.loadMap(map);
85
99
 
86
100
  // Setup Player Join & Spawn Controlled Entity
87
101
  world.onPlayerJoin = player => {
88
- const playerEntity = new PlayerEntity({
102
+ const playerEntity = new PlayerEntity({ // Create an entity our newly joined player controls
89
103
  player,
90
104
  name: 'Player',
91
105
  modelUri: 'models/player-with-gun.gltf',
@@ -93,21 +107,30 @@ void startServer(world => {
93
107
  modelScale: 0.5,
94
108
  });
95
109
 
110
+ // Spawn the player entity at a random coordinate
96
111
  const randomSpawnCoordinate = PLAYER_SPAWN_COORDINATES[Math.floor(Math.random() * PLAYER_SPAWN_COORDINATES.length)];
97
112
  playerEntity.spawn(world, randomSpawnCoordinate);
98
113
 
114
+ // We need to do some custom logic for player inputs, so let's assign custom onTick handler to the default player controller.
99
115
  playerEntity.characterController!.onTickPlayerMovement = onTickPlayerMovement;
100
116
 
117
+ // Set custom collision groups for the player entity, this is so we can reference the PLAYER collision group
118
+ // specifically in enemy collision sensors.
101
119
  playerEntity.setCollisionGroupsForSolidColliders({
102
120
  belongsTo: [ CollisionGroup.ENTITY, CollisionGroup.PLAYER ],
103
121
  collidesWith: [ CollisionGroup.ALL ],
104
122
  });
105
123
 
124
+ // Initialize player health
106
125
  playerEntityHealth[playerEntity.id!] = 20;
126
+
127
+ // Increment player count
107
128
  playerCount++;
108
129
 
130
+ // Send a message to all players informing them that a new player has joined
109
131
  chatManager.sendBroadcastMessage(`Player ${player.username} has joined the game!`, 'FFFFFF');
110
132
 
133
+ // If the game hasn't started yet, send a message to all players to start the game
111
134
  if (!started) {
112
135
  chatManager.sendBroadcastMessage('Enter command /start to start the game!', 'FFFFFF');
113
136
  }
@@ -115,9 +138,13 @@ void startServer(world => {
115
138
 
116
139
  // Setup Player Leave & Despawn Controlled Entity
117
140
  world.onPlayerLeave = player => {
141
+ // Despawn all player entities for the player that left
142
+ // We apply a translation prior to despawn because of a bug in the RAPIER
143
+ // physics engine we use where entities despawned to not trigger a collision
144
+ // event for leaving a sensor. This is a workaround till a better solution is found.
118
145
  world.entityManager.getAllPlayerEntities(player).forEach(entity => {
119
146
  entity.setTranslation({ x: 0, y: 100, z: 0 });
120
- setTimeout(() => entity.despawn(), 50);
147
+ setTimeout(() => entity.despawn(), 50); // Despawn after a short delay so we step the physics after translating it so leaving the sensor registers.
121
148
  });
122
149
 
123
150
  playerCount--;
@@ -128,7 +155,6 @@ void startServer(world => {
128
155
  // Spawn Payload
129
156
  spawnPayloadEntity(world);
130
157
 
131
-
132
158
  // Start spawning enemies
133
159
  startEnemySpawnLoop(world);
134
160
 
@@ -143,7 +169,7 @@ void startServer(world => {
143
169
  started = false;
144
170
  });
145
171
 
146
- // Start Ambient Music
172
+ // Start ambient music for all players
147
173
  (new Audio({
148
174
  uri: 'audio/music/game.mp3',
149
175
  loop: true,
@@ -155,7 +181,7 @@ void startServer(world => {
155
181
  function startEnemySpawnLoop(world: World) {
156
182
  let spawnInterval;
157
183
 
158
- const spawn = () => {
184
+ const spawn = () => { // Simple spawn loop that spawns enemies relative to the payload's current waypoint
159
185
  const possibleSpawnCoordinate = PAYLOAD_WAYPOINT_ENEMY_SPAWNS[targetWaypointCoordinateIndex];
160
186
 
161
187
  if (!possibleSpawnCoordinate) {
@@ -177,18 +203,19 @@ function startEnemySpawnLoop(world: World) {
177
203
  }
178
204
 
179
205
  function spawnBullet(world: World, coordinate: Vector3, direction: Vector3) {
206
+ // Spawn a bullet when the player shoots.
180
207
  const bullet = new Entity({
181
208
  name: 'Bullet',
182
209
  modelUri: 'models/bullet.gltf',
183
210
  modelScale: 0.3,
184
211
  rigidBodyOptions: {
185
- type: RigidBodyType.KINEMATIC_VELOCITY,
212
+ type: RigidBodyType.KINEMATIC_VELOCITY, // Kinematic means entity's rigid body will not be affected by physics. KINEMATIC_VELOCITY means the entity is moved by setting velocity.
186
213
  linearVelocity: {
187
214
  x: direction.x * BULLET_SPEED,
188
215
  y: direction.y * BULLET_SPEED,
189
216
  z: direction.z * BULLET_SPEED,
190
217
  },
191
- rotation: getRotationFromDirection(direction),
218
+ rotation: getRotationFromDirection(direction), // Get the rotation from the direction vector so it's facing the right way we shot it
192
219
  colliders: [
193
220
  {
194
221
  shape: ColliderShape.BALL,
@@ -203,19 +230,22 @@ function spawnBullet(world: World, coordinate: Vector3, direction: Vector3) {
203
230
  },
204
231
  });
205
232
 
206
- bullet.onBlockCollision = (block: BlockType, started: boolean) => {
233
+ bullet.onBlockCollision = (block: BlockType, started: boolean) => { // If the bullet hits a block, despawn it
207
234
  if (started) {
208
235
  bullet.despawn();
209
236
  }
210
237
  };
211
238
 
212
- bullet.onEntityCollision = (entity: Entity, started: boolean) => {
213
- if (!started || ![ 'Spider', 'Zombie' ].includes(entity.name)) {
239
+ bullet.onEntityCollision = (entity: Entity, started: boolean) => { // If the bullet hits an enemy, deal damage if it is a Spider
240
+ if (!started || entity.name !== 'Spider') {
214
241
  return;
215
242
  }
216
243
 
217
244
  enemyHealth[entity.id!]--;
218
245
 
246
+ // Apply knockback, the knockback effect is less if the spider is larger, and more if it is smaller
247
+ // because of how the physics simulation applies forces relative to automatically calculated mass from the spider's
248
+ // size
219
249
  const bulletDirection = bullet.getDirectionFromRotation();
220
250
  const mass = entity.getMass();
221
251
  const knockback = 14 * mass;
@@ -227,9 +257,9 @@ function spawnBullet(world: World, coordinate: Vector3, direction: Vector3) {
227
257
  });
228
258
 
229
259
  if (enemyHealth[entity.id!] <= 0) {
230
- // have to YEET the spider to register it leaving the sensor
260
+ // YEET the spider before despawning it so it registers leaving the sensor
231
261
  entity.setTranslation({ x: 0, y: 100, z: 0 });
232
- setTimeout(() => { entity.despawn(); }, 50);
262
+ setTimeout(() => { entity.despawn(); }, 50); // Despawn after a short delay so we step the physics after translating it so leaving the sensor registers.
233
263
  }
234
264
 
235
265
  bullet.despawn();
@@ -237,6 +267,7 @@ function spawnBullet(world: World, coordinate: Vector3, direction: Vector3) {
237
267
 
238
268
  bullet.spawn(world, coordinate);
239
269
 
270
+ // Play a bullet noise that follows the bullet spatially
240
271
  (new Audio({
241
272
  uri: 'audio/sfx/shoot.mp3',
242
273
  playbackRate: 2,
@@ -263,21 +294,23 @@ function spawnPayloadEntity(world: World) {
263
294
  colliders: [
264
295
  {
265
296
  shape: ColliderShape.BLOCK,
266
- halfExtents: { x: 0.9, y: 1.6, z: 2.5 },
297
+ halfExtents: { x: 0.9, y: 1.6, z: 2.5 }, // Note: We manually set the collider size, the SDK currently does not support automatic sizing of colliders to a model.
267
298
  collisionGroups: {
268
299
  belongsTo: [ CollisionGroup.ALL ],
269
300
  collidesWith: [ CollisionGroup.ENTITY, CollisionGroup.ENTITY_SENSOR, CollisionGroup.PLAYER ],
270
301
  },
271
302
  },
272
303
  {
273
- shape: ColliderShape.BLOCK,
304
+ shape: ColliderShape.BLOCK, // Create a proximity sensor for movement when players are near.
274
305
  halfExtents: { x: 3.75, y: 2, z: 6 },
275
306
  isSensor: true,
276
307
  collisionGroups: {
277
308
  belongsTo: [ CollisionGroup.ENTITY_SENSOR ],
278
309
  collidesWith: [ CollisionGroup.PLAYER, CollisionGroup.ENTITY ],
279
310
  },
280
- onCollision: (other: BlockType | Entity, started: boolean) => {
311
+ // We use a onCollision handler specific to this sensor, and
312
+ // not the whole entity, so we can track the number of players in the payload sensor.
313
+ onCollision: (other: BlockType | Entity, started: boolean) => {
281
314
  if (other instanceof PlayerEntity) {
282
315
  started ? payloadPlayerEntityCount++ : payloadPlayerEntityCount--;
283
316
  } else if (other instanceof Entity) {
@@ -289,22 +322,23 @@ function spawnPayloadEntity(world: World) {
289
322
  },
290
323
  });
291
324
 
292
- payloadEntity.onTick = onTickPathfindPayload;
293
- payloadEntity.spawn(world, PAYLOAD_SPAWN_COORDINATE);
325
+ payloadEntity.onTick = onTickPathfindPayload; // Use our own basic pathfinding function each tick of the game for the payload.
326
+ payloadEntity.spawn(world, PAYLOAD_SPAWN_COORDINATE); // Spawn the payload at the designated spawn coordinate
294
327
 
295
- (new Audio({
328
+ (new Audio({ // Play a looped idle sound that follows the payload spatially
296
329
  uri: 'audio/sfx/payload-idle.mp3',
297
330
  loop: true,
298
331
  attachedToEntity: payloadEntity,
299
332
  volume: 0.25,
300
- referenceDistance: 5,
333
+ referenceDistance: 5, // Reference distance affects how loud the audio is relative to a player's proximity to the entity
301
334
  })).play(world);
302
335
  }
303
336
 
304
337
  function spawnSpider(world: World, coordinate: Vector3) {
305
338
  const baseScale = 0.5;
306
339
  const baseSpeed = 3;
307
- const randomScaleMultiplier = Math.random() * 2 + 1; // Random value between 1 and 3
340
+ const randomScaleMultiplier = Math.random() * 2 + 1; // Random value between 1 and 3 // Random scale multiplier to make each spider a different size
341
+ const targetPlayers = new Set<PlayerEntity>();
308
342
 
309
343
  const spider = new Entity({
310
344
  name: 'Spider',
@@ -320,7 +354,7 @@ function spawnSpider(world: World, coordinate: Vector3) {
320
354
  borderRadius: 0.1 * randomScaleMultiplier,
321
355
  halfHeight: 0.225 * randomScaleMultiplier,
322
356
  radius: 0.5 * randomScaleMultiplier,
323
- tag: 'body',
357
+ tag: 'body', // Note we use tags here, they don't really serve a purpose in this example other than showing that they can be used.
324
358
  collisionGroups: {
325
359
  belongsTo: [ CollisionGroup.ENTITY ],
326
360
  collidesWith: [ CollisionGroup.BLOCK, CollisionGroup.ENTITY_SENSOR, CollisionGroup.PLAYER ],
@@ -336,7 +370,7 @@ function spawnSpider(world: World, coordinate: Vector3) {
336
370
  belongsTo: [ CollisionGroup.ENTITY_SENSOR ],
337
371
  collidesWith: [ CollisionGroup.PLAYER ],
338
372
  },
339
- onCollision: (other: BlockType | Entity, started: boolean) => {
373
+ onCollision: (other: BlockType | Entity, started: boolean) => { // If a player enters or exits the aggro sensor, add or remove them from the target players set
340
374
  if (other instanceof PlayerEntity) {
341
375
  started ? targetPlayers.add(other) : targetPlayers.delete(other);
342
376
  }
@@ -346,16 +380,14 @@ function spawnSpider(world: World, coordinate: Vector3) {
346
380
  },
347
381
  });
348
382
 
349
- const targetPlayers = new Set<PlayerEntity>();
350
-
351
- spider.onTick = (tickDeltaMs: number) => onTickPathfindEnemy(
383
+ spider.onTick = (tickDeltaMs: number) => onTickPathfindEnemy( // Use our own basic pathfinding function each tick of the game for the enemy
352
384
  spider,
353
385
  targetPlayers,
354
386
  baseSpeed * randomScaleMultiplier,
355
387
  tickDeltaMs,
356
388
  );
357
389
 
358
- spider.onEntityCollision = (entity: Entity, started: boolean) => {
390
+ spider.onEntityCollision = (entity: Entity, started: boolean) => { // If the spider hits a player, deal damage and apply knockback
359
391
  if (started && entity instanceof PlayerEntity && entity.isSpawned) {
360
392
  const spiderDirection = spider.getDirectionFromRotation();
361
393
  const knockback = 4 * randomScaleMultiplier;
@@ -379,15 +411,16 @@ function spawnSpider(world: World, coordinate: Vector3) {
379
411
 
380
412
  spider.spawn(world, coordinate);
381
413
 
414
+ // Give the spider a health value relative to its size, bigger = more health
382
415
  enemyHealth[spider.id!] = 2 * Math.round(randomScaleMultiplier);
383
416
  }
384
417
 
385
- function onTickPathfindPayload(this: Entity, tickDeltaMs: number) {
386
- const speed = started
418
+ function onTickPathfindPayload(this: Entity, tickDeltaMs: number) { // Movement logic for the payload
419
+ const speed = started // Set the payload speed relative to the number of players in the payload sensor
387
420
  ? Math.max(Math.min(PAYLOAD_PER_PLAYER_SPEED * payloadPlayerEntityCount, PAYLOAD_MAX_SPEED), 0)
388
421
  : 0;
389
422
 
390
- if (!speed) {
423
+ if (!speed) { // Play animations based on if its moving or not
391
424
  this.stopModelAnimations(Array.from(this.modelLoopedAnimations).filter(v => v !== 'idle'));
392
425
  this.startModelLoopedAnimations([ 'idle' ]);
393
426
  } else {
@@ -395,7 +428,7 @@ function onTickPathfindPayload(this: Entity, tickDeltaMs: number) {
395
428
  this.startModelLoopedAnimations([ 'walk' ]);
396
429
  }
397
430
 
398
- // Calculate direction to target
431
+ // Calculate direction to target waypoint
399
432
  const targetWaypointCoordinate = PAYLOAD_WAYPOINT_COORDINATES[targetWaypointCoordinateIndex];
400
433
  const currentPosition = this.getTranslation();
401
434
  const deltaX = targetWaypointCoordinate.x - currentPosition.x;
@@ -406,7 +439,7 @@ function onTickPathfindPayload(this: Entity, tickDeltaMs: number) {
406
439
  z: Math.abs(deltaZ) > 0.1 ? Math.sign(deltaZ) : 0,
407
440
  };
408
441
 
409
- // Apply rotation to face direction if necessary
442
+ // Apply rotation to face direction if necessary based on the current target waypoint
410
443
  const rotation = this.getRotation();
411
444
  const currentAngle = 2 * Math.atan2(rotation.y, rotation.w);
412
445
  const targetAngle = Math.atan2(direction.x, direction.z) + Math.PI; // Add PI to face forward
@@ -452,7 +485,7 @@ function onTickPathfindEnemy(entity: Entity, targetPlayers: Set<PlayerEntity>, s
452
485
  if (!entity.isSpawned || !payloadEntity) return;
453
486
 
454
487
  const entityId = entity.id!;
455
- enemyPathfindAccumulators[entityId] ??= 0;
488
+ enemyPathfindAccumulators[entityId] ??= 0; // Initialize the accumulator for this enemy if it isn't initialized yet
456
489
 
457
490
  // Handle pathfinding
458
491
  if (!enemyPathfindingTargets[entityId] || enemyPathfindAccumulators[entityId] >= PATHFIND_ACCUMULATOR_THRESHOLD) {
@@ -542,7 +575,7 @@ function damagePlayer(playerEntity: PlayerEntity) {
542
575
  );
543
576
 
544
577
  if (playerEntityHealth[playerEntity.id!] <= 0) {
545
- chatManager.sendPlayerMessage(
578
+ chatManager.sendPlayerMessage( // Alert the player they've been damaged, since we don't have UI support yet, we just use chat
546
579
  playerEntity.player,
547
580
  'You have died!',
548
581
  'FF0000',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hytopia",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "The HYTOPIA SDK makes it easy for developers to create multiplayer games on the HYTOPIA platform using JavaScript or TypeScript.",
5
5
  "main": "server.js",
6
6
  "bin": {
Binary file