hytopia 0.1.60 → 0.1.62

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.
Files changed (38) hide show
  1. package/bin/scripts.js +6 -4
  2. package/boilerplate/assets/map.json +2 -1
  3. package/docs/server.blocktypeoptions.customcollideroptions.md +2 -0
  4. package/docs/server.blocktypeoptions.id.md +2 -0
  5. package/docs/server.blocktypeoptions.isliquid.md +2 -0
  6. package/docs/server.blocktypeoptions.md +8 -2
  7. package/docs/server.blocktypeoptions.name.md +2 -0
  8. package/docs/server.blocktypeoptions.textureuri.md +2 -0
  9. package/docs/server.collisiongroup.md +168 -0
  10. package/docs/server.entity.md +35 -0
  11. package/docs/server.entity.opacity.md +13 -0
  12. package/docs/server.entity.setopacity.md +53 -0
  13. package/docs/server.entityeventpayload.md +9 -0
  14. package/docs/server.entityeventpayload.setopacity.entity.md +11 -0
  15. package/docs/server.entityeventpayload.setopacity.md +70 -0
  16. package/docs/server.entityeventpayload.setopacity.opacity.md +11 -0
  17. package/docs/server.entityeventtype.md +14 -0
  18. package/docs/server.entityoptions.md +19 -0
  19. package/docs/server.entityoptions.opacity.md +13 -0
  20. package/docs/server.playerentitycontroller.md +19 -0
  21. package/docs/server.playerentitycontroller.stickstoplatforms.md +13 -0
  22. package/docs/server.playerentitycontrolleroptions.md +19 -0
  23. package/docs/server.playerentitycontrolleroptions.stickstoplatforms.md +13 -0
  24. package/examples/big-world/package.json +3 -2
  25. package/examples/block-entity/package.json +3 -2
  26. package/examples/custom-ui/package.json +3 -2
  27. package/examples/entity-controller/package.json +3 -2
  28. package/examples/entity-spawn/package.json +3 -2
  29. package/examples/payload-game/package.json +3 -2
  30. package/examples/wall-dodge-game/assets/map.json +3995 -0
  31. package/examples/wall-dodge-game/assets/textures/water.png +0 -0
  32. package/examples/wall-dodge-game/assets/ui/index.html +249 -0
  33. package/examples/wall-dodge-game/index.ts +336 -0
  34. package/examples/wall-dodge-game/package.json +16 -0
  35. package/package.json +1 -1
  36. package/server.api.json +512 -5
  37. package/server.d.ts +48 -12
  38. package/server.js +71 -71
