hytopia 0.3.27 → 0.3.29

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 (58) hide show
  1. package/docs/server.audio.cutoffdistance.md +13 -0
  2. package/docs/server.audio.md +35 -0
  3. package/docs/server.audio.setcutoffdistance.md +57 -0
  4. package/docs/server.audio.setreferencedistance.md +4 -0
  5. package/docs/server.audioevent.md +14 -0
  6. package/docs/server.audioeventpayloads._audio.set_cutoff_distance_.md +16 -0
  7. package/docs/server.audioeventpayloads.md +19 -0
  8. package/docs/server.audiooptions.cutoffdistance.md +13 -0
  9. package/docs/server.audiooptions.md +20 -1
  10. package/docs/server.audiooptions.referencedistance.md +1 -1
  11. package/examples/hygrounds/assets/icons/ranks/bronze-1.png +0 -0
  12. package/examples/hygrounds/assets/icons/ranks/bronze-2.png +0 -0
  13. package/examples/hygrounds/assets/icons/ranks/bronze-3.png +0 -0
  14. package/examples/hygrounds/assets/icons/ranks/bronze-4.png +0 -0
  15. package/examples/hygrounds/assets/icons/ranks/bronze-5.png +0 -0
  16. package/examples/hygrounds/assets/icons/ranks/diamond-1.png +0 -0
  17. package/examples/hygrounds/assets/icons/ranks/diamond-2.png +0 -0
  18. package/examples/hygrounds/assets/icons/ranks/diamond-3.png +0 -0
  19. package/examples/hygrounds/assets/icons/ranks/diamond-4.png +0 -0
  20. package/examples/hygrounds/assets/icons/ranks/diamond-5.png +0 -0
  21. package/examples/hygrounds/assets/icons/ranks/elite-1.png +0 -0
  22. package/examples/hygrounds/assets/icons/ranks/elite-2.png +0 -0
  23. package/examples/hygrounds/assets/icons/ranks/elite-3.png +0 -0
  24. package/examples/hygrounds/assets/icons/ranks/elite-4.png +0 -0
  25. package/examples/hygrounds/assets/icons/ranks/elite-5.png +0 -0
  26. package/examples/hygrounds/assets/icons/ranks/gold-1.png +0 -0
  27. package/examples/hygrounds/assets/icons/ranks/gold-2.png +0 -0
  28. package/examples/hygrounds/assets/icons/ranks/gold-3.png +0 -0
  29. package/examples/hygrounds/assets/icons/ranks/gold-4.png +0 -0
  30. package/examples/hygrounds/assets/icons/ranks/gold-5.png +0 -0
  31. package/examples/hygrounds/assets/icons/ranks/platinum-1.png +0 -0
  32. package/examples/hygrounds/assets/icons/ranks/platinum-2.png +0 -0
  33. package/examples/hygrounds/assets/icons/ranks/platinum-3.png +0 -0
  34. package/examples/hygrounds/assets/icons/ranks/platinum-4.png +0 -0
  35. package/examples/hygrounds/assets/icons/ranks/platinum-5.png +0 -0
  36. package/examples/hygrounds/assets/icons/ranks/silver-1.png +0 -0
  37. package/examples/hygrounds/assets/icons/ranks/silver-2.png +0 -0
  38. package/examples/hygrounds/assets/icons/ranks/silver-3.png +0 -0
  39. package/examples/hygrounds/assets/icons/ranks/silver-4.png +0 -0
  40. package/examples/hygrounds/assets/icons/ranks/silver-5.png +0 -0
  41. package/examples/hygrounds/assets/icons/ranks/unranked.png +0 -0
  42. package/examples/hygrounds/assets/ui/index.html +305 -44
  43. package/examples/hygrounds/classes/ChestEntity.ts +1 -0
  44. package/examples/hygrounds/classes/GameManager.ts +17 -7
  45. package/examples/hygrounds/classes/GamePlayerEntity.ts +103 -3
  46. package/examples/hygrounds/classes/GunEntity.ts +3 -0
  47. package/examples/hygrounds/classes/ItemEntity.ts +1 -0
  48. package/examples/hygrounds/classes/MeleeWeaponEntity.ts +2 -0
  49. package/examples/hygrounds/classes/weapons/PistolEntity.ts +1 -1
  50. package/examples/hygrounds/classes/weapons/RocketLauncherEntity.ts +2 -1
  51. package/examples/hygrounds/dev/persistence/player-player-1.json +3 -0
  52. package/examples/hygrounds/dev/persistence/player-player-2.json +3 -0
  53. package/examples/hygrounds/dev/persistence/player-player-3.json +3 -0
  54. package/examples/hygrounds/gameConfig.ts +281 -21
  55. package/package.json +1 -1
  56. package/server.api.json +173 -2
  57. package/server.d.ts +33 -1
  58. package/server.js +114 -114
