hytopia 0.1.18 → 0.1.20
Sign up to get free protection for your applications and to get access to all the features.
- package/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
- package/README.md +15 -8
- package/bin/scripts.js +2 -1
- package/examples/README.md +22 -0
- package/examples/payload-game/index.ts +86 -53
- package/package.json +1 -1
- package/readme/assets/demo.gif +0 -0
@@ -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
|
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)
|
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
|
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
|
-
//
|
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
|
-
|
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(
|
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
|
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 ||
|
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
|
-
//
|
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
|
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
|
-
|
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
Binary file
|