hytopia 0.1.18 → 0.1.19
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
![HYTOPIA
|
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:
|
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
|