@@ -0,0 +1,249 @@
1
+ <!-- Game Start Animation-->
2
+ <script>
3
+ function formatTime(ms) {
4
+ const totalSeconds = Math.floor(ms / 1000);
5
+ const minutes = Math.floor(totalSeconds / 60);
6
+ const seconds = totalSeconds % 60;
7
+
8
+ const paddedMinutes = minutes.toString().padStart(2, '0');
9
+ const paddedSeconds = seconds.toString().padStart(2, '0');
10
+
11
+ return `${paddedMinutes}:${paddedSeconds}`;
12
+ }
13
+
14
+ function showGameOver(scoreTime, lastTopScoreTime) {
15
+ const gameOver = document.getElementById('game-over');
16
+ const scoreValue = document.getElementById('score-value');
17
+ const highScoreText = document.getElementById('high-score-text');
18
+
19
+ scoreValue.textContent = formatTime(scoreTime);
20
+ highScoreText.style.display = scoreTime > lastTopScoreTime ? 'block' : 'none';
21
+ gameOver.style.opacity = 1;
22
+
23
+ setTimeout(() => {
24
+ gameOver.style.opacity = 0;
25
+ }, 3000);
26
+ }
27
+
28
+ function showGameCountdown() {
29
+ const el = document.getElementById('countdown');
30
+ const show = (text, color) => {
31
+ el.style.opacity = 0;
32
+ setTimeout(() => {
33
+ el.textContent = text;
34
+ el.style.color = color;
35
+ el.style.opacity = 1;
36
+ }, 300);
37
+ };
38
+
39
+ [3, 2, 1].forEach((num, i) => {
40
+ setTimeout(() => show(num, '#ff0000'), i * 1000);
41
+ });
42
+
43
+ setTimeout(() => {
44
+ show('GO!', '#00ff00');
45
+ setTimeout(() => el.style.opacity = 0, 1000);
46
+ }, 3000);
47
+ }
48
+
49
+ function updateLeaderboard(scores) {
50
+ const entriesDiv = document.getElementById('leaderboard-entries');
51
+ entriesDiv.innerHTML = '';
52
+
53
+ if (!scores.length) {
54
+ const noScoresRow = document.createElement('div');
55
+ noScoresRow.className = 'leaderboard-row';
56
+ noScoresRow.textContent = 'No Top Scores';
57
+ noScoresRow.style.display = 'flex';
58
+ noScoresRow.style.justifyContent = 'center';
59
+ entriesDiv.appendChild(noScoresRow);
60
+ return;
61
+ }
62
+
63
+ scores.forEach(({name, score}) => {
64
+ const row = document.createElement('div');
65
+ row.className = 'leaderboard-row';
66
+
67
+ const username = document.createElement('span');
68
+ username.className = 'username';
69
+ username.textContent = name;
70
+
71
+ const time = document.createElement('span');
72
+ time.className = 'time';
73
+ time.textContent = formatTime(score);
74
+
75
+ row.appendChild(username);
76
+ row.appendChild(time);
77
+ entriesDiv.appendChild(row);
78
+ });
79
+ }
80
+
81
+ // Server to client UI data handlers
82
+ hytopia.onData(data => {
83
+ if (data.type === 'game-end') {
84
+ showGameOver(data.scoreTime, data.lastTopScoreTime);
85
+ }
86
+
87
+ if (data.type === 'game-start') {
88
+ showGameCountdown();
89
+ }
90
+
91
+ if (data.type === 'leaderboard') {
92
+ updateLeaderboard(data.scores);
93
+ }
94
+ });
95
+
96
+ // Register in-game scene UI templates, so server can
97
+ // instantiate instance with new SceneUI({ templateId: 'join-npc-message', ...etc });
98
+ hytopia.registerSceneUITemplate('join-npc-message', () => {
99
+ const template = document.getElementById('join-npc-message');
100
+ const clone = template.content.cloneNode(true);
101
+ return clone;
102
+ });
103
+ </script>
104
+
105
+ <!-- Game Start Countdown -->
106
+ <div id="countdown"></div>
107
+
108
+ <!-- Game End Animation -->
109
+ <div id="game-over">
110
+ <div class="main-text">Game Over!</div>
111
+ <div class="score-text">Score: <span id="score-value"></span></div>
112
+ <div id="high-score-text">New personal high score!</div>
113
+ </div>
114
+
115
+ <!-- Leaderboard -->
116
+ <div class="leaderboard">
117
+ <h2>Top Survivors</h2>
118
+ <div id="leaderboard-entries" class="leaderboard-entries">
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Template for Join NPC Scene UI-->
123
+ <template id="join-npc-message">
124
+ <div class="join-npc-message">
125
+ <h1>Join the game</h1>
126
+ <p>Jump on my platform to join the game</p>
127
+ <p style="margin-top: 5px;">(WASD to move, Spacebar to jump)</p>
128
+ </div>
129
+ </template>
130
+
131
+ <!-- Styles -->
132
+ <style>
133
+ * {
134
+ font-family: Arial, sans-serif;
135
+ user-select: none;
136
+ }
137
+
138
+ .join-npc-message {
139
+ background: rgba(0, 0, 0, 0.8);
140
+ border-radius: 12px;
141
+ padding: 12px 20px;
142
+ color: white;
143
+ text-align: center;
144
+ position: relative;
145
+ margin-bottom: 15px;
146
+ }
147
+
148
+ .join-npc-message:after {
149
+ content: '';
150
+ position: absolute;
151
+ bottom: -10px;
152
+ left: 50%;
153
+ transform: translateX(-50%);
154
+ border-left: 10px solid transparent;
155
+ border-right: 10px solid transparent;
156
+ border-top: 10px solid rgba(0, 0, 0, 0.8);
157
+ }
158
+
159
+ .join-npc-message h1 {
160
+ font-size: 18px;
161
+ margin: 0 0 8px 0;
162
+ }
163
+
164
+ .join-npc-message p {
165
+ font-size: 14px;
166
+ margin: 0;
167
+ }
168
+
169
+ #countdown {
170
+ position: fixed;
171
+ top: 50%;
172
+ left: 50%;
173
+ transform: translate(-50%, -50%);
174
+ font-size: 120px;
175
+ font-weight: bold;
176
+ opacity: 0;
177
+ transition: opacity 0.3s;
178
+ text-shadow: 2px 2px 8px rgba(0,0,0,0.8);
179
+ }
180
+
181
+ #game-over {
182
+ position: fixed;
183
+ top: 50%;
184
+ left: 50%;
185
+ transform: translate(-50%, -50%);
186
+ text-align: center;
187
+ opacity: 0;
188
+ transition: opacity 0.5s;
189
+ }
190
+
191
+ #game-over .main-text {
192
+ font-size: 120px;
193
+ font-weight: bold;
194
+ color: #ff0000;
195
+ text-shadow: 2px 2px 8px rgba(0,0,0,0.8);
196
+ }
197
+
198
+ #game-over .score-text {
199
+ font-size: 48px;
200
+ margin-top: 20px;
201
+ color: white;
202
+ text-shadow: 2px 2px 8px rgba(0,0,0,0.8);
203
+ }
204
+
205
+ #game-over #high-score-text {
206
+ font-size: 36px;
207
+ margin-top: 15px;
208
+ color: #ffd700;
209
+ text-shadow: 2px 2px 8px rgba(0,0,0,0.8);
210
+ display: none;
211
+ }
212
+
213
+ .leaderboard {
214
+ position: fixed;
215
+ top: 20px;
216
+ right: 20px;
217
+ background: rgba(0, 0, 0, 0.8);
218
+ border-radius: 12px;
219
+ padding: 15px;
220
+ color: white;
221
+ min-width: 200px;
222
+ }
223
+
224
+ .leaderboard h2 {
225
+ font-size: 18px;
226
+ margin: 0 0 12px 0;
227
+ text-align: center;
228
+ }
229
+
230
+ .leaderboard-entries {
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 8px;
234
+ }
235
+
236
+ .leaderboard-row {
237
+ display: flex;
238
+ justify-content: space-between;
239
+ font-size: 14px;
240
+ }
241
+
242
+ .username {
243
+ color: #fff;
244
+ }
245
+
246
+ .time {
247
+ color: #ffd700;
248
+ }
249
+ </style>
@@ -0,0 +1,336 @@
1
+ import {
2
+ CollisionGroup,
3
+ ColliderShape,
4
+ BlockType,
5
+ Entity,
6
+ GameServer,
7
+ SceneUI,
8
+ startServer,
9
+ Player,
10
+ PlayerEntity,
11
+ PlayerEntityController,
12
+ RigidBodyType,
13
+ SimpleEntityController,
14
+ World,
15
+ } from 'hytopia';
16
+
17
+ import worldMap from './assets/map.json';
18
+
19
+ const GAME_BLOCK_SIZE_RANGE = {
20
+ x: [ 0.5, 4 ],
21
+ y: [ 0.5, 4 ],
22
+ z: [ 0.5, 1.5 ],
23
+ };
24
+
25
+ const GAME_BLOCK_SPAWN_RANGE = {
26
+ x: [ -7, 7 ],
27
+ y: [ 1, 4 ],
28
+ z: [ -25, -25 ],
29
+ };
30
+
31
+ const GAME_BLOCK_RANDOM_TEXTURES = [
32
+ 'textures/grass',
33
+ 'textures/bricks.png',
34
+ 'textures/glass.png',
35
+ 'textures/gravel.png',
36
+ 'textures/sand.png',
37
+ 'textures/void_sand.png',
38
+ ];
39
+
40
+ const GAME_BLOCK_DESPAWN_Z = 12; // Blocks will despawn when block z position is > than this.
41
+
42
+ const GAME_BLOCK_COLLISION_GROUP = CollisionGroup.GROUP_1;
43
+
44
+ const PLAYER_GAME_START_TIME = new Map<Player, number>(); // Player -> start time of current game
45
+ const PLAYER_TOP_SCORES = new Map<Player, number>(); // Player -> highest ever score
46
+ let GAME_TOP_SCORES: { name: string, score: number }[] = []; // array user [name, score]
47
+
48
+ startServer(world => {
49
+ // Uncomment this to visualize physics vertices, will cause noticable lag.
50
+ // world.simulation.enableDebugRendering(true);
51
+
52
+ world.loadMap(worldMap);
53
+ world.onPlayerJoin = player => onPlayerJoin(world, player);
54
+ world.onPlayerLeave = player => onPlayerLeave(world, player);
55
+
56
+ setupJoinNPC(world);
57
+ startBlockSpawner(world);
58
+ });
59
+
60
+ /**
61
+ * Creates and sets up the NPC the player can interact
62
+ * with to join the game.
63
+ */
64
+ function setupJoinNPC(world: World) {
65
+ let focusedPlayer: PlayerEntity | null = null;
66
+
67
+ // Create our NPC
68
+ const joinNPC = new Entity({
69
+ controller: new SimpleEntityController(),
70
+ name: 'Join NPC',
71
+ modelUri: 'models/mindflayer.gltf',
72
+ modelLoopedAnimations: [ 'idle' ],
73
+ modelScale: 0.5,
74
+ rigidBodyOptions: {
75
+ type: RigidBodyType.FIXED, // It won't ever move, so we can use a fixed body
76
+ rotation: { x: 0, y: 1, z: 0, w: 0 }, // Rotate the NPC to face the player
77
+ colliders: [
78
+ { // Hitbox/body collider
79
+ shape: ColliderShape.CYLINDER,
80
+ radius: 0.3,
81
+ halfHeight: 1.2,
82
+ tag: 'body',
83
+ },
84
+ { // Create a sensor that teleports the player into the game
85
+ shape: ColliderShape.BLOCK,
86
+ halfExtents: { x: 1.5, y: 1, z: 1.5 }, // size it slightly smaller than the platform the join NPC is standing on
87
+ isSensor: true,
88
+ tag: 'teleport-sensor',
89
+ onCollision: (other: BlockType | Entity, started: boolean) => {
90
+ if (started && other instanceof PlayerEntity) {
91
+ startGame(other); // When a player entity enters this sensor, start the game for them
92
+ }
93
+ },
94
+ },
95
+ { // Create a sensor to detect players for a fun rotation effect
96
+ shape: ColliderShape.CYLINDER,
97
+ radius: 5,
98
+ halfHeight: 2,
99
+ isSensor: true, // This makes the collider not collide with other entities/objets
100
+ tag: 'rotate-sensor',
101
+ onCollision: (other: BlockType | Entity, started: boolean) => {
102
+ if (started && other instanceof PlayerEntity) {
103
+ focusedPlayer = other;
104
+ }
105
+ },
106
+ },
107
+ ],
108
+ },
109
+ });
110
+
111
+ // Rotate to face the last focused player position every 250ms
112
+ setInterval(() => {
113
+ if (focusedPlayer?.isSpawned) {
114
+ (joinNPC.controller! as SimpleEntityController).face(focusedPlayer.position, 2);
115
+ }
116
+ }, 250);
117
+
118
+ // Create the Scene UI over the NPC
119
+ const npcMessageUI = new SceneUI({
120
+ templateId: 'join-npc-message',
121
+ attachedToEntity: joinNPC,
122
+ offset: { x: 0, y: 1.75, z: 0 },
123
+ });
124
+
125
+ npcMessageUI.load(world);
126
+
127
+ joinNPC.spawn(world, { x: 1, y: 3.1, z: 15 });
128
+ }
129
+
130
+ /**
131
+ * Start spawning blocks in the game
132
+ */
133
+ function startBlockSpawner(world: World) {
134
+ const spawnBlock = () => {
135
+ // Calculate random number of blocks to spawn (1-8)
136
+ const numBlocks = Math.floor(Math.random() * 8) + 1;
137
+
138
+ for (let i = 0; i < numBlocks; i++) {
139
+ // Calculate random half extents within allowed ranges
140
+ const x = Math.max(0.5, Math.random() * GAME_BLOCK_SIZE_RANGE.x[1]);
141
+ const y = Math.max(0.5, Math.random() * GAME_BLOCK_SIZE_RANGE.y[1]);
142
+ const z = Math.max(0.5, Math.random() * GAME_BLOCK_SIZE_RANGE.z[1]);
143
+
144
+ const halfExtents = {
145
+ x: y > 0.5 ? 0.5 : x,
146
+ y: x > 0.5 ? 0.5 : y,
147
+ z,
148
+ };
149
+
150
+ // Calculate spawn point ranges accounting for block size
151
+ const spawnPoint = {
152
+ x: Math.random() * (GAME_BLOCK_SPAWN_RANGE.x[1] - GAME_BLOCK_SPAWN_RANGE.x[0] - halfExtents.x * 2) + GAME_BLOCK_SPAWN_RANGE.x[0] + halfExtents.x,
153
+ y: Math.random() * (GAME_BLOCK_SPAWN_RANGE.y[1] - GAME_BLOCK_SPAWN_RANGE.y[0] - halfExtents.y * 2) + GAME_BLOCK_SPAWN_RANGE.y[0] + halfExtents.y,
154
+ z: Math.random() * (GAME_BLOCK_SPAWN_RANGE.z[1] - GAME_BLOCK_SPAWN_RANGE.z[0] - halfExtents.z * 2) + GAME_BLOCK_SPAWN_RANGE.z[0] + halfExtents.z,
155
+ };
156
+
157
+ // Calculate random velocity between 5-10
158
+ const linearVelocity = 5 + Math.random() * 5;
159
+
160
+ // Apply random angular velocity 10% of the time
161
+ const angularVelocity = Math.random() < 0.1 ? {
162
+ x: (Math.random() - 0.5) * 5, // -5 to 5
163
+ y: (Math.random() - 0.5) * 5,
164
+ z: (Math.random() - 0.5) * 5,
165
+ } : {
166
+ x: 0,
167
+ y: 0,
168
+ z: 0,
169
+ };
170
+
171
+ const blockEntity = new Entity({
172
+ blockTextureUri: GAME_BLOCK_RANDOM_TEXTURES[Math.floor(Math.random() * GAME_BLOCK_RANDOM_TEXTURES.length)],
173
+ blockHalfExtents: halfExtents,
174
+ rigidBodyOptions: {
175
+ type: RigidBodyType.KINEMATIC_VELOCITY,
176
+ linearVelocity: { x: 0, y: 0, z: linearVelocity },
177
+ angularVelocity,
178
+ },
179
+ });
180
+
181
+ blockEntity.onTick = () => {
182
+ if (blockEntity.isSpawned && blockEntity.position.z > GAME_BLOCK_DESPAWN_Z) {
183
+ // TODO: drop it out of the world, despawn to fix platform collision bug
184
+ blockEntity.despawn();
185
+ }
186
+ };
187
+
188
+ blockEntity.spawn(world, spawnPoint);
189
+
190
+ // Set groups after spawn so they apply to internally generated colliders too.
191
+ blockEntity.setCollisionGroupsForSolidColliders({
192
+ belongsTo: [ GAME_BLOCK_COLLISION_GROUP ],
193
+ collidesWith: [ CollisionGroup.PLAYER ],
194
+ });
195
+ }
196
+
197
+ setTimeout(spawnBlock, 250 + Math.random() * 750);
198
+ };
199
+
200
+ spawnBlock();
201
+ }
202
+
203
+ function startGame(playerEntity: PlayerEntity) {
204
+ playerEntity.setPosition({ x: 1, y: 4, z: 1 });
205
+ playerEntity.setOpacity(0.3);
206
+ playerEntity.player.ui.sendData({ type: 'game-start' });
207
+ enablePlayerEntityGameCollisions(playerEntity, false);
208
+
209
+ PLAYER_GAME_START_TIME.set(playerEntity.player, Date.now());
210
+
211
+ setTimeout(() => { // Game starts!
212
+ if (!playerEntity.isSpawned) return;
213
+
214
+ playerEntity.setOpacity(1);
215
+ enablePlayerEntityGameCollisions(playerEntity, true);
216
+ }, 3500);
217
+ }
218
+
219
+ function endGame(playerEntity: PlayerEntity) {
220
+ const startTime = PLAYER_GAME_START_TIME.get(playerEntity.player) ?? Date.now();
221
+ const scoreTime = Date.now() - startTime;
222
+ const lastTopScoreTime = PLAYER_TOP_SCORES.get(playerEntity.player) ?? 0;
223
+
224
+ if (scoreTime > lastTopScoreTime) {
225
+ PLAYER_TOP_SCORES.set(playerEntity.player, scoreTime);
226
+ }
227
+
228
+ playerEntity.player.ui.sendData({
229
+ type: 'game-end',
230
+ scoreTime,
231
+ lastTopScoreTime,
232
+ });
233
+
234
+ // Reset player to lobby area
235
+ playerEntity.setLinearVelocity({ x: 0, y: 0, z: 0 });
236
+ playerEntity.setPosition(getRandomSpawnCoordinate());
237
+
238
+ if (playerEntity.world) {
239
+ updateTopScores();
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Handle players joining the game.
245
+ * We create an initial player entity they control
246
+ * and set up their entity's collision groups to not collider
247
+ * with other players.
248
+ */
249
+ function onPlayerJoin(world: World, player: Player) {
250
+ // Load the game UI for the player
251
+ player.ui.load('ui/index.html');
252
+ sendPlayerLeaderboardData(player);
253
+
254
+ // Create the player entity
255
+ const playerEntity = new PlayerEntity({
256
+ player,
257
+ name: 'Player',
258
+ modelUri: 'models/player.gltf',
259
+ modelLoopedAnimations: [ 'idle' ],
260
+ modelScale: 0.5,
261
+ });
262
+
263
+ (playerEntity.controller as PlayerEntityController).sticksToPlatforms = false;
264
+
265
+ playerEntity.onTick = () => {
266
+ if (playerEntity.position.y < -3) {
267
+ // Assume the player has fallen off the map in the game
268
+ endGame(playerEntity);
269
+ }
270
+ };
271
+
272
+ // Spawn with a random X coordinate to spread players out a bit.
273
+ playerEntity.spawn(world, getRandomSpawnCoordinate());
274
+ }
275
+
276
+ /**
277
+ * Despawn the player's entity and perform any other
278
+ * cleanup when they leave the game.
279
+ */
280
+ function onPlayerLeave(world: World, player: Player) {
281
+ world.entityManager.getAllPlayerEntities(player).forEach(entity => {
282
+ endGame(entity); // explicitly end their game if they leave
283
+ entity.despawn(); // despawn their entity
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Set collision groups for in the game.
289
+ * Enabled determines if we hit moving blocks, we can set this to false
290
+ * to give players a bit of time to setup before the game starts.
291
+ * We also con't collide with other players.
292
+ * Collision groups work on if both contacted colliders belong to a group the other collides with.
293
+ */
294
+ function enablePlayerEntityGameCollisions(playerEntity: PlayerEntity, enabled: boolean) {
295
+ playerEntity.colliders.forEach(collider => {
296
+ collider.setCollisionGroups({
297
+ belongsTo: [ CollisionGroup.ENTITY, CollisionGroup.PLAYER ],
298
+ collidesWith: enabled ?
299
+ [ CollisionGroup.BLOCK, CollisionGroup.ENTITY_SENSOR, GAME_BLOCK_COLLISION_GROUP ] :
300
+ [ CollisionGroup.BLOCK, CollisionGroup.ENTITY_SENSOR ],
301
+ });
302
+ });
303
+ }
304
+
305
+ function getRandomSpawnCoordinate() {
306
+ const randomX = Math.floor(Math.random() * 15) - 6;
307
+
308
+ return { x: randomX, y: 10, z: 22 };
309
+ }
310
+
311
+ function updateTopScores() {
312
+ const topScores = Array.from(PLAYER_TOP_SCORES.entries())
313
+ .sort((a, b) => a[1] - b[1])
314
+ .map(([ player, score ]) => ({ player, score }));
315
+
316
+ // Get the top 10 highest scores
317
+ const updatedTopScores = topScores.slice(0, 10).map(({ player, score }) => ({ name: player.username, score }));
318
+
319
+ // Convert both arrays to strings for comparison
320
+ const currentScoresStr = JSON.stringify(GAME_TOP_SCORES);
321
+ const updatedScoresStr = JSON.stringify(updatedTopScores);
322
+
323
+ // Only update if scores have changed
324
+ if (currentScoresStr !== updatedScoresStr) {
325
+ GAME_TOP_SCORES = updatedTopScores;
326
+ }
327
+
328
+ GameServer.instance.playerManager.getConnectedPlayers().forEach(sendPlayerLeaderboardData);
329
+ }
330
+
331
+ function sendPlayerLeaderboardData(player: Player) {
332
+ player.ui.sendData({
333
+ type: 'leaderboard',
334
+ scores: GAME_TOP_SCORES,
335
+ });
336
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "payload-game",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "hytopia": "latest",
14
+ "@hytopia.com/assets": "latest"
15
+ }
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hytopia",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "The HYTOPIA SDK makes it easy for developers to create massively multiplayer games using JavaScript or TypeScript.",
5
5
  "main": "server.js",
6
6
  "bin": {