hytopia 0.3.6 → 0.3.7

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