hytopia 0.1.98 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/server.entity.height.md +13 -0
- package/docs/server.entity.md +22 -1
- package/docs/server.entity.opacity.md +1 -1
- package/docs/server.modelregistry.getheight.md +55 -0
- package/docs/server.modelregistry.md +14 -0
- package/docs/server.player.md +21 -0
- package/docs/server.player.profilepictureurl.md +13 -0
- package/examples/pathfinding/index.ts +4 -4
- package/examples/payload-game/index.ts +12 -10
- package/examples/zombies-fps/README.md +5 -0
- package/examples/zombies-fps/assets/audio/music/bg.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/pistol-reload.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/pistol-shoot.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/player-hurt.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/purchase.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/rifle-reload.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/rifle-shoot.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/ripper-idle.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/roulette.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/shotgun-reload.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/shotgun-shoot.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/wave-start.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/zombie-idle.mp3 +0 -0
- package/examples/zombies-fps/assets/icons/ak-47.png +0 -0
- package/examples/zombies-fps/assets/icons/ar-15.png +0 -0
- package/examples/zombies-fps/assets/icons/auto-pistol.png +0 -0
- package/examples/zombies-fps/assets/icons/auto-shotgun.png +0 -0
- package/examples/zombies-fps/assets/icons/heart.png +0 -0
- package/examples/zombies-fps/assets/icons/pistol.png +0 -0
- package/examples/zombies-fps/assets/icons/shotgun.png +0 -0
- package/examples/zombies-fps/assets/models/environment/bombbox.gltf +1 -0
- package/examples/zombies-fps/assets/models/environment/bullet-hole.gltf +1 -0
- package/examples/zombies-fps/assets/models/environment/healthkit.gltf +1 -0
- package/examples/zombies-fps/assets/models/environment/muzzle-flash.gltf +1 -0
- package/examples/zombies-fps/assets/models/items/ak-47.glb +0 -0
- package/examples/zombies-fps/assets/models/items/ar-15.glb +0 -0
- package/examples/zombies-fps/assets/models/items/auto-pistol.glb +0 -0
- package/examples/zombies-fps/assets/models/items/auto-shotgun.glb +0 -0
- package/examples/zombies-fps/assets/models/items/shotgun.glb +0 -0
- package/examples/zombies-fps/assets/models/npcs/ripper-boss.gltf +1 -0
- package/examples/zombies-fps/assets/models/players/soldier-player.gltf +1 -1
- package/examples/zombies-fps/assets/models/projectiles/bullet-trace.gltf +1 -0
- package/examples/zombies-fps/assets/ui/index.html +620 -27
- package/examples/zombies-fps/classes/EnemyEntity.ts +183 -4
- package/examples/zombies-fps/classes/GameManager.ts +165 -0
- package/examples/zombies-fps/classes/GamePlayerEntity.ts +263 -14
- package/examples/zombies-fps/classes/GunEntity.ts +225 -13
- package/examples/zombies-fps/classes/InteractableEntity.ts +9 -0
- package/examples/zombies-fps/classes/PurchaseBarrierEntity.ts +70 -17
- package/examples/zombies-fps/classes/WeaponCrateEntity.ts +173 -0
- package/examples/zombies-fps/classes/enemies/RipperEntity.ts +67 -0
- package/examples/zombies-fps/classes/enemies/ZombieEntity.ts +30 -0
- package/examples/zombies-fps/classes/guns/AK47Entity.ts +43 -0
- package/examples/zombies-fps/classes/guns/AR15Entity.ts +32 -0
- package/examples/zombies-fps/classes/guns/AutoPistolEntity.ts +36 -0
- package/examples/zombies-fps/classes/guns/AutoShotgunEntity.ts +37 -0
- package/examples/zombies-fps/classes/guns/PistolEntity.ts +23 -15
- package/examples/zombies-fps/classes/guns/ShotgunEntity.ts +92 -0
- package/examples/zombies-fps/gameConfig.ts +125 -21
- package/examples/zombies-fps/index.ts +14 -31
- package/package.json +1 -1
- package/server.api.json +126 -18
- package/server.d.ts +17 -5
- package/server.js +98 -90
- package/tsdoc-metadata.json +1 -1
- package/examples/zombies-fps/assets/audio/sfx/pistol-shoot-1.mp3 +0 -0
- package/examples/zombies-fps/assets/audio/sfx/pistol-shoot-2.mp3 +0 -0
- package/examples/zombies-fps/classes/guns/BulletEntity.ts +0 -0
@@ -1,27 +1,53 @@
|
|
1
1
|
import {
|
2
|
+
Audio,
|
3
|
+
CollisionGroup,
|
4
|
+
CollisionGroupsBuilder,
|
2
5
|
Entity,
|
3
6
|
EntityOptions,
|
7
|
+
PlayerEntity,
|
8
|
+
Vector3Like,
|
9
|
+
QuaternionLike,
|
10
|
+
World,
|
11
|
+
PlayerEntityController,
|
4
12
|
} from 'hytopia';
|
5
13
|
|
14
|
+
import EnemyEntity from './EnemyEntity';
|
15
|
+
import type GamePlayerEntity from './GamePlayerEntity';
|
16
|
+
|
6
17
|
export type GunHand = 'left' | 'right' | 'both';
|
7
18
|
|
8
19
|
export interface GunEntityOptions extends EntityOptions {
|
9
|
-
ammo: number;
|
10
|
-
damage: number;
|
11
|
-
fireRate: number;
|
12
|
-
|
13
|
-
|
14
|
-
|
20
|
+
ammo: number; // The amount of ammo in the clip.
|
21
|
+
damage: number; // The damage of the gun.
|
22
|
+
fireRate: number; // Bullets shot per second.
|
23
|
+
hand: GunHand; // The hand the weapon is held in.
|
24
|
+
iconImageUri: string; // The image uri of the weapon icon.
|
25
|
+
idleAnimation: string; // The animation played when the gun is idle.
|
26
|
+
maxAmmo: number; // The amount of ammo the clip can hold.
|
27
|
+
parent?: GamePlayerEntity; // The parent player entity.
|
28
|
+
range: number; // The max range bullets travel for raycast hits
|
29
|
+
reloadAudioUri: string; // The audio played when reloading
|
30
|
+
reloadTimeMs: number; // Seconds to reload.
|
31
|
+
shootAnimation: string; // The animation played when the gun is shooting.
|
32
|
+
shootAudioUri: string; // The audio played when shooting
|
15
33
|
}
|
16
34
|
|
17
|
-
export default class GunEntity extends Entity {
|
35
|
+
export default abstract class GunEntity extends Entity {
|
18
36
|
public ammo: number;
|
19
37
|
public damage: number;
|
20
38
|
public fireRate: number;
|
21
39
|
public hand: GunHand;
|
40
|
+
public iconImageUri: string;
|
41
|
+
public idleAnimation: string;
|
22
42
|
public maxAmmo: number;
|
23
|
-
public
|
43
|
+
public range: number;
|
44
|
+
public reloadTimeMs: number;
|
45
|
+
public shootAnimation: string;
|
24
46
|
private _lastFireTime: number = 0;
|
47
|
+
private _muzzleFlashChildEntity: Entity | undefined;
|
48
|
+
private _reloadAudio: Audio;
|
49
|
+
private _reloading: boolean = false;
|
50
|
+
private _shootAudio: Audio;
|
25
51
|
|
26
52
|
public constructor(options: GunEntityOptions) {
|
27
53
|
super({
|
@@ -30,12 +56,76 @@ export default class GunEntity extends Entity {
|
|
30
56
|
parentNodeName: options.parent ? GunEntity._getParentNodeName(options.hand) : undefined,
|
31
57
|
});
|
32
58
|
|
33
|
-
this.fireRate = options.fireRate;
|
34
|
-
this.damage = options.damage;
|
35
59
|
this.ammo = options.ammo;
|
60
|
+
this.damage = options.damage;
|
61
|
+
this.fireRate = options.fireRate;
|
36
62
|
this.hand = options.hand;
|
63
|
+
this.iconImageUri = options.iconImageUri;
|
64
|
+
this.idleAnimation = options.idleAnimation;
|
37
65
|
this.maxAmmo = options.maxAmmo;
|
38
|
-
this.
|
66
|
+
this.range = options.range;
|
67
|
+
this.reloadTimeMs = options.reloadTimeMs;
|
68
|
+
this.shootAnimation = options.shootAnimation;
|
69
|
+
|
70
|
+
// Create reusable audio instances
|
71
|
+
this._reloadAudio = new Audio({
|
72
|
+
attachedToEntity: this,
|
73
|
+
uri: options.reloadAudioUri,
|
74
|
+
});
|
75
|
+
|
76
|
+
this._shootAudio = new Audio({
|
77
|
+
attachedToEntity: this,
|
78
|
+
uri: options.shootAudioUri,
|
79
|
+
volume: 0.3,
|
80
|
+
referenceDistance: 8,
|
81
|
+
});
|
82
|
+
|
83
|
+
if (options.parent) {
|
84
|
+
this._updateParentAnimations();
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
public get isEquipped(): boolean { return !!this.parent; }
|
89
|
+
|
90
|
+
public override spawn(world: World, position: Vector3Like, rotation: QuaternionLike) {
|
91
|
+
super.spawn(world, position, rotation);
|
92
|
+
this.createMuzzleFlashChildEntity();
|
93
|
+
this._updatePlayerUIAmmo();
|
94
|
+
this._updatePlayerUIWeapon();
|
95
|
+
}
|
96
|
+
|
97
|
+
public createMuzzleFlashChildEntity() {
|
98
|
+
if (!this.isSpawned || !this.world) {
|
99
|
+
return;
|
100
|
+
}
|
101
|
+
|
102
|
+
this._muzzleFlashChildEntity = new Entity({
|
103
|
+
parent: this,
|
104
|
+
modelUri: 'models/environment/muzzle-flash.gltf',
|
105
|
+
modelScale: 0.5,
|
106
|
+
opacity: 0,
|
107
|
+
});
|
108
|
+
|
109
|
+
// pistol specific atm
|
110
|
+
const { position, rotation } = this.getMuzzleFlashPositionRotation();
|
111
|
+
this._muzzleFlashChildEntity.spawn(this.world, position, rotation);
|
112
|
+
}
|
113
|
+
|
114
|
+
public abstract getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike };
|
115
|
+
|
116
|
+
public getShootOriginDirection(): { origin: Vector3Like, direction: Vector3Like } {
|
117
|
+
const parentPlayerEntity = this.parent as GamePlayerEntity;
|
118
|
+
|
119
|
+
const { x, y, z } = parentPlayerEntity.position;
|
120
|
+
const cameraYOffset = parentPlayerEntity.player.camera.offset.y;
|
121
|
+
const direction = parentPlayerEntity.player.camera.facingDirection;
|
122
|
+
const origin = {
|
123
|
+
x: x + (direction.x * 0.5),
|
124
|
+
y: y + (direction.y * 0.5) + cameraYOffset,
|
125
|
+
z: z + (direction.z * 0.5),
|
126
|
+
};
|
127
|
+
|
128
|
+
return { origin, direction };
|
39
129
|
}
|
40
130
|
|
41
131
|
// simple convenience helper for handling ammo and fire rate in shoot() overrides.
|
@@ -47,7 +137,8 @@ export default class GunEntity extends Entity {
|
|
47
137
|
}
|
48
138
|
|
49
139
|
if (this.ammo <= 0) {
|
50
|
-
|
140
|
+
this.reload();
|
141
|
+
return false;
|
51
142
|
}
|
52
143
|
|
53
144
|
this.ammo--;
|
@@ -56,8 +147,129 @@ export default class GunEntity extends Entity {
|
|
56
147
|
return true;
|
57
148
|
}
|
58
149
|
|
150
|
+
public reload() {
|
151
|
+
if (!this.parent || !this.parent.world || this._reloading) {
|
152
|
+
return;
|
153
|
+
}
|
154
|
+
|
155
|
+
this.ammo = 0; // set the ammo to 0 to prevent fire while reloading if clip wasn't empty.
|
156
|
+
this._reloading = true;
|
157
|
+
this._reloadAudio.play(this.parent.world, true);
|
158
|
+
this._updatePlayerUIReload();
|
159
|
+
|
160
|
+
setTimeout(() => {
|
161
|
+
if (!this.isEquipped) {
|
162
|
+
return;
|
163
|
+
}
|
164
|
+
|
165
|
+
this.ammo = this.maxAmmo;
|
166
|
+
this._reloading = false;
|
167
|
+
this._updatePlayerUIAmmo();
|
168
|
+
}, this.reloadTimeMs);
|
169
|
+
}
|
170
|
+
|
59
171
|
// override to create specific gun shoot logic
|
60
|
-
public shoot() {
|
172
|
+
public shoot() {
|
173
|
+
if (!this.parent || !this.parent.world) {
|
174
|
+
return;
|
175
|
+
}
|
176
|
+
|
177
|
+
const parentPlayerEntity = this.parent as GamePlayerEntity;
|
178
|
+
|
179
|
+
// Deal damage and raycast
|
180
|
+
const { origin, direction } = this.getShootOriginDirection();
|
181
|
+
this.shootRaycast(origin, direction, this.range);
|
182
|
+
|
183
|
+
// Play shoot animation
|
184
|
+
parentPlayerEntity.startModelOneshotAnimations([ this.shootAnimation ]);
|
185
|
+
|
186
|
+
// Show Muzzle Flash
|
187
|
+
if (this._muzzleFlashChildEntity) {
|
188
|
+
this._muzzleFlashChildEntity.setOpacity(1);
|
189
|
+
setTimeout(() => {
|
190
|
+
if (this.isSpawned && this._muzzleFlashChildEntity?.isSpawned) {
|
191
|
+
this._muzzleFlashChildEntity.setOpacity(0);
|
192
|
+
}
|
193
|
+
}, 35);
|
194
|
+
}
|
195
|
+
|
196
|
+
// Update player ammo
|
197
|
+
this._updatePlayerUIAmmo();
|
198
|
+
|
199
|
+
// Play shoot audio
|
200
|
+
this._shootAudio.play(this.parent.world, true);
|
201
|
+
}
|
202
|
+
|
203
|
+
public shootRaycast(origin: Vector3Like, direction: Vector3Like, length: number) {
|
204
|
+
if (!this.parent || !this.parent.world) {
|
205
|
+
return;
|
206
|
+
}
|
207
|
+
|
208
|
+
const parentPlayerEntity = this.parent as GamePlayerEntity;
|
209
|
+
|
210
|
+
const raycastHit = this.parent.world.simulation.raycast(origin, direction, length, {
|
211
|
+
filterGroups: CollisionGroupsBuilder.buildRawCollisionGroups({ // filter group is the group the raycast belongs to.
|
212
|
+
belongsTo: [ CollisionGroup.ALL ],
|
213
|
+
collidesWith: [ CollisionGroup.BLOCK, CollisionGroup.ENTITY ],
|
214
|
+
}),
|
215
|
+
});
|
216
|
+
|
217
|
+
const hitEntity = raycastHit?.hitEntity;
|
218
|
+
|
219
|
+
if (hitEntity && hitEntity instanceof EnemyEntity) {
|
220
|
+
hitEntity.takeDamage(this.damage, parentPlayerEntity);
|
221
|
+
}
|
222
|
+
}
|
223
|
+
|
224
|
+
private _updateParentAnimations() {
|
225
|
+
if (!this.parent || !this.parent.world) {
|
226
|
+
return;
|
227
|
+
}
|
228
|
+
|
229
|
+
const playerEntityController = this.parent.controller as PlayerEntityController;
|
230
|
+
|
231
|
+
playerEntityController.idleLoopedAnimations = [ this.idleAnimation, 'idle_lower' ];
|
232
|
+
playerEntityController.walkLoopedAnimations = [ this.idleAnimation, 'walk_lower' ];
|
233
|
+
playerEntityController.runLoopedAnimations = [ this.idleAnimation, 'run_lower' ];
|
234
|
+
}
|
235
|
+
|
236
|
+
private _updatePlayerUIAmmo() {
|
237
|
+
if (!this.parent || !this.parent.world) {
|
238
|
+
return;
|
239
|
+
}
|
240
|
+
|
241
|
+
const parentPlayerEntity = this.parent as PlayerEntity;
|
242
|
+
|
243
|
+
parentPlayerEntity.player.ui.sendData({
|
244
|
+
type: 'ammo',
|
245
|
+
ammo: this.ammo,
|
246
|
+
maxAmmo: this.maxAmmo,
|
247
|
+
});
|
248
|
+
}
|
249
|
+
|
250
|
+
private _updatePlayerUIReload() {
|
251
|
+
if (!this.parent || !this.parent.world) {
|
252
|
+
return;
|
253
|
+
}
|
254
|
+
|
255
|
+
const parentPlayerEntity = this.parent as PlayerEntity;
|
256
|
+
|
257
|
+
parentPlayerEntity.player.ui.sendData({ type: 'reload' });
|
258
|
+
}
|
259
|
+
|
260
|
+
private _updatePlayerUIWeapon() {
|
261
|
+
if (!this.parent || !this.parent.world) {
|
262
|
+
return;
|
263
|
+
}
|
264
|
+
|
265
|
+
const parentPlayerEntity = this.parent as PlayerEntity;
|
266
|
+
|
267
|
+
parentPlayerEntity.player.ui.sendData({
|
268
|
+
type: 'weapon',
|
269
|
+
name: this.name,
|
270
|
+
iconImageUri: this.iconImageUri,
|
271
|
+
});
|
272
|
+
}
|
61
273
|
|
62
274
|
// convenience helper for getting the node name of the hand the gun is held in.
|
63
275
|
private static _getParentNodeName(hand: GunHand): string {
|
@@ -1,19 +1,24 @@
|
|
1
1
|
import {
|
2
2
|
ColliderOptions,
|
3
3
|
ColliderShape,
|
4
|
+
CollisionGroup,
|
4
5
|
Entity,
|
5
|
-
RigidBodyType,
|
6
6
|
QuaternionLike,
|
7
|
+
RigidBodyType,
|
8
|
+
SceneUI,
|
7
9
|
Vector3Like,
|
8
10
|
World,
|
9
11
|
} from 'hytopia';
|
10
12
|
|
13
|
+
import GameManager from './GameManager';
|
14
|
+
import GamePlayerEntity from './GamePlayerEntity';
|
15
|
+
import InteractableEntity from './InteractableEntity';
|
16
|
+
|
11
17
|
const WALL_COLLIDER_OPTIONS: ColliderOptions = {
|
12
18
|
shape: ColliderShape.BLOCK,
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
z: 0.5,
|
19
|
+
collisionGroups: {
|
20
|
+
belongsTo: [ CollisionGroup.BLOCK ],
|
21
|
+
collidesWith: [ CollisionGroup.PLAYER ],
|
17
22
|
},
|
18
23
|
};
|
19
24
|
|
@@ -21,33 +26,66 @@ export interface PurchaseBarrierEntityOptions {
|
|
21
26
|
name: string;
|
22
27
|
removalPrice: number;
|
23
28
|
width: number;
|
29
|
+
unlockIds: string[];
|
24
30
|
}
|
25
31
|
|
26
|
-
export default class PurchaseBarrierEntity extends
|
32
|
+
export default class PurchaseBarrierEntity extends InteractableEntity {
|
27
33
|
public removalPrice: number;
|
34
|
+
private _unlockIds: string[];
|
28
35
|
private _width: number;
|
29
|
-
|
30
36
|
public constructor(options: PurchaseBarrierEntityOptions) {
|
31
37
|
super({
|
32
38
|
name: options.name,
|
33
39
|
modelUri: 'models/environment/barbedfence.gltf',
|
34
40
|
rigidBodyOptions: {
|
35
41
|
type: RigidBodyType.FIXED,
|
36
|
-
colliders: [ WALL_COLLIDER_OPTIONS ],
|
37
42
|
},
|
38
43
|
});
|
39
44
|
|
40
45
|
this.removalPrice = options.removalPrice;
|
46
|
+
this._unlockIds = options.unlockIds;
|
41
47
|
this._width = options.width;
|
42
48
|
}
|
43
49
|
|
44
50
|
public get width(): number {
|
45
51
|
return this._width;
|
46
52
|
}
|
53
|
+
|
54
|
+
public override interact(interactingPlayer: GamePlayerEntity) {
|
55
|
+
if (!this.isSpawned || !this.world) {
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
|
59
|
+
if (!interactingPlayer.spendMoney(this.removalPrice)) {
|
60
|
+
this.world.chatManager.sendPlayerMessage(interactingPlayer.player, `You don't have enough money to unlock this barrier!`, 'FF0000');
|
61
|
+
return;
|
62
|
+
}
|
63
|
+
|
64
|
+
this.world.chatManager.sendBroadcastMessage(`The ${this.name} barrier has been unlocked!`, '00FF00');
|
65
|
+
|
66
|
+
// Add unlocked ids to game state, so zombies can spawn in the new areas
|
67
|
+
this._unlockIds.forEach(id => {
|
68
|
+
GameManager.instance.addUnlockedId(id);
|
69
|
+
});
|
70
|
+
|
71
|
+
this.despawn();
|
72
|
+
}
|
47
73
|
|
48
74
|
public override spawn(world: World, position: Vector3Like, rotation?: QuaternionLike): void {
|
75
|
+
// Add the barrier collider based on spawn position and width before spawning the barrier.
|
76
|
+
this.createAndAddChildCollider({
|
77
|
+
...WALL_COLLIDER_OPTIONS,
|
78
|
+
halfExtents: {
|
79
|
+
x: rotation?.w === 1 ? (this._width * 0.5) : 0.5,
|
80
|
+
y: 5,
|
81
|
+
z: rotation?.w === 1 ? 0.5 : (this._width * 0.5),
|
82
|
+
},
|
83
|
+
relativeRotation: rotation,
|
84
|
+
});
|
85
|
+
|
49
86
|
super.spawn(world, position, rotation);
|
50
87
|
|
88
|
+
// Add children barriers for visual barrier width
|
51
89
|
if (this._width > 1) {
|
52
90
|
const offset = Math.floor((this._width - 1) / 2);
|
53
91
|
for (let i = -offset; i <= offset; i++) {
|
@@ -56,19 +94,34 @@ export default class PurchaseBarrierEntity extends Entity {
|
|
56
94
|
const barrier = new Entity({
|
57
95
|
name: `${this.name} (${Math.abs(i)})`,
|
58
96
|
modelUri: 'models/environment/barbedfence.gltf',
|
59
|
-
|
60
|
-
type: RigidBodyType.FIXED,
|
61
|
-
colliders: [ WALL_COLLIDER_OPTIONS ],
|
62
|
-
},
|
97
|
+
parent: this,
|
63
98
|
});
|
64
99
|
|
100
|
+
// Because of the anchor point of the barbedfence model
|
101
|
+
// being in the lower corner and not centered, we need
|
102
|
+
// to offset the child barriers by half the height to
|
103
|
+
// center them, this is just a unique for to this model.
|
104
|
+
const halfHeight = this.height / 2;
|
105
|
+
|
65
106
|
barrier.spawn(world, {
|
66
|
-
x:
|
67
|
-
y:
|
68
|
-
z:
|
69
|
-
}
|
107
|
+
x: i - halfHeight,
|
108
|
+
y: -halfHeight,
|
109
|
+
z: -halfHeight,
|
110
|
+
});
|
70
111
|
}
|
71
|
-
}
|
112
|
+
}
|
113
|
+
|
114
|
+
// Spawn Scene UI that shows barrier removal price
|
115
|
+
(new SceneUI({
|
116
|
+
attachedToEntity: this,
|
117
|
+
offset: { x: 0, y: 1, z: 0 },
|
118
|
+
templateId: 'purchase-label',
|
119
|
+
viewDistance: 4,
|
120
|
+
state: {
|
121
|
+
name: this.name,
|
122
|
+
cost: this.removalPrice,
|
123
|
+
},
|
124
|
+
})).load(world);
|
72
125
|
}
|
73
126
|
}
|
74
127
|
|
@@ -0,0 +1,173 @@
|
|
1
|
+
import {
|
2
|
+
Audio,
|
3
|
+
Collider,
|
4
|
+
RigidBodyType,
|
5
|
+
QuaternionLike,
|
6
|
+
Vector3Like,
|
7
|
+
World,
|
8
|
+
SceneUI,
|
9
|
+
} from 'hytopia';
|
10
|
+
|
11
|
+
import GamePlayerEntity from './GamePlayerEntity';
|
12
|
+
import InteractableEntity from './InteractableEntity';
|
13
|
+
|
14
|
+
import AK47Entity from './guns/AK47Entity';
|
15
|
+
import AR15Entity from './guns/AR15Entity';
|
16
|
+
import AutoPistolEntity from './guns/AutoPistolEntity';
|
17
|
+
import AutoShotgunEntity from './guns/AutoShotgunEntity';
|
18
|
+
import PistolEntity from './guns/PistolEntity';
|
19
|
+
import ShotgunEntity from './guns/ShotgunEntity';
|
20
|
+
import type { GunEntityOptions } from './GunEntity';
|
21
|
+
import type GunEntity from './GunEntity';
|
22
|
+
|
23
|
+
const POSSIBLE_WEAPONS = [
|
24
|
+
{
|
25
|
+
id: 'ak47',
|
26
|
+
name: 'AK-47',
|
27
|
+
iconUri: 'icons/ak-47.png',
|
28
|
+
},
|
29
|
+
{
|
30
|
+
id: 'ar15',
|
31
|
+
name: 'AR-15',
|
32
|
+
iconUri: 'icons/ar-15.png',
|
33
|
+
},
|
34
|
+
{
|
35
|
+
id: 'auto-pistol',
|
36
|
+
name: 'Auto Pistol',
|
37
|
+
iconUri: 'icons/auto-pistol.png',
|
38
|
+
},
|
39
|
+
{
|
40
|
+
id: 'auto-shotgun',
|
41
|
+
name: 'Auto Shotgun',
|
42
|
+
iconUri: 'icons/auto-shotgun.png',
|
43
|
+
},
|
44
|
+
{
|
45
|
+
id: 'pistol',
|
46
|
+
name: 'Pistol',
|
47
|
+
iconUri: 'icons/pistol.png',
|
48
|
+
},
|
49
|
+
{
|
50
|
+
id: 'shotgun',
|
51
|
+
name: 'Shotgun',
|
52
|
+
iconUri: 'icons/shotgun.png',
|
53
|
+
},
|
54
|
+
]
|
55
|
+
|
56
|
+
export interface WeaponCrateEntityOptions {
|
57
|
+
name: string,
|
58
|
+
price: number,
|
59
|
+
rollableWeaponIds: string[],
|
60
|
+
};
|
61
|
+
|
62
|
+
export default class WeaponCrateEntity extends InteractableEntity {
|
63
|
+
public purchasePrice: number;
|
64
|
+
private _purchaseSceneUI: SceneUI;
|
65
|
+
private _rouletteAudio: Audio;
|
66
|
+
private _rouletteSceneUI: SceneUI;
|
67
|
+
private _rollableWeaponIds: string[];
|
68
|
+
private _rolledWeaponId: string | undefined;
|
69
|
+
|
70
|
+
public constructor(options: WeaponCrateEntityOptions) {
|
71
|
+
const colliderOptions = Collider.optionsFromModelUri('models/environment/weaponbox.gltf');
|
72
|
+
|
73
|
+
if (colliderOptions.halfExtents) { // make it taller for better interact area
|
74
|
+
colliderOptions.halfExtents.y = 3;
|
75
|
+
}
|
76
|
+
|
77
|
+
super({
|
78
|
+
name: options.name,
|
79
|
+
modelUri: 'models/environment/weaponbox.gltf',
|
80
|
+
rigidBodyOptions: {
|
81
|
+
type: RigidBodyType.FIXED,
|
82
|
+
colliders: [ colliderOptions ]
|
83
|
+
},
|
84
|
+
tintColor: { r: 255, g: 255, b: 255 },
|
85
|
+
});
|
86
|
+
|
87
|
+
this.purchasePrice = options.price;
|
88
|
+
this._rollableWeaponIds = options.rollableWeaponIds;
|
89
|
+
|
90
|
+
this._purchaseSceneUI = new SceneUI({
|
91
|
+
attachedToEntity: this,
|
92
|
+
offset: { x: 0, y: 1, z: 0 },
|
93
|
+
templateId: 'purchase-label',
|
94
|
+
viewDistance: 4,
|
95
|
+
state: {
|
96
|
+
name: this.name,
|
97
|
+
cost: this.purchasePrice,
|
98
|
+
},
|
99
|
+
});
|
100
|
+
|
101
|
+
this._rouletteAudio = new Audio({
|
102
|
+
attachedToEntity: this,
|
103
|
+
uri: 'audio/sfx/roulette.mp3',
|
104
|
+
volume: 0.3,
|
105
|
+
referenceDistance: 4,
|
106
|
+
});
|
107
|
+
|
108
|
+
this._rouletteSceneUI = new SceneUI({
|
109
|
+
attachedToEntity: this,
|
110
|
+
offset: { x: 0, y: 1, z: 0 },
|
111
|
+
templateId: 'weapon-roulette',
|
112
|
+
viewDistance: 4,
|
113
|
+
});
|
114
|
+
}
|
115
|
+
|
116
|
+
public override interact(interactingPlayer: GamePlayerEntity) {
|
117
|
+
if (!this.isSpawned || !this.world) {
|
118
|
+
return;
|
119
|
+
}
|
120
|
+
|
121
|
+
// If interacting and a weapon is rolled, equip it.
|
122
|
+
if (this._rolledWeaponId) {
|
123
|
+
this._rouletteSceneUI.unload();
|
124
|
+
this._purchaseSceneUI.load(this.world);
|
125
|
+
|
126
|
+
const GunClass = this._weaponIdToGunClass(this._rolledWeaponId);
|
127
|
+
if (GunClass) {
|
128
|
+
interactingPlayer.equipGun(new GunClass({ parent: interactingPlayer }));
|
129
|
+
}
|
130
|
+
|
131
|
+
this._rolledWeaponId = undefined;
|
132
|
+
return;
|
133
|
+
}
|
134
|
+
|
135
|
+
// If interacting and no weapon is rolled, spend $ to roll a weapon.
|
136
|
+
if (!interactingPlayer.spendMoney(this.purchasePrice)) {
|
137
|
+
this.world.chatManager.sendPlayerMessage(interactingPlayer.player, `You don't have enough money to purchase this weapon crate!`, 'FF0000');
|
138
|
+
return;
|
139
|
+
}
|
140
|
+
|
141
|
+
// Unload purchase scene UI
|
142
|
+
this._purchaseSceneUI.unload();
|
143
|
+
|
144
|
+
// Roll a weapon and show roll UI
|
145
|
+
this._rolledWeaponId = this._rollableWeaponIds[Math.floor(Math.random() * this._rollableWeaponIds.length)];
|
146
|
+
this._rouletteSceneUI.setState({
|
147
|
+
selectedWeaponId: this._rolledWeaponId,
|
148
|
+
possibleWeapons: POSSIBLE_WEAPONS,
|
149
|
+
});
|
150
|
+
this._rouletteSceneUI.load(this.world);
|
151
|
+
|
152
|
+
this._rouletteAudio.play(this.world, true);
|
153
|
+
}
|
154
|
+
|
155
|
+
public override spawn(world: World, position: Vector3Like, rotation?: QuaternionLike): void {
|
156
|
+
super.spawn(world, position, rotation);
|
157
|
+
|
158
|
+
// Spawn Scene UI that shows purchase price
|
159
|
+
this._purchaseSceneUI.load(world);
|
160
|
+
}
|
161
|
+
|
162
|
+
private _weaponIdToGunClass(weaponId: string): (new (options: Partial<GunEntityOptions>) => GunEntity) | undefined {
|
163
|
+
switch (weaponId) {
|
164
|
+
case 'ak47': return AK47Entity;
|
165
|
+
case 'ar15': return AR15Entity;
|
166
|
+
case 'auto-pistol': return AutoPistolEntity;
|
167
|
+
case 'auto-shotgun': return AutoShotgunEntity;
|
168
|
+
case 'pistol': return PistolEntity;
|
169
|
+
case 'shotgun': return ShotgunEntity;
|
170
|
+
default: return undefined;
|
171
|
+
}
|
172
|
+
}
|
173
|
+
}
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import { GameServer, PathfindingEntityController } from 'hytopia';
|
2
|
+
import type { GamePlayerEntity, QuaternionLike, Vector3Like, World } from 'hytopia';
|
3
|
+
|
4
|
+
import EnemyEntity from '../EnemyEntity';
|
5
|
+
import type { EnemyEntityOptions } from '../EnemyEntity';
|
6
|
+
|
7
|
+
export default class RipperEntity extends EnemyEntity {
|
8
|
+
public constructor(options: Partial<EnemyEntityOptions> = {}) {
|
9
|
+
const speed = options.speed ?? 2;
|
10
|
+
const animation = speed > 6 ? 'animation.ripper_zombie.sprint' : 'animation.ripper_zombie.walk';
|
11
|
+
|
12
|
+
super({
|
13
|
+
damage: options.damage ?? 6,
|
14
|
+
damageAudioUri: options.damageAudioUri ?? 'audio/sfx/entity/zombie/zombie-hurt.mp3',
|
15
|
+
health: options.health ?? 300,
|
16
|
+
idleAudioUri: options.idleAudioUri ?? 'audio/sfx/ripper-idle.mp3',
|
17
|
+
idleAudioVolume: 1,
|
18
|
+
idleAudioReferenceDistance: 8,
|
19
|
+
jumpHeight: options.jumpHeight ?? 2,
|
20
|
+
preferJumping: true,
|
21
|
+
reward: options.reward ?? 300,
|
22
|
+
speed,
|
23
|
+
controller: new PathfindingEntityController(),
|
24
|
+
modelUri: 'models/npcs/ripper-boss.gltf',
|
25
|
+
modelLoopedAnimations: [ animation ],
|
26
|
+
modelScale: 0.5,
|
27
|
+
rigidBodyOptions: {
|
28
|
+
enabledRotations: { x: false, y: true, z: false },
|
29
|
+
ccdEnabled: true,
|
30
|
+
},
|
31
|
+
});
|
32
|
+
}
|
33
|
+
|
34
|
+
public override spawn(world: World, position: Vector3Like, rotation?: QuaternionLike) {
|
35
|
+
super.spawn(world, position, rotation);
|
36
|
+
|
37
|
+
this._updateBossUI({
|
38
|
+
type: 'boss',
|
39
|
+
name: 'BOSS: RIPPER',
|
40
|
+
health: this.health,
|
41
|
+
maxHealth: this.maxHealth,
|
42
|
+
show: true,
|
43
|
+
});
|
44
|
+
}
|
45
|
+
|
46
|
+
public override takeDamage(damage: number, fromPlayer: GamePlayerEntity) {
|
47
|
+
// Do the UI check first, because otherwise
|
48
|
+
// takeDamage can trigger a despawn if health < 0
|
49
|
+
this._updateBossUI({
|
50
|
+
type: 'boss',
|
51
|
+
show: this.health - damage > 0,
|
52
|
+
healthPercent: ((this.health - damage) / this.maxHealth) * 100,
|
53
|
+
});
|
54
|
+
|
55
|
+
super.takeDamage(damage, fromPlayer);
|
56
|
+
}
|
57
|
+
|
58
|
+
private _updateBossUI(data = {}) {
|
59
|
+
if (!this.world) {
|
60
|
+
return;
|
61
|
+
}
|
62
|
+
|
63
|
+
GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
|
64
|
+
player.ui.sendData(data);
|
65
|
+
});
|
66
|
+
}
|
67
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import { PathfindingEntityController } from 'hytopia';
|
2
|
+
|
3
|
+
import EnemyEntity from '../EnemyEntity';
|
4
|
+
import type { EnemyEntityOptions } from '../EnemyEntity';
|
5
|
+
|
6
|
+
export default class ZombieEntity extends EnemyEntity {
|
7
|
+
public constructor(options: Partial<EnemyEntityOptions> = {}) {
|
8
|
+
const speed = options.speed ?? 1 + Math.random() * 4;
|
9
|
+
const animation = speed > 5 ? 'run' : speed > 3 ? 'walk' : 'crawling';
|
10
|
+
|
11
|
+
super({
|
12
|
+
damage: options.damage ?? 2,
|
13
|
+
damageAudioUri: options.damageAudioUri ?? 'audio/sfx/entity/zombie/zombie-hurt.mp3',
|
14
|
+
health: options.health ?? 7,
|
15
|
+
idleAudioUri: options.idleAudioUri ?? 'audio/sfx/zombie-idle.mp3',
|
16
|
+
jumpHeight: options.jumpHeight ?? 2,
|
17
|
+
reward: options.reward ?? 20,
|
18
|
+
speed: options.speed ?? speed,
|
19
|
+
|
20
|
+
controller: new PathfindingEntityController(),
|
21
|
+
modelUri: 'models/npcs/zombie.gltf',
|
22
|
+
modelLoopedAnimations: [ animation ],
|
23
|
+
modelScale: 0.5 + Math.random() * 0.2,
|
24
|
+
rigidBodyOptions: {
|
25
|
+
enabledRotations: { x: false, y: true, z: false },
|
26
|
+
ccdEnabled: true,
|
27
|
+
},
|
28
|
+
});
|
29
|
+
}
|
30
|
+
}
|