@@ -23,6 +23,13 @@
23
23
  <!-- Winner Announcement -->
24
24
  <div class="winner-announcement"></div>
25
25
 
26
+ <!-- Rank Up Announcement -->
27
+ <div class="rank-up-announcement">
28
+ <div class="rank-up-title">Rank Up!</div>
29
+ <img src="" alt="Rank Icon" class="rank-up-icon">
30
+ <div class="rank-up-name"></div>
31
+ </div>
32
+
26
33
  <!-- Leaderboard -->
27
34
  <div class="leaderboard">
28
35
  <div class="leaderboard-title">Deathmatch</div>
@@ -40,7 +47,7 @@
40
47
  </div>
41
48
  </div>
42
49
 
43
- <div class="hud">
50
+ <div class="hud">
44
51
  <div class="info-container">
45
52
  <div class="ammo-indicator" style="display: none;">
46
53
  <img src="{{CDN_ASSETS_URL}}/icons/ammo.png" alt="Ammo Icon" class="ammo-icon">
@@ -48,6 +55,13 @@
48
55
  <span class="ammo-divider">/</span>
49
56
  <span class="total-ammo">30</span>
50
57
  </div>
58
+
59
+ <div class="exp-bar">
60
+ <div class="exp-bar-fill"></div>
61
+ <img src="{{CDN_ASSETS_URL}}/icons/ranks/unranked.png" alt="Exp Rank Icon" class="exp-icon">
62
+ <div class="exp-text">Unranked</div>
63
+ </div>
64
+
51
65
  <div class="shield-bar">
52
66
  <div class="shield-bar-fill"></div>
53
67
  <img src="{{CDN_ASSETS_URL}}/icons/shield.png" alt="Shield Icon" class="shield-icon">
@@ -64,18 +78,18 @@
64
78
  <div class="mobile-buttons-container">
65
79
  <div id="mobile-reload-button" class="mobile-button">
66
80
  <img src="{{CDN_ASSETS_URL}}/icons/mobile-reload.png" />
67
- </div>
68
-
81
+ </div>
82
+
69
83
  <div id="mobile-interact-button" class="mobile-button">E</div>
70
-
84
+
71
85
  <div id="mobile-jump-button" class="mobile-button">
72
86
  <img src="{{CDN_ASSETS_URL}}/icons/mobile-jump.png" />
73
87
  </div>
74
-
88
+
75
89
  <div id="mobile-attack-button" class="mobile-button">
76
90
  <img src="{{CDN_ASSETS_URL}}/icons/mobile-shoot.png" />
77
91
  </div>
78
-
92
+
79
93
  <div id="mobile-place-block-button" class="mobile-button">
80
94
  <img src="{{CDN_ASSETS_URL}}/icons/mobile-place-block.png" />
81
95
  </div>
@@ -100,21 +114,21 @@
100
114
  <div class="slot-quantity" style="display: none;"></div>
