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.
Files changed (147) 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/potion-consume.mp3 +0 -0
  14. package/examples/hygrounds/assets/audio/sfx/rifle-reload.mp3 +0 -0
  15. package/examples/hygrounds/assets/audio/sfx/rifle-shoot.mp3 +0 -0
  16. package/examples/hygrounds/assets/audio/sfx/rocket-launcher-explosion.mp3 +0 -0
  17. package/examples/hygrounds/assets/audio/sfx/rocket-launcher-reload.mp3 +0 -0
  18. package/examples/hygrounds/assets/audio/sfx/rocket-launcher-shoot.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/auto-sniper.png +0 -0
  28. package/examples/hygrounds/assets/icons/block.png +0 -0
  29. package/examples/hygrounds/assets/icons/bolt-action-sniper.png +0 -0
  30. package/examples/hygrounds/assets/icons/crown-bronze.png +0 -0
  31. package/examples/hygrounds/assets/icons/crown-gold.png +0 -0
  32. package/examples/hygrounds/assets/icons/crown-silver.png +0 -0
  33. package/examples/hygrounds/assets/icons/gravity-potion.png +0 -0
  34. package/examples/hygrounds/assets/icons/heart.png +0 -0
  35. package/examples/hygrounds/assets/icons/light-machine-gun.png +0 -0
  36. package/examples/hygrounds/assets/icons/medpack.png +0 -0
  37. package/examples/hygrounds/assets/icons/mining-drill.png +0 -0
  38. package/examples/hygrounds/assets/icons/pickaxe.png +0 -0
  39. package/examples/hygrounds/assets/icons/pistol.png +0 -0
  40. package/examples/hygrounds/assets/icons/revolver.png +0 -0
  41. package/examples/hygrounds/assets/icons/rocket-launcher.png +0 -0
  42. package/examples/hygrounds/assets/icons/shield-potion.png +0 -0
  43. package/examples/hygrounds/assets/icons/shield.png +0 -0
  44. package/examples/hygrounds/assets/icons/shotgun.png +0 -0
  45. package/examples/hygrounds/assets/icons/submachine-gun.png +0 -0
  46. package/examples/hygrounds/assets/map.json +31796 -0
  47. package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion-named-nodes.glb +0 -0
  48. package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion.glb +0 -0
  49. package/examples/hygrounds/assets/models/environment/.optimized/explosion/explosion.glb.md5 +1 -0
  50. package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2-named-nodes.glb +0 -0
  51. package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2.glb +0 -0
  52. package/examples/hygrounds/assets/models/environment/.optimized/explosion-2/explosion-2.glb.md5 +1 -0
  53. package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3-named-nodes.glb +0 -0
  54. package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3.glb +0 -0
  55. package/examples/hygrounds/assets/models/environment/.optimized/explosion-3/explosion-3.glb.md5 +1 -0
  56. package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter-named-nodes.glb +0 -0
  57. package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter.glb +0 -0
  58. package/examples/hygrounds/assets/models/environment/.optimized/helicopter/helicopter.glb.md5 +1 -0
  59. package/examples/hygrounds/assets/models/environment/chest.gltf +1 -0
  60. package/examples/hygrounds/assets/models/environment/explosion.glb +0 -0
  61. package/examples/hygrounds/assets/models/environment/muzzle-flash.gltf +1 -0
  62. package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper-named-nodes.glb +0 -0
  63. package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb +0 -0
  64. package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb.md5 +1 -0
  65. package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion-named-nodes.glb +0 -0
  66. package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb +0 -0
  67. package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb.md5 +1 -0
  68. package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit-named-nodes.glb +0 -0
  69. package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit.glb +0 -0
  70. package/examples/hygrounds/assets/models/items/.optimized/medkit/medkit.glb.md5 +1 -0
  71. package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack-named-nodes.glb +0 -0
  72. package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack.glb +0 -0
  73. package/examples/hygrounds/assets/models/items/.optimized/medpack/medpack.glb.md5 +1 -0
  74. package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill-named-nodes.glb +0 -0
  75. package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill.glb +0 -0
  76. package/examples/hygrounds/assets/models/items/.optimized/mining-drill/mining-drill.glb.md5 +1 -0
  77. package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver-named-nodes.glb +0 -0
  78. package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.glb +0 -0
  79. package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.glb.md5 +1 -0
  80. package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile-named-nodes.glb +0 -0
  81. package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile.glb +0 -0
  82. package/examples/hygrounds/assets/models/items/.optimized/rocket-missile/rocket-missile.glb.md5 +1 -0
  83. package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion-named-nodes.glb +0 -0
  84. package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion.glb +0 -0
  85. package/examples/hygrounds/assets/models/items/.optimized/shield-potion/shield-potion.glb.md5 +1 -0
  86. package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2-named-nodes.glb +0 -0
  87. package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2.glb +0 -0
  88. package/examples/hygrounds/assets/models/items/.optimized/shield-potion-2/shield-potion-2.glb.md5 +1 -0
  89. package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun-named-nodes.glb +0 -0
  90. package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.glb +0 -0
  91. package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.glb.md5 +1 -0
  92. package/examples/hygrounds/assets/models/items/ak-47.glb +0 -0
  93. package/examples/hygrounds/assets/models/items/auto-shotgun.glb +0 -0
  94. package/examples/hygrounds/assets/models/items/auto-sniper.glb +0 -0
  95. package/examples/hygrounds/assets/models/items/bolt-action-sniper.glb +0 -0
  96. package/examples/hygrounds/assets/models/items/gravity-potion.glb +0 -0
  97. package/examples/hygrounds/assets/models/items/light-machine-gun.glb +0 -0
  98. package/examples/hygrounds/assets/models/items/medpack.glb +0 -0
  99. package/examples/hygrounds/assets/models/items/mining-drill.glb +0 -0
  100. package/examples/hygrounds/assets/models/items/pickaxe.gltf +1 -0
  101. package/examples/hygrounds/assets/models/items/pistol.glb +0 -0
  102. package/examples/hygrounds/assets/models/items/revolver.glb +0 -0
  103. package/examples/hygrounds/assets/models/items/rocket-launcher.glb +0 -0
  104. package/examples/hygrounds/assets/models/items/rocket-missile.glb +0 -0
  105. package/examples/hygrounds/assets/models/items/shield-potion.glb +0 -0
  106. package/examples/hygrounds/assets/models/items/shotgun.glb +0 -0
  107. package/examples/hygrounds/assets/models/items/submachine-gun.glb +0 -0
  108. package/examples/hygrounds/assets/models/players/soldier-player.gltf +1 -0
  109. package/examples/hygrounds/assets/ui/images/scope.png +0 -0
  110. package/examples/hygrounds/assets/ui/index.html +1122 -0
  111. package/examples/hygrounds/bun.lock +503 -0
  112. package/examples/hygrounds/classes/ChestEntity.ts +133 -0
  113. package/examples/hygrounds/classes/GameManager.ts +422 -0
  114. package/examples/hygrounds/classes/GamePlayerEntity.ts +595 -0
  115. package/examples/hygrounds/classes/GunEntity.ts +259 -0
  116. package/examples/hygrounds/classes/ItemEntity.ts +225 -0
  117. package/examples/hygrounds/classes/ItemFactory.ts +61 -0
  118. package/examples/hygrounds/classes/MeleeWeaponEntity.ts +138 -0
  119. package/examples/hygrounds/classes/TerrainDamageManager.ts +56 -0
  120. package/examples/hygrounds/classes/items/GravityPotionEntity.ts +48 -0
  121. package/examples/hygrounds/classes/items/MedPackEntity.ts +43 -0
  122. package/examples/hygrounds/classes/items/ShieldPotionEntity.ts +43 -0
  123. package/examples/hygrounds/classes/weapons/AK47Entity.ts +43 -0
  124. package/examples/hygrounds/classes/weapons/AutoShotgunEntity.ts +80 -0
  125. package/examples/hygrounds/classes/weapons/AutoSniperEntity.ts +43 -0
  126. package/examples/hygrounds/classes/weapons/BoltActionSniperEntity.ts +46 -0
  127. package/examples/hygrounds/classes/weapons/LightMachineGunEntity.ts +43 -0
  128. package/examples/hygrounds/classes/weapons/MiningDrillEntity.ts +38 -0
  129. package/examples/hygrounds/classes/weapons/PickaxeEntity.ts +38 -0
  130. package/examples/hygrounds/classes/weapons/PistolEntity.ts +46 -0
  131. package/examples/hygrounds/classes/weapons/RevolverEntity.ts +46 -0
  132. package/examples/hygrounds/classes/weapons/RocketLauncherEntity.ts +195 -0
  133. package/examples/hygrounds/classes/weapons/ShotgunEntity.ts +84 -0
  134. package/examples/hygrounds/classes/weapons/SubmachineGunEntity.ts +43 -0
  135. package/examples/hygrounds/gameConfig.ts +430 -0
  136. package/examples/hygrounds/index.ts +41 -0
  137. package/examples/hygrounds/package.json +16 -0
  138. package/examples/player-persistence/README.md +3 -0
  139. package/examples/player-persistence/assets/map.json +2623 -0
  140. package/examples/player-persistence/dev/persistence/player-player-1.json +6 -0
  141. package/examples/player-persistence/dev/persistence/test.json +3 -0
  142. package/examples/player-persistence/index.ts +126 -0
  143. package/examples/player-persistence/package.json +16 -0
  144. package/package.json +1 -1
  145. package/server.api.json +21 -0
  146. package/server.d.ts +2 -1
  147. package/server.js +113 -113
@@ -0,0 +1,259 @@
1
+ import {
2
+ Audio,
3
+ Entity,
4
+ Vector3Like,
5
+ Quaternion,
6
+ QuaternionLike,
7
+ World,
8
+ } from 'hytopia';
9
+
10
+ import GamePlayerEntity from './GamePlayerEntity';
11
+ import ItemEntity from './ItemEntity';
12
+ import TerrainDamageManager from './TerrainDamageManager';
13
+ import type { ItemEntityOptions } from './ItemEntity';
14
+
15
+ export type GunHand = 'left' | 'right' | 'both';
16
+
17
+ export interface GunEntityOptions extends ItemEntityOptions {
18
+ ammo: number; // The amount of ammo in the clip.
19
+ damage: number; // The damage of the gun.
20
+ fireRate: number; // Bullets shot per second.
21
+ maxAmmo: number; // The amount of ammo the clip can hold.
22
+ totalAmmo: number; // The amount of ammo remaining for this gun.
23
+ range: number; // The max range bullets travel for raycast hits
24
+ reloadAudioUri: string; // The audio played when reloading
25
+ reloadTimeMs: number; // Seconds to reload.
26
+ shootAudioUri: string; // The audio played when shooting
27
+ scopeZoom?: number; // The zoom level when scoped in.
28
+ }
29
+
30
+ export default abstract class GunEntity extends ItemEntity {
31
+ protected readonly damage: number;
32
+ protected readonly fireRate: number;
33
+ protected readonly maxAmmo: number;
34
+ protected readonly range: number;
35
+ protected readonly reloadTimeMs: number;
36
+ protected readonly scopeZoom: number = 1;
37
+
38
+ protected ammo: number;
39
+ protected totalAmmo: number;
40
+ private _lastFireTime: number = 0;
41
+ private _muzzleFlashChildEntity: Entity | undefined;
42
+ private _reloadAudio: Audio;
43
+ private _reloading: boolean = false;
44
+ private _shootAudio: Audio;
45
+
46
+ public constructor(options: GunEntityOptions) {
47
+ if (!options.modelUri) {
48
+ throw new Error('GunEntity requires modelUri');
49
+ }
50
+
51
+ super(options);
52
+
53
+ this.ammo = options.ammo;
54
+ this.damage = options.damage;
55
+ this.fireRate = options.fireRate;
56
+ this.maxAmmo = options.maxAmmo;
57
+ this.totalAmmo = options.totalAmmo;
58
+ this.range = options.range;
59
+ this.reloadTimeMs = options.reloadTimeMs;
60
+ this.scopeZoom = options.scopeZoom ?? 1;
61
+
62
+ this._reloadAudio = new Audio({
63
+ attachedToEntity: this,
64
+ uri: options.reloadAudioUri,
65
+ });
66
+
67
+ this._shootAudio = new Audio({
68
+ attachedToEntity: this,
69
+ uri: options.shootAudioUri,
70
+ volume: 0.3,
71
+ referenceDistance: 8,
72
+ });
73
+ }
74
+
75
+ public override equip(): void {
76
+ if (!this.world) return;
77
+
78
+ super.equip();
79
+
80
+ this.setPosition({ x: 0, y: 0, z: -0.2 });
81
+ this.setRotation(Quaternion.fromEuler(-90, 0, 0));
82
+ this._reloadAudio.play(this.world, true);
83
+
84
+ }
85
+
86
+ public override unequip(): void {
87
+ super.unequip();
88
+
89
+ // reset any scope zoom
90
+ const player = this.parent as GamePlayerEntity;
91
+ this.zoomScope(true);
92
+ }
93
+
94
+ public override spawn(world: World, position: Vector3Like, rotation?: QuaternionLike): void {
95
+ super.spawn(world, position, rotation);
96
+
97
+ this._createMuzzleFlash();
98
+ }
99
+
100
+ public override getQuantity(): number {
101
+ return this.totalAmmo;
102
+ }
103
+
104
+ public reload(): void {
105
+ if (!this.parent?.world || this._reloading || !this.totalAmmo) return;
106
+ this._startReload();
107
+ this._reloadAudio.play(this.parent.world, true);
108
+
109
+ setTimeout(() => this._finishReload(), this.reloadTimeMs);
110
+ }
111
+
112
+ public abstract getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike };
113
+
114
+ public shoot(): void {
115
+ if (!this.parent?.world) return;
116
+
117
+ const player = this.parent as GamePlayerEntity;
118
+ const { origin, direction } = this.getShootOriginDirection();
119
+
120
+ this._performShootEffects(player);
121
+ this.shootRaycast(origin, direction, this.range);
122
+ this._updateUI(player);
123
+ }
124
+
125
+ public zoomScope(reset: boolean = false): void {
126
+ if (!this.parent?.world || this.scopeZoom === 1) return;
127
+
128
+ const player = this.parent as GamePlayerEntity;
129
+ const zoom = player.player.camera.zoom === 1 && !reset ? this.scopeZoom : 1;
130
+
131
+ player.player.camera.setZoom(zoom);
132
+ player.player.ui.sendData({
133
+ type: 'scope-zoom',
134
+ zoom,
135
+ });
136
+ }
137
+
138
+ protected getShootOriginDirection(): { origin: Vector3Like, direction: Vector3Like } {
139
+ const player = this.parent as GamePlayerEntity;
140
+ const { x, y, z } = player.position;
141
+ const cameraYOffset = player.player.camera.offset.y;
142
+ const direction = player.player.camera.facingDirection;
143
+
144
+ return {
145
+ origin: { x, y: y + cameraYOffset, z },
146
+ direction
147
+ };
148
+ }
149
+
150
+ protected processShoot(): boolean {
151
+ if (this.totalAmmo <= 0 || this._reloading) return false;
152
+
153
+ const now = performance.now();
154
+ if (this._lastFireTime && now - this._lastFireTime < 1000 / this.fireRate) return false;
155
+
156
+ if (this.ammo <= 0) {
157
+ this.reload();
158
+ return false;
159
+ }
160
+
161
+ this.ammo--;
162
+ this.totalAmmo--;
163
+ this._lastFireTime = now;
164
+
165
+ return true;
166
+ }
167
+
168
+ protected shootRaycast(origin: Vector3Like, direction: Vector3Like, length: number): void {
169
+ if (!this.parent?.world) return;
170
+
171
+ const { world } = this.parent;
172
+ const raycastHit = this.parent.world.simulation.raycast(origin, direction, length, {
173
+ filterExcludeRigidBody: this.parent.rawRigidBody,
174
+ });
175
+
176
+ if (raycastHit?.hitBlock) {
177
+ TerrainDamageManager.instance.damageBlock(world, raycastHit.hitBlock, this.damage);
178
+ }
179
+
180
+ if (raycastHit?.hitEntity) {
181
+ this._handleHitEntity(raycastHit.hitEntity, direction);
182
+ }
183
+ }
184
+
185
+ private _createMuzzleFlash(): void {
186
+ if (!this.isSpawned || !this.world) return;
187
+
188
+ this._muzzleFlashChildEntity = new Entity({
189
+ parent: this,
190
+ modelUri: 'models/environment/muzzle-flash.gltf',
191
+ modelScale: 0.5,
192
+ opacity: 0,
193
+ });
194
+
195
+ const { position, rotation } = this.getMuzzleFlashPositionRotation();
196
+ this._muzzleFlashChildEntity.spawn(this.world, position, rotation);
197
+ }
198
+
199
+ private _startReload(): void {
200
+ this.ammo = 0;
201
+ this._reloading = true;
202
+ this.updateAmmoIndicatorUI(true);
203
+ }
204
+
205
+ private _finishReload(): void {
206
+ this._reloading = false;
207
+
208
+ // prevent reloads if they swapped active item mid reload.
209
+ if (!this.parent || !(this.parent as GamePlayerEntity).isItemActiveInInventory(this)) return;
210
+
211
+ this.ammo = Math.min(this.maxAmmo, this.totalAmmo);
212
+ this.updateAmmoIndicatorUI();
213
+ }
214
+
215
+ private _performShootEffects(player: GamePlayerEntity): void {
216
+ player.startModelOneshotAnimations([ this.mlAnimation ]);
217
+ this._showMuzzleFlash();
218
+ this._shootAudio.play(this.parent!.world!, true);
219
+ }
220
+
221
+ private _showMuzzleFlash(): void {
222
+ if (!this._muzzleFlashChildEntity) return;
223
+
224
+ this._muzzleFlashChildEntity.setOpacity(1);
225
+ setTimeout(() => {
226
+ if (this.isSpawned && this._muzzleFlashChildEntity?.isSpawned) {
227
+ this._muzzleFlashChildEntity.setOpacity(0);
228
+ }
229
+ }, 35);
230
+ }
231
+
232
+ private _updateUI(player: GamePlayerEntity): void {
233
+ player.updateItemInventoryQuantity(this);
234
+ this.updateAmmoIndicatorUI();
235
+ }
236
+
237
+ protected _handleHitEntity(hitEntity: Entity, hitDirection: Vector3Like): void {
238
+ if (!(hitEntity instanceof GamePlayerEntity) || hitEntity.isDead) return;
239
+
240
+ const attacker = this.parent as GamePlayerEntity;
241
+
242
+ attacker.dealtDamage(this.damage);
243
+ hitEntity.takeDamage(this.damage, hitDirection, attacker);
244
+ }
245
+
246
+ public updateAmmoIndicatorUI(reloading: boolean = false): void {
247
+ const player = this.parent as GamePlayerEntity;
248
+
249
+ player.player.ui.sendData(reloading ? {
250
+ type: 'ammo-indicator',
251
+ reloading: true,
252
+ } : {
253
+ type: 'ammo-indicator',
254
+ ammo: this.ammo,
255
+ totalAmmo: this.totalAmmo,
256
+ show: true,
257
+ });
258
+ }
259
+ }
@@ -0,0 +1,225 @@
1
+ import {
2
+ Audio,
3
+ Collider,
4
+ CollisionGroup,
5
+ Entity,
6
+ EntityOptions,
7
+ PlayerEntityController,
8
+ QuaternionLike,
9
+ SceneUI,
10
+ Vector3Like,
11
+ World,
12
+ } from 'hytopia';
13
+
14
+ import GamePlayerEntity from './GamePlayerEntity';
15
+ import { ITEM_DESPAWN_TIME_MS } from '../gameConfig';
16
+
17
+ const INVENTORIED_POSITION = { x: 0, y: -300, z: 0 };
18
+
19
+ export type HeldHand = 'left' | 'right' | 'both';
20
+
21
+ export interface ItemEntityOptions extends EntityOptions {
22
+ heldHand: HeldHand; // The hand the item is held in.
23
+ iconImageUri: string; // The image uri of the weapon icon.
24
+ idleAnimation: string; // The animation played when the player holding it is idle.
25
+ mlAnimation: string; // The animation played when the player holding it clicks the left mouse button.
26
+ quantity?: number;
27
+ consumable?: boolean;
28
+ consumeAudioUri?: string;
29
+ consumeTimeMs?: number;
30
+ }
31
+
32
+ export default class ItemEntity extends Entity {
33
+ public readonly consumable: boolean;
34
+ public quantity: number;
35
+ public readonly heldHand: HeldHand;
36
+ public readonly iconImageUri: string;
37
+ protected readonly consumeAudioUri: string | undefined;
38
+ protected readonly consumeTimeMs: number;
39
+ protected readonly idleAnimation: string;
40
+ protected readonly mlAnimation: string;
41
+ private _despawnTimer: NodeJS.Timeout | undefined;
42
+ private readonly _labelSceneUI: SceneUI;
43
+
44
+ public constructor(options: ItemEntityOptions) {
45
+ if (!options.modelUri && !options.blockHalfExtents) {
46
+ throw new Error('ItemEntity requires either modelUri or blockHalfExtents');
47
+ }
48
+
49
+ const colliderOptions = options.modelUri
50
+ ? Collider.optionsFromModelUri(options.modelUri)
51
+ : Collider.optionsFromBlockHalfExtents(options.blockHalfExtents!);
52
+
53
+
54
+ super({
55
+ ...options,
56
+ parentNodeName: ItemEntity._getHandAnchorNode(options.heldHand),
57
+ rigidBodyOptions: ItemEntity._createRigidBodyOptions(colliderOptions, options.modelScale ?? 1),
58
+ });
59
+
60
+ this.consumable = options.consumable ?? false;
61
+ this.consumeAudioUri = options.consumeAudioUri;
62
+ this.consumeTimeMs = options.consumeTimeMs ?? 0;
63
+ this.quantity = options.quantity ?? -1;
64
+ this.heldHand = options.heldHand;
65
+ this.iconImageUri = options.iconImageUri;
66
+ this.idleAnimation = options.idleAnimation;
67
+ this.mlAnimation = options.mlAnimation;
68
+
69
+ this._labelSceneUI = this._createLabelUI();
70
+
71
+ if (options.parent) {
72
+ this.setParentAnimations();
73
+ }
74
+ }
75
+
76
+ public consume(): void {
77
+ if (!this.consumable || !this.consumeAudioUri || this.quantity <= 0 || !this.parent || !this.world) return;
78
+
79
+ if (!(this.parent instanceof GamePlayerEntity)) {
80
+ return;
81
+ }
82
+
83
+ this.parent.player.input.ml = false;
84
+
85
+ this.quantity--;
86
+
87
+ this.parent.updateItemInventoryQuantity(this);
88
+
89
+ if (!this.quantity) {
90
+ this.parent.dropActiveInventoryItem();
91
+ setTimeout(() => {
92
+ this.despawn();
93
+ }, 0);
94
+ }
95
+
96
+ (new Audio({
97
+ attachedToEntity: this.parent,
98
+ uri: this.consumeAudioUri,
99
+ volume: 0.5,
100
+ referenceDistance: 5,
101
+ })).play(this.world);
102
+
103
+ }
104
+
105
+ public drop(fromPosition: Vector3Like, direction: Vector3Like): void {
106
+ if (!this.world) return;
107
+
108
+ this.startDespawnTimer();
109
+
110
+ this.setParent(undefined, undefined, fromPosition);
111
+
112
+ // Apply impulse in next tick to avoid physics issues
113
+ setTimeout(() => {
114
+ this.applyImpulse({
115
+ x: direction.x * this.mass * 7,
116
+ y: direction.y * this.mass * 15,
117
+ z: direction.z * this.mass * 7,
118
+ });
119
+ }, 0);
120
+
121
+ this._updateVisualEffects();
122
+ }
123
+
124
+ public equip() {
125
+ this.setPosition({ x: 0, y: 0, z: 0 });
126
+ this.setParentAnimations();
127
+ }
128
+
129
+ public unequip() {
130
+ this.setPosition(INVENTORIED_POSITION);
131
+
132
+ if (this.parent instanceof GamePlayerEntity) {
133
+ this.parent.resetAnimations();
134
+ }
135
+ }
136
+
137
+ public getQuantity(): number {
138
+ return this.quantity;
139
+ }
140
+
141
+ public pickup(player: GamePlayerEntity): void {
142
+ if (!player.world) return;
143
+
144
+ this.stopDespawnTimer();
145
+
146
+ this.setParent(player, ItemEntity._getHandAnchorNode(this.heldHand), INVENTORIED_POSITION);
147
+ this._updateVisualEffects();
148
+
149
+ player.addItemToInventory(this);
150
+ }
151
+
152
+ public setParentAnimations(): void {
153
+ if (!this.parent || !this.parent.world || !(this.parent instanceof GamePlayerEntity)) return;
154
+
155
+ const controller = this.parent.controller as PlayerEntityController;
156
+ controller.idleLoopedAnimations = [ this.idleAnimation, 'idle_lower' ];
157
+ controller.walkLoopedAnimations = [ this.idleAnimation, 'walk_lower' ];
158
+ controller.runLoopedAnimations = [ this.idleAnimation, 'run_lower' ];
159
+ }
160
+
161
+ public override spawn(world: World, position: Vector3Like, rotation?: QuaternionLike): void {
162
+ super.spawn(world, position, rotation);
163
+ this._updateVisualEffects();
164
+ }
165
+
166
+ public startDespawnTimer(): void {
167
+ if (this._despawnTimer) return;
168
+
169
+ this._despawnTimer = setTimeout(() => {
170
+ this.despawn();
171
+ }, ITEM_DESPAWN_TIME_MS);
172
+ }
173
+
174
+ public stopDespawnTimer(): void {
175
+ if (!this._despawnTimer) return;
176
+
177
+ clearTimeout(this._despawnTimer);
178
+ this._despawnTimer = undefined;
179
+ }
180
+
181
+ private _createLabelUI(): SceneUI {
182
+ return new SceneUI({
183
+ attachedToEntity: this,
184
+ templateId: 'item-label',
185
+ state: { name: this.name, quantity: this.getQuantity() },
186
+ viewDistance: 8,
187
+ offset: { x: 0, y: 1, z: 0 },
188
+ });
189
+ }
190
+
191
+ private _updateVisualEffects(): void {
192
+ if (!this.world) return;
193
+
194
+ if (!this.parent) {
195
+ this._labelSceneUI.setState({ quantity: this.getQuantity() });
196
+ this._labelSceneUI.load(this.world);
197
+ } else {
198
+ this._labelSceneUI.unload();
199
+ }
200
+ }
201
+
202
+ private static _getHandAnchorNode(heldHand: HeldHand): string {
203
+ return heldHand === 'left' ? 'hand_left_anchor' : 'hand_right_anchor';
204
+ }
205
+
206
+ private static _createRigidBodyOptions(colliderOptions: any, modelScale: number) {
207
+ return {
208
+ enabledRotations: { x: false, y: true, z: false },
209
+ colliders: [{
210
+ ...colliderOptions,
211
+ collisionGroups: {
212
+ belongsTo: [ CollisionGroup.ENTITY ],
213
+ collidesWith: [ CollisionGroup.BLOCK ],
214
+ },
215
+ halfExtents: colliderOptions.halfExtents ? {
216
+ x: colliderOptions.halfExtents.x * modelScale,
217
+ y: colliderOptions.halfExtents.y * modelScale * 1.5,
218
+ z: colliderOptions.halfExtents.z * modelScale,
219
+ } : undefined,
220
+ halfHeight: colliderOptions.halfHeight ? colliderOptions.halfHeight * modelScale * 1.5 : undefined,
221
+ radius: colliderOptions.radius ? colliderOptions.radius * modelScale * 1.5 : undefined,
222
+ }]
223
+ };
224
+ }
225
+ }
@@ -0,0 +1,61 @@
1
+
2
+ export default class ItemFactory {
3
+ public static async createItem(itemId: string) {
4
+ let itemModule
5
+
6
+ // We do imports here to avoid circular dependencies
7
+ // We should really just refactor import patterns,
8
+ // but this is a quick fix for now.
9
+ switch(itemId) {
10
+ case 'ak47':
11
+ itemModule = await import('./weapons/AK47Entity');
12
+ break;
13
+ case 'auto-shotgun':
14
+ itemModule = await import('./weapons/AutoShotgunEntity');
15
+ break;
16
+ case 'auto-sniper':
17
+ itemModule = await import('./weapons/AutoSniperEntity');
18
+ break;
19
+ case 'bolt-action-sniper':
20
+ itemModule = await import('./weapons/BoltActionSniperEntity');
21
+ break;
22
+ case 'gravity-potion':
23
+ itemModule = await import('./items/GravityPotionEntity');
24
+ break;
25
+ case 'light-machine-gun':
26
+ itemModule = await import('./weapons/LightMachineGunEntity');
27
+ break;
28
+ case 'medpack':
29
+ itemModule = await import('./items/MedPackEntity');
30
+ break;
31
+ case 'mining-drill':
32
+ itemModule = await import('./weapons/MiningDrillEntity');
33
+ break;
34
+ case 'pistol':
35
+ itemModule = await import('./weapons/PistolEntity');
36
+ break;
37
+ case 'revolver':
38
+ itemModule = await import('./weapons/RevolverEntity');
39
+ break;
40
+ case 'rocket-launcher':
41
+ itemModule = await import('./weapons/RocketLauncherEntity');
42
+ break;
43
+ case 'submachine-gun':
44
+ itemModule = await import('./weapons/SubmachineGunEntity');
45
+ break;
46
+ case 'shotgun':
47
+ itemModule = await import('./weapons/ShotgunEntity');
48
+ break;
49
+ case 'shield-potion':
50
+ itemModule = await import('./items/ShieldPotionEntity');
51
+ break;
52
+ default:
53
+ throw new Error(`Unknown chest item id: ${itemId}`);
54
+ }
55
+
56
+ const itemClass = itemModule.default;
57
+ const item = new itemClass();
58
+
59
+ return item;
60
+ }
61
+ }
@@ -0,0 +1,138 @@
1
+ import {
2
+ Audio,
3
+ Entity,
4
+ Quaternion,
5
+ RaycastHit,
6
+ Vector3Like,
7
+ } from 'hytopia';
8
+
9
+ import GamePlayerEntity from './GamePlayerEntity';
10
+ import ItemEntity from './ItemEntity';
11
+ import TerrainDamageManager from './TerrainDamageManager';
12
+ import type { ItemEntityOptions } from './ItemEntity';
13
+
14
+ export interface MeleeWeaponEntityOptions extends ItemEntityOptions {
15
+ damage: number; // The damage dealt by the weapon
16
+ attackRate: number; // Attacks per second
17
+ range: number; // The range of the melee attack
18
+ attackAudioUri: string; // The audio played when attacking
19
+ hitAudioUri: string; // The audio played when hitting an entity or block
20
+ minesMaterials: boolean; // Whether the weapon mines materials when it hits a block
21
+ }
22
+
23
+ export default abstract class MeleeWeaponEntity extends ItemEntity {
24
+ protected readonly damage: number;
25
+ protected readonly attackRate: number;
26
+ protected readonly range: number;
27
+ protected readonly minesMaterials: boolean;
28
+
29
+ private _lastAttackTime: number = 0;
30
+ private _attackAudio: Audio;
31
+ private _hitAudio: Audio;
32
+
33
+ public constructor(options: MeleeWeaponEntityOptions) {
34
+ super(options);
35
+
36
+ this.damage = options.damage;
37
+ this.attackRate = options.attackRate;
38
+ this.range = options.range;
39
+ this.minesMaterials = options.minesMaterials;
40
+
41
+ this._attackAudio = new Audio({
42
+ attachedToEntity: this,
43
+ uri: options.attackAudioUri,
44
+ volume: 0.3,
45
+ referenceDistance: 3,
46
+ });
47
+
48
+ this._hitAudio = new Audio({
49
+ attachedToEntity: this,
50
+ uri: options.hitAudioUri,
51
+ volume: 0.3,
52
+ referenceDistance: 3,
53
+ });
54
+ }
55
+
56
+ public override equip(): void {
57
+ if (!this.world) return;
58
+
59
+ super.equip();
60
+
61
+ this.setRotation(Quaternion.fromEuler(-90, 0, 0));
62
+ }
63
+
64
+ public attack(): void {
65
+ if (!this.parent?.world) return;
66
+
67
+ const player = this.parent as GamePlayerEntity;
68
+ const { origin, direction } = this.getAttackOriginDirection();
69
+
70
+ this._performAttackEffects(player);
71
+ this.attackRaycast(origin, direction, this.range);
72
+ }
73
+
74
+ protected getAttackOriginDirection(): { origin: Vector3Like, direction: Vector3Like } {
75
+ const player = this.parent as GamePlayerEntity;
76
+ const { x, y, z } = player.position;
77
+ const cameraYOffset = player.player.camera.offset.y;
78
+ const direction = player.player.camera.facingDirection;
79
+
80
+ return {
81
+ origin: { x, y: y + cameraYOffset, z },
82
+ direction
83
+ };
84
+ }
85
+
86
+ protected processAttack(): boolean {
87
+ const now = performance.now();
88
+ if (this._lastAttackTime && now - this._lastAttackTime < 1000 / this.attackRate) return false;
89
+
90
+ this._lastAttackTime = now;
91
+ return true;
92
+ }
93
+
94
+ protected attackRaycast(origin: Vector3Like, direction: Vector3Like, length: number): RaycastHit | null | undefined {
95
+ if (!this.parent?.world) return;
96
+
97
+ const { world } = this.parent;
98
+ const raycastHit = world.simulation.raycast(origin, direction, length, {
99
+ filterExcludeRigidBody: this.parent.rawRigidBody,
100
+ });
101
+
102
+ if (raycastHit?.hitBlock) {
103
+ const brokeBlock = TerrainDamageManager.instance.damageBlock(world, raycastHit.hitBlock, this.damage);
104
+
105
+ if (this.minesMaterials && brokeBlock) {
106
+ const player = this.parent as GamePlayerEntity;
107
+ const blockId = raycastHit.hitBlock.blockType.id;
108
+ const materialCount = TerrainDamageManager.getBreakMaterialCount(blockId);
109
+
110
+ player.addMaterial(materialCount);
111
+ }
112
+ }
113
+
114
+ if (raycastHit?.hitEntity) {
115
+ this._handleHitEntity(raycastHit.hitEntity, direction);
116
+ }
117
+
118
+ if (raycastHit?.hitBlock || raycastHit?.hitEntity) {
119
+ this._hitAudio.play(world, true);
120
+ }
121
+
122
+ return raycastHit;
123
+ }
124
+
125
+ private _performAttackEffects(player: GamePlayerEntity): void {
126
+ player.startModelOneshotAnimations([ this.mlAnimation ]);
127
+ this._attackAudio.play(this.parent!.world!, true);
128
+ }
129
+
130
+ protected _handleHitEntity(hitEntity: Entity, hitDirection: Vector3Like): void {
131
+ if (!(hitEntity instanceof GamePlayerEntity) || hitEntity.isDead) return;
132
+
133
+ const attacker = this.parent as GamePlayerEntity;
134
+
135
+ attacker.dealtDamage(this.damage);
136
+ hitEntity.takeDamage(this.damage, hitDirection, attacker);
137
+ }
138
+ }