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,1122 @@
1
+ <!-- Fonts-->
2
+ <link rel="preconnect" href="https://fonts.googleapis.com">
3
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
4
+ <link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">
5
+
6
+ <!-- UI-->
7
+ <div class="crosshair"></div>
8
+
9
+ <div class="damage-indicator-container">
10
+ <div class="damage-indicator top"></div>
11
+ <div class="damage-indicator right"></div>
12
+ <div class="damage-indicator bottom"></div>
13
+ <div class="damage-indicator left"></div>
14
+ </div>
15
+
16
+ <div class="hit-damage-container"></div>
17
+
18
+ <img src="{{CDN_ASSETS_URL}}/ui/images/scope.png" class="scope-overlay">
19
+
20
+ <!-- Game Start Announcement -->
21
+ <div class="game-start-announcement">DEATHMATCH!</div>
22
+
23
+ <!-- Winner Announcement -->
24
+ <div class="winner-announcement"></div>
25
+
26
+ <!-- Leaderboard -->
27
+ <div class="leaderboard">
28
+ <div class="leaderboard-title">HyGrounds Deathmatch</div>
29
+ <div class="leaderboard-timer">0:00</div>
30
+ <div class="leaderboard-players-count">Players: 0</div>
31
+ <div class="leaderboard-header">
32
+ <div class="header-name">Player</div>
33
+ <div class="header-kills">Kills</div>
34
+ </div>
35
+ <div class="leaderboard-players">
36
+ <div class="leaderboard-player no-kills">
37
+ <div class="player-name">Waiting for players...</div>
38
+ <div class="player-kills"></div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="hud">
44
+ <div class="info-container">
45
+ <div class="ammo-indicator" style="display: none;">
46
+ <img src="{{CDN_ASSETS_URL}}/icons/ammo.png" alt="Ammo Icon" class="ammo-icon">
47
+ <span class="current-ammo">30</span>
48
+ <span class="ammo-divider">/</span>
49
+ <span class="total-ammo">30</span>
50
+ </div>
51
+ <div class="shield-bar">
52
+ <div class="shield-bar-fill"></div>
53
+ <img src="{{CDN_ASSETS_URL}}/icons/shield.png" alt="Shield Icon" class="shield-icon">
54
+ <div class="shield-text">0</div>
55
+ </div>
56
+ <div class="health-bar">
57
+ <div class="health-bar-fill"></div>
58
+ <img src="{{CDN_ASSETS_URL}}/icons/heart.png" alt="Health Icon" class="health-icon">
59
+ <div class="health-text">100</div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="materials-counter">
65
+ <img src="{{CDN_ASSETS_URL}}/icons/block.png" alt="Materials Icon" class="materials-icon">
66
+ <span class="materials-amount">0</span>
67
+ </div>
68
+
69
+ <div class="inventory-hud">
70
+ <div class="inventory-slot inventory-active-slot" data-slot="0">
71
+ <div class="slot-number">F</div>
72
+ <img class="slot-icon" style="display: none;">
73
+ <div class="slot-quantity" style="display: none;"></div>
74
+ <div class="slot-name" style="display: none;"></div>
75
+ </div>
76
+
77
+ <div class="inventory-slot" data-slot="1">
78
+ <div class="slot-number">1</div>
79
+ <img class="slot-icon" style="display: none;">
80
+ <div class="slot-quantity" style="display: none;"></div>
81
+ <div class="slot-name" style="display: none;"></div>
82
+ </div>
83
+
84
+ <div class="inventory-slot" data-slot="2">
85
+ <div class="slot-number">2</div>
86
+ <img class="slot-icon" style="display: none;">
87
+ <div class="slot-quantity" style="display: none;"></div>
88
+ <div class="slot-name" style="display: none;"></div>
89
+ </div>
90
+
91
+ <div class="inventory-slot" data-slot="3">
92
+ <div class="slot-number">3</div>
93
+ <img class="slot-icon" style="display: none;">
94
+ <div class="slot-quantity" style="display: none;"></div>
95
+ <div class="slot-name" style="display: none;"></div>
96
+ </div>
97
+
98
+ <div class="inventory-slot" data-slot="4">
99
+ <div class="slot-number">4</div>
100
+ <img class="slot-icon" style="display: none;">
101
+ <div class="slot-quantity" style="display: none;"></div>
102
+ <div class="slot-name" style="display: none;"></div>
103
+ </div>
104
+
105
+ <div class="inventory-slot" data-slot="5">
106
+ <div class="slot-number">5</div>
107
+ <img class="slot-icon" style="display: none;">
108
+ <div class="slot-quantity" style="display: none;"></div>
109
+ <div class="slot-name" style="display: none;"></div>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- Scene UI Templates -->
114
+ <template id="item-label-template">
115
+ <div class="item-label">
116
+ <div class="label-quantity"></div>
117
+ <div class="label-name"></div>
118
+ <div class="label-prompt">"E" to Pickup</div>
119
+ <div class="label-caret"></div>
120
+ </div>
121
+ </template>
122
+
123
+ <template id="chest-label-template">
124
+ <div class="item-label">
125
+ <div class="label-name"></div>
126
+ <div class="label-prompt">"E" to Open</div>
127
+ <div class="label-caret"></div>
128
+ </div>
129
+ </template>
130
+
131
+ <!-- UI Scripts-->
132
+ <script>
133
+ const CDN_ASSETS_URL = '{{CDN_ASSETS_URL}}';
134
+ let leaderboardKillCounts = {};
135
+ let gameEndTime = 0;
136
+ let timerInterval;
137
+
138
+ function updateLeaderboard() {
139
+ const leaderboardPlayers = document.querySelector('.leaderboard-players');
140
+ leaderboardPlayers.innerHTML = '';
141
+
142
+ // Check if game has started
143
+ if (gameEndTime === 0) {
144
+ // Show "Waiting for players..." message if game hasn't started
145
+ const waitingElement = document.createElement('div');
146
+ waitingElement.className = 'leaderboard-player no-kills';
147
+ waitingElement.innerHTML = `
148
+ <div class="player-name">Waiting for players...</div>
149
+ <div class="player-kills"></div>
150
+ `;
151
+ leaderboardPlayers.appendChild(waitingElement);
152
+ return;
153
+ }
154
+
155
+ // Get sorted players by kill count
156
+ const sortedPlayers = Object.entries(leaderboardKillCounts)
157
+ .sort((a, b) => b[1] - a[1]);
158
+
159
+ if (sortedPlayers.length === 0 || sortedPlayers.every(player => player[1] === 0)) {
160
+ // Show "No kills yet" message if no players have kills
161
+ const noKillsElement = document.createElement('div');
162
+ noKillsElement.className = 'leaderboard-player no-kills';
163
+ noKillsElement.innerHTML = `
164
+ <div class="player-name">No kills yet</div>
165
+ <div class="player-kills"></div>
166
+ `;
167
+ leaderboardPlayers.appendChild(noKillsElement);
168
+ return;
169
+ }
170
+
171
+ // Create player elements for the leaderboard
172
+ sortedPlayers.forEach((player, index) => {
173
+ const [username, killCount] = player;
174
+
175
+ // Skip players with 0 kills
176
+ if (killCount === 0) return;
177
+
178
+ const playerElement = document.createElement('div');
179
+ playerElement.className = 'leaderboard-player';
180
+
181
+ let rankIcon = '';
182
+ if (index === 0) {
183
+ rankIcon = `<img src="${CDN_ASSETS_URL}/icons/crown-gold.png" class="rank-icon">`;
184
+ } else if (index === 1) {
185
+ rankIcon = `<img src="${CDN_ASSETS_URL}/icons/crown-silver.png" class="rank-icon">`;
186
+ } else if (index === 2) {
187
+ rankIcon = `<img src="${CDN_ASSETS_URL}/icons/crown-bronze.png" class="rank-icon">`;
188
+ }
189
+
190
+ playerElement.innerHTML = `
191
+ <div class="player-name">
192
+ ${rankIcon}
193
+ ${username}
194
+ </div>
195
+ <div class="player-kills">${killCount}</div>
196
+ `;
197
+
198
+ leaderboardPlayers.appendChild(playerElement);
199
+ });
200
+ }
201
+
202
+ function updateTimer() {
203
+ const now = Date.now();
204
+ const timeRemaining = Math.max(0, gameEndTime - now);
205
+
206
+ // Format time as mm:ss
207
+ const minutes = Math.floor(timeRemaining / 60000);
208
+ const seconds = Math.floor((timeRemaining % 60000) / 1000);
209
+ const formattedTime = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
210
+
211
+ const timerElement = document.querySelector('.leaderboard-timer');
212
+ timerElement.textContent = formattedTime;
213
+
214
+ // Stop the timer when it reaches zero
215
+ if (timeRemaining <= 0) {
216
+ clearInterval(timerInterval);
217
+ }
218
+ }
219
+
220
+ function showGameStartAnnouncement() {
221
+ const announcement = document.querySelector('.game-start-announcement');
222
+ announcement.classList.add('active');
223
+
224
+ // Remove the active class after animation completes
225
+ setTimeout(() => {
226
+ announcement.classList.remove('active');
227
+ }, 3000);
228
+ }
229
+
230
+ function showWinnerAnnouncement(username) {
231
+ const announcement = document.querySelector('.winner-announcement');
232
+ announcement.textContent = `${username} wins!`;
233
+ announcement.classList.add('active');
234
+
235
+ // Remove the active class after animation completes
236
+ setTimeout(() => {
237
+ announcement.classList.remove('active');
238
+ }, 5000);
239
+ }
240
+
241
+ hytopia.registerSceneUITemplate('item-label', (id, onState) => {
242
+ const template = document.getElementById('item-label-template');
243
+ const clone = template.content.cloneNode(true);
244
+ const labelName = clone.querySelector('.label-name');
245
+ const labelQuantity = clone.querySelector('.label-quantity');
246
+
247
+ onState(state => {
248
+ if (state.name) {
249
+ labelName.textContent = state.name;
250
+ }
251
+
252
+ if (state.quantity !== undefined) {
253
+ labelQuantity.textContent = state.quantity !== -1 ? state.quantity : '∞';
254
+ }
255
+ });
256
+
257
+ return clone;
258
+ });
259
+
260
+ hytopia.registerSceneUITemplate('chest-label', (id, onState) => {
261
+ const template = document.getElementById('chest-label-template');
262
+ const clone = template.content.cloneNode(true);
263
+ const labelName = clone.querySelector('.label-name');
264
+
265
+ onState(state => {
266
+ if (state.name) {
267
+ labelName.textContent = state.name;
268
+ }
269
+ });
270
+
271
+ return clone;
272
+ });
273
+
274
+ hytopia.onData(data => {
275
+ const { type } = data;
276
+
277
+ if (!type) {
278
+ return console.warn('No type received for data', data);
279
+ }
280
+
281
+ if (type === 'game-start') {
282
+ showGameStartAnnouncement();
283
+ }
284
+
285
+ if (type === 'announce-winner') {
286
+ const { username } = data;
287
+ showWinnerAnnouncement(username);
288
+ }
289
+
290
+ if (type === 'ammo-indicator') {
291
+ const { ammo, totalAmmo, show, reloading } = data;
292
+
293
+ if (show !== undefined) {
294
+ const ammoIndicator = document.querySelector('.ammo-indicator');
295
+ ammoIndicator.style.display = show ? 'block' : 'none';
296
+ }
297
+
298
+ if (reloading !== undefined) {
299
+ const currentAmmo = document.querySelector('.current-ammo');
300
+ currentAmmo.textContent = reloading ? '...' : ammo;
301
+ }
302
+
303
+ if (ammo !== undefined) {
304
+ const currentAmmo = document.querySelector('.current-ammo');
305
+ currentAmmo.textContent = ammo;
306
+ }
307
+
308
+ if (totalAmmo !== undefined) {
309
+ const totalAmmoElement = document.querySelector('.total-ammo');
310
+ totalAmmoElement.textContent = totalAmmo;
311
+ }
312
+ }
313
+
314
+ if (type === 'damage-indicator') {
315
+ const { direction } = data;
316
+
317
+ // Make sure we have a valid direction object
318
+ if (!direction || typeof direction.x === 'undefined' || typeof direction.z === 'undefined') {
319
+ console.error('Invalid direction object:', direction);
320
+ return;
321
+ }
322
+
323
+ // Hide all indicators first
324
+ document.querySelectorAll('.damage-indicator').forEach(el => {
325
+ el.classList.remove('active');
326
+ });
327
+
328
+ // Determine which indicator to show based on the dominant direction
329
+ const absX = Math.abs(direction.x);
330
+ const absZ = Math.abs(direction.z);
331
+
332
+ let indicatorClass;
333
+ if (absX > absZ) {
334
+ indicatorClass = direction.x > 0 ? '.damage-indicator.right' : '.damage-indicator.left';
335
+ } else {
336
+ indicatorClass = direction.z > 0 ? '.damage-indicator.top' : '.damage-indicator.bottom';
337
+ }
338
+
339
+ // Show and animate the indicator
340
+ const indicator = document.querySelector(indicatorClass);
341
+ if (indicator) {
342
+ indicator.classList.add('active');
343
+ indicator.style.animation = 'none';
344
+ indicator.offsetHeight; // Force reflow
345
+ indicator.style.animation = 'fadeOut 1s forwards';
346
+ }
347
+ }
348
+
349
+ if (type === 'show-damage') {
350
+ const { damage } = data;
351
+
352
+ // Create a new damage number element
353
+ const damageNumber = document.createElement('div');
354
+ damageNumber.className = 'hit-damage-number';
355
+ damageNumber.textContent = damage;
356
+
357
+ // Add some randomness to position
358
+ const randomX = Math.random() * 60 - 30; // -30 to 30px
359
+ const randomY = Math.random() * 40 - 20; // -20 to 20px
360
+
361
+ damageNumber.style.transform = `translate(${randomX}px, ${randomY}px)`;
362
+
363
+ // Add to container
364
+ const container = document.querySelector('.hit-damage-container');
365
+ container.appendChild(damageNumber);
366
+
367
+ // Remove after animation completes
368
+ setTimeout(() => {
369
+ damageNumber.remove();
370
+ }, 500);
371
+ }
372
+
373
+ if (type === 'health') {
374
+ const { health, maxHealth } = data;
375
+ const healthText = document.querySelector('.health-text');
376
+ const healthBarFill = document.querySelector('.health-bar-fill');
377
+
378
+ healthText.textContent = health;
379
+ healthBarFill.style.width = `${(health / maxHealth) * 100}%`;
380
+ }
381
+
382
+ if (type === 'inventory') {
383
+ const { inventory } = data;
384
+
385
+ inventory.forEach((item, i) => {
386
+ const slot = document.querySelector(`.inventory-slot[data-slot="${i}"]`);
387
+ const icon = slot.querySelector('.slot-icon');
388
+ const name = slot.querySelector('.slot-name');
389
+ const quantity = slot.querySelector('.slot-quantity');
390
+
391
+ icon.style.display = item ? 'block' : 'none';
392
+ name.style.display = item ? 'block' : 'none';
393
+ quantity.style.display = item ? 'block' : 'none';
394
+
395
+ if (!item) {
396
+ return;
397
+ }
398
+
399
+ icon.src = `${CDN_ASSETS_URL}/${item.iconImageUri}`;
400
+ name.textContent = item.name;
401
+ quantity.textContent = item.quantity !== -1 ? item.quantity : '∞';
402
+ });
403
+ }
404
+
405
+ if (type === 'inventory-active-slot') {
406
+ const { index } = data;
407
+
408
+ // Remove active slot class from all slots
409
+ document.querySelectorAll('.inventory-slot').forEach(slot => {
410
+ slot.classList.remove('inventory-active-slot');
411
+ });
412
+
413
+ // Add active slot class to selected slot
414
+ const activeSlot = document.querySelector(`.inventory-slot[data-slot="${index}"]`);
415
+ if (activeSlot) {
416
+ activeSlot.classList.add('inventory-active-slot');
417
+ }
418
+ }
419
+
420
+ if (type === 'inventory-quantity-update') {
421
+ const { index, quantity } = data;
422
+
423
+ const slot = document.querySelector(`.inventory-slot[data-slot="${index}"]`);
424
+ const quantityElement = slot.querySelector('.slot-quantity');
425
+
426
+ quantityElement.textContent = quantity;
427
+ }
428
+
429
+ if (type === 'leaderboard-sync') {
430
+ const { killCounts } = data;
431
+ leaderboardKillCounts = killCounts;
432
+ updateLeaderboard();
433
+ }
434
+
435
+ if (type === 'leaderboard-update') {
436
+ const { username, killCount } = data;
437
+ leaderboardKillCounts[username] = killCount;
438
+ updateLeaderboard();
439
+ }
440
+
441
+ if (type === 'players-count') {
442
+ const { count } = data;
443
+ const playersCount = document.querySelector('.leaderboard-players-count');
444
+ playersCount.textContent = `Players: ${count}`;
445
+ }
446
+
447
+ if (type === 'materials') {
448
+ const { materials } = data;
449
+ const materialsAmount = document.querySelector('.materials-amount');
450
+ const currentMaterials = parseInt(materialsAmount.textContent);
451
+ const difference = materials - currentMaterials;
452
+
453
+ if (difference !== 0) {
454
+ const materialsCounter = document.querySelector('.materials-counter');
455
+ const floatingNumber = document.createElement('div');
456
+ floatingNumber.className = 'floating-number';
457
+ floatingNumber.textContent = difference > 0 ? `+${difference}` : difference;
458
+ materialsCounter.appendChild(floatingNumber);
459
+
460
+ // Remove the element after animation completes
461
+ setTimeout(() => {
462
+ floatingNumber.remove();
463
+ }, 1000);
464
+ }
465
+
466
+ materialsAmount.textContent = materials;
467
+ }
468
+
469
+ if (type === 'shield') {
470
+ const { shield, maxShield } = data;
471
+ const shieldText = document.querySelector('.shield-text');
472
+ const shieldBarFill = document.querySelector('.shield-bar-fill');
473
+
474
+ shieldText.textContent = shield;
475
+ shieldBarFill.style.width = `${(shield / maxShield) * 100}%`;
476
+ }
477
+
478
+ if (type === 'scope-zoom') {
479
+ const { zoom } = data;
480
+ const scopeOverlay = document.querySelector('.scope-overlay');
481
+
482
+ if (zoom === 1) {
483
+ scopeOverlay.classList.remove('active');
484
+ } else {
485
+ scopeOverlay.classList.add('active');
486
+ }
487
+ }
488
+
489
+ if (type === 'timer-sync') {
490
+ const { startedAt, endsAt } = data;
491
+
492
+ // Clear any existing timer interval
493
+ if (timerInterval) {
494
+ clearInterval(timerInterval);
495
+ }
496
+
497
+ // Set the end time
498
+ gameEndTime = endsAt;
499
+
500
+ // Update timer immediately
501
+ updateTimer();
502
+
503
+ // Set up interval to update timer every second
504
+ timerInterval = setInterval(updateTimer, 1000);
505
+ }
506
+ });
507
+ </script>
508
+
509
+ <!-- UI Styles -->
510
+ <style>
511
+ .crosshair {
512
+ position: fixed;
513
+ top: 50%;
514
+ left: 50%;
515
+ transform: translate(-50%, -50%);
516
+ width: 20px;
517
+ height: 20px;
518
+ pointer-events: none;
519
+ opacity: 0.7;
520
+ }
521
+
522
+ .crosshair::before,
523
+ .crosshair::after {
524
+ content: '';
525
+ position: absolute;
526
+ background-color: rgba(255, 255, 255, 0.8);
527
+ }
528
+
529
+ .crosshair::before {
530
+ width: 2px;
531
+ height: 100%;
532
+ left: 50%;
533
+ transform: translateX(-50%);
534
+ }
535
+
536
+ .crosshair::after {
537
+ width: 100%;
538
+ height: 2px;
539
+ top: 50%;
540
+ transform: translateY(-50%);
541
+ }
542
+
543
+ .game-start-announcement {
544
+ position: fixed;
545
+ top: 50%;
546
+ left: 50%;
547
+ transform: translate(-50%, -50%);
548
+ font-family: 'Inter', sans-serif;
549
+ font-size: 60px;
550
+ font-weight: bold;
551
+ color: #ff3333;
552
+ text-shadow: 0 0 10px rgba(0, 0, 0, 0.7), 0 0 20px rgba(0, 0, 0, 0.5);
553
+ text-transform: uppercase;
554
+ letter-spacing: 2px;
555
+ opacity: 0;
556
+ z-index: 1000;
557
+ pointer-events: none;
558
+ text-align: center;
559
+ }
560
+
561
+ .game-start-announcement.active {
562
+ animation: announcementFade 5s ease-in-out forwards;
563
+ }
564
+
565
+ .winner-announcement {
566
+ position: fixed;
567
+ top: 50%;
568
+ left: 50%;
569
+ transform: translate(-50%, -50%);
570
+ font-family: 'Inter', sans-serif;
571
+ font-size: 80px;
572
+ font-weight: bold;
573
+ color: #ffd700;
574
+ text-shadow: 0 0 15px rgba(0, 0, 0, 0.8), 0 0 30px rgba(255, 215, 0, 0.6);
575
+ text-transform: uppercase;
576
+ letter-spacing: 3px;
577
+ opacity: 0;
578
+ z-index: 1000;
579
+ pointer-events: none;
580
+ text-align: center;
581
+ }
582
+
583
+ .winner-announcement.active {
584
+ animation: winnerFade 5s ease-in-out forwards;
585
+ }
586
+
587
+ @keyframes announcementFade {
588
+ 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); }
589
+ 20% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
590
+ 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
591
+ 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
592
+ }
593
+
594
+ @keyframes winnerFade {
595
+ 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); }
596
+ 20% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
597
+ 40% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); }
598
+ 60% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
599
+ 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
600
+ 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
601
+ }
602
+
603
+ .hit-damage-container {
604
+ position: fixed;
605
+ top: 50%;
606
+ left: 50%;
607
+ transform: translate(-50%, -50%);
608
+ pointer-events: none;
609
+ z-index: 100;
610
+ width: 100px;
611
+ height: 100px;
612
+ display: flex;
613
+ justify-content: center;
614
+ align-items: center;
615
+ }
616
+
617
+ .hit-damage-number {
618
+ color: white;
619
+ font-family: 'Inter', sans-serif;
620
+ font-size: 18px;
621
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
622
+ animation: damagePopup 0.5s ease-out forwards;
623
+ position: absolute;
624
+ }
625
+
626
+ @keyframes damagePopup {
627
+ 0% {
628
+ opacity: 0;
629
+ transform: scale(0.5) translateY(0);
630
+ }
631
+ 20% {
632
+ opacity: 1;
633
+ transform: scale(1.2) translateY(0);
634
+ }
635
+ 100% {
636
+ opacity: 0;
637
+ transform: scale(1) translateY(-50px);
638
+ }
639
+ }
640
+
641
+ .materials-counter {
642
+ position: fixed;
643
+ bottom: 120px;
644
+ right: 20px;
645
+ background: rgba(0, 0, 0, 0.5);
646
+ padding: 8px 15px;
647
+ border-radius: 5px;
648
+ display: flex;
649
+ align-items: center;
650
+ gap: 10px;
651
+ font-family: 'Inter', sans-serif;
652
+ color: #ffffff;
653
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
654
+ border: 2px solid rgba(255, 255, 255, 0.3);
655
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
656
+ }
657
+
658
+ .floating-number {
659
+ position: absolute;
660
+ color: #ffffff;
661
+ font-weight: bold;
662
+ font-size: 16px;
663
+ pointer-events: none;
664
+ animation: floatUp 1s ease-out forwards;
665
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
666
+ }
667
+
668
+ @keyframes floatUp {
669
+ 0% {
670
+ opacity: 1;
671
+ transform: translateY(0);
672
+ }
673
+ 100% {
674
+ opacity: 0;
675
+ transform: translateY(-50px);
676
+ }
677
+ }
678
+
679
+ .materials-icon {
680
+ width: 20px;
681
+ height: 20px;
682
+ object-fit: contain;
683
+ }
684
+
685
+ .materials-amount {
686
+ font-size: 16px;
687
+ font-weight: bold;
688
+ }
689
+
690
+ .hud {
691
+ position: fixed;
692
+ bottom: 20px;
693
+ left: 50%;
694
+ transform: translateX(-50%);
695
+ display: flex;
696
+ flex-direction: column;
697
+ z-index: 99;
698
+ gap: 10px;
699
+ font-family: 'Inter', sans-serif;
700
+ text-transform: uppercase;
701
+ color: #ffffff;
702
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
703
+ }
704
+
705
+ .ammo-indicator {
706
+ text-align: center;
707
+ font-size: 18px;
708
+ font-weight: bold;
709
+ margin-bottom: 5px;
710
+ background: rgba(0, 0, 0, 0.5);
711
+ padding: 5px 15px;
712
+ border-radius: 3px;
713
+ box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
714
+ display: flex;
715
+ align-items: center;
716
+ justify-content: center;
717
+ user-select: none;
718
+ gap: 10px;
719
+ }
720
+
721
+ .ammo-icon {
722
+ width: 18px;
723
+ height: 18px;
724
+ object-fit: contain;
725
+ position: relative;
726
+ top: 2px;
727
+ }
728
+
729
+ .ammo-divider {
730
+ margin: 0 5px;
731
+ opacity: 0.7;
732
+ }
733
+
734
+ .inventory-hud {
735
+ position: fixed;
736
+ bottom: 20px;
737
+ right: 20px;
738
+ display: flex;
739
+ gap: 10px;
740
+ z-index: 99;
741
+ }
742
+
743
+ .inventory-slot {
744
+ width: 60px;
745
+ height: 60px;
746
+ background: rgba(0, 0, 0, 0.5);
747
+ border-radius: 5px;
748
+ border: 2px solid rgba(255, 255, 255, 0.3);
749
+ display: flex;
750
+ align-items: center;
751
+ justify-content: center;
752
+ position: relative;
753
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
754
+ }
755
+
756
+ .inventory-active-slot {
757
+ border-color: rgba(255, 255, 255, 0.6);
758
+ }
759
+
760
+ .slot-number {
761
+ position: absolute;
762
+ top: -20px;
763
+ left: 50%;
764
+ transform: translateX(-50%);
765
+ font-family: 'Inter', sans-serif;
766
+ font-size: 14px;
767
+ font-weight: bold;
768
+ color: rgba(255, 255, 255, 0.8);
769
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
770
+ }
771
+
772
+ .slot-icon {
773
+ max-width: 70%;
774
+ max-height: 70%;
775
+ object-fit: contain;
776
+ }
777
+
778
+ .slot-quantity {
779
+ position: absolute;
780
+ bottom: 2px;
781
+ left: 2px;
782
+ font-family: 'Inter', sans-serif;
783
+ font-size: 12px;
784
+ font-weight: bold;
785
+ color: rgba(255, 255, 255, 0.9);
786
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
787
+ background: rgba(0, 0, 0, 0.5);
788
+ padding: 1px 4px;
789
+ border-radius: 3px;
790
+ }
791
+
792
+ .slot-name {
793
+ position: absolute;
794
+ bottom: -18px;
795
+ left: 50%;
796
+ transform: translateX(-50%);
797
+ font-family: 'Inter', sans-serif;
798
+ font-size: 10px;
799
+ font-weight: bold;
800
+ color: rgba(255, 255, 255, 0.8);
801
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
802
+ white-space: nowrap;
803
+ }
804
+
805
+ .info-container {
806
+ display: flex;
807
+ align-items: center;
808
+ gap: 10px;
809
+ flex-direction: column;
810
+ }
811
+
812
+ .shield-bar {
813
+ width: 250px;
814
+ height: 20px;
815
+ background: rgba(0, 0, 0, 0.5);
816
+ border-radius: 3px;
817
+ overflow: hidden;
818
+ box-shadow: 0 0 10px rgba(0, 0, 255, 0.3);
819
+ position: relative;
820
+ }
821
+
822
+ .shield-bar-fill {
823
+ width: 0%;
824
+ height: 100%;
825
+ background: linear-gradient(to right, #0000ff, #3333ff);
826
+ transition: width 0.3s ease;
827
+ }
828
+
829
+ .shield-icon {
830
+ position: absolute;
831
+ left: 5px;
832
+ top: 50%;
833
+ transform: translateY(-50%);
834
+ height: 12px;
835
+ width: 12px;
836
+ z-index: 1;
837
+ }
838
+
839
+ .shield-text {
840
+ position: absolute;
841
+ left: 50%;
842
+ top: 50%;
843
+ transform: translate(-50%, -50%);
844
+ font-size: 0.8em;
845
+ font-weight: bold;
846
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
847
+ z-index: 1;
848
+ }
849
+
850
+ .health-bar {
851
+ width: 250px;
852
+ height: 20px;
853
+ background: rgba(0, 0, 0, 0.5);
854
+ border-radius: 3px;
855
+ overflow: hidden;
856
+ box-shadow: 0 0 10px rgba(255, 0, 0, 0.3);
857
+ position: relative;
858
+ }
859
+
860
+ .health-bar-fill {
861
+ width: 100%;
862
+ height: 100%;
863
+ background: linear-gradient(to right, #ff0000, #ff3333);
864
+ transition: width 0.3s ease;
865
+ }
866
+
867
+ .health-icon {
868
+ position: absolute;
869
+ left: 5px;
870
+ top: 50%;
871
+ transform: translateY(-50%);
872
+ height: 12px;
873
+ width: 12px;
874
+ z-index: 1;
875
+ }
876
+
877
+ .health-text {
878
+ position: absolute;
879
+ left: 50%;
880
+ top: 50%;
881
+ transform: translate(-50%, -50%);
882
+ font-size: 0.8em;
883
+ font-weight: bold;
884
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
885
+ z-index: 1;
886
+ }
887
+
888
+ .item-label {
889
+ background-color: rgba(0, 0, 0, 0.6);
890
+ padding: 12px 20px;
891
+ border-radius: 4px;
892
+ text-align: center;
893
+ color: #ffffff;
894
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
895
+ font-family: 'Inter', sans-serif;
896
+ text-transform: uppercase;
897
+ position: relative;
898
+ max-width: 220px;
899
+ margin: 0 auto;
900
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);
901
+ }
902
+
903
+ .label-name {
904
+ font-size: 20px;
905
+ margin-bottom: 6px;
906
+ font-weight: bold;
907
+ }
908
+
909
+ .label-quantity {
910
+ position: absolute;
911
+ top: -12px;
912
+ right: -10px;
913
+ background: #ffb700;
914
+ color: black;
915
+ font-weight: bold;
916
+ padding: 2px 4px;
917
+ font-size: 14px;
918
+ text-transform: none;
919
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
920
+ text-shadow: none;
921
+ border: 1px solid #ffffff;
922
+ }
923
+
924
+ .label-prompt {
925
+ font-size: 14px;
926
+ opacity: 0.8;
927
+ }
928
+
929
+ .label-caret {
930
+ position: absolute;
931
+ bottom: -8px;
932
+ left: 50%;
933
+ transform: translateX(-50%);
934
+ width: 0;
935
+ height: 0;
936
+ border-left: 8px solid transparent;
937
+ border-right: 8px solid transparent;
938
+ border-top: 8px solid rgba(0, 0, 0, 0.6);
939
+ }
940
+
941
+ .scope-overlay {
942
+ position: fixed;
943
+ top: 0;
944
+ left: 0;
945
+ pointer-events: none;
946
+ width: 100%;
947
+ height: 100%;
948
+ z-index: 99;
949
+ object-fit: cover;
950
+ opacity: 0;
951
+ transition: opacity 0.3s ease;
952
+ }
953
+
954
+ .scope-overlay.active {
955
+ opacity: 1;
956
+ }
957
+
958
+ #chat-window {
959
+ width: 25% !important;
960
+ }
961
+
962
+ .damage-indicator-container {
963
+ position: fixed;
964
+ top: 0;
965
+ left: 0;
966
+ width: 100%;
967
+ height: 100%;
968
+ pointer-events: none;
969
+ z-index: 5;
970
+ }
971
+
972
+ .damage-indicator {
973
+ position: absolute;
974
+ opacity: 0;
975
+ background: linear-gradient(to center, rgba(255,0,0,0.7), rgba(255,0,0,0));
976
+ }
977
+
978
+ .damage-indicator.active {
979
+ opacity: 1;
980
+ }
981
+
982
+ /* Top damage indicator */
983
+ .damage-indicator.top {
984
+ top: 0;
985
+ left: 0;
986
+ width: 100%;
987
+ height: 150px;
988
+ background: linear-gradient(to bottom, rgba(255,0,0,0.7), rgba(255,0,0,0));
989
+ }
990
+
991
+ /* Right damage indicator */
992
+ .damage-indicator.right {
993
+ top: 0;
994
+ right: 0;
995
+ width: 150px;
996
+ height: 100%;
997
+ background: linear-gradient(to left, rgba(255,0,0,0.7), rgba(255,0,0,0));
998
+ }
999
+
1000
+ /* Bottom damage indicator */
1001
+ .damage-indicator.bottom {
1002
+ bottom: 0;
1003
+ left: 0;
1004
+ width: 100%;
1005
+ height: 150px;
1006
+ background: linear-gradient(to top, rgba(255,0,0,0.7), rgba(255,0,0,0));
1007
+ }
1008
+
1009
+ /* Left damage indicator */
1010
+ .damage-indicator.left {
1011
+ top: 0;
1012
+ left: 0;
1013
+ width: 150px;
1014
+ height: 100%;
1015
+ background: linear-gradient(to right, rgba(255,0,0,0.7), rgba(255,0,0,0));
1016
+ }
1017
+
1018
+ @keyframes fadeOut {
1019
+ 0% { opacity: 1; }
1020
+ 100% { opacity: 0; }
1021
+ }
1022
+
1023
+ /* Leaderboard Styles */
1024
+ .leaderboard {
1025
+ position: fixed;
1026
+ top: 20px;
1027
+ right: 20px;
1028
+ background: rgba(0, 0, 0, 0.7);
1029
+ border-radius: 5px;
1030
+ padding: 10px;
1031
+ width: 200px;
1032
+ font-family: 'Arial', sans-serif;
1033
+ color: white;
1034
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
1035
+ border: 2px solid rgba(255, 255, 255, 0.2);
1036
+ z-index: 100;
1037
+ }
1038
+
1039
+ .leaderboard-title {
1040
+ text-align: center;
1041
+ font-size: 18px;
1042
+ font-weight: bold;
1043
+ padding-bottom: 5px;
1044
+ margin-bottom: 5px;
1045
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3);
1046
+ text-transform: uppercase;
1047
+ letter-spacing: 1px;
1048
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
1049
+ }
1050
+
1051
+ .leaderboard-timer {
1052
+ text-align: center;
1053
+ font-size: 16px;
1054
+ font-weight: bold;
1055
+ margin-bottom: 10px;
1056
+ padding: 3px;
1057
+ background: rgba(255, 255, 255, 0.1);
1058
+ border-radius: 3px;
1059
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
1060
+ }
1061
+
1062
+ .leaderboard-header {
1063
+ display: flex;
1064
+ justify-content: space-between;
1065
+ padding: 3px 8px;
1066
+ font-size: 12px;
1067
+ font-weight: bold;
1068
+ text-transform: uppercase;
1069
+ color: rgba(255, 255, 255, 0.7);
1070
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
1071
+ margin-bottom: 5px;
1072
+ }
1073
+
1074
+ .leaderboard-players {
1075
+ display: flex;
1076
+ flex-direction: column;
1077
+ gap: 5px;
1078
+ }
1079
+
1080
+ .leaderboard-player {
1081
+ display: flex;
1082
+ justify-content: space-between;
1083
+ padding: 5px 8px;
1084
+ background: rgba(255, 255, 255, 0.1);
1085
+ border-radius: 3px;
1086
+ transition: background 0.2s ease;
1087
+ }
1088
+
1089
+ .leaderboard-player:hover {
1090
+ background: rgba(255, 255, 255, 0.2);
1091
+ }
1092
+
1093
+ .leaderboard-players-count {
1094
+ font-size: 14px;
1095
+ font-weight: bold;
1096
+ color: rgba(255, 255, 255, 0.7);
1097
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
1098
+ text-transform: uppercase;
1099
+ text-align: center;
1100
+ margin-bottom: 10px;
1101
+ padding: 3px;
1102
+ background: rgba(255, 255, 255, 0.1);
1103
+ border-radius: 3px;
1104
+ }
1105
+
1106
+ .rank-icon {
1107
+ width: 16px;
1108
+ height: 16px;
1109
+ margin-right: 5px;
1110
+ position: relative;
1111
+ top: 1px;
1112
+ }
1113
+
1114
+ .player-name {
1115
+ font-weight: bold;
1116
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
1117
+ white-space: nowrap;
1118
+ overflow: hidden;
1119
+ max-width: 160px;
1120
+ text-overflow: ellipsis;
1121
+ }
1122
+ </style>