101
115
  <div class="slot-name" style="display: none;"></div>
102
116
  </div>
103
-
117
+
104
118
  <div class="inventory-slot" data-slot="2">
105
119
  <div class="slot-number">2</div>
106
120
  <img class="slot-icon" style="display: none;">
107
121
  <div class="slot-quantity" style="display: none;"></div>
108
122
  <div class="slot-name" style="display: none;"></div>
109
123
  </div>
110
-
124
+
111
125
  <div class="inventory-slot" data-slot="3">
112
126
  <div class="slot-number">3</div>
113
127
  <img class="slot-icon" style="display: none;">
114
128
  <div class="slot-quantity" style="display: none;"></div>
115
129
  <div class="slot-name" style="display: none;"></div>
116
130
  </div>
117
-
131
+
118
132
  <div class="inventory-slot" data-slot="4">
119
133
  <div class="slot-number">4</div>
120
134
  <img class="slot-icon" style="display: none;">
@@ -131,6 +145,10 @@
131
145
  </div>
132
146
 
133
147
  <!-- Scene UI Templates -->
148
+ <template id="player-rank-template">
149
+ <img src="" alt="Rank Icon" class="player-rank-icon">
150
+ </template>
151
+
134
152
  <template id="item-label-template">
135
153
  <div class="item-label">
136
154
  <div class="label-quantity"></div>
@@ -151,9 +169,13 @@
151
169
  <!-- UI Scripts-->
152
170
  <script>
153
171
  const CDN_ASSETS_URL = '{{CDN_ASSETS_URL}}';
172
+ let ranks = [];
173
+ let lastExp = 0;
174
+ let lastRank;
154
175
  let leaderboardKillCounts = {};
155
176
  let gameEndTime = 0;
156
177
  let timerInterval;
178
+ let isLoadExpUpdate = true; // Flag to track the first EXP update
157
179
 
