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
@@ -20,6 +20,7 @@ import {
20
20
  ITEM_SPAWN_ITEMS,
21
21
  MINIMUM_PLAYERS_TO_START,
22
22
  SPAWN_REGION_AABB,
23
+ RANK_WIN_EXP,
23
24
  } from '../gameConfig';
24
25
 
25
26
  import GamePlayerEntity from './GamePlayerEntity';
@@ -96,7 +97,7 @@ export default class GameManager {
96
97
  this._gameActive = false;
97
98
  this.world.chatManager.sendBroadcastMessage('Game over! Starting the next round in 10 seconds...', 'FF0000');
98
99
 
99
- this._focusWinningPlayer();
100
+ this._identifyWinningPlayer();
100
101
 
101
102
  // Clear any existing restart timer
102
103
  if (this._restartTimer) {
@@ -113,8 +114,9 @@ export default class GameManager {
113
114
  if (!this.world) return;
114
115
 
115
116
  const playerEntity = new GamePlayerEntity(player);
117
+
116
118
  playerEntity.spawn(this.world, this.getRandomSpawnPosition());
117
-
119
+
118
120
  // Sync UI for the new player
119
121
  this.syncTimer(player);
120
122
  this.syncLeaderboard(player);
@@ -124,17 +126,20 @@ export default class GameManager {
124
126
  player.ui.sendData({ type: 'game-start' });
125
127
  this._sendGameStartAnnouncements(player);
126
128
  }
129
+
130
+ // Load player's data
131
+ playerEntity.loadPersistedData();
127
132
  }
128
133
 
129
134
  /**
130
135
  * Increments kill count for a player and updates the leaderboard
131
136
  */
132
- public addKill(playerId: string): void {
133
- const killCount = this._killCounter.get(playerId) ?? 0;
137
+ public addKill(playerUsername: string): void {
138
+ const killCount = this._killCounter.get(playerUsername) ?? 0;
134
139
  const newKillCount = killCount + 1;
135
140
 
136
- this._killCounter.set(playerId, newKillCount);
137
- this._updateLeaderboardUI(playerId, newKillCount);
141
+ this._killCounter.set(playerUsername, newKillCount);
142
+ this._updateLeaderboardUI(playerUsername, newKillCount);
138
143
  }
139
144
 
140
145
  /**
@@ -242,7 +247,7 @@ export default class GameManager {
242
247
  this.resetLeaderboard();
243
248
  }
244
249
 
245
- public _focusWinningPlayer() {
250
+ public _identifyWinningPlayer() {
246
251
  if (!this.world) return;
247
252
 
248
253
  // Find player with most kills
@@ -263,6 +268,11 @@ export default class GameManager {
263
268
 
264
269
  if (!winningPlayerEntity) return;
265
270
 
271
+ // Give winning player XP for winning
272
+ if (winningPlayerEntity instanceof GamePlayerEntity) {
273
+ winningPlayerEntity.addExp(RANK_WIN_EXP);
274
+ }
275
+
266
276
  this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
267
277
  if (playerEntity instanceof GamePlayerEntity) {
268
278
  if (playerEntity.player.username !== winningPlayer) { // don't change camera for the winner
@@ -10,6 +10,8 @@ import {
10
10
  World,
11
11
  PlayerEntityController,
12
12
  PlayerUIEvent,
13
+ SceneUI,
14
+ ErrorHandler,
13
15
  } from 'hytopia';
14
16
 
15
17
  import ChestEntity from './ChestEntity';
@@ -17,7 +19,7 @@ import GunEntity from './GunEntity';
17
19
  import ItemEntity from './ItemEntity';
18
20
  import PickaxeEntity from './weapons/PickaxeEntity';
19
21
  import MeleeWeaponEntity from './MeleeWeaponEntity';
20
- import { BUILD_BLOCK_ID } from '../gameConfig';
22
+ import { BUILD_BLOCK_ID, RANKS, RANK_KILL_EXP, RANK_SAVE_INTERVAL_EXP } from '../gameConfig';
21
23
  import GameManager from './GameManager';
22
24
 
23
25
  const BASE_HEALTH = 100;
@@ -27,6 +29,7 @@ const INTERACT_RANGE = 4;
27
29
  const MAX_HEALTH = 100;
28
30
  const MAX_SHIELD = 100;
29
31
  const TOTAL_INVENTORY_SLOTS = 6;
32
+ const STARTING_MATERIALS = 30;
30
33
 
31
34
  interface InventoryItem {
32
35
  name: string;
@@ -34,17 +37,25 @@ interface InventoryItem {
34
37
  quantity: number;
35
38
  }
36
39
 
40
+ interface PlayerPersistedData extends Record<string, unknown> {
41
+ totalExp: number
42
+ }
43
+
37
44
  export default class GamePlayerEntity extends PlayerEntity {
38
45
  private readonly _damageAudio: Audio;
39
46
  private readonly _inventory: (ItemEntity | undefined)[] = new Array(TOTAL_INVENTORY_SLOTS).fill(undefined);
40
47
  private _dead: boolean = false;
41
48
  private _health: number = BASE_HEALTH;
42
49
  private _inventoryActiveSlotIndex: number = 0;
50
+ private _lastExpSave: number = 0;
43
51
  private _maxHealth: number = MAX_HEALTH;
44
52
  private _maxShield: number = MAX_SHIELD;
45
- private _materials: number = 0;
53
+ private _materials: number = STARTING_MATERIALS;
54
+ private _rankIndex: number = 0;
55
+ private _rankSceneUI: SceneUI;
46
56
  private _respawnTimer: NodeJS.Timeout | undefined;
47
57
  private _shield: number = BASE_SHIELD;
58
+ private _totalExp: number = 0;
48
59
 
49
60
  // Player entities always assign a PlayerController to the entity
50
61
  public get playerController(): PlayerEntityController {
@@ -80,6 +91,14 @@ export default class GamePlayerEntity extends PlayerEntity {
80
91
  this._setupPlayerUI();
81
92
  this._setupPlayerCamera();
82
93
  this._setupPlayerHeadshotCollider();
94
+
95
+ this._rankSceneUI = new SceneUI({
96
+ attachedToEntity: this,
97
+ templateId: 'player-rank',
98
+ state: { iconUri: 'icons/ranks/unranked.png' },
99
+ viewDistance: 8,
100
+ offset: { x: 0, y: 1.15, z: 0 },
101
+ });
83
102
 
84
103
  this._damageAudio = new Audio({
85
104
  attachedToEntity: this,
@@ -95,6 +114,20 @@ export default class GamePlayerEntity extends PlayerEntity {
95
114
  this._autoHealTicker();
96
115
  this._outOfWorldTicker();
97
116
  this._updatePlayerUIHealth();
117
+ this._updatePlayerUIMaterials();
118
+
119
+ this._rankSceneUI.load(world);
120
+ }
121
+
122
+ public addExp(exp: number): void {
123
+ this._totalExp += exp;
124
+ this._updatePlayerUIExp();
125
+
126
+ // Save the player's data to persisted storage every so often.
127
+ if (this._totalExp - this._lastExpSave >= RANK_SAVE_INTERVAL_EXP) {
128
+ this.savePersistedData();
129
+ this._lastExpSave = this._totalExp;
130
+ }
98
131
  }
99
132
 
100
133
  public addItemToInventory(item: ItemEntity): void {
@@ -123,6 +156,7 @@ export default class GamePlayerEntity extends PlayerEntity {
123
156
 
124
157
  if (attacker) {
125
158
  GameManager.instance.addKill(attacker.player.username);
159
+ attacker.addExp(RANK_KILL_EXP);
126
160
  this.focusCameraOnPlayer(attacker);
127
161
  }
128
162
 
@@ -207,6 +241,23 @@ export default class GamePlayerEntity extends PlayerEntity {
207
241
  return this._inventory[this._inventoryActiveSlotIndex] === item;
208
242
  }
209
243
 
244
+ public async loadPersistedData(): Promise<void> {
245
+ if (!this.isSpawned) {
246
+ return ErrorHandler.error('PlayerEntity not spawned');
247
+ }
248
+
249
+ const data = await this.player.getPersistedData();
250
+
251
+ if (data) {
252
+ const persistedData = data as unknown as PlayerPersistedData;
253
+
254
+ this._lastExpSave = persistedData.totalExp;
255
+ this._totalExp = persistedData.totalExp;
256
+ }
257
+
258
+ this._updatePlayerUIExp();
259
+ }
260
+
210
261
  public resetAnimations(): void {
211
262
  this.playerController.idleLoopedAnimations = ['idle_lower', 'idle_upper'];
212
263
  this.playerController.interactOneshotAnimations = [];
@@ -220,7 +271,7 @@ export default class GamePlayerEntity extends PlayerEntity {
220
271
  }
221
272
 
222
273
  public resetMaterials(): void {
223
- this._materials = 0;
274
+ this._materials = STARTING_MATERIALS;
224
275
  this._updatePlayerUIMaterials();
225
276
  }
226
277
 
@@ -237,6 +288,14 @@ export default class GamePlayerEntity extends PlayerEntity {
237
288
  this.setPosition(GameManager.instance.getRandomSpawnPosition());
238
289
  }
239
290
 
291
+ public savePersistedData(): void {
292
+ let data: PlayerPersistedData = {
293
+ totalExp: this._totalExp,
294
+ };
295
+
296
+ this.player.setPersistedData(data);
297
+ }
298
+
240
299
  public setActiveInventorySlotIndex(index: number): void {
241
300
  if (index !== this._inventoryActiveSlotIndex) {
242
301
  this._inventory[this._inventoryActiveSlotIndex]?.unequip();
@@ -339,6 +398,12 @@ export default class GamePlayerEntity extends PlayerEntity {
339
398
  this.nametagSceneUI.setViewDistance(8); // lessen view distance so you only see player names when close
340
399
  this.player.ui.load('ui/index.html');
341
400
 
401
+ // Load rank data
402
+ this.player.ui.sendData({
403
+ type: 'ranks',
404
+ ranks: RANKS,
405
+ });
406
+
342
407
  // Handle inventory selection from mobile UI
343
408
  this.player.ui.on(PlayerUIEvent.DATA, (payload) => {
344
409
  const { data } = payload;
@@ -574,6 +639,24 @@ export default class GamePlayerEntity extends PlayerEntity {
574
639
  });
575
640
  }
576
641
 
642
+ private _updatePlayerUIExp(): void {
643
+ const rankIndex = this._totalExpToRankIndex(this._totalExp);
644
+
645
+ if (rankIndex !== this._rankIndex) {
646
+ this._rankIndex = rankIndex;
647
+
648
+ this._rankSceneUI.setState({
649
+ iconUri: RANKS[this._rankIndex].iconUri,
650
+ });
651
+ }
652
+
653
+ this.player.ui.sendData({
654
+ type: 'exp-update',
655
+ totalExp: this._totalExp,
656
+ rankIndex,
657
+ });
658
+ }
659
+
577
660
  private _playDamageAudio(): void {
578
661
  this._damageAudio.setDetune(-200 + Math.random() * 800);
579
662
  this._damageAudio.play(this.world!, true);
@@ -602,4 +685,21 @@ export default class GamePlayerEntity extends PlayerEntity {
602
685
  this._outOfWorldTicker();
603
686
  }, 3000);
604
687
  }
688
+
689
+ private _totalExpToRankIndex(totalExp: number): number {
690
+ // Get the rank index for the player's total exp
691
+ for (let i = 0; i < RANKS.length - 1; i++) {
692
+ if (totalExp >= RANKS[i].totalExp && totalExp < RANKS[i + 1].totalExp) {
693
+ return i;
694
+ }
695
+ }
696
+
697
+ // If we've reached the highest rank
698
+ if (totalExp >= RANKS[RANKS.length - 1].totalExp) {
699
+ return RANKS.length - 1;
700
+ }
701
+
702
+ // Default to unranked (index 0) if below first rank
703
+ return 0;
704
+ }
605
705
  }
@@ -62,6 +62,8 @@ export default abstract class GunEntity extends ItemEntity {
62
62
  this._reloadAudio = new Audio({
63
63
  attachedToEntity: this,
64
64
  uri: options.reloadAudioUri,
65
+ referenceDistance: 8,
66
+ cutoffDistance: 20,
65
67
  });
66
68
 
67
69
  this._shootAudio = new Audio({
@@ -69,6 +71,7 @@ export default abstract class GunEntity extends ItemEntity {
69
71
  uri: options.shootAudioUri,
70
72
  volume: 0.3,
71
73
  referenceDistance: 8,
74
+ cutoffDistance: 60,
72
75
  });
73
76
  }
74
77
 
@@ -103,6 +103,7 @@ export default class ItemEntity extends Entity {
103
103
  uri: this.consumeAudioUri,
104
104
  volume: 0.5,
105
105
  referenceDistance: 5,
106
+ cutoffDistance: 15,
106
107
  })).play(this.world);
107
108
 
108
109
  }
@@ -43,6 +43,7 @@ export default abstract class MeleeWeaponEntity extends ItemEntity {
43
43
  uri: options.attackAudioUri,
44
44
  volume: 0.3,
45
45
  referenceDistance: 3,
46
+ cutoffDistance: 15,
46
47
  });
47
48
 
48
49
  this._hitAudio = new Audio({
@@ -50,6 +51,7 @@ export default abstract class MeleeWeaponEntity extends ItemEntity {
50
51
  uri: options.hitAudioUri,
51
52
  volume: 0.3,
52
53
  referenceDistance: 3,
54
+ cutoffDistance: 15,
53
55
  });
54
56
  }
55
57
 
@@ -5,7 +5,7 @@ import type GamePlayerEntity from '../GamePlayerEntity';
5
5
 
6
6
  const DEFAULT_PISTOL_OPTIONS: GunEntityOptions = {
7
7
  ammo: 15,
8
- damage: 18,
8
+ damage: 14,
9
9
  fireRate: 4,
10
10
  heldHand: 'right',
11
11
  iconImageUri: 'icons/pistol.png',
@@ -183,7 +183,8 @@ export default class RocketLauncherEntity extends GunEntity {
183
183
  // Explosion Audio
184
184
  (new Audio({
185
185
  uri: 'audio/sfx/rocket-launcher-explosion.mp3',
186
- referenceDistance: 20,
186
+ referenceDistance: 15,
187
+ cutoffDistance: 100,
187
188
  volume: 0.4,
188
189
  })).play(world);
189
190
 
@@ -0,0 +1,3 @@
1
+ {
2
+ "totalExp": 2400
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "totalExp": 7700
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "totalExp": 800
3
+ }