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.
- package/docs/server.audio.cutoffdistance.md +13 -0
- package/docs/server.audio.md +35 -0
- package/docs/server.audio.setcutoffdistance.md +57 -0
- package/docs/server.audio.setreferencedistance.md +4 -0
- package/docs/server.audioevent.md +14 -0
- package/docs/server.audioeventpayloads._audio.set_cutoff_distance_.md +16 -0
- package/docs/server.audioeventpayloads.md +19 -0
- package/docs/server.audiooptions.cutoffdistance.md +13 -0
- package/docs/server.audiooptions.md +20 -1
- package/docs/server.audiooptions.referencedistance.md +1 -1
- package/examples/hygrounds/assets/icons/ranks/bronze-1.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/bronze-2.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/bronze-3.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/bronze-4.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/bronze-5.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/diamond-1.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/diamond-2.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/diamond-3.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/diamond-4.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/diamond-5.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/elite-1.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/elite-2.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/elite-3.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/elite-4.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/elite-5.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/gold-1.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/gold-2.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/gold-3.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/gold-4.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/gold-5.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/platinum-1.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/platinum-2.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/platinum-3.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/platinum-4.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/platinum-5.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/silver-1.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/silver-2.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/silver-3.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/silver-4.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/silver-5.png +0 -0
- package/examples/hygrounds/assets/icons/ranks/unranked.png +0 -0
- package/examples/hygrounds/assets/ui/index.html +305 -44
- package/examples/hygrounds/classes/ChestEntity.ts +1 -0
- package/examples/hygrounds/classes/GameManager.ts +17 -7
- package/examples/hygrounds/classes/GamePlayerEntity.ts +103 -3
- package/examples/hygrounds/classes/GunEntity.ts +3 -0
- package/examples/hygrounds/classes/ItemEntity.ts +1 -0
- package/examples/hygrounds/classes/MeleeWeaponEntity.ts +2 -0
- package/examples/hygrounds/classes/weapons/PistolEntity.ts +1 -1
- package/examples/hygrounds/classes/weapons/RocketLauncherEntity.ts +2 -1
- package/examples/hygrounds/dev/persistence/player-player-1.json +3 -0
- package/examples/hygrounds/dev/persistence/player-player-2.json +3 -0
- package/examples/hygrounds/dev/persistence/player-player-3.json +3 -0
- package/examples/hygrounds/gameConfig.ts +281 -21
- package/package.json +1 -1
- package/server.api.json +173 -2
- package/server.d.ts +33 -1
- 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.
|
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(
|
133
|
-
const killCount = this._killCounter.get(
|
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(
|
137
|
-
this._updateLeaderboardUI(
|
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
|
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 =
|
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 =
|
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
|
|
@@ -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
|
|
@@ -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:
|
186
|
+
referenceDistance: 15,
|
187
|
+
cutoffDistance: 100,
|
187
188
|
volume: 0.4,
|
188
189
|
})).play(world);
|
189
190
|
|