hytopia 0.1.17 → 0.1.19
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/README.md +14 -7
- package/examples/payload-game/index.ts +72 -53
- package/package.json +1 -1
- package/readme/assets/demo.gif +0 -0
package/README.md
CHANGED
@@ -5,10 +5,11 @@
|
|
5
5
|
|
6
6
|
## What is HYTOPIA?
|
7
7
|
|
8
|
-

|
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:
|
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
|
25
|
-
- Default Assets: Textures, models and
|
26
|
-
-
|
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)
|
@@ -1,3 +1,7 @@
|
|
1
|
+
/**
|
2
|
+
*
|
3
|
+
*/
|
4
|
+
|
1
5
|
import {
|
2
6
|
Audio,
|
3
7
|
BlockType,
|
@@ -5,7 +9,6 @@ import {
|
|
5
9
|
CollisionGroup,
|
6
10
|
DefaultCharacterController,
|
7
11
|
Entity,
|
8
|
-
EventRouter,
|
9
12
|
GameServer,
|
10
13
|
PlayerEntity,
|
11
14
|
RigidBodyType,
|
@@ -20,7 +23,7 @@ import type {
|
|
20
23
|
Vector3,
|
21
24
|
} from 'hytopia';
|
22
25
|
|
23
|
-
import
|
26
|
+
import map from './assets/map.json';
|
24
27
|
|
25
28
|
// Constants
|
26
29
|
const BULLET_SPEED = 50;
|
@@ -57,35 +60,32 @@ const PAYLOAD_WAYPOINT_ENEMY_SPAWNS = [
|
|
57
60
|
],
|
58
61
|
];
|
59
62
|
|
60
|
-
//
|
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;
|
63
|
+
// Simple game state tracking via globals.
|
64
|
+
const enemyHealth: Record<number, number> = {}; // Entity id -> health
|
65
|
+
const enemyPathfindAccumulators: Record<number, number> = {}; // Entity id -> accumulator, so we don't pathfind each tick
|
66
|
+
const enemyPathfindingTargets: Record<number, Vector3> = {}; // Entity id -> target coordinate
|
67
|
+
const playerEntityHealth: Record<number, number> = {}; // Player entity id -> health
|
68
|
+
let started = false; // Game started flag
|
69
|
+
let payloadEntity: Entity | null = null; // Payload entity
|
70
|
+
let payloadPlayerEntityCount = 0; // Number of player entities within the payload sensor, minus number of enemies
|
71
|
+
let playerCount = 0; // Number of players in the game
|
72
|
+
let targetWaypointCoordinateIndex = 0; // Current waypoint coordinate index for the payload
|
75
73
|
|
76
74
|
// Run
|
77
|
-
|
75
|
+
startServer(world => { // Perform our game setup logic in the startServer init callback here.
|
78
76
|
const chatManager = world.chatManager;
|
79
77
|
|
80
|
-
// Enable local ssl
|
78
|
+
// Enable local ssl, so we can connect to https://localhost:8080 from play.hytopia.com for testing
|
79
|
+
// If using NGROK or a reverse proxy that handles SSL, you need to comment this out to be able to
|
80
|
+
// connect to the server from the client using the reverse proxy URL.
|
81
81
|
GameServer.instance.webServer.enableLocalSSL();
|
82
82
|
|
83
83
|
// Load Map
|
84
|
-
world.loadMap(
|
84
|
+
world.loadMap(map);
|
85
85
|
|
86
86
|
// Setup Player Join & Spawn Controlled Entity
|
87
87
|
world.onPlayerJoin = player => {
|
88
|
-
const playerEntity = new PlayerEntity({
|
88
|
+
const playerEntity = new PlayerEntity({ // Create an entity our newly joined player controls
|
89
89
|
player,
|
90
90
|
name: 'Player',
|
91
91
|
modelUri: 'models/player-with-gun.gltf',
|
@@ -93,21 +93,30 @@ void startServer(world => {
|
|
93
93
|
modelScale: 0.5,
|
94
94
|
});
|
95
95
|
|
96
|
+
// Spawn the player entity at a random coordinate
|
96
97
|
const randomSpawnCoordinate = PLAYER_SPAWN_COORDINATES[Math.floor(Math.random() * PLAYER_SPAWN_COORDINATES.length)];
|
97
98
|
playerEntity.spawn(world, randomSpawnCoordinate);
|
98
99
|
|
100
|
+
// We need to do some custom logic for player inputs, so let's assign custom onTick handler to the default player controller.
|
99
101
|
playerEntity.characterController!.onTickPlayerMovement = onTickPlayerMovement;
|
100
102
|
|
103
|
+
// Set custom collision groups for the player entity, this is so we can reference the PLAYER collision group
|
104
|
+
// specifically in enemy collision sensors.
|
101
105
|
playerEntity.setCollisionGroupsForSolidColliders({
|
102
106
|
belongsTo: [ CollisionGroup.ENTITY, CollisionGroup.PLAYER ],
|
103
107
|
collidesWith: [ CollisionGroup.ALL ],
|
104
108
|
});
|
105
109
|
|
110
|
+
// Initialize player health
|
106
111
|
playerEntityHealth[playerEntity.id!] = 20;
|
112
|
+
|
113
|
+
// Increment player count
|
107
114
|
playerCount++;
|
108
115
|
|
116
|
+
// Send a message to all players informing them that a new player has joined
|
109
117
|
chatManager.sendBroadcastMessage(`Player ${player.username} has joined the game!`, 'FFFFFF');
|
110
118
|
|
119
|
+
// If the game hasn't started yet, send a message to all players to start the game
|
111
120
|
if (!started) {
|
112
121
|
chatManager.sendBroadcastMessage('Enter command /start to start the game!', 'FFFFFF');
|
113
122
|
}
|
@@ -115,9 +124,13 @@ void startServer(world => {
|
|
115
124
|
|
116
125
|
// Setup Player Leave & Despawn Controlled Entity
|
117
126
|
world.onPlayerLeave = player => {
|
127
|
+
// Despawn all player entities for the player that left
|
128
|
+
// We apply a translation prior to despawn because of a bug in the RAPIER
|
129
|
+
// physics engine we use where entities despawned to not trigger a collision
|
130
|
+
// event for leaving a sensor. This is a workaround till a better solution is found.
|
118
131
|
world.entityManager.getAllPlayerEntities(player).forEach(entity => {
|
119
132
|
entity.setTranslation({ x: 0, y: 100, z: 0 });
|
120
|
-
setTimeout(() => entity.despawn(), 50);
|
133
|
+
setTimeout(() => entity.despawn(), 50); // Despawn after a short delay so we step the physics after translating it so leaving the sensor registers.
|
121
134
|
});
|
122
135
|
|
123
136
|
playerCount--;
|
@@ -128,7 +141,6 @@ void startServer(world => {
|
|
128
141
|
// Spawn Payload
|
129
142
|
spawnPayloadEntity(world);
|
130
143
|
|
131
|
-
|
132
144
|
// Start spawning enemies
|
133
145
|
startEnemySpawnLoop(world);
|
134
146
|
|
@@ -143,7 +155,7 @@ void startServer(world => {
|
|
143
155
|
started = false;
|
144
156
|
});
|
145
157
|
|
146
|
-
// Start
|
158
|
+
// Start ambient music for all players
|
147
159
|
(new Audio({
|
148
160
|
uri: 'audio/music/game.mp3',
|
149
161
|
loop: true,
|
@@ -155,7 +167,7 @@ void startServer(world => {
|
|
155
167
|
function startEnemySpawnLoop(world: World) {
|
156
168
|
let spawnInterval;
|
157
169
|
|
158
|
-
const spawn = () => {
|
170
|
+
const spawn = () => { // Simple spawn loop that spawns enemies relative to the payload's current waypoint
|
159
171
|
const possibleSpawnCoordinate = PAYLOAD_WAYPOINT_ENEMY_SPAWNS[targetWaypointCoordinateIndex];
|
160
172
|
|
161
173
|
if (!possibleSpawnCoordinate) {
|
@@ -177,18 +189,19 @@ function startEnemySpawnLoop(world: World) {
|
|
177
189
|
}
|
178
190
|
|
179
191
|
function spawnBullet(world: World, coordinate: Vector3, direction: Vector3) {
|
192
|
+
// Spawn a bullet when the player shoots.
|
180
193
|
const bullet = new Entity({
|
181
194
|
name: 'Bullet',
|
182
195
|
modelUri: 'models/bullet.gltf',
|
183
196
|
modelScale: 0.3,
|
184
197
|
rigidBodyOptions: {
|
185
|
-
type: RigidBodyType.KINEMATIC_VELOCITY,
|
198
|
+
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
199
|
linearVelocity: {
|
187
200
|
x: direction.x * BULLET_SPEED,
|
188
201
|
y: direction.y * BULLET_SPEED,
|
189
202
|
z: direction.z * BULLET_SPEED,
|
190
203
|
},
|
191
|
-
rotation: getRotationFromDirection(direction),
|
204
|
+
rotation: getRotationFromDirection(direction), // Get the rotation from the direction vector so it's facing the right way we shot it
|
192
205
|
colliders: [
|
193
206
|
{
|
194
207
|
shape: ColliderShape.BALL,
|
@@ -203,19 +216,22 @@ function spawnBullet(world: World, coordinate: Vector3, direction: Vector3) {
|
|
203
216
|
},
|
204
217
|
});
|
205
218
|
|
206
|
-
bullet.onBlockCollision = (block: BlockType, started: boolean) => {
|
219
|
+
bullet.onBlockCollision = (block: BlockType, started: boolean) => { // If the bullet hits a block, despawn it
|
207
220
|
if (started) {
|
208
221
|
bullet.despawn();
|
209
222
|
}
|
210
223
|
};
|
211
224
|
|
212
|
-
bullet.onEntityCollision = (entity: Entity, started: boolean) => {
|
213
|
-
if (!started ||
|
225
|
+
bullet.onEntityCollision = (entity: Entity, started: boolean) => { // If the bullet hits an enemy, deal damage if it is a Spider
|
226
|
+
if (!started || entity.name !== 'Spider') {
|
214
227
|
return;
|
215
228
|
}
|
216
229
|
|
217
230
|
enemyHealth[entity.id!]--;
|
218
231
|
|
232
|
+
// Apply knockback, the knockback effect is less if the spider is larger, and more if it is smaller
|
233
|
+
// because of how the physics simulation applies forces relative to automatically calculated mass from the spider's
|
234
|
+
// size
|
219
235
|
const bulletDirection = bullet.getDirectionFromRotation();
|
220
236
|
const mass = entity.getMass();
|
221
237
|
const knockback = 14 * mass;
|
@@ -227,9 +243,9 @@ function spawnBullet(world: World, coordinate: Vector3, direction: Vector3) {
|
|
227
243
|
});
|
228
244
|
|
229
245
|
if (enemyHealth[entity.id!] <= 0) {
|
230
|
-
//
|
246
|
+
// YEET the spider before despawning it so it registers leaving the sensor
|
231
247
|
entity.setTranslation({ x: 0, y: 100, z: 0 });
|
232
|
-
setTimeout(() => { entity.despawn(); }, 50);
|
248
|
+
setTimeout(() => { entity.despawn(); }, 50); // Despawn after a short delay so we step the physics after translating it so leaving the sensor registers.
|
233
249
|
}
|
234
250
|
|
235
251
|
bullet.despawn();
|
@@ -237,6 +253,7 @@ function spawnBullet(world: World, coordinate: Vector3, direction: Vector3) {
|
|
237
253
|
|
238
254
|
bullet.spawn(world, coordinate);
|
239
255
|
|
256
|
+
// Play a bullet noise that follows the bullet spatially
|
240
257
|
(new Audio({
|
241
258
|
uri: 'audio/sfx/shoot.mp3',
|
242
259
|
playbackRate: 2,
|
@@ -263,21 +280,23 @@ function spawnPayloadEntity(world: World) {
|
|
263
280
|
colliders: [
|
264
281
|
{
|
265
282
|
shape: ColliderShape.BLOCK,
|
266
|
-
halfExtents: { x: 0.9, y: 1.6, z: 2.5 },
|
283
|
+
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
284
|
collisionGroups: {
|
268
285
|
belongsTo: [ CollisionGroup.ALL ],
|
269
286
|
collidesWith: [ CollisionGroup.ENTITY, CollisionGroup.ENTITY_SENSOR, CollisionGroup.PLAYER ],
|
270
287
|
},
|
271
288
|
},
|
272
289
|
{
|
273
|
-
shape: ColliderShape.BLOCK,
|
290
|
+
shape: ColliderShape.BLOCK, // Create a proximity sensor for movement when players are near.
|
274
291
|
halfExtents: { x: 3.75, y: 2, z: 6 },
|
275
292
|
isSensor: true,
|
276
293
|
collisionGroups: {
|
277
294
|
belongsTo: [ CollisionGroup.ENTITY_SENSOR ],
|
278
295
|
collidesWith: [ CollisionGroup.PLAYER, CollisionGroup.ENTITY ],
|
279
296
|
},
|
280
|
-
onCollision
|
297
|
+
// We use a onCollision handler specific to this sensor, and
|
298
|
+
// not the whole entity, so we can track the number of players in the payload sensor.
|
299
|
+
onCollision: (other: BlockType | Entity, started: boolean) => {
|
281
300
|
if (other instanceof PlayerEntity) {
|
282
301
|
started ? payloadPlayerEntityCount++ : payloadPlayerEntityCount--;
|
283
302
|
} else if (other instanceof Entity) {
|
@@ -289,22 +308,23 @@ function spawnPayloadEntity(world: World) {
|
|
289
308
|
},
|
290
309
|
});
|
291
310
|
|
292
|
-
payloadEntity.onTick = onTickPathfindPayload;
|
293
|
-
payloadEntity.spawn(world, PAYLOAD_SPAWN_COORDINATE);
|
311
|
+
payloadEntity.onTick = onTickPathfindPayload; // Use our own basic pathfinding function each tick of the game for the payload.
|
312
|
+
payloadEntity.spawn(world, PAYLOAD_SPAWN_COORDINATE); // Spawn the payload at the designated spawn coordinate
|
294
313
|
|
295
|
-
(new Audio({
|
314
|
+
(new Audio({ // Play a looped idle sound that follows the payload spatially
|
296
315
|
uri: 'audio/sfx/payload-idle.mp3',
|
297
316
|
loop: true,
|
298
317
|
attachedToEntity: payloadEntity,
|
299
318
|
volume: 0.25,
|
300
|
-
referenceDistance: 5,
|
319
|
+
referenceDistance: 5, // Reference distance affects how loud the audio is relative to a player's proximity to the entity
|
301
320
|
})).play(world);
|
302
321
|
}
|
303
322
|
|
304
323
|
function spawnSpider(world: World, coordinate: Vector3) {
|
305
324
|
const baseScale = 0.5;
|
306
325
|
const baseSpeed = 3;
|
307
|
-
const randomScaleMultiplier = Math.random() * 2 + 1; // Random value between 1 and 3
|
326
|
+
const randomScaleMultiplier = Math.random() * 2 + 1; // Random value between 1 and 3 // Random scale multiplier to make each spider a different size
|
327
|
+
const targetPlayers = new Set<PlayerEntity>();
|
308
328
|
|
309
329
|
const spider = new Entity({
|
310
330
|
name: 'Spider',
|
@@ -320,7 +340,7 @@ function spawnSpider(world: World, coordinate: Vector3) {
|
|
320
340
|
borderRadius: 0.1 * randomScaleMultiplier,
|
321
341
|
halfHeight: 0.225 * randomScaleMultiplier,
|
322
342
|
radius: 0.5 * randomScaleMultiplier,
|
323
|
-
tag: 'body',
|
343
|
+
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
344
|
collisionGroups: {
|
325
345
|
belongsTo: [ CollisionGroup.ENTITY ],
|
326
346
|
collidesWith: [ CollisionGroup.BLOCK, CollisionGroup.ENTITY_SENSOR, CollisionGroup.PLAYER ],
|
@@ -336,7 +356,7 @@ function spawnSpider(world: World, coordinate: Vector3) {
|
|
336
356
|
belongsTo: [ CollisionGroup.ENTITY_SENSOR ],
|
337
357
|
collidesWith: [ CollisionGroup.PLAYER ],
|
338
358
|
},
|
339
|
-
onCollision: (other: BlockType | Entity, started: boolean) => {
|
359
|
+
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
360
|
if (other instanceof PlayerEntity) {
|
341
361
|
started ? targetPlayers.add(other) : targetPlayers.delete(other);
|
342
362
|
}
|
@@ -346,16 +366,14 @@ function spawnSpider(world: World, coordinate: Vector3) {
|
|
346
366
|
},
|
347
367
|
});
|
348
368
|
|
349
|
-
|
350
|
-
|
351
|
-
spider.onTick = (tickDeltaMs: number) => onTickPathfindEnemy(
|
369
|
+
spider.onTick = (tickDeltaMs: number) => onTickPathfindEnemy( // Use our own basic pathfinding function each tick of the game for the enemy
|
352
370
|
spider,
|
353
371
|
targetPlayers,
|
354
372
|
baseSpeed * randomScaleMultiplier,
|
355
373
|
tickDeltaMs,
|
356
374
|
);
|
357
375
|
|
358
|
-
spider.onEntityCollision = (entity: Entity, started: boolean) => {
|
376
|
+
spider.onEntityCollision = (entity: Entity, started: boolean) => { // If the spider hits a player, deal damage and apply knockback
|
359
377
|
if (started && entity instanceof PlayerEntity && entity.isSpawned) {
|
360
378
|
const spiderDirection = spider.getDirectionFromRotation();
|
361
379
|
const knockback = 4 * randomScaleMultiplier;
|
@@ -379,15 +397,16 @@ function spawnSpider(world: World, coordinate: Vector3) {
|
|
379
397
|
|
380
398
|
spider.spawn(world, coordinate);
|
381
399
|
|
400
|
+
// Give the spider a health value relative to its size, bigger = more health
|
382
401
|
enemyHealth[spider.id!] = 2 * Math.round(randomScaleMultiplier);
|
383
402
|
}
|
384
403
|
|
385
|
-
function onTickPathfindPayload(this: Entity, tickDeltaMs: number) {
|
386
|
-
const speed = started
|
404
|
+
function onTickPathfindPayload(this: Entity, tickDeltaMs: number) { // Movement logic for the payload
|
405
|
+
const speed = started // Set the payload speed relative to the number of players in the payload sensor
|
387
406
|
? Math.max(Math.min(PAYLOAD_PER_PLAYER_SPEED * payloadPlayerEntityCount, PAYLOAD_MAX_SPEED), 0)
|
388
407
|
: 0;
|
389
408
|
|
390
|
-
if (!speed) {
|
409
|
+
if (!speed) { // Play animations based on if its moving or not
|
391
410
|
this.stopModelAnimations(Array.from(this.modelLoopedAnimations).filter(v => v !== 'idle'));
|
392
411
|
this.startModelLoopedAnimations([ 'idle' ]);
|
393
412
|
} else {
|
@@ -395,7 +414,7 @@ function onTickPathfindPayload(this: Entity, tickDeltaMs: number) {
|
|
395
414
|
this.startModelLoopedAnimations([ 'walk' ]);
|
396
415
|
}
|
397
416
|
|
398
|
-
// Calculate direction to target
|
417
|
+
// Calculate direction to target waypoint
|
399
418
|
const targetWaypointCoordinate = PAYLOAD_WAYPOINT_COORDINATES[targetWaypointCoordinateIndex];
|
400
419
|
const currentPosition = this.getTranslation();
|
401
420
|
const deltaX = targetWaypointCoordinate.x - currentPosition.x;
|
@@ -406,7 +425,7 @@ function onTickPathfindPayload(this: Entity, tickDeltaMs: number) {
|
|
406
425
|
z: Math.abs(deltaZ) > 0.1 ? Math.sign(deltaZ) : 0,
|
407
426
|
};
|
408
427
|
|
409
|
-
// Apply rotation to face direction if necessary
|
428
|
+
// Apply rotation to face direction if necessary based on the current target waypoint
|
410
429
|
const rotation = this.getRotation();
|
411
430
|
const currentAngle = 2 * Math.atan2(rotation.y, rotation.w);
|
412
431
|
const targetAngle = Math.atan2(direction.x, direction.z) + Math.PI; // Add PI to face forward
|
@@ -452,7 +471,7 @@ function onTickPathfindEnemy(entity: Entity, targetPlayers: Set<PlayerEntity>, s
|
|
452
471
|
if (!entity.isSpawned || !payloadEntity) return;
|
453
472
|
|
454
473
|
const entityId = entity.id!;
|
455
|
-
enemyPathfindAccumulators[entityId] ??= 0;
|
474
|
+
enemyPathfindAccumulators[entityId] ??= 0; // Initialize the accumulator for this enemy if it isn't initialized yet
|
456
475
|
|
457
476
|
// Handle pathfinding
|
458
477
|
if (!enemyPathfindingTargets[entityId] || enemyPathfindAccumulators[entityId] >= PATHFIND_ACCUMULATOR_THRESHOLD) {
|
@@ -542,7 +561,7 @@ function damagePlayer(playerEntity: PlayerEntity) {
|
|
542
561
|
);
|
543
562
|
|
544
563
|
if (playerEntityHealth[playerEntity.id!] <= 0) {
|
545
|
-
chatManager.sendPlayerMessage(
|
564
|
+
chatManager.sendPlayerMessage( // Alert the player they've been damaged, since we don't have UI support yet, we just use chat
|
546
565
|
playerEntity.player,
|
547
566
|
'You have died!',
|
548
567
|
'FF0000',
|
package/package.json
CHANGED
Binary file
|