hytopia 0.3.6 → 0.3.8

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 (147) 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/potion-consume.mp3 +0 -0
  14. package/examples/hygrounds/assets/audio/sfx/rifle-reload.mp3 +0 -0
  15. package/examples/hygrounds/assets/audio/sfx/rifle-shoot.mp3 +0 -0
  16. package/examples/hygrounds/assets/audio/sfx/rocket-launcher-explosion.mp3 +0 -0
  17. package/examples/hygrounds/assets/audio/sfx/rocket-launcher-reload.mp3 +0 -0
  18. package/examples/hygrounds/assets/audio/sfx/rocket-launcher-shoot.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/auto-sniper.png +0 -0
  28. package/examples/hygrounds/assets/icons/block.png +0 -0
  29. package/examples/hygrounds/assets/icons/bolt-action-sniper.png +0 -0
  30. package/examples/hygrounds/assets/icons/crown-bronze.png +0 -0
  31. package/examples/hygrounds/assets/icons/crown-gold.png +0 -0
  32. package/examples/hygrounds/assets/icons/crown-silver.png +0 -0
  33. package/examples/hygrounds/assets/icons/gravity-potion.png +0 -0
  34. package/examples/hygrounds/assets/icons/heart.png +0 -0
  35. package/examples/hygrounds/assets/icons/light-machine-gun.png +0 -0
  36. package/examples/hygrounds/assets/icons/medpack.png +0 -0
  37. package/examples/hygrounds/assets/icons/mining-drill.png +0 -0
  38. package/examples/hygrounds/assets/icons/pickaxe.png +0 -0
  39. package/examples/hygrounds/assets/icons/pistol.png +0 -0
  40. package/examples/hygrounds/assets/icons/revolver.png +0 -0
  41. package/examples/hygrounds/assets/icons/rocket-launcher.png +0 -0
  42. package/examples/hygrounds/assets/icons/shield-potion.png +0 -0
  43. package/examples/hygrounds/assets/icons/shield.png +0 -0
  44. package/examples/hygrounds/assets/icons/shotgun.png +0 -0
  45. package/examples/hygrounds/assets/icons/submachine-gun.png +0 -0
  46. package/examples/hygrounds/assets/map.json +31796 -0
  47. package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion-named-nodes.glb +0 -0
  48. package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion.glb +0 -0
  49. package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion.glb.md5 +1 -0
  50. package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2-named-nodes.glb +0 -0
  51. package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2.glb +0 -0
  52. package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2.glb.md5 +1 -0
  53. package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3-named-nodes.glb +0 -0
  54. package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3.glb +0 -0
  55. package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3.glb.md5 +1 -0
  56. package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter-named-nodes.glb +0 -0
  57. package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter.glb +0 -0
  58. package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter.glb.md5 +1 -0
  59. package/examples/hygrounds/assets/models/environment/chest.gltf +1 -0
  60. package/examples/hygrounds/assets/models/environment/explosion.glb +0 -0
  61. package/examples/hygrounds/assets/models/environment/muzzle-flash.gltf +1 -0
  62. package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper-named-nodes.glb +0 -0
  63. package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb +0 -0
  64. package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb.md5 +1 -0
  65. package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion-named-nodes.glb +0 -0
  66. package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb +0 -0
  67. package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb.md5 +1 -0
  68. package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit-named-nodes.glb +0 -0
  69. package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit.glb +0 -0
  70. package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit.glb.md5 +1 -0
  71. package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack-named-nodes.glb +0 -0
  72. package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack.glb +0 -0
  73. package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack.glb.md5 +1 -0
  74. package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill-named-nodes.glb +0 -0
  75. package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill.glb +0 -0
  76. package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill.glb.md5 +1 -0
  77. package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver-named-nodes.glb +0 -0
  78. package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.glb +0 -0
  79. package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.glb.md5 +1 -0
  80. package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile-named-nodes.glb +0 -0
  81. package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile.glb +0 -0
  82. package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile.glb.md5 +1 -0
  83. package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion-named-nodes.glb +0 -0
  84. package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion.glb +0 -0
  85. package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion.glb.md5 +1 -0
  86. package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2-named-nodes.glb +0 -0
  87. package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2.glb +0 -0
  88. package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2.glb.md5 +1 -0
  89. package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun-named-nodes.glb +0 -0
  90. package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.glb +0 -0
  91. package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.glb.md5 +1 -0
  92. package/examples/hygrounds/assets/models/items/ak-47.glb +0 -0
  93. package/examples/hygrounds/assets/models/items/auto-shotgun.glb +0 -0
  94. package/examples/hygrounds/assets/models/items/auto-sniper.glb +0 -0
  95. package/examples/hygrounds/assets/models/items/bolt-action-sniper.glb +0 -0
  96. package/examples/hygrounds/assets/models/items/gravity-potion.glb +0 -0
  97. package/examples/hygrounds/assets/models/items/light-machine-gun.glb +0 -0
  98. package/examples/hygrounds/assets/models/items/medpack.glb +0 -0
  99. package/examples/hygrounds/assets/models/items/mining-drill.glb +0 -0
  100. package/examples/hygrounds/assets/models/items/pickaxe.gltf +1 -0
  101. package/examples/hygrounds/assets/models/items/pistol.glb +0 -0
  102. package/examples/hygrounds/assets/models/items/revolver.glb +0 -0
  103. package/examples/hygrounds/assets/models/items/rocket-launcher.glb +0 -0
  104. package/examples/hygrounds/assets/models/items/rocket-missile.glb +0 -0
  105. package/examples/hygrounds/assets/models/items/shield-potion.glb +0 -0
  106. package/examples/hygrounds/assets/models/items/shotgun.glb +0 -0
  107. package/examples/hygrounds/assets/models/items/submachine-gun.glb +0 -0
  108. package/examples/hygrounds/assets/models/players/soldier-player.gltf +1 -0
  109. package/examples/hygrounds/assets/ui/images/scope.png +0 -0
  110. package/examples/hygrounds/assets/ui/index.html +1122 -0
  111. package/examples/hygrounds/bun.lock +503 -0
  112. package/examples/hygrounds/classes/ChestEntity.ts +133 -0
  113. package/examples/hygrounds/classes/GameManager.ts +422 -0
  114. package/examples/hygrounds/classes/GamePlayerEntity.ts +595 -0
  115. package/examples/hygrounds/classes/GunEntity.ts +259 -0
  116. package/examples/hygrounds/classes/ItemEntity.ts +225 -0
  117. package/examples/hygrounds/classes/ItemFactory.ts +61 -0
  118. package/examples/hygrounds/classes/MeleeWeaponEntity.ts +138 -0
  119. package/examples/hygrounds/classes/TerrainDamageManager.ts +56 -0
  120. package/examples/hygrounds/classes/items/GravityPotionEntity.ts +48 -0
  121. package/examples/hygrounds/classes/items/MedPackEntity.ts +43 -0
  122. package/examples/hygrounds/classes/items/ShieldPotionEntity.ts +43 -0
  123. package/examples/hygrounds/classes/weapons/AK47Entity.ts +43 -0
  124. package/examples/hygrounds/classes/weapons/AutoShotgunEntity.ts +80 -0
  125. package/examples/hygrounds/classes/weapons/AutoSniperEntity.ts +43 -0
  126. package/examples/hygrounds/classes/weapons/BoltActionSniperEntity.ts +46 -0
  127. package/examples/hygrounds/classes/weapons/LightMachineGunEntity.ts +43 -0
  128. package/examples/hygrounds/classes/weapons/MiningDrillEntity.ts +38 -0
  129. package/examples/hygrounds/classes/weapons/PickaxeEntity.ts +38 -0
  130. package/examples/hygrounds/classes/weapons/PistolEntity.ts +46 -0
  131. package/examples/hygrounds/classes/weapons/RevolverEntity.ts +46 -0
  132. package/examples/hygrounds/classes/weapons/RocketLauncherEntity.ts +195 -0
  133. package/examples/hygrounds/classes/weapons/ShotgunEntity.ts +84 -0
  134. package/examples/hygrounds/classes/weapons/SubmachineGunEntity.ts +43 -0
  135. package/examples/hygrounds/gameConfig.ts +430 -0
  136. package/examples/hygrounds/index.ts +41 -0
  137. package/examples/hygrounds/package.json +16 -0
  138. package/examples/player-persistence/README.md +3 -0
  139. package/examples/player-persistence/assets/map.json +2623 -0
  140. package/examples/player-persistence/dev/persistence/player-player-1.json +6 -0
  141. package/examples/player-persistence/dev/persistence/test.json +3 -0
  142. package/examples/player-persistence/index.ts +126 -0
  143. package/examples/player-persistence/package.json +16 -0
  144. package/package.json +1 -1
  145. package/server.api.json +21 -0
  146. package/server.d.ts +2 -1
  147. 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,422 @@
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
+ this._focusWinningPlayer();
100
+
101
+ // Clear any existing restart timer
102
+ if (this._restartTimer) {
103
+ clearTimeout(this._restartTimer);
104
+ }
105
+
106
+ this._restartTimer = setTimeout(() => this.startGame(), 10 * 1000);
107
+ }
108
+
109
+ /**
110
+ * Spawns a player entity in the world
111
+ */
112
+ public spawnPlayerEntity(player: Player) {
113
+ if (!this.world) return;
114
+
115
+ const playerEntity = new GamePlayerEntity(player);
116
+ playerEntity.spawn(this.world, this.getRandomSpawnPosition());
117
+
118
+ // Sync UI for the new player
119
+ this.syncTimer(player);
120
+ this.syncLeaderboard(player);
121
+
122
+ // Send start announcement if game is active
123
+ if (this._gameActive) {
124
+ player.ui.sendData({ type: 'game-start' });
125
+ this._sendGameStartAnnouncements(player);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Increments kill count for a player and updates the leaderboard
131
+ */
132
+ public addKill(playerId: string): void {
133
+ const killCount = this._killCounter.get(playerId) ?? 0;
134
+ const newKillCount = killCount + 1;
135
+
136
+ this._killCounter.set(playerId, newKillCount);
137
+ this._updateLeaderboardUI(playerId, newKillCount);
138
+ }
139
+
140
+ /**
141
+ * Gets a random spawn position within the defined spawn region
142
+ */
143
+ public getRandomSpawnPosition(): Vector3Like {
144
+ return {
145
+ x: SPAWN_REGION_AABB.min.x + Math.random() * (SPAWN_REGION_AABB.max.x - SPAWN_REGION_AABB.min.x),
146
+ y: SPAWN_REGION_AABB.min.y + Math.random() * (SPAWN_REGION_AABB.max.y - SPAWN_REGION_AABB.min.y),
147
+ z: SPAWN_REGION_AABB.min.z + Math.random() * (SPAWN_REGION_AABB.max.z - SPAWN_REGION_AABB.min.z),
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Returns the current kill counts for all players
153
+ */
154
+ public getKillCounts(): Record<string, number> {
155
+ return Object.fromEntries(this._killCounter);
156
+ }
157
+
158
+ /**
159
+ * Syncs the leaderboard UI for a specific player
160
+ */
161
+ public syncLeaderboard(player: Player) {
162
+ if (!this.world) return;
163
+
164
+ player.ui.sendData({
165
+ type: 'leaderboard-sync',
166
+ killCounts: this.getKillCounts(),
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Syncs the game timer UI for a specific player
172
+ */
173
+ public syncTimer(player: Player) {
174
+ if (!this.world || !this._gameStartAt) return;
175
+
176
+ player.ui.sendData({
177
+ type: 'timer-sync',
178
+ startedAt: this._gameStartAt,
179
+ endsAt: this._gameStartAt + GAME_DURATION_MS,
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Resets the leaderboard and syncs it for all players
185
+ */
186
+ public resetLeaderboard() {
187
+ if (!this.world) return;
188
+
189
+ this._killCounter.clear();
190
+
191
+ GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
192
+ this.syncLeaderboard(player);
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Cleans up the game state for a new round
198
+ */
199
+ private _cleanup() {
200
+ if (!this.world) return;
201
+
202
+ // Reset map to initial state
203
+ this.world.loadMap(worldMap);
204
+
205
+ // Reset player state
206
+ this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
207
+ if (playerEntity instanceof GamePlayerEntity) {
208
+ playerEntity.setActiveInventorySlotIndex(0); // reset to pickaxe at slot 0
209
+ playerEntity.dropAllInventoryItems();
210
+ playerEntity.resetCamera();
211
+ playerEntity.resetMaterials();
212
+ playerEntity.health = 100;
213
+ playerEntity.shield = 0;
214
+ }
215
+ });
216
+
217
+ // Remove non-player entities except pickaxes
218
+ this.world.entityManager.getAllEntities().forEach(entity => {
219
+ if (!(entity instanceof GamePlayerEntity) && entity.tag !== 'pickaxe') {
220
+ // allow 1 event loop for drop to resolve, there's some
221
+ // weird bug here otherwise we need to investigate later.
222
+ setTimeout(() => {
223
+ if (entity.isSpawned) {
224
+ entity.despawn();
225
+ }
226
+ }, 0);
227
+ }
228
+ });
229
+
230
+ // Clear timers
231
+ if (this._gameTimer) {
232
+ clearTimeout(this._gameTimer);
233
+ this._gameTimer = undefined;
234
+ }
235
+
236
+ if (this._chestDropInterval) {
237
+ clearInterval(this._chestDropInterval);
238
+ this._chestDropInterval = undefined;
239
+ }
240
+
241
+ // Reset leaderboard
242
+ this.resetLeaderboard();
243
+ }
244
+
245
+ public _focusWinningPlayer() {
246
+ if (!this.world) return;
247
+
248
+ // Find player with most kills
249
+ let highestKills = 0;
250
+ let winningPlayer = '';
251
+
252
+ this._killCounter.forEach((kills, player) => {
253
+ if (kills > highestKills) {
254
+ highestKills = kills;
255
+ winningPlayer = player;
256
+ }
257
+ });
258
+
259
+ // Get winning player entity
260
+ const winningPlayerEntity = this.world.entityManager
261
+ .getAllPlayerEntities()
262
+ .find(entity => entity.player.username === winningPlayer);
263
+
264
+ if (!winningPlayerEntity) return;
265
+
266
+ this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
267
+ if (playerEntity instanceof GamePlayerEntity) {
268
+ if (playerEntity.player.username !== winningPlayer) { // don't change camera for the winner
269
+ playerEntity.focusCameraOnPlayer(winningPlayerEntity as GamePlayerEntity);
270
+ }
271
+
272
+ playerEntity.player.ui.sendData({
273
+ type: 'announce-winner',
274
+ username: winningPlayer,
275
+ });
276
+ }
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Syncs UI for all connected players
282
+ */
283
+ private _syncAllPlayersUI() {
284
+ if (!this.world) return;
285
+
286
+ const players = GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world);
287
+ players.forEach(player => {
288
+ this.syncTimer(player);
289
+ this.syncLeaderboard(player);
290
+ });
291
+ }
292
+
293
+ /**
294
+ * Sends game start announcements to a specific player
295
+ */
296
+ private _sendGameStartAnnouncements(player: Player) {
297
+ if (!this.world) return;
298
+
299
+ this.world.chatManager.sendPlayerMessage(player, 'Game started - most kills wins!', '00FF00');
300
+ this.world.chatManager.sendPlayerMessage(player, '- Search for chests and weapons to survive');
301
+ this.world.chatManager.sendPlayerMessage(player, '- Break blocks with your pickaxe to gain materials');
302
+ this.world.chatManager.sendPlayerMessage(player, '- Right click to spend 3 materials to place a block');
303
+ this.world.chatManager.sendPlayerMessage(player, '- Some weapons zoom with "Z". Drop items with "Q"');
304
+ }
305
+
306
+ /**
307
+ * Creates bedrock floor for the game world
308
+ */
309
+ private _spawnBedrock(world: World) {
310
+ for (let x = -50; x <= 50; x++) {
311
+ for (let z = -50; z <= 50; z++) {
312
+ world.chunkLattice.setBlock({ x, y: -1, z }, BEDROCK_BLOCK_ID);
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Spawns initial chests at random positions
319
+ */
320
+ private _spawnStartingChests() {
321
+ if (!this.world) return;
322
+
323
+ const shuffledChestSpawns = [...CHEST_SPAWNS].sort(() => Math.random() - 0.5);
324
+ const selectedChestSpawns = shuffledChestSpawns.slice(0, CHEST_SPAWNS_AT_START);
325
+
326
+ for (const spawn of selectedChestSpawns) {
327
+ const chest = new ChestEntity();
328
+ chest.spawn(this.world, spawn.position, spawn.rotation);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Spawns initial items at random positions
334
+ */
335
+ private _spawnStartingItems() {
336
+ if (!this.world) return;
337
+
338
+ const shuffledItemSpawns = [...ITEM_SPAWNS].sort(() => Math.random() - 0.5);
339
+ const selectedItemSpawns = shuffledItemSpawns.slice(0, ITEM_SPAWNS_AT_START);
340
+ const totalWeight = ITEM_SPAWN_ITEMS.reduce((sum, item) => sum + item.pickWeight, 0);
341
+
342
+ selectedItemSpawns.forEach(async spawn => {
343
+ // Select random item based on weight
344
+ let random = Math.random() * totalWeight;
345
+ let selectedItem = ITEM_SPAWN_ITEMS[0];
346
+
347
+ for (const item of ITEM_SPAWN_ITEMS) {
348
+ random -= item.pickWeight;
349
+ if (random <= 0) {
350
+ selectedItem = item;
351
+ break;
352
+ }
353
+ }
354
+
355
+ const item = await ItemFactory.createItem(selectedItem.itemId);
356
+ item.spawn(this.world!, spawn.position, Quaternion.fromEuler(0, Math.random() * 360 - 180, 0));
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Starts the interval for dropping chests during the game
362
+ */
363
+ private _startChestDropInterval() {
364
+ if (this._chestDropInterval) {
365
+ clearInterval(this._chestDropInterval);
366
+ }
367
+
368
+ this._chestDropInterval = setInterval(() => {
369
+ if (!this.world || !this._gameActive) return;
370
+
371
+ const randomPosition = {
372
+ x: Math.floor(Math.random() * (CHEST_DROP_REGION_AABB.max.x - CHEST_DROP_REGION_AABB.min.x + 1)) + CHEST_DROP_REGION_AABB.min.x,
373
+ y: CHEST_DROP_REGION_AABB.min.y,
374
+ z: Math.floor(Math.random() * (CHEST_DROP_REGION_AABB.max.z - CHEST_DROP_REGION_AABB.min.z + 1)) + CHEST_DROP_REGION_AABB.min.z,
375
+ };
376
+
377
+ const chest = new ChestEntity();
378
+ const randomRotation = Quaternion.fromEuler(0, [0, 90, -90, 180][Math.floor(Math.random() * 4)], 0);
379
+ chest.spawn(this.world, randomPosition, randomRotation);
380
+ }, CHEST_DROP_INTERVAL_MS);
381
+ }
382
+
383
+ /**
384
+ * Updates the leaderboard UI for all players
385
+ */
386
+ private _updateLeaderboardUI(username: string, killCount: number) {
387
+ if (!this.world) return;
388
+
389
+ GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
390
+ player.ui.sendData({
391
+ type: 'leaderboard-update',
392
+ username,
393
+ killCount,
394
+ });
395
+ });
396
+ }
397
+
398
+ private _updatePlayerCountUI() {
399
+ setTimeout(() => { // have to wait 1 tick, we need to figure out this race condition later
400
+ if (!this.world) return;
401
+
402
+ GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
403
+ player.ui.sendData({ type: 'players-count', count: this._playerCount });
404
+ });
405
+ }, 25);
406
+ }
407
+
408
+ /**
409
+ * Waits for enough players to join before starting the game
410
+ */
411
+ private _waitForPlayersToStart() {
412
+ if (!this.world) return;
413
+
414
+ const connectedPlayers = GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).length;
415
+
416
+ if (connectedPlayers >= MINIMUM_PLAYERS_TO_START) {
417
+ this.startGame();
418
+ } else {
419
+ setTimeout(() => this._waitForPlayersToStart(), 1000);
420
+ }
421
+ }
422
+ }