hytopia 0.3.6 → 0.3.7

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 (117) hide show
  1. package/boilerplate/assets/map.json +191 -43
  2. package/docs/server.playercameramode.md +14 -0
  3. package/examples/hygrounds/README.md +0 -0
  4. package/examples/hygrounds/assets/audio/sfx/chest-open-1.mp3 +0 -0
  5. package/examples/hygrounds/assets/audio/sfx/chest-open-2.mp3 +0 -0
  6. package/examples/hygrounds/assets/audio/sfx/machine-gun-reload.mp3 +0 -0
  7. package/examples/hygrounds/assets/audio/sfx/machine-gun-shoot.mp3 +0 -0
  8. package/examples/hygrounds/assets/audio/sfx/medpack-consume.mp3 +0 -0
  9. package/examples/hygrounds/assets/audio/sfx/mining-drill-drilling.mp3 +0 -0
  10. package/examples/hygrounds/assets/audio/sfx/pistol-reload.mp3 +0 -0
  11. package/examples/hygrounds/assets/audio/sfx/pistol-shoot.mp3 +0 -0
  12. package/examples/hygrounds/assets/audio/sfx/player-hurt.mp3 +0 -0
  13. package/examples/hygrounds/assets/audio/sfx/rifle-reload.mp3 +0 -0
  14. package/examples/hygrounds/assets/audio/sfx/rifle-shoot.mp3 +0 -0
  15. package/examples/hygrounds/assets/audio/sfx/rocket-launcher-explosion.mp3 +0 -0
  16. package/examples/hygrounds/assets/audio/sfx/rocket-launcher-reload.mp3 +0 -0
  17. package/examples/hygrounds/assets/audio/sfx/rocket-launcher-shoot.mp3 +0 -0
  18. package/examples/hygrounds/assets/audio/sfx/shield-potion-consume.mp3 +0 -0
  19. package/examples/hygrounds/assets/audio/sfx/shield.png +0 -0
  20. package/examples/hygrounds/assets/audio/sfx/shotgun-reload.mp3 +0 -0
  21. package/examples/hygrounds/assets/audio/sfx/shotgun-shoot.mp3 +0 -0
  22. package/examples/hygrounds/assets/audio/sfx/sniper-reload.mp3 +0 -0
  23. package/examples/hygrounds/assets/audio/sfx/sniper-shoot.mp3 +0 -0
  24. package/examples/hygrounds/assets/icons/ak-47.png +0 -0
  25. package/examples/hygrounds/assets/icons/ammo.png +0 -0
  26. package/examples/hygrounds/assets/icons/auto-shotgun.png +0 -0
  27. package/examples/hygrounds/assets/icons/block.png +0 -0
  28. package/examples/hygrounds/assets/icons/bolt-action-sniper.png +0 -0
  29. package/examples/hygrounds/assets/icons/crown-bronze.png +0 -0
  30. package/examples/hygrounds/assets/icons/crown-gold.png +0 -0
  31. package/examples/hygrounds/assets/icons/crown-silver.png +0 -0
  32. package/examples/hygrounds/assets/icons/heart.png +0 -0
  33. package/examples/hygrounds/assets/icons/light-machine-gun.png +0 -0
  34. package/examples/hygrounds/assets/icons/medpack.png +0 -0
  35. package/examples/hygrounds/assets/icons/mining-drill.png +0 -0
  36. package/examples/hygrounds/assets/icons/pickaxe.png +0 -0
  37. package/examples/hygrounds/assets/icons/pistol.png +0 -0
  38. package/examples/hygrounds/assets/icons/rocket-launcher.png +0 -0
  39. package/examples/hygrounds/assets/icons/shield-potion.png +0 -0
  40. package/examples/hygrounds/assets/icons/shield.png +0 -0
  41. package/examples/hygrounds/assets/icons/shotgun.png +0 -0
  42. package/examples/hygrounds/assets/map.json +31796 -0
  43. package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion-named-nodes.glb +0 -0
  44. package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion.glb +0 -0
  45. package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion.glb.md5 +1 -0
  46. package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2-named-nodes.glb +0 -0
  47. package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2.glb +0 -0
  48. package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2.glb.md5 +1 -0
  49. package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3-named-nodes.glb +0 -0
  50. package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3.glb +0 -0
  51. package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3.glb.md5 +1 -0
  52. package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter-named-nodes.glb +0 -0
  53. package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter.glb +0 -0
  54. package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter.glb.md5 +1 -0
  55. package/examples/hygrounds/assets/models/environment/chest.gltf +1 -0
  56. package/examples/hygrounds/assets/models/environment/explosion.glb +0 -0
  57. package/examples/hygrounds/assets/models/environment/muzzle-flash.gltf +1 -0
  58. package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit-named-nodes.glb +0 -0
  59. package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit.glb +0 -0
  60. package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit.glb.md5 +1 -0
  61. package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack-named-nodes.glb +0 -0
  62. package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack.glb +0 -0
  63. package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack.glb.md5 +1 -0
  64. package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill-named-nodes.glb +0 -0
  65. package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill.glb +0 -0
  66. package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill.glb.md5 +1 -0
  67. package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile-named-nodes.glb +0 -0
  68. package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile.glb +0 -0
  69. package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile.glb.md5 +1 -0
  70. package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion-named-nodes.glb +0 -0
  71. package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion.glb +0 -0
  72. package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion.glb.md5 +1 -0
  73. package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2-named-nodes.glb +0 -0
  74. package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2.glb +0 -0
  75. package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2.glb.md5 +1 -0
  76. package/examples/hygrounds/assets/models/items/ak-47.glb +0 -0
  77. package/examples/hygrounds/assets/models/items/auto-shotgun.glb +0 -0
  78. package/examples/hygrounds/assets/models/items/bolt-action-sniper.glb +0 -0
  79. package/examples/hygrounds/assets/models/items/light-machine-gun.glb +0 -0
  80. package/examples/hygrounds/assets/models/items/medpack.glb +0 -0
  81. package/examples/hygrounds/assets/models/items/mining-drill.glb +0 -0
  82. package/examples/hygrounds/assets/models/items/pickaxe.gltf +1 -0
  83. package/examples/hygrounds/assets/models/items/pistol.glb +0 -0
  84. package/examples/hygrounds/assets/models/items/rocket-launcher.glb +0 -0
  85. package/examples/hygrounds/assets/models/items/rocket-missile.glb +0 -0
  86. package/examples/hygrounds/assets/models/items/shield-potion.glb +0 -0
  87. package/examples/hygrounds/assets/models/items/shotgun.glb +0 -0
  88. package/examples/hygrounds/assets/models/players/soldier-player.gltf +1 -0
  89. package/examples/hygrounds/assets/ui/images/scope.png +0 -0
  90. package/examples/hygrounds/assets/ui/index.html +1072 -0
  91. package/examples/hygrounds/bun.lock +503 -0
  92. package/examples/hygrounds/classes/ChestEntity.ts +133 -0
  93. package/examples/hygrounds/classes/GameManager.ts +384 -0
  94. package/examples/hygrounds/classes/GamePlayerEntity.ts +564 -0
  95. package/examples/hygrounds/classes/GunEntity.ts +263 -0
  96. package/examples/hygrounds/classes/ItemEntity.ts +225 -0
  97. package/examples/hygrounds/classes/ItemFactory.ts +49 -0
  98. package/examples/hygrounds/classes/MeleeWeaponEntity.ts +138 -0
  99. package/examples/hygrounds/classes/TerrainDamageManager.ts +56 -0
  100. package/examples/hygrounds/classes/items/MedPackEntity.ts +43 -0
  101. package/examples/hygrounds/classes/items/ShieldPotionEntity.ts +43 -0
  102. package/examples/hygrounds/classes/weapons/AK47Entity.ts +43 -0
  103. package/examples/hygrounds/classes/weapons/AutoShotgunEntity.ts +80 -0
  104. package/examples/hygrounds/classes/weapons/BoltActionSniperEntity.ts +46 -0
  105. package/examples/hygrounds/classes/weapons/LightMachineGunEntity.ts +43 -0
  106. package/examples/hygrounds/classes/weapons/MiningDrillEntity.ts +38 -0
  107. package/examples/hygrounds/classes/weapons/PickaxeEntity.ts +38 -0
  108. package/examples/hygrounds/classes/weapons/PistolEntity.ts +46 -0
  109. package/examples/hygrounds/classes/weapons/RocketLauncherEntity.ts +186 -0
  110. package/examples/hygrounds/classes/weapons/ShotgunEntity.ts +84 -0
  111. package/examples/hygrounds/gameConfig.ts +398 -0
  112. package/examples/hygrounds/index.ts +40 -0
  113. package/examples/hygrounds/package.json +16 -0
  114. package/package.json +1 -1
  115. package/server.api.json +21 -0
  116. package/server.d.ts +2 -1
  117. package/server.js +113 -113
@@ -0,0 +1,133 @@
1
+ import {
2
+ Audio,
3
+ Collider,
4
+ Entity,
5
+ EntityOptions,
6
+ QuaternionLike,
7
+ SceneUI,
8
+ Vector3Like,
9
+ World,
10
+ } from 'hytopia';
11
+
12
+ import { CHEST_DROP_ITEMS, CHEST_MAX_DROP_ITEMS, CHEST_OPEN_DESPAWN_MS } from '../gameConfig';
13
+ import ItemFactory from './ItemFactory';
14
+
15
+ export default class ChestEntity extends Entity {
16
+ private _labelSceneUI: SceneUI;
17
+ private _openAudio: Audio;
18
+ private _opened: boolean = false;
19
+
20
+ public constructor(options: EntityOptions = {}) {
21
+ super({
22
+ modelUri: 'models/environment/chest.gltf',
23
+ modelScale: 1,
24
+ name: 'Item Chest',
25
+ rigidBodyOptions: {
26
+ additionalMass: 10000,
27
+ enabledPositions: { x: false, y: true, z: false },
28
+ enabledRotations: { x: false, y: false, z: false },
29
+ ccdEnabled: true,
30
+ gravityScale: 0.3, // we want it to drop slow when spawned mid-game in the sky randomly.
31
+ colliders: [
32
+ {
33
+ ...Collider.optionsFromModelUri('models/environment/chest.gltf'),
34
+ radius: 0.45, // collider isn't calculating perfect because of the coin positions in the model.
35
+ bounciness: 0.25,
36
+ }
37
+ ]
38
+ },
39
+ ...options,
40
+ });
41
+
42
+ this._labelSceneUI = this._createLabelUI();
43
+
44
+ this._openAudio = new Audio({
45
+ attachedToEntity: this,
46
+ uri: 'audio/sfx/chest-open-2.mp3',
47
+ volume: 0.7,
48
+ referenceDistance: 8,
49
+ });
50
+ }
51
+
52
+ public open(): void {
53
+ if (this._opened || !this.world) return;
54
+
55
+ this._opened = true;
56
+ this._openAudio.play(this.world, true);
57
+ this._labelSceneUI.unload();
58
+
59
+ this.startModelOneshotAnimations(['opening']);
60
+
61
+ setTimeout(() => {
62
+ this.startModelLoopedAnimations([ 'open' ]);
63
+
64
+ const numItems = Math.floor(Math.random() * CHEST_MAX_DROP_ITEMS) + 1;
65
+
66
+ for (let i = 0; i < numItems; i++) {
67
+ this._spawnRandomChestItem();
68
+ }
69
+ }, 600);
70
+
71
+ // despawn chest after 20 seconds
72
+ setTimeout(() => {
73
+ if (this.isSpawned) {
74
+ this.despawn();
75
+ }
76
+ }, CHEST_OPEN_DESPAWN_MS);
77
+ }
78
+
79
+ public override spawn(world: World, position: Vector3Like, rotation?: QuaternionLike): void {
80
+ super.spawn(world, position, rotation);
81
+ this._labelSceneUI.load(world);
82
+ }
83
+
84
+ private _createLabelUI(): SceneUI {
85
+ return new SceneUI({
86
+ attachedToEntity: this,
87
+ templateId: 'chest-label',
88
+ state: { name: this.name },
89
+ viewDistance: 8,
90
+ offset: { x: 0, y: 0.85, z: 0 },
91
+ });
92
+ }
93
+
94
+ private async _spawnRandomChestItem(): Promise<void> {
95
+ if (!this.world) return;
96
+
97
+ // Calculate total weight
98
+ const totalWeight = CHEST_DROP_ITEMS.reduce((sum, item) => sum + item.pickWeight, 0);
99
+
100
+ // Get random value between 0 and total weight
101
+ let random = Math.random() * totalWeight;
102
+
103
+ // Find the selected item
104
+ let selectedItem = CHEST_DROP_ITEMS[0];
105
+ for (const item of CHEST_DROP_ITEMS) {
106
+ random -= item.pickWeight;
107
+ if (random <= 0) {
108
+ selectedItem = item;
109
+ break;
110
+ }
111
+ }
112
+
113
+ const item = await ItemFactory.createItem(selectedItem.itemId);
114
+
115
+ if (item) {
116
+ item.spawn(this.world, {
117
+ x: this.position.x,
118
+ y: this.position.y + 2,
119
+ z: this.position.z,
120
+ });
121
+
122
+ item.startDespawnTimer();
123
+
124
+ item.applyImpulse({ // apply an impulse in a random x/z direction
125
+ x: (Math.random() - 0.5) * 10 * item.mass,
126
+ y: 5 * item.mass,
127
+ z: (Math.random() - 0.5) * 10 * item.mass,
128
+ });
129
+ } else {
130
+ console.error(`Failed to create item: ${selectedItem.itemId}`);
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,384 @@
1
+ import {
2
+ GameServer,
3
+ Player,
4
+ Quaternion,
5
+ Vector3Like,
6
+ World,
7
+ } from 'hytopia';
8
+
9
+ import worldMap from '../assets/map.json';
10
+
11
+ import {
12
+ BEDROCK_BLOCK_ID,
13
+ CHEST_SPAWNS,
14
+ CHEST_SPAWNS_AT_START,
15
+ CHEST_DROP_INTERVAL_MS,
16
+ CHEST_DROP_REGION_AABB,
17
+ GAME_DURATION_MS,
18
+ ITEM_SPAWNS,
19
+ ITEM_SPAWNS_AT_START,
20
+ ITEM_SPAWN_ITEMS,
21
+ MINIMUM_PLAYERS_TO_START,
22
+ SPAWN_REGION_AABB,
23
+ } from '../gameConfig';
24
+
25
+ import GamePlayerEntity from './GamePlayerEntity';
26
+ import ChestEntity from './ChestEntity';
27
+ import ItemFactory from './ItemFactory';
28
+
29
+ export default class GameManager {
30
+ public static readonly instance = new GameManager();
31
+
32
+ public world: World | undefined;
33
+ private _chestDropInterval: NodeJS.Timeout | undefined;
34
+ private _gameStartAt: number = 0;
35
+ private _gameTimer: NodeJS.Timeout | undefined;
36
+ private _playerCount: number = 0;
37
+ private _restartTimer: NodeJS.Timeout | undefined;
38
+ private _killCounter: Map<string, number> = new Map();
39
+ private _gameActive: boolean = false;
40
+
41
+ public get isGameActive(): boolean { return this._gameActive; }
42
+
43
+ public get playerCount(): number { return this._playerCount; }
44
+ public set playerCount(value: number) {
45
+ this._playerCount = value;
46
+ this._updatePlayerCountUI();
47
+ }
48
+
49
+ /**
50
+ * Sets up the game world and waits for players to join
51
+ */
52
+ public setupGame(world: World) {
53
+ this.world = world;
54
+ this._spawnBedrock(world);
55
+ this._waitForPlayersToStart();
56
+ }
57
+
58
+ /**
59
+ * Starts a new game round
60
+ */
61
+ public startGame() {
62
+ if (!this.world) return;
63
+
64
+ // Clean up any previous game state
65
+ this._cleanup();
66
+
67
+ // Set game as active
68
+ this._gameActive = true;
69
+ this._gameStartAt = Date.now();
70
+
71
+ // Spawn initial game elements
72
+ this._spawnStartingChests();
73
+ this._spawnStartingItems();
74
+ this._startChestDropInterval();
75
+
76
+ // Move all players to random spawn positions
77
+ this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
78
+ playerEntity.setPosition(this.getRandomSpawnPosition());
79
+ playerEntity.player.ui.sendData({ type: 'game-start' });
80
+ this._sendGameStartAnnouncements(playerEntity.player);
81
+ });
82
+
83
+ // Set game timer
84
+ this._gameTimer = setTimeout(() => this.endGame(), GAME_DURATION_MS);
85
+
86
+ // Sync UI for all players
87
+ this._syncAllPlayersUI();
88
+ }
89
+
90
+ /**
91
+ * Ends the current game round and schedules the next one
92
+ */
93
+ public endGame() {
94
+ if (!this.world || !this._gameActive) return;
95
+
96
+ this._gameActive = false;
97
+ this.world.chatManager.sendBroadcastMessage('Game over! Starting the next round in 10 seconds...', 'FF0000');
98
+
99
+ // Clear any existing restart timer
100
+ if (this._restartTimer) {
101
+ clearTimeout(this._restartTimer);
102
+ }
103
+
104
+ this._restartTimer = setTimeout(() => this.startGame(), 10 * 1000);
105
+ }
106
+
107
+ /**
108
+ * Spawns a player entity in the world
109
+ */
110
+ public spawnPlayerEntity(player: Player) {
111
+ if (!this.world) return;
112
+
113
+ const playerEntity = new GamePlayerEntity(player);
114
+ playerEntity.spawn(this.world, this.getRandomSpawnPosition());
115
+
116
+ // Sync UI for the new player
117
+ this.syncTimer(player);
118
+ this.syncLeaderboard(player);
119
+
120
+ // Send start announcement if game is active
121
+ if (this._gameActive) {
122
+ player.ui.sendData({ type: 'game-start' });
123
+ this._sendGameStartAnnouncements(player);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Increments kill count for a player and updates the leaderboard
129
+ */
130
+ public addKill(playerId: string): void {
131
+ const killCount = this._killCounter.get(playerId) ?? 0;
132
+ const newKillCount = killCount + 1;
133
+
134
+ this._killCounter.set(playerId, newKillCount);
135
+ this._updateLeaderboardUI(playerId, newKillCount);
136
+ }
137
+
138
+ /**
139
+ * Gets a random spawn position within the defined spawn region
140
+ */
141
+ public getRandomSpawnPosition(): Vector3Like {
142
+ return {
143
+ x: SPAWN_REGION_AABB.min.x + Math.random() * (SPAWN_REGION_AABB.max.x - SPAWN_REGION_AABB.min.x),
144
+ y: SPAWN_REGION_AABB.min.y + Math.random() * (SPAWN_REGION_AABB.max.y - SPAWN_REGION_AABB.min.y),
145
+ z: SPAWN_REGION_AABB.min.z + Math.random() * (SPAWN_REGION_AABB.max.z - SPAWN_REGION_AABB.min.z),
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Returns the current kill counts for all players
151
+ */
152
+ public getKillCounts(): Record<string, number> {
153
+ return Object.fromEntries(this._killCounter);
154
+ }
155
+
156
+ /**
157
+ * Syncs the leaderboard UI for a specific player
158
+ */
159
+ public syncLeaderboard(player: Player) {
160
+ if (!this.world) return;
161
+
162
+ player.ui.sendData({
163
+ type: 'leaderboard-sync',
164
+ killCounts: this.getKillCounts(),
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Syncs the game timer UI for a specific player
170
+ */
171
+ public syncTimer(player: Player) {
172
+ if (!this.world || !this._gameStartAt) return;
173
+
174
+ player.ui.sendData({
175
+ type: 'timer-sync',
176
+ startedAt: this._gameStartAt,
177
+ endsAt: this._gameStartAt + GAME_DURATION_MS,
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Resets the leaderboard and syncs it for all players
183
+ */
184
+ public resetLeaderboard() {
185
+ if (!this.world) return;
186
+
187
+ this._killCounter.clear();
188
+
189
+ GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
190
+ this.syncLeaderboard(player);
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Cleans up the game state for a new round
196
+ */
197
+ private _cleanup() {
198
+ if (!this.world) return;
199
+
200
+ // Reset map to initial state
201
+ this.world.loadMap(worldMap);
202
+
203
+ // Reset player state
204
+ this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
205
+ if (playerEntity instanceof GamePlayerEntity) {
206
+ playerEntity.setActiveInventorySlotIndex(0); // reset to pickaxe at slot 0
207
+ playerEntity.dropAllInventoryItems();
208
+ playerEntity.resetMaterials();
209
+ playerEntity.health = 100;
210
+ playerEntity.shield = 0;
211
+ }
212
+ });
213
+
214
+ // Remove non-player entities except pickaxes
215
+ this.world.entityManager.getAllEntities().forEach(entity => {
216
+ if (!(entity instanceof GamePlayerEntity) && entity.tag !== 'pickaxe') {
217
+ // allow 1 event loop for drop to resolve, there's some
218
+ // weird bug here otherwise we need to investigate later.
219
+ setTimeout(() => {
220
+ if (entity.isSpawned) {
221
+ entity.despawn();
222
+ }
223
+ }, 0);
224
+ }
225
+ });
226
+
227
+ // Clear timers
228
+ if (this._gameTimer) {
229
+ clearTimeout(this._gameTimer);
230
+ this._gameTimer = undefined;
231
+ }
232
+
233
+ if (this._chestDropInterval) {
234
+ clearInterval(this._chestDropInterval);
235
+ this._chestDropInterval = undefined;
236
+ }
237
+
238
+ // Reset leaderboard
239
+ this.resetLeaderboard();
240
+ }
241
+
242
+ /**
243
+ * Syncs UI for all connected players
244
+ */
245
+ private _syncAllPlayersUI() {
246
+ if (!this.world) return;
247
+
248
+ const players = GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world);
249
+ players.forEach(player => {
250
+ this.syncTimer(player);
251
+ this.syncLeaderboard(player);
252
+ });
253
+ }
254
+
255
+ /**
256
+ * Sends game start announcements to a specific player
257
+ */
258
+ private _sendGameStartAnnouncements(player: Player) {
259
+ if (!this.world) return;
260
+
261
+ this.world.chatManager.sendPlayerMessage(player, 'Game started - most kills wins!', '00FF00');
262
+ this.world.chatManager.sendPlayerMessage(player, '- Search for chests and weapons to survive');
263
+ this.world.chatManager.sendPlayerMessage(player, '- Break blocks with your pickaxe to gain materials');
264
+ this.world.chatManager.sendPlayerMessage(player, '- Right click to spend 3 materials to place a block');
265
+ this.world.chatManager.sendPlayerMessage(player, '- Some weapons can zoom with "Z".');
266
+ }
267
+
268
+ /**
269
+ * Creates bedrock floor for the game world
270
+ */
271
+ private _spawnBedrock(world: World) {
272
+ for (let x = -50; x <= 50; x++) {
273
+ for (let z = -50; z <= 50; z++) {
274
+ world.chunkLattice.setBlock({ x, y: -1, z }, BEDROCK_BLOCK_ID);
275
+ }
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Spawns initial chests at random positions
281
+ */
282
+ private _spawnStartingChests() {
283
+ if (!this.world) return;
284
+
285
+ const shuffledChestSpawns = [...CHEST_SPAWNS].sort(() => Math.random() - 0.5);
286
+ const selectedChestSpawns = shuffledChestSpawns.slice(0, CHEST_SPAWNS_AT_START);
287
+
288
+ for (const spawn of selectedChestSpawns) {
289
+ const chest = new ChestEntity();
290
+ chest.spawn(this.world, spawn.position, spawn.rotation);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Spawns initial items at random positions
296
+ */
297
+ private _spawnStartingItems() {
298
+ if (!this.world) return;
299
+
300
+ const shuffledItemSpawns = [...ITEM_SPAWNS].sort(() => Math.random() - 0.5);
301
+ const selectedItemSpawns = shuffledItemSpawns.slice(0, ITEM_SPAWNS_AT_START);
302
+ const totalWeight = ITEM_SPAWN_ITEMS.reduce((sum, item) => sum + item.pickWeight, 0);
303
+
304
+ selectedItemSpawns.forEach(async spawn => {
305
+ // Select random item based on weight
306
+ let random = Math.random() * totalWeight;
307
+ let selectedItem = ITEM_SPAWN_ITEMS[0];
308
+
309
+ for (const item of ITEM_SPAWN_ITEMS) {
310
+ random -= item.pickWeight;
311
+ if (random <= 0) {
312
+ selectedItem = item;
313
+ break;
314
+ }
315
+ }
316
+
317
+ const item = await ItemFactory.createItem(selectedItem.itemId);
318
+ item.spawn(this.world!, spawn.position, Quaternion.fromEuler(0, Math.random() * 360 - 180, 0));
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Starts the interval for dropping chests during the game
324
+ */
325
+ private _startChestDropInterval() {
326
+ if (this._chestDropInterval) {
327
+ clearInterval(this._chestDropInterval);
328
+ }
329
+
330
+ this._chestDropInterval = setInterval(() => {
331
+ if (!this.world || !this._gameActive) return;
332
+
333
+ const randomPosition = {
334
+ x: Math.floor(Math.random() * (CHEST_DROP_REGION_AABB.max.x - CHEST_DROP_REGION_AABB.min.x + 1)) + CHEST_DROP_REGION_AABB.min.x,
335
+ y: CHEST_DROP_REGION_AABB.min.y,
336
+ z: Math.floor(Math.random() * (CHEST_DROP_REGION_AABB.max.z - CHEST_DROP_REGION_AABB.min.z + 1)) + CHEST_DROP_REGION_AABB.min.z,
337
+ };
338
+
339
+ const chest = new ChestEntity();
340
+ const randomRotation = Quaternion.fromEuler(0, [0, 90, -90, 180][Math.floor(Math.random() * 4)], 0);
341
+ chest.spawn(this.world, randomPosition, randomRotation);
342
+ }, CHEST_DROP_INTERVAL_MS);
343
+ }
344
+
345
+ /**
346
+ * Updates the leaderboard UI for all players
347
+ */
348
+ private _updateLeaderboardUI(username: string, killCount: number) {
349
+ if (!this.world) return;
350
+
351
+ GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
352
+ player.ui.sendData({
353
+ type: 'leaderboard-update',
354
+ username,
355
+ killCount,
356
+ });
357
+ });
358
+ }
359
+
360
+ private _updatePlayerCountUI() {
361
+ setTimeout(() => { // have to wait 1 tick, we need to figure out this race condition later
362
+ if (!this.world) return;
363
+
364
+ GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
365
+ player.ui.sendData({ type: 'players-count', count: this._playerCount });
366
+ });
367
+ }, 25);
368
+ }
369
+
370
+ /**
371
+ * Waits for enough players to join before starting the game
372
+ */
373
+ private _waitForPlayersToStart() {
374
+ if (!this.world) return;
375
+
376
+ const connectedPlayers = GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).length;
377
+
378
+ if (connectedPlayers >= MINIMUM_PLAYERS_TO_START) {
379
+ this.startGame();
380
+ } else {
381
+ setTimeout(() => this._waitForPlayersToStart(), 1000);
382
+ }
383
+ }
384
+ }