158
180
  function updateLeaderboard() {
159
181
  const leaderboardPlayers = document.querySelector('.leaderboard-players');
@@ -191,13 +213,13 @@
191
213
  // Create player elements for the leaderboard
192
214
  sortedPlayers.forEach((player, index) => {
193
215
  const [username, killCount] = player;
194
-
216
+
195
217
  // Skip players with 0 kills
196
218
  if (killCount === 0) return;
197
-
219
+
198
220
  const playerElement = document.createElement('div');
199
221
  playerElement.className = 'leaderboard-player';
200
-
222
+
201
223
  let rankIcon = '';
202
224
  if (index === 0) {
203
225
  rankIcon = `<img src="${CDN_ASSETS_URL}/icons/crown-gold.png" class="rank-icon">`;
@@ -214,7 +236,7 @@
214
236
  </div>
215
237
  <div class="player-kills">${killCount}</div>
216
238
  `;
217
-
239
+
218
240
  leaderboardPlayers.appendChild(playerElement);
219
241
  });
220
242
  }
@@ -222,15 +244,15 @@
222
244
  function updateTimer() {
223
245
  const now = Date.now();
224
246
  const timeRemaining = Math.max(0, gameEndTime - now);
225
-
247
+
226
248
  // Format time as mm:ss
227
249
  const minutes = Math.floor(timeRemaining / 60000);
228
250
  const seconds = Math.floor((timeRemaining % 60000) / 1000);
229
251
  const formattedTime = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
230
-
252
+
231
253
  const timerElement = document.querySelector('.leaderboard-timer');
232
254
  timerElement.textContent = formattedTime;
233
-
255
+
234
256
  // Stop the timer when it reaches zero
235
257
  if (timeRemaining <= 0) {
236
258
  clearInterval(timerInterval);
@@ -240,7 +262,7 @@
240
262
  function showGameStartAnnouncement() {
241
263
  const announcement = document.querySelector('.game-start-announcement');
242
264
  announcement.classList.add('active');
243
-
265
+
244
266
  // Remove the active class after animation completes
245
267
  setTimeout(() => {
246
268
  announcement.classList.remove('active');
@@ -251,13 +273,45 @@
251
273
  const announcement = document.querySelector('.winner-announcement');
252
274
  announcement.textContent = `${username} wins!`;
253
275
  announcement.classList.add('active');
254
-
276
+
255
277
  // Remove the active class after animation completes
256
278
  setTimeout(() => {
257
279
  announcement.classList.remove('active');
258
280
  }, 5000);
259
281
  }
260
282
 
283
+ function showRankUpAnnouncement(rank) {
284
+ const announcement = document.querySelector('.rank-up-announcement');
285
+ const rankIcon = announcement.querySelector('.rank-up-icon');
286
+ const rankName = announcement.querySelector('.rank-up-name');
287
+
288
+ rankIcon.src = `${CDN_ASSETS_URL}/${rank.iconUri}`;
289
+ rankName.textContent = rank.name;
290
+
291
+ announcement.classList.add('active');
292
+
293
+ // Remove the active class after animation completes (e.g., 5 seconds)
294
+ setTimeout(() => {
295
+ announcement.classList.remove('active');
296
+ }, 5000);
297
+ }
298
+
299
+ function showExpGainIndicator(amount) {
300
+ const expBar = document.querySelector('.exp-bar');
301
+ if (!expBar) return;
302
+
303
+ const indicator = document.createElement('div');
304
+ indicator.className = 'exp-gain-indicator';
305
+ indicator.textContent = `+${amount} Exp`;
306
+
307
+ expBar.appendChild(indicator);
308
+
309
+ // Remove after animation completes
310
+ setTimeout(() => {
311
+ indicator.remove();
312
+ }, 1500); // Match animation duration
313
+ }
314
+
261
315
  hytopia.registerSceneUITemplate('item-label', (id, onState) => {
262
316
  const template = document.getElementById('item-label-template');
263
317
  const clone = template.content.cloneNode(true);
@@ -291,6 +345,20 @@
291
345
  return clone;
292
346
  });
293
347
 
348
+ hytopia.registerSceneUITemplate('player-rank', (id, onState) => {
349
+ const template = document.getElementById('player-rank-template');
350
+ const clone = template.content.cloneNode(true);
351
+ const playerRankIcon = clone.querySelector('.player-rank-icon');
352
+
353
+ onState(state => {
354
+ if (state.iconUri) {
355
+ playerRankIcon.src = `${CDN_ASSETS_URL}/${state.iconUri}`;
356
+ }
357
+ });
358
+
359
+ return clone;
360
+ });
361
+
294
362
  hytopia.onData(data => {
295
363
  const { type } = data;
296
364
 
@@ -333,29 +401,29 @@
333
401
 
334
402
  if (type === 'damage-indicator') {
335
403
  const { direction } = data;
336
-
404
+
337
405
  // Make sure we have a valid direction object
338
406
  if (!direction || typeof direction.x === 'undefined' || typeof direction.z === 'undefined') {
339
407
  console.error('Invalid direction object:', direction);
340
408
  return;
341
409
  }
342
-
410
+
343
411
  // Hide all indicators first
344
412
  document.querySelectorAll('.damage-indicator').forEach(el => {
345
413
  el.classList.remove('active');
346
414
  });
347
-
415
+
348
416
  // Determine which indicator to show based on the dominant direction
349
417
  const absX = Math.abs(direction.x);
350
418
  const absZ = Math.abs(direction.z);
351
-
419
+
352
420
  let indicatorClass;
353
421
  if (absX > absZ) {
354
422
  indicatorClass = direction.x > 0 ? '.damage-indicator.right' : '.damage-indicator.left';
355
423
  } else {
356
424
  indicatorClass = direction.z > 0 ? '.damage-indicator.top' : '.damage-indicator.bottom';
357
425
  }
358
-
426
+
359
427
  // Show and animate the indicator
360
428
  const indicator = document.querySelector(indicatorClass);
361
429
  if (indicator) {
@@ -366,24 +434,58 @@
366
434
  }
367
435
  }
368
436
 
437
+ if (type === 'exp-update') {
438
+ const { totalExp, rankIndex } = data;
439
+ const currentRank = ranks[rankIndex];
440
+ const nextRank = ranks[rankIndex + 1];
441
+ let gainedExp = 0;
442
+
443
+ // Calculate gained exp only after the first update, which is for loading existing exp
444
+ if (!isLoadExpUpdate) {
445
+ gainedExp = totalExp - lastExp;
446
+ } else {
447
+ isLoadExpUpdate = false; // Mark load update as done
448
+ }
449
+
450
+ if (lastRank && lastRank.name !== currentRank.name && totalExp > 0) {
451
+ showRankUpAnnouncement(currentRank);
452
+ }
453
+
454
+ // Show the gained exp number if positive
455
+ if (gainedExp > 0) {
456
+ showExpGainIndicator(gainedExp);
457
+ }
458
+
459
+ const expBarFill = document.querySelector('.exp-bar-fill');
460
+ const expBarText = document.querySelector('.exp-text');
461
+ const expRankIcon = document.querySelector('.exp-icon');
462
+
463
+ expBarFill.style.width = `${((totalExp - currentRank.totalExp) / (nextRank.totalExp - currentRank.totalExp)) * 100}%`;
464
+ expBarText.textContent = currentRank.name;
465
+ expRankIcon.src = `${CDN_ASSETS_URL}/${currentRank.iconUri}`;
466
+
467
+ lastExp = totalExp;
468
+ lastRank = currentRank;
469
+ }
470
+
369
471
  if (type === 'show-damage') {
370
472
  const { damage } = data;
371
-
473
+
372
474
  // Create a new damage number element
373
475
  const damageNumber = document.createElement('div');
374
476
  damageNumber.className = 'hit-damage-number';
375
477
  damageNumber.textContent = damage;
376
-
478
+
377
479
  // Add some randomness to position
378
480
  const randomX = Math.random() * 60 - 30; // -30 to 30px
379
481
  const randomY = Math.random() * 40 - 20; // -20 to 20px
380
-
482
+
381
483
  damageNumber.style.transform = `translate(${randomX}px, ${randomY}px)`;
382
-
484
+
383
485
  // Add to container
384
486
  const container = document.querySelector('.hit-damage-container');
385
487
  container.appendChild(damageNumber);
386
-
488
+
387
489
  // Remove after animation completes
388
490
  setTimeout(() => {
389
491
  damageNumber.remove();
@@ -394,20 +496,20 @@
394
496
  const { health, maxHealth } = data;
395
497
  const healthText = document.querySelector('.health-text');
396
498
  const healthBarFill = document.querySelector('.health-bar-fill');
397
-
499
+
398
500
  healthText.textContent = health;
399
501
  healthBarFill.style.width = `${(health / maxHealth) * 100}%`;
400
502
  }
401
503
 
402
504
  if (type === 'inventory') {
403
505
  const { inventory } = data;
404
-
506
+
405
507
  inventory.forEach((item, i) => {
406
508
  const slot = document.querySelector(`.inventory-slot[data-slot="${i}"]`);
407
509
  const icon = slot.querySelector('.slot-icon');
408
510
  const name = slot.querySelector('.slot-name');
409
511
  const quantity = slot.querySelector('.slot-quantity');
410
-
512
+
411
513
  icon.style.display = item ? 'block' : 'none';
412
514
  name.style.display = item ? 'block' : 'none';
413
515
  quantity.style.display = item ? 'block' : 'none';
@@ -424,7 +526,7 @@
424
526
 
425
527
  if (type === 'inventory-active-slot') {
426
528
  const { index } = data;
427
-
529
+
428
530
  // Remove active slot class from all slots
429
531
  document.querySelectorAll('.inventory-slot').forEach(slot => {
430
532
  slot.classList.remove('inventory-active-slot');
@@ -469,7 +571,7 @@
469
571
  const materialsAmount = document.querySelector('.materials-amount');
470
572
  const currentMaterials = parseInt(materialsAmount.textContent);
471
573
  const difference = materials - currentMaterials;
472
-
574
+
473
575
  if (difference !== 0) {
474
576
  const materialsCounter = document.querySelector('.materials-counter');
475
577
  const floatingNumber = document.createElement('div');
@@ -482,15 +584,19 @@
482
584
  floatingNumber.remove();
483
585
  }, 1000);
484
586
  }
485
-
587
+
486
588
  materialsAmount.textContent = materials;
487
589
  }
488
590
 
591
+ if (type === 'ranks') {
592
+ ranks = data.ranks;
593
+ }
594
+
489
595
  if (type === 'shield') {
490
596
  const { shield, maxShield } = data;
491
597
  const shieldText = document.querySelector('.shield-text');
492
598
  const shieldBarFill = document.querySelector('.shield-bar-fill');
493
-
599
+
494
600
  shieldText.textContent = shield;
495
601
  shieldBarFill.style.width = `${(shield / maxShield) * 100}%`;
496
602
  }
@@ -498,7 +604,7 @@
498
604
  if (type === 'scope-zoom') {
499
605
  const { zoom } = data;
500
606
  const scopeOverlay = document.querySelector('.scope-overlay');
501
-
607
+
502
608
  if (zoom === 1) {
503
609
  scopeOverlay.classList.remove('active');
504
610
  } else {
@@ -508,18 +614,18 @@
508
614
 
509
615
  if (type === 'timer-sync') {
510
616
  const { startedAt, endsAt } = data;
511
-
617
+
512
618
  // Clear any existing timer interval
513
619
  if (timerInterval) {
514
620
  clearInterval(timerInterval);
515
621
  }
516
-
622
+
517
623
  // Set the end time
518
624
  gameEndTime = endsAt;
519
-
625
+
520
626
  // Update timer immediately
521
627
  updateTimer();
522
-
628
+
523
629
  // Set up interval to update timer every second
524
630
  timerInterval = setInterval(updateTimer, 1000);
525
631
  }
@@ -671,6 +777,53 @@
671
777
  animation: winnerFade 5s ease-in-out forwards;
672
778
  }
673
779
 
780
+ .rank-up-announcement {
781
+ position: fixed;
782
+ top: 0;
783
+ left: 0;
784
+ width: 100%;
785
+ height: 100%;
786
+ display: flex;
787
+ flex-direction: column;
788
+ align-items: center;
789
+ justify-content: center;
790
+ opacity: 0;
791
+ pointer-events: none;
792
+ z-index: 1001;
793
+ font-family: 'Inter', sans-serif;
794
+ color: white;
795
+ text-align: center;
796
+ text-shadow: 0 0 10px rgba(0, 0, 0, 0.7), 0 0 20px rgba(0, 0, 0, 0.5);
797
+ }
798
+
799
+ .rank-up-announcement.active {
800
+ animation: rankUpFade 5s ease-in-out forwards;
801
+ }
802
+
803
+ .rank-up-title {
804
+ font-size: 60px;
805
+ font-weight: bold;
806
+ color: #4CAF50; /* Green color for rank up */
807
+ text-transform: uppercase;
808
+ letter-spacing: 2px;
809
+ margin-bottom: 20px;
810
+ }
811
+
812
+ .rank-up-icon {
813
+ width: 150px;
814
+ height: 150px;
815
+ margin-bottom: 20px;
816
+ filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.5));
817
+ }
818
+
819
+ .rank-up-name {
820
+ font-size: 40px;
821
+ font-weight: bold;
822
+ color: #FFEB3B; /* Yellow color for rank name */
823
+ text-transform: uppercase;
824
+ letter-spacing: 1px;
825
+ }
826
+
674
827
  @keyframes announcementFade {
675
828
  0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); }
676
829
  20% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
@@ -687,6 +840,13 @@
687
840
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
688
841
  }
689
842
 
843
+ @keyframes rankUpFade {
844
+ 0% { opacity: 0; transform: scale(0.7); }
845
+ 20% { opacity: 1; transform: scale(1.1); }
846
+ 80% { opacity: 1; transform: scale(1); }
847
+ 100% { opacity: 0; transform: scale(0.9); }
848
+ }
849
+
690
850
  .hit-damage-container {
691
851
  position: fixed;
692
852
  top: 50%;
@@ -855,7 +1015,7 @@
855
1015
  color: rgba(255, 255, 255, 0.8);
856
1016
  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
857
1017
  }
858
-
1018
+
859
1019
  .slot-icon {
860
1020
  max-width: 70%;
861
1021
  max-height: 70%;
@@ -875,7 +1035,7 @@
875
1035
  padding: 1px 4px;
876
1036
  border-radius: 3px;
877
1037
  }
878
-
1038
+
879
1039
  .slot-name {
880
1040
  position: absolute;
881
1041
  bottom: -18px;
@@ -910,6 +1070,7 @@
910
1070
  width: 0%;
911
1071
  height: 100%;
912
1072
  background: linear-gradient(to right, #0000ff, #3333ff);
1073
+ border-radius: 3px;
913
1074
  transition: width 0.3s ease;
914
1075
  }
915
1076
 
@@ -947,6 +1108,7 @@
947
1108
  .health-bar-fill {
948
1109
  width: 100%;
949
1110
  height: 100%;
1111
+ border-radius: 3px;
950
1112
  background: linear-gradient(to right, #ff0000, #ff3333);
951
1113
  transition: width 0.3s ease;
952
1114
  }
@@ -972,6 +1134,74 @@
972
1134
  z-index: 1;
973
1135
  }
974
1136
 
1137
+ .exp-bar {
1138
+ width: 250px;
1139
+ height: 20px;
1140
+ background: rgba(0, 0, 0, 0.5);
1141
+ border-radius: 3px;
1142
+ box-shadow: 0 0 10px rgba(230, 190, 40, 0.3);
1143
+ position: relative; /* Needed for absolute positioning of children */
1144
+ }
1145
+
1146
+ .exp-bar-fill {
1147
+ width: 0%;
1148
+ height: 100%;
1149
+ border-radius: 3px;
1150
+ background: linear-gradient(to right, #e6be28, #f0cc40);
1151
+ transition: width 0.3s ease;
1152
+ }
1153
+
1154
+ .exp-icon {
1155
+ position: absolute;
1156
+ left: -5px;
1157
+ top: 80%;
1158
+ transform: translateY(-80%);
1159
+ height: 60px;
1160
+ width: 60px;
1161
+ z-index: 1;
1162
+ }
1163
+
1164
+ .exp-text {
1165
+ position: absolute;
1166
+ left: 50%;
1167
+ top: 50%;
1168
+ transform: translate(-50%, -50%);
1169
+ font-size: 0.8em;
1170
+ font-weight: bold;
1171
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
1172
+ z-index: 1;
1173
+ }
1174
+
1175
+ .exp-gain-indicator {
1176
+ position: absolute;
1177
+ z-index: 9999;
1178
+ bottom: 100%; /* Start just above the bar */
1179
+ left: 50%;
1180
+ transform: translateX(-50%);
1181
+ color: #e6be28; /* Match EXP bar color */
1182
+ font-family: 'Inter', sans-serif;
1183
+ font-size: 14px;
1184
+ font-weight: bold;
1185
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
1186
+ pointer-events: none;
1187
+ white-space: nowrap;
1188
+ animation: expGainFadeUp 1.5s ease-out forwards;
1189
+ }
1190
+
1191
+ @keyframes expGainFadeUp {
1192
+ 0% {
1193
+ opacity: 0;
1194
+ transform: translate(-50%, 10px); /* Start slightly lower and faded out */
1195
+ }
1196
+ 20% {
1197
+ opacity: 1;
1198
+ }
1199
+ 100% {
1200
+ opacity: 0;
1201
+ transform: translate(-50%, -30px); /* Move up and fade out */
1202
+ }
1203
+ }
1204
+
975
1205
  .item-label {
976
1206
  background-color: rgba(0, 0, 0, 0.6);
977
1207
  padding: 12px 20px;
@@ -1025,6 +1255,11 @@
1025
1255
  border-top: 8px solid rgba(0, 0, 0, 0.6);
1026
1256
  }
1027
1257
 
1258
+ .player-rank-icon {
1259
+ width: 48px;
1260
+ height: 48px;
1261
+ }
1262
+
1028
1263
  .scope-overlay {
1029
1264
  position: fixed;
1030
1265
  top: 0;
@@ -1164,6 +1399,7 @@
1164
1399
  display: flex;
1165
1400
  flex-direction: column;
1166
1401
  gap: 5px;
1402
+ overflow-y: scroll;
1167
1403
  }
1168
1404
 
1169
1405
  .leaderboard-player {
@@ -1245,13 +1481,15 @@
1245
1481
  }
1246
1482
 
1247
1483
  body.mobile .shield-bar,
1248
- body.mobile .health-bar {
1484
+ body.mobile .health-bar,
1485
+ body.mobile .exp-bar {
1249
1486
  width: 120px;
1250
1487
  height: 16px;
1251
1488
  }
1252
1489
 
1253
1490
  body.mobile .shield-text,
1254
- body.mobile .health-text {
1491
+ body.mobile .health-text,
1492
+ body.mobile .exp-text {
1255
1493
  font-size: 0.7em;
1256
1494
  }
1257
1495
 
@@ -1261,6 +1499,11 @@
1261
1499
  height: 10px;
1262
1500
  }
1263
1501
 
1502
+ body.mobile .exp-icon {
1503
+ width: 35px;
1504
+ height: 35px;
1505
+ }
1506
+
1264
1507
  body.mobile .leaderboard-title {
1265
1508
  font-size: 14px;
1266
1509
  }
@@ -1270,6 +1513,7 @@
1270
1513
  }
1271
1514
 
1272
1515
  body.mobile .leaderboard-players-count {
1516
+ display: none;
1273
1517
  font-size: 12px;
1274
1518
  }
1275
1519
 
@@ -1291,6 +1535,10 @@
1291
1535
  font-size: 14px;
1292
1536
  }
1293
1537
 
1538
+ body.mobile .exp-gain-indicator {
1539
+ font-size: 12px;
1540
+ }
1541
+
1294
1542
  body.mobile div > canvas { /* hide three debugger panels */
1295
1543
  display: none !important;
1296
1544
  }
@@ -1335,4 +1583,17 @@
1335
1583
  width: 25px;
1336
1584
  height: 25px;
1337
1585
  }
1586
+
1587
+ body.mobile .rank-up-title {
1588
+ font-size: 40px;
1589
+ }
1590
+
1591
+ body.mobile .rank-up-icon {
1592
+ width: 100px;
1593
+ height: 100px;
1594
+ }
1595
+
1596
+ body.mobile .rank-up-name {
1597
+ font-size: 30px;
1598
+ }
1338
1599
  </style>
@@ -47,6 +47,7 @@ export default class ChestEntity extends Entity {
47
47
  uri: 'audio/sfx/chest-open-2.mp3',
48
48
  volume: 0.7,
49
49
  referenceDistance: 8,
50
+ cutoffDistance: 20,
50
51
  });
51
52
  }
52
53