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.
- package/boilerplate/assets/map.json +191 -43
- package/docs/server.playercameramode.md +14 -0
- package/examples/hygrounds/README.md +0 -0
- package/examples/hygrounds/assets/audio/sfx/chest-open-1.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/chest-open-2.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/machine-gun-reload.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/machine-gun-shoot.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/medpack-consume.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/mining-drill-drilling.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/pistol-reload.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/pistol-shoot.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/player-hurt.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/potion-consume.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/rifle-reload.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/rifle-shoot.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/rocket-launcher-explosion.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/rocket-launcher-reload.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/rocket-launcher-shoot.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/shield.png +0 -0
- package/examples/hygrounds/assets/audio/sfx/shotgun-reload.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/shotgun-shoot.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/sniper-reload.mp3 +0 -0
- package/examples/hygrounds/assets/audio/sfx/sniper-shoot.mp3 +0 -0
- package/examples/hygrounds/assets/icons/ak-47.png +0 -0
- package/examples/hygrounds/assets/icons/ammo.png +0 -0
- package/examples/hygrounds/assets/icons/auto-shotgun.png +0 -0
- package/examples/hygrounds/assets/icons/auto-sniper.png +0 -0
- package/examples/hygrounds/assets/icons/block.png +0 -0
- package/examples/hygrounds/assets/icons/bolt-action-sniper.png +0 -0
- package/examples/hygrounds/assets/icons/crown-bronze.png +0 -0
- package/examples/hygrounds/assets/icons/crown-gold.png +0 -0
- package/examples/hygrounds/assets/icons/crown-silver.png +0 -0
- package/examples/hygrounds/assets/icons/gravity-potion.png +0 -0
- package/examples/hygrounds/assets/icons/heart.png +0 -0
- package/examples/hygrounds/assets/icons/light-machine-gun.png +0 -0
- package/examples/hygrounds/assets/icons/medpack.png +0 -0
- package/examples/hygrounds/assets/icons/mining-drill.png +0 -0
- package/examples/hygrounds/assets/icons/pickaxe.png +0 -0
- package/examples/hygrounds/assets/icons/pistol.png +0 -0
- package/examples/hygrounds/assets/icons/revolver.png +0 -0
- package/examples/hygrounds/assets/icons/rocket-launcher.png +0 -0
- package/examples/hygrounds/assets/icons/shield-potion.png +0 -0
- package/examples/hygrounds/assets/icons/shield.png +0 -0
- package/examples/hygrounds/assets/icons/shotgun.png +0 -0
- package/examples/hygrounds/assets/icons/submachine-gun.png +0 -0
- package/examples/hygrounds/assets/map.json +31796 -0
- package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion.glb +0 -0
- package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2.glb +0 -0
- package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3.glb +0 -0
- package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter.glb +0 -0
- package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/environment/chest.gltf +1 -0
- package/examples/hygrounds/assets/models/environment/explosion.glb +0 -0
- package/examples/hygrounds/assets/models/environment/muzzle-flash.gltf +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/ak-47.glb +0 -0
- package/examples/hygrounds/assets/models/items/auto-shotgun.glb +0 -0
- package/examples/hygrounds/assets/models/items/auto-sniper.glb +0 -0
- package/examples/hygrounds/assets/models/items/bolt-action-sniper.glb +0 -0
- package/examples/hygrounds/assets/models/items/gravity-potion.glb +0 -0
- package/examples/hygrounds/assets/models/items/light-machine-gun.glb +0 -0
- package/examples/hygrounds/assets/models/items/medpack.glb +0 -0
- package/examples/hygrounds/assets/models/items/mining-drill.glb +0 -0
- package/examples/hygrounds/assets/models/items/pickaxe.gltf +1 -0
- package/examples/hygrounds/assets/models/items/pistol.glb +0 -0
- package/examples/hygrounds/assets/models/items/revolver.glb +0 -0
- package/examples/hygrounds/assets/models/items/rocket-launcher.glb +0 -0
- package/examples/hygrounds/assets/models/items/rocket-missile.glb +0 -0
- package/examples/hygrounds/assets/models/items/shield-potion.glb +0 -0
- package/examples/hygrounds/assets/models/items/shotgun.glb +0 -0
- package/examples/hygrounds/assets/models/items/submachine-gun.glb +0 -0
- package/examples/hygrounds/assets/models/players/soldier-player.gltf +1 -0
- package/examples/hygrounds/assets/ui/images/scope.png +0 -0
- package/examples/hygrounds/assets/ui/index.html +1122 -0
- package/examples/hygrounds/bun.lock +503 -0
- package/examples/hygrounds/classes/ChestEntity.ts +133 -0
- package/examples/hygrounds/classes/GameManager.ts +422 -0
- package/examples/hygrounds/classes/GamePlayerEntity.ts +595 -0
- package/examples/hygrounds/classes/GunEntity.ts +259 -0
- package/examples/hygrounds/classes/ItemEntity.ts +225 -0
- package/examples/hygrounds/classes/ItemFactory.ts +61 -0
- package/examples/hygrounds/classes/MeleeWeaponEntity.ts +138 -0
- package/examples/hygrounds/classes/TerrainDamageManager.ts +56 -0
- package/examples/hygrounds/classes/items/GravityPotionEntity.ts +48 -0
- package/examples/hygrounds/classes/items/MedPackEntity.ts +43 -0
- package/examples/hygrounds/classes/items/ShieldPotionEntity.ts +43 -0
- package/examples/hygrounds/classes/weapons/AK47Entity.ts +43 -0
- package/examples/hygrounds/classes/weapons/AutoShotgunEntity.ts +80 -0
- package/examples/hygrounds/classes/weapons/AutoSniperEntity.ts +43 -0
- package/examples/hygrounds/classes/weapons/BoltActionSniperEntity.ts +46 -0
- package/examples/hygrounds/classes/weapons/LightMachineGunEntity.ts +43 -0
- package/examples/hygrounds/classes/weapons/MiningDrillEntity.ts +38 -0
- package/examples/hygrounds/classes/weapons/PickaxeEntity.ts +38 -0
- package/examples/hygrounds/classes/weapons/PistolEntity.ts +46 -0
- package/examples/hygrounds/classes/weapons/RevolverEntity.ts +46 -0
- package/examples/hygrounds/classes/weapons/RocketLauncherEntity.ts +195 -0
- package/examples/hygrounds/classes/weapons/ShotgunEntity.ts +84 -0
- package/examples/hygrounds/classes/weapons/SubmachineGunEntity.ts +43 -0
- package/examples/hygrounds/gameConfig.ts +430 -0
- package/examples/hygrounds/index.ts +41 -0
- package/examples/hygrounds/package.json +16 -0
- package/examples/player-persistence/README.md +3 -0
- package/examples/player-persistence/assets/map.json +2623 -0
- package/examples/player-persistence/dev/persistence/player-player-1.json +6 -0
- package/examples/player-persistence/dev/persistence/test.json +3 -0
- package/examples/player-persistence/index.ts +126 -0
- package/examples/player-persistence/package.json +16 -0
- package/package.json +1 -1
- package/server.api.json +21 -0
- package/server.d.ts +2 -1
- package/server.js +113 -113
@@ -0,0 +1,595 @@
|
|
1
|
+
import {
|
2
|
+
Audio,
|
3
|
+
BaseEntityControllerEvent,
|
4
|
+
EventPayloads,
|
5
|
+
Player,
|
6
|
+
PlayerEntity,
|
7
|
+
PlayerCameraMode,
|
8
|
+
Vector3Like,
|
9
|
+
QuaternionLike,
|
10
|
+
World,
|
11
|
+
PlayerEntityController,
|
12
|
+
} from 'hytopia';
|
13
|
+
|
14
|
+
import ChestEntity from './ChestEntity';
|
15
|
+
import GunEntity from './GunEntity';
|
16
|
+
import ItemEntity from './ItemEntity';
|
17
|
+
import PickaxeEntity from './weapons/PickaxeEntity';
|
18
|
+
import MeleeWeaponEntity from './MeleeWeaponEntity';
|
19
|
+
import { BUILD_BLOCK_ID } from '../gameConfig';
|
20
|
+
import GameManager from './GameManager';
|
21
|
+
|
22
|
+
const BASE_HEALTH = 100;
|
23
|
+
const BASE_SHIELD = 0;
|
24
|
+
const BLOCK_MATERIAL_COST = 3;
|
25
|
+
const INTERACT_RANGE = 4;
|
26
|
+
const MAX_HEALTH = 100;
|
27
|
+
const MAX_SHIELD = 100;
|
28
|
+
const TOTAL_INVENTORY_SLOTS = 6;
|
29
|
+
|
30
|
+
interface InventoryItem {
|
31
|
+
name: string;
|
32
|
+
iconImageUri: string;
|
33
|
+
quantity: number;
|
34
|
+
}
|
35
|
+
|
36
|
+
export default class GamePlayerEntity extends PlayerEntity {
|
37
|
+
private readonly _damageAudio: Audio;
|
38
|
+
private readonly _inventory: (ItemEntity | undefined)[] = new Array(TOTAL_INVENTORY_SLOTS).fill(undefined);
|
39
|
+
private _dead: boolean = false;
|
40
|
+
private _health: number = BASE_HEALTH;
|
41
|
+
private _inventoryActiveSlotIndex: number = 0;
|
42
|
+
private _maxHealth: number = MAX_HEALTH;
|
43
|
+
private _maxShield: number = MAX_SHIELD;
|
44
|
+
private _materials: number = 0;
|
45
|
+
private _respawnTimer: NodeJS.Timeout | undefined;
|
46
|
+
private _shield: number = BASE_SHIELD;
|
47
|
+
|
48
|
+
// Player entities always assign a PlayerController to the entity
|
49
|
+
public get playerController(): PlayerEntityController {
|
50
|
+
return this.controller as PlayerEntityController;
|
51
|
+
}
|
52
|
+
|
53
|
+
public get health(): number { return this._health; }
|
54
|
+
public set health(value: number) {
|
55
|
+
this._health = Math.max(0, Math.min(value, this._maxHealth));
|
56
|
+
this._updatePlayerUIHealth();
|
57
|
+
}
|
58
|
+
|
59
|
+
public get shield(): number { return this._shield; }
|
60
|
+
public set shield(value: number) {
|
61
|
+
this._shield = Math.max(0, Math.min(value, this._maxShield));
|
62
|
+
this._updatePlayerUIShield();
|
63
|
+
}
|
64
|
+
|
65
|
+
public get maxHealth(): number { return this._maxHealth; }
|
66
|
+
public get maxShield(): number { return this._maxShield; }
|
67
|
+
|
68
|
+
public get isDead(): boolean { return this._dead; }
|
69
|
+
|
70
|
+
public constructor(player: Player) {
|
71
|
+
super({
|
72
|
+
player,
|
73
|
+
name: 'Player',
|
74
|
+
modelUri: 'models/players/soldier-player.gltf',
|
75
|
+
modelScale: 0.5,
|
76
|
+
});
|
77
|
+
|
78
|
+
this._setupPlayerController();
|
79
|
+
this._setupPlayerUI();
|
80
|
+
this._setupPlayerCamera();
|
81
|
+
this._setupPlayerHeadshotCollider();
|
82
|
+
|
83
|
+
this._damageAudio = new Audio({
|
84
|
+
attachedToEntity: this,
|
85
|
+
uri: 'audio/sfx/player-hurt.mp3',
|
86
|
+
loop: false,
|
87
|
+
volume: 0.7,
|
88
|
+
});
|
89
|
+
}
|
90
|
+
|
91
|
+
public override spawn(world: World, position: Vector3Like, rotation?: QuaternionLike): void {
|
92
|
+
super.spawn(world, position, rotation);
|
93
|
+
this._setupPlayerInventory();
|
94
|
+
this._autoHealTicker();
|
95
|
+
this._outOfWorldTicker();
|
96
|
+
this._updatePlayerUIHealth();
|
97
|
+
}
|
98
|
+
|
99
|
+
public addItemToInventory(item: ItemEntity): void {
|
100
|
+
const slot = this._findInventorySlot();
|
101
|
+
|
102
|
+
if (slot === this._inventoryActiveSlotIndex) {
|
103
|
+
this.dropActiveInventoryItem();
|
104
|
+
}
|
105
|
+
|
106
|
+
this._inventory[slot] = item;
|
107
|
+
this._updatePlayerUIInventory();
|
108
|
+
this._updatePlayerUIInventoryActiveSlot();
|
109
|
+
this.setActiveInventorySlotIndex(this._inventoryActiveSlotIndex);
|
110
|
+
}
|
111
|
+
|
112
|
+
public addMaterial(quantity: number): void {
|
113
|
+
if (!quantity) return;
|
114
|
+
|
115
|
+
this._materials += quantity;
|
116
|
+
this._updatePlayerUIMaterials();
|
117
|
+
}
|
118
|
+
|
119
|
+
public checkDeath(attacker?: GamePlayerEntity): void {
|
120
|
+
if (this.health <= 0) {
|
121
|
+
this._dead = true;
|
122
|
+
|
123
|
+
if (attacker) {
|
124
|
+
GameManager.instance.addKill(attacker.player.username);
|
125
|
+
this.focusCameraOnPlayer(attacker);
|
126
|
+
}
|
127
|
+
|
128
|
+
this.dropAllInventoryItems();
|
129
|
+
|
130
|
+
if (this.isSpawned && this.world) {
|
131
|
+
// reset player inputs
|
132
|
+
Object.keys(this.player.input).forEach(key => {
|
133
|
+
this.player.input[key] = false;
|
134
|
+
});
|
135
|
+
|
136
|
+
this.playerController.idleLoopedAnimations = [ 'sleep' ];
|
137
|
+
this.world.chatManager.sendPlayerMessage(this.player, 'You have died! Respawning in 5 seconds...', 'FF0000');
|
138
|
+
this._respawnTimer = setTimeout(() => this.respawn(), 5 * 1000);
|
139
|
+
|
140
|
+
if (attacker) {
|
141
|
+
if (this.player.username !== attacker.player.username) {
|
142
|
+
this.world.chatManager.sendBroadcastMessage(`${attacker.player.username} killed ${this.player.username} with a ${attacker.getActiveItemName()}!`, 'FF0000');
|
143
|
+
} else {
|
144
|
+
this.world.chatManager.sendBroadcastMessage(`${this.player.username} self-destructed!`, 'FF0000');
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
151
|
+
public focusCameraOnPlayer(player: GamePlayerEntity): void {
|
152
|
+
this.player.camera.setMode(PlayerCameraMode.THIRD_PERSON);
|
153
|
+
this.player.camera.setAttachedToEntity(player);
|
154
|
+
this.player.camera.setModelHiddenNodes([]);
|
155
|
+
}
|
156
|
+
|
157
|
+
public dealtDamage(damage: number): void {
|
158
|
+
this.player.ui.sendData({
|
159
|
+
type: 'show-damage',
|
160
|
+
damage,
|
161
|
+
});
|
162
|
+
}
|
163
|
+
|
164
|
+
public dropActiveInventoryItem(): void {
|
165
|
+
if (this._inventoryActiveSlotIndex === 0) {
|
166
|
+
this.world?.chatManager?.sendPlayerMessage(this.player, 'You cannot drop your pickaxe!');
|
167
|
+
return;
|
168
|
+
}
|
169
|
+
|
170
|
+
const item = this._inventory[this._inventoryActiveSlotIndex];
|
171
|
+
if (!item) return;
|
172
|
+
|
173
|
+
item.unequip();
|
174
|
+
item.drop(this.position, this.player.camera.facingDirection);
|
175
|
+
this._inventory[this._inventoryActiveSlotIndex] = undefined;
|
176
|
+
this._updatePlayerUIInventory();
|
177
|
+
this._updatePlayerUIInventoryActiveSlot();
|
178
|
+
}
|
179
|
+
|
180
|
+
public dropAllInventoryItems(): void {
|
181
|
+
// skip 0, we cannot drop the pickaxe
|
182
|
+
for (let i = 1; i < this._inventory.length; i++) {
|
183
|
+
const item = this._inventory[i];
|
184
|
+
if (!item) continue;
|
185
|
+
|
186
|
+
item.unequip();
|
187
|
+
item.drop(this.position, this.player.camera.facingDirection);
|
188
|
+
this._inventory[i] = undefined;
|
189
|
+
}
|
190
|
+
|
191
|
+
this._updatePlayerUIInventory();
|
192
|
+
}
|
193
|
+
|
194
|
+
public getActiveItemName(): string {
|
195
|
+
const activeItem = this._inventory[this._inventoryActiveSlotIndex];
|
196
|
+
if (!activeItem) return '';
|
197
|
+
|
198
|
+
return activeItem.name;
|
199
|
+
}
|
200
|
+
|
201
|
+
public getItemInventorySlot(item: ItemEntity): number {
|
202
|
+
return this._inventory.findIndex(slot => slot === item);
|
203
|
+
}
|
204
|
+
|
205
|
+
public isItemActiveInInventory(item: ItemEntity): boolean {
|
206
|
+
return this._inventory[this._inventoryActiveSlotIndex] === item;
|
207
|
+
}
|
208
|
+
|
209
|
+
public resetAnimations(): void {
|
210
|
+
this.playerController.idleLoopedAnimations = ['idle_lower', 'idle_upper'];
|
211
|
+
this.playerController.interactOneshotAnimations = [];
|
212
|
+
this.playerController.walkLoopedAnimations = ['walk_lower', 'walk_upper'];
|
213
|
+
this.playerController.runLoopedAnimations = ['run_lower', 'run_upper'];
|
214
|
+
}
|
215
|
+
|
216
|
+
public resetCamera(): void {
|
217
|
+
this._setupPlayerCamera();
|
218
|
+
this.player.camera.setAttachedToEntity(this);
|
219
|
+
}
|
220
|
+
|
221
|
+
public resetMaterials(): void {
|
222
|
+
this._materials = 0;
|
223
|
+
this._updatePlayerUIMaterials();
|
224
|
+
}
|
225
|
+
|
226
|
+
public respawn(): void {
|
227
|
+
if (!this.world) return;
|
228
|
+
|
229
|
+
this._dead = false;
|
230
|
+
this.health = this._maxHealth;
|
231
|
+
this.shield = 0;
|
232
|
+
this.resetAnimations();
|
233
|
+
this.player.camera.setAttachedToEntity(this);
|
234
|
+
this._setupPlayerCamera();
|
235
|
+
this.setActiveInventorySlotIndex(0);
|
236
|
+
this.setPosition(GameManager.instance.getRandomSpawnPosition());
|
237
|
+
}
|
238
|
+
|
239
|
+
public setActiveInventorySlotIndex(index: number): void {
|
240
|
+
if (index !== this._inventoryActiveSlotIndex) {
|
241
|
+
this._inventory[this._inventoryActiveSlotIndex]?.unequip();
|
242
|
+
}
|
243
|
+
|
244
|
+
this._inventoryActiveSlotIndex = index;
|
245
|
+
|
246
|
+
if (this._inventory[index]) {
|
247
|
+
this._inventory[index].equip();
|
248
|
+
}
|
249
|
+
|
250
|
+
this._updatePlayerUIInventoryActiveSlot();
|
251
|
+
}
|
252
|
+
|
253
|
+
public setGravity(gravityScale: number): void {
|
254
|
+
this.setGravityScale(gravityScale);
|
255
|
+
}
|
256
|
+
|
257
|
+
public takeDamage(damage: number, hitDirection: Vector3Like, attacker?: GamePlayerEntity): void {
|
258
|
+
if (!this.isSpawned || !this.world || !GameManager.instance.isGameActive || this._dead) return;
|
259
|
+
|
260
|
+
this._playDamageAudio();
|
261
|
+
|
262
|
+
// Flash for damage
|
263
|
+
this.setTintColor({ r: 255, g: 0, b: 0});
|
264
|
+
setTimeout(() => this.setTintColor({ r: 255, g: 255, b: 255 }), 100); // reset tint color after 100ms
|
265
|
+
|
266
|
+
// Convert hit direction to screen space coordinates
|
267
|
+
const facingDir = this.player.camera.facingDirection;
|
268
|
+
this.player.ui.sendData({
|
269
|
+
type: 'damage-indicator',
|
270
|
+
direction: {
|
271
|
+
x: -(facingDir.x * hitDirection.z - facingDir.z * hitDirection.x),
|
272
|
+
y: 0,
|
273
|
+
z: -(facingDir.x * hitDirection.x + facingDir.z * hitDirection.z)
|
274
|
+
}
|
275
|
+
});
|
276
|
+
|
277
|
+
// Handle shield damage first
|
278
|
+
if (this.shield > 0) {
|
279
|
+
const shieldDamage = Math.min(damage, this.shield);
|
280
|
+
this.shield -= shieldDamage;
|
281
|
+
damage -= shieldDamage;
|
282
|
+
if (damage === 0) return;
|
283
|
+
}
|
284
|
+
|
285
|
+
// Handle health damage
|
286
|
+
this.health -= damage;
|
287
|
+
this.checkDeath(attacker);
|
288
|
+
}
|
289
|
+
|
290
|
+
public updateHealth(amount: number): void {
|
291
|
+
this.health = Math.min(this.health + amount, this._maxHealth);
|
292
|
+
|
293
|
+
this._updatePlayerUIHealth();
|
294
|
+
}
|
295
|
+
|
296
|
+
public updateShield(amount: number): void {
|
297
|
+
this.shield = Math.min(this.shield + amount, this._maxShield);
|
298
|
+
|
299
|
+
this._updatePlayerUIShield();
|
300
|
+
}
|
301
|
+
|
302
|
+
public updateItemInventoryQuantity(item: ItemEntity): void {
|
303
|
+
const index = this.getItemInventorySlot(item);
|
304
|
+
if (index === -1) return;
|
305
|
+
|
306
|
+
this.player.ui.sendData({
|
307
|
+
type: 'inventory-quantity-update',
|
308
|
+
index,
|
309
|
+
quantity: item.getQuantity(),
|
310
|
+
});
|
311
|
+
}
|
312
|
+
|
313
|
+
private _setupPlayerController(): void {
|
314
|
+
this.playerController.autoCancelMouseLeftClick = false;
|
315
|
+
|
316
|
+
this.resetAnimations();
|
317
|
+
|
318
|
+
this.playerController.on(BaseEntityControllerEvent.TICK_WITH_PLAYER_INPUT, this._onTickWithPlayerInput);
|
319
|
+
}
|
320
|
+
|
321
|
+
private _setupPlayerHeadshotCollider(): void {
|
322
|
+
// TODO
|
323
|
+
// this.createAndAddChildCollider({
|
324
|
+
// shape: ColliderShape.BALL,
|
325
|
+
// radius: 0.45,
|
326
|
+
// relativePosition: { x: 0, y: 0.4, z: 0 },
|
327
|
+
// isSensor: true,
|
328
|
+
// });
|
329
|
+
}
|
330
|
+
|
331
|
+
private _setupPlayerInventory(): void {
|
332
|
+
const pickaxe = new PickaxeEntity();
|
333
|
+
pickaxe.spawn(this.world!, this.position);
|
334
|
+
pickaxe.pickup(this);
|
335
|
+
}
|
336
|
+
|
337
|
+
private _setupPlayerUI(): void {
|
338
|
+
this.nametagSceneUI.setViewDistance(8); // lessen view distance so you only see player names when close
|
339
|
+
this.player.ui.load('ui/index.html');
|
340
|
+
}
|
341
|
+
|
342
|
+
private _setupPlayerCamera(): void {
|
343
|
+
this.player.camera.setMode(PlayerCameraMode.FIRST_PERSON);
|
344
|
+
this.player.camera.setModelHiddenNodes([ 'head', 'neck', 'torso', 'leg_right', 'leg_left' ]);
|
345
|
+
this.player.camera.setOffset({ x: 0, y: 0.5, z: 0 });
|
346
|
+
}
|
347
|
+
|
348
|
+
private _onTickWithPlayerInput = (payload: EventPayloads[BaseEntityControllerEvent.TICK_WITH_PLAYER_INPUT]): void => {
|
349
|
+
const { input } = payload;
|
350
|
+
|
351
|
+
if (this._dead) {
|
352
|
+
return;
|
353
|
+
}
|
354
|
+
|
355
|
+
if (input.ml) {
|
356
|
+
this._handleMouseLeftClick();
|
357
|
+
}
|
358
|
+
|
359
|
+
if (input.mr) {
|
360
|
+
this._handleMouseRightClick();
|
361
|
+
}
|
362
|
+
|
363
|
+
if (input.e) {
|
364
|
+
this._handleInteract();
|
365
|
+
input.e = false;
|
366
|
+
}
|
367
|
+
|
368
|
+
if (input.q) {
|
369
|
+
this.dropActiveInventoryItem();
|
370
|
+
input.q = false;
|
371
|
+
}
|
372
|
+
|
373
|
+
if (input.r) {
|
374
|
+
this._handleReload();
|
375
|
+
input.r = false;
|
376
|
+
}
|
377
|
+
|
378
|
+
if (input.z) {
|
379
|
+
this._handleZoomScope();
|
380
|
+
input.z = false;
|
381
|
+
}
|
382
|
+
|
383
|
+
this._handleInventoryHotkeys(input);
|
384
|
+
}
|
385
|
+
|
386
|
+
private _handleMouseLeftClick(): void {
|
387
|
+
const activeItem = this._inventory[this._inventoryActiveSlotIndex];
|
388
|
+
|
389
|
+
if (activeItem instanceof ItemEntity && activeItem.consumable) {
|
390
|
+
activeItem.consume();
|
391
|
+
}
|
392
|
+
|
393
|
+
if (activeItem instanceof GunEntity) {
|
394
|
+
activeItem.shoot();
|
395
|
+
}
|
396
|
+
|
397
|
+
if (activeItem instanceof MeleeWeaponEntity) {
|
398
|
+
activeItem.attack();
|
399
|
+
}
|
400
|
+
}
|
401
|
+
|
402
|
+
private _handleMouseRightClick(): void {
|
403
|
+
this.player.input.mr = false;
|
404
|
+
|
405
|
+
if (!this.world) return;
|
406
|
+
|
407
|
+
if (this._materials < BLOCK_MATERIAL_COST) {
|
408
|
+
this.world?.chatManager?.sendPlayerMessage(this.player, `You need at least ${BLOCK_MATERIAL_COST} materials to build! Break blocks with your pickaxe to gather materials.`, 'FF0000');
|
409
|
+
return;
|
410
|
+
}
|
411
|
+
|
412
|
+
const { world } = this;
|
413
|
+
const position = this.position;
|
414
|
+
const facingDirection = this.player.camera.facingDirection;
|
415
|
+
const origin = {
|
416
|
+
x: position.x + (facingDirection.x * 0.5),
|
417
|
+
y: position.y + (facingDirection.y * 0.5) + this.player.camera.offset.y,
|
418
|
+
z: position.z + (facingDirection.z * 0.5),
|
419
|
+
};
|
420
|
+
|
421
|
+
const raycastHit = world.simulation.raycast(origin, facingDirection, 4, {
|
422
|
+
filterExcludeRigidBody: this.rawRigidBody,
|
423
|
+
});
|
424
|
+
|
425
|
+
if (raycastHit?.hitBlock) {
|
426
|
+
const { hitBlock } = raycastHit;
|
427
|
+
const placementCoordinate = hitBlock.getNeighborGlobalCoordinateFromHitPoint(raycastHit.hitPoint);
|
428
|
+
|
429
|
+
world.chunkLattice.setBlock(placementCoordinate, BUILD_BLOCK_ID);
|
430
|
+
|
431
|
+
this._materials -= BLOCK_MATERIAL_COST;
|
432
|
+
this._updatePlayerUIMaterials();
|
433
|
+
}
|
434
|
+
}
|
435
|
+
|
436
|
+
private _handleReload(): void {
|
437
|
+
const activeItem = this._inventory[this._inventoryActiveSlotIndex];
|
438
|
+
if (activeItem instanceof GunEntity) {
|
439
|
+
activeItem.reload();
|
440
|
+
}
|
441
|
+
}
|
442
|
+
|
443
|
+
private _handleZoomScope(): void {
|
444
|
+
const activeItem = this._inventory[this._inventoryActiveSlotIndex];
|
445
|
+
if (activeItem instanceof GunEntity) {
|
446
|
+
activeItem.zoomScope();
|
447
|
+
}
|
448
|
+
}
|
449
|
+
|
450
|
+
private _handleInventoryHotkeys(input: any): void {
|
451
|
+
if (input.f) {
|
452
|
+
this.setActiveInventorySlotIndex(0);
|
453
|
+
input.f = false;
|
454
|
+
}
|
455
|
+
|
456
|
+
for (let i = 1; i <= TOTAL_INVENTORY_SLOTS; i++) {
|
457
|
+
const key = i.toString();
|
458
|
+
if (input[key]) {
|
459
|
+
this.setActiveInventorySlotIndex(i);
|
460
|
+
input[key] = false;
|
461
|
+
}
|
462
|
+
}
|
463
|
+
}
|
464
|
+
|
465
|
+
private _handleInteract(): void {
|
466
|
+
if (!this.world) return;
|
467
|
+
|
468
|
+
const origin = {
|
469
|
+
x: this.position.x,
|
470
|
+
y: this.position.y + this.player.camera.offset.y,
|
471
|
+
z: this.position.z,
|
472
|
+
};
|
473
|
+
|
474
|
+
const raycastHit = this.world.simulation.raycast(
|
475
|
+
origin,
|
476
|
+
this.player.camera.facingDirection,
|
477
|
+
INTERACT_RANGE,
|
478
|
+
{
|
479
|
+
filterExcludeRigidBody: this.rawRigidBody,
|
480
|
+
filterFlags: 8, // Rapier exclude sensors,
|
481
|
+
}
|
482
|
+
);
|
483
|
+
|
484
|
+
const hitEntity = raycastHit?.hitEntity;
|
485
|
+
|
486
|
+
if (hitEntity instanceof ChestEntity) {
|
487
|
+
hitEntity.open();
|
488
|
+
}
|
489
|
+
|
490
|
+
if (hitEntity instanceof ItemEntity) {
|
491
|
+
if (this._findInventorySlot() === 0) {
|
492
|
+
this.world?.chatManager?.sendPlayerMessage(this.player, 'You cannot replace your pickaxe! Switch to a different item first to pick up this item.');
|
493
|
+
return;
|
494
|
+
}
|
495
|
+
|
496
|
+
hitEntity.pickup(this);
|
497
|
+
}
|
498
|
+
}
|
499
|
+
|
500
|
+
private _findInventorySlot(): number {
|
501
|
+
// Try active slot first if empty
|
502
|
+
if (!this._inventory[this._inventoryActiveSlotIndex]) {
|
503
|
+
return this._inventoryActiveSlotIndex;
|
504
|
+
}
|
505
|
+
|
506
|
+
// Find first empty slot or use active slot if none found
|
507
|
+
const emptySlot = this._inventory.findIndex(slot => !slot);
|
508
|
+
|
509
|
+
return emptySlot !== -1 ? emptySlot : this._inventoryActiveSlotIndex;
|
510
|
+
}
|
511
|
+
|
512
|
+
private _updatePlayerUIInventory(): void {
|
513
|
+
this.player.ui.sendData({
|
514
|
+
type: 'inventory',
|
515
|
+
inventory: this._inventory.map(item => {
|
516
|
+
if (!item) return;
|
517
|
+
|
518
|
+
return {
|
519
|
+
name: item.name,
|
520
|
+
iconImageUri: item.iconImageUri,
|
521
|
+
quantity: item.getQuantity(),
|
522
|
+
} as InventoryItem;
|
523
|
+
})
|
524
|
+
});
|
525
|
+
}
|
526
|
+
|
527
|
+
private _updatePlayerUIInventoryActiveSlot(): void {
|
528
|
+
this.player.ui.sendData({
|
529
|
+
type: 'inventory-active-slot',
|
530
|
+
index: this._inventoryActiveSlotIndex,
|
531
|
+
});
|
532
|
+
|
533
|
+
const activeItem = this._inventory[this._inventoryActiveSlotIndex];
|
534
|
+
if (activeItem instanceof GunEntity) {
|
535
|
+
activeItem.updateAmmoIndicatorUI();
|
536
|
+
} else {
|
537
|
+
this.player.ui.sendData({
|
538
|
+
type: 'ammo-indicator',
|
539
|
+
show: false,
|
540
|
+
});
|
541
|
+
}
|
542
|
+
}
|
543
|
+
|
544
|
+
private _updatePlayerUIHealth(): void {
|
545
|
+
this.player.ui.sendData({
|
546
|
+
type: 'health',
|
547
|
+
health: this._health,
|
548
|
+
maxHealth: this._maxHealth
|
549
|
+
});
|
550
|
+
}
|
551
|
+
|
552
|
+
private _updatePlayerUIMaterials(): void {
|
553
|
+
this.player.ui.sendData({
|
554
|
+
type: 'materials',
|
555
|
+
materials: this._materials,
|
556
|
+
});
|
557
|
+
}
|
558
|
+
|
559
|
+
private _updatePlayerUIShield(): void {
|
560
|
+
this.player.ui.sendData({
|
561
|
+
type: 'shield',
|
562
|
+
shield: this._shield,
|
563
|
+
maxShield: this._maxShield,
|
564
|
+
});
|
565
|
+
}
|
566
|
+
|
567
|
+
private _playDamageAudio(): void {
|
568
|
+
this._damageAudio.setDetune(-200 + Math.random() * 800);
|
569
|
+
this._damageAudio.play(this.world!, true);
|
570
|
+
}
|
571
|
+
|
572
|
+
private _autoHealTicker(): void {
|
573
|
+
setTimeout(() => {
|
574
|
+
if (!this.isSpawned) return;
|
575
|
+
|
576
|
+
if (this.health < this._maxHealth && !this._dead) {
|
577
|
+
this.health += 1;
|
578
|
+
}
|
579
|
+
|
580
|
+
this._autoHealTicker();
|
581
|
+
}, 2000);
|
582
|
+
}
|
583
|
+
|
584
|
+
private _outOfWorldTicker(): void {
|
585
|
+
setTimeout(() => {
|
586
|
+
if (!this.isSpawned) return;
|
587
|
+
|
588
|
+
if (this.position.y < -100 && !this._dead) {
|
589
|
+
this.takeDamage(MAX_HEALTH + MAX_SHIELD, { x: 0, y: 0, z: -1 });
|
590
|
+
}
|
591
|
+
|
592
|
+
this._outOfWorldTicker();
|
593
|
+
}, 3000);
|
594
|
+
}
|
595
|
+
}
|