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.
- 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/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-potion-consume.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/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/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/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/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/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/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/ak-47.glb +0 -0
- package/examples/hygrounds/assets/models/items/auto-shotgun.glb +0 -0
- package/examples/hygrounds/assets/models/items/bolt-action-sniper.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/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/players/soldier-player.gltf +1 -0
- package/examples/hygrounds/assets/ui/images/scope.png +0 -0
- package/examples/hygrounds/assets/ui/index.html +1072 -0
- package/examples/hygrounds/bun.lock +503 -0
- package/examples/hygrounds/classes/ChestEntity.ts +133 -0
- package/examples/hygrounds/classes/GameManager.ts +384 -0
- package/examples/hygrounds/classes/GamePlayerEntity.ts +564 -0
- package/examples/hygrounds/classes/GunEntity.ts +263 -0
- package/examples/hygrounds/classes/ItemEntity.ts +225 -0
- package/examples/hygrounds/classes/ItemFactory.ts +49 -0
- package/examples/hygrounds/classes/MeleeWeaponEntity.ts +138 -0
- package/examples/hygrounds/classes/TerrainDamageManager.ts +56 -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/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/RocketLauncherEntity.ts +186 -0
- package/examples/hygrounds/classes/weapons/ShotgunEntity.ts +84 -0
- package/examples/hygrounds/gameConfig.ts +398 -0
- package/examples/hygrounds/index.ts +40 -0
- package/examples/hygrounds/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,263 @@
|
|
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: {
|
146
|
+
x: x + (direction.x * 0.5),
|
147
|
+
y: y + (direction.y * 0.5) + cameraYOffset,
|
148
|
+
z: z + (direction.z * 0.5),
|
149
|
+
},
|
150
|
+
direction
|
151
|
+
};
|
152
|
+
}
|
153
|
+
|
154
|
+
protected processShoot(): boolean {
|
155
|
+
if (this.totalAmmo <= 0 || this._reloading) return false;
|
156
|
+
|
157
|
+
const now = performance.now();
|
158
|
+
if (this._lastFireTime && now - this._lastFireTime < 1000 / this.fireRate) return false;
|
159
|
+
|
160
|
+
if (this.ammo <= 0) {
|
161
|
+
this.reload();
|
162
|
+
return false;
|
163
|
+
}
|
164
|
+
|
165
|
+
this.ammo--;
|
166
|
+
this.totalAmmo--;
|
167
|
+
this._lastFireTime = now;
|
168
|
+
|
169
|
+
return true;
|
170
|
+
}
|
171
|
+
|
172
|
+
protected shootRaycast(origin: Vector3Like, direction: Vector3Like, length: number): void {
|
173
|
+
if (!this.parent?.world) return;
|
174
|
+
|
175
|
+
const { world } = this.parent;
|
176
|
+
const raycastHit = this.parent.world.simulation.raycast(origin, direction, length, {
|
177
|
+
filterExcludeRigidBody: this.parent.rawRigidBody,
|
178
|
+
});
|
179
|
+
|
180
|
+
if (raycastHit?.hitBlock) {
|
181
|
+
TerrainDamageManager.instance.damageBlock(world, raycastHit.hitBlock, this.damage);
|
182
|
+
}
|
183
|
+
|
184
|
+
if (raycastHit?.hitEntity) {
|
185
|
+
this._handleHitEntity(raycastHit.hitEntity, direction);
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
private _createMuzzleFlash(): void {
|
190
|
+
if (!this.isSpawned || !this.world) return;
|
191
|
+
|
192
|
+
this._muzzleFlashChildEntity = new Entity({
|
193
|
+
parent: this,
|
194
|
+
modelUri: 'models/environment/muzzle-flash.gltf',
|
195
|
+
modelScale: 0.5,
|
196
|
+
opacity: 0,
|
197
|
+
});
|
198
|
+
|
199
|
+
const { position, rotation } = this.getMuzzleFlashPositionRotation();
|
200
|
+
this._muzzleFlashChildEntity.spawn(this.world, position, rotation);
|
201
|
+
}
|
202
|
+
|
203
|
+
private _startReload(): void {
|
204
|
+
this.ammo = 0;
|
205
|
+
this._reloading = true;
|
206
|
+
this.updateAmmoIndicatorUI(true);
|
207
|
+
}
|
208
|
+
|
209
|
+
private _finishReload(): void {
|
210
|
+
this._reloading = false;
|
211
|
+
|
212
|
+
// prevent reloads if they swapped active item mid reload.
|
213
|
+
if (!this.parent || !(this.parent as GamePlayerEntity).isItemActiveInInventory(this)) return;
|
214
|
+
|
215
|
+
this.ammo = Math.min(this.maxAmmo, this.totalAmmo);
|
216
|
+
this.updateAmmoIndicatorUI();
|
217
|
+
}
|
218
|
+
|
219
|
+
private _performShootEffects(player: GamePlayerEntity): void {
|
220
|
+
player.startModelOneshotAnimations([ this.mlAnimation ]);
|
221
|
+
this._showMuzzleFlash();
|
222
|
+
this._shootAudio.play(this.parent!.world!, true);
|
223
|
+
}
|
224
|
+
|
225
|
+
private _showMuzzleFlash(): void {
|
226
|
+
if (!this._muzzleFlashChildEntity) return;
|
227
|
+
|
228
|
+
this._muzzleFlashChildEntity.setOpacity(1);
|
229
|
+
setTimeout(() => {
|
230
|
+
if (this.isSpawned && this._muzzleFlashChildEntity?.isSpawned) {
|
231
|
+
this._muzzleFlashChildEntity.setOpacity(0);
|
232
|
+
}
|
233
|
+
}, 35);
|
234
|
+
}
|
235
|
+
|
236
|
+
private _updateUI(player: GamePlayerEntity): void {
|
237
|
+
player.updateItemInventoryQuantity(this);
|
238
|
+
this.updateAmmoIndicatorUI();
|
239
|
+
}
|
240
|
+
|
241
|
+
protected _handleHitEntity(hitEntity: Entity, hitDirection: Vector3Like): void {
|
242
|
+
if (!(hitEntity instanceof GamePlayerEntity) || hitEntity.isDead) return;
|
243
|
+
|
244
|
+
const attacker = this.parent as GamePlayerEntity;
|
245
|
+
|
246
|
+
attacker.dealtDamage(this.damage);
|
247
|
+
hitEntity.takeDamage(this.damage, hitDirection, attacker);
|
248
|
+
}
|
249
|
+
|
250
|
+
public updateAmmoIndicatorUI(reloading: boolean = false): void {
|
251
|
+
const player = this.parent as GamePlayerEntity;
|
252
|
+
|
253
|
+
player.player.ui.sendData(reloading ? {
|
254
|
+
type: 'ammo-indicator',
|
255
|
+
reloading: true,
|
256
|
+
} : {
|
257
|
+
type: 'ammo-indicator',
|
258
|
+
ammo: this.ammo,
|
259
|
+
totalAmmo: this.totalAmmo,
|
260
|
+
show: true,
|
261
|
+
});
|
262
|
+
}
|
263
|
+
}
|
@@ -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,49 @@
|
|
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 'bolt-action-sniper':
|
17
|
+
itemModule = await import('./weapons/BoltActionSniperEntity');
|
18
|
+
break;
|
19
|
+
case 'light-machine-gun':
|
20
|
+
itemModule = await import('./weapons/LightMachineGunEntity');
|
21
|
+
break;
|
22
|
+
case 'medpack':
|
23
|
+
itemModule = await import('./items/MedPackEntity');
|
24
|
+
break;
|
25
|
+
case 'mining-drill':
|
26
|
+
itemModule = await import('./weapons/MiningDrillEntity');
|
27
|
+
break;
|
28
|
+
case 'pistol':
|
29
|
+
itemModule = await import('./weapons/PistolEntity');
|
30
|
+
break;
|
31
|
+
case 'rocket-launcher':
|
32
|
+
itemModule = await import('./weapons/RocketLauncherEntity');
|
33
|
+
break;
|
34
|
+
case 'shotgun':
|
35
|
+
itemModule = await import('./weapons/ShotgunEntity');
|
36
|
+
break;
|
37
|
+
case 'shield-potion':
|
38
|
+
itemModule = await import('./items/ShieldPotionEntity');
|
39
|
+
break;
|
40
|
+
default:
|
41
|
+
throw new Error(`Unknown chest item id: ${itemId}`);
|
42
|
+
}
|
43
|
+
|
44
|
+
const itemClass = itemModule.default;
|
45
|
+
const item = new itemClass();
|
46
|
+
|
47
|
+
return item;
|
48
|
+
}
|
49
|
+
}
|
@@ -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
|
+
}
|