hytopia 0.2.0 → 0.2.1
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/examples/hole-in-wall-game/index.ts +4 -4
- package/examples/wall-dodge-game/assets/audio/bgm.mp3 +0 -0
- package/examples/wall-dodge-game/index.ts +7 -0
- package/examples/zombies-fps/assets/ui/index.html +7 -1
- package/examples/zombies-fps/classes/EnemyEntity.ts +5 -3
- package/examples/zombies-fps/classes/GameManager.ts +110 -18
- package/examples/zombies-fps/classes/GamePlayerEntity.ts +11 -1
- package/examples/zombies-fps/classes/GunEntity.ts +13 -13
- package/examples/zombies-fps/classes/PurchaseBarrierEntity.ts +1 -0
- package/examples/zombies-fps/index.ts +2 -7
- package/package.json +1 -1
@@ -63,7 +63,7 @@ let gameUiState: object = {};
|
|
63
63
|
|
64
64
|
// Audio
|
65
65
|
const gameActiveAudio = new Audio({
|
66
|
-
uri: 'audio/music
|
66
|
+
uri: 'audio/music.mp3',
|
67
67
|
loop: true,
|
68
68
|
volume: 0.2,
|
69
69
|
});
|
@@ -297,9 +297,9 @@ function generateWall(world: World, direction: GameWallDirection, speedModifier:
|
|
297
297
|
const yOffset = (selectedShape.length - y - 1) * 1 + 0.5;
|
298
298
|
|
299
299
|
const wallSegment = new Entity({
|
300
|
-
blockTextureUri: selectedShape[y][x] === 2 ? '
|
301
|
-
selectedShape[y][x] === 3 ? '
|
302
|
-
'
|
300
|
+
blockTextureUri: selectedShape[y][x] === 2 ? 'blocks/dirt.png' :
|
301
|
+
selectedShape[y][x] === 3 ? 'blocks/sand.png' :
|
302
|
+
'blocks/oak-leaves.png',
|
303
303
|
blockHalfExtents: {
|
304
304
|
x: isNorthSouth ? 0.5 : 0.5,
|
305
305
|
y: 0.5,
|
Binary file
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import {
|
2
|
+
Audio,
|
2
3
|
CollisionGroup,
|
3
4
|
ColliderShape,
|
4
5
|
BlockType,
|
@@ -55,6 +56,12 @@ startServer(world => {
|
|
55
56
|
|
56
57
|
setupJoinNPC(world);
|
57
58
|
startBlockSpawner(world);
|
59
|
+
|
60
|
+
(new Audio({
|
61
|
+
uri: 'audio/bgm.mp3',
|
62
|
+
loop: true,
|
63
|
+
volume: 0.05,
|
64
|
+
})).play(world);
|
58
65
|
});
|
59
66
|
|
60
67
|
/**
|
@@ -73,6 +73,7 @@
|
|
73
73
|
<!-- UI Scripts-->
|
74
74
|
<script>
|
75
75
|
const CDN_ASSETS_URL = '{{CDN_ASSETS_URL}}';
|
76
|
+
let timerInterval;
|
76
77
|
|
77
78
|
hytopia.registerSceneUITemplate('downed-player', (id, onState) => {
|
78
79
|
const template = document.getElementById('downed-player-template');
|
@@ -158,10 +159,15 @@
|
|
158
159
|
document.querySelector('.game-info').style.display = 'block';
|
159
160
|
document.querySelector('.game-info .timer').textContent = 'Time: 00:00';
|
160
161
|
document.querySelector('.game-info .wave').textContent = 'Wave: 1';
|
162
|
+
|
163
|
+
// Clear previous timer
|
164
|
+
if (timerInterval) {
|
165
|
+
clearInterval(timerInterval);
|
166
|
+
}
|
161
167
|
|
162
168
|
// Start game timer
|
163
169
|
const startTime = Date.now();
|
164
|
-
setInterval(() => {
|
170
|
+
timerInterval = setInterval(() => {
|
165
171
|
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
|
166
172
|
const minutes = Math.floor(elapsedSeconds / 60).toString().padStart(2, '0');
|
167
173
|
const seconds = (elapsedSeconds % 60).toString().padStart(2, '0');
|
@@ -42,7 +42,7 @@ export default class EnemyEntity extends Entity {
|
|
42
42
|
private _targetEntity: Entity | undefined;
|
43
43
|
|
44
44
|
public constructor(options: EnemyEntityOptions) {
|
45
|
-
super(options);
|
45
|
+
super({ ...options, tag: 'enemy' });
|
46
46
|
this.damage = options.damage;
|
47
47
|
this.health = options.health;
|
48
48
|
this.jumpHeight = options.jumpHeight ?? 1;
|
@@ -84,7 +84,7 @@ export default class EnemyEntity extends Entity {
|
|
84
84
|
}
|
85
85
|
}
|
86
86
|
|
87
|
-
public takeDamage(damage: number, fromPlayer
|
87
|
+
public takeDamage(damage: number, fromPlayer?: GamePlayerEntity) {
|
88
88
|
if (!this.world) {
|
89
89
|
return;
|
90
90
|
}
|
@@ -96,7 +96,9 @@ export default class EnemyEntity extends Entity {
|
|
96
96
|
}
|
97
97
|
|
98
98
|
// Give reward based on damage as % of health
|
99
|
-
fromPlayer
|
99
|
+
if (fromPlayer) {
|
100
|
+
fromPlayer.addMoney((this.damage / this.maxHealth) * this.reward);
|
101
|
+
}
|
100
102
|
|
101
103
|
if (this.health <= 0 && this.isSpawned) {
|
102
104
|
// Enemy is dead, give half reward & despawn
|
@@ -1,16 +1,18 @@
|
|
1
1
|
import { Audio, Collider, ColliderShape, CollisionGroup, GameServer } from 'hytopia';
|
2
|
+
import GamePlayerEntity from './GamePlayerEntity';
|
2
3
|
import PurchaseBarrierEntity from './PurchaseBarrierEntity';
|
3
4
|
import { INVISIBLE_WALLS, INVISIBLE_WALL_COLLISION_GROUP, PURCHASE_BARRIERS, ENEMY_SPAWN_POINTS, WEAPON_CRATES } from '../gameConfig';
|
4
|
-
import type { World, Vector3Like } from 'hytopia';
|
5
|
-
|
6
|
-
// temp
|
7
|
-
import ZombieEntity from './enemies/ZombieEntity';
|
8
5
|
import RipperEntity from './enemies/RipperEntity';
|
6
|
+
import ZombieEntity from './enemies/ZombieEntity';
|
9
7
|
import WeaponCrateEntity from './WeaponCrateEntity';
|
8
|
+
import type { World, Vector3Like } from 'hytopia';
|
9
|
+
import type EnemyEntity from './EnemyEntity';
|
10
|
+
import type { Player } from 'hytopia';
|
10
11
|
|
11
12
|
const GAME_WAVE_INTERVAL_MS = 30 * 1000; // 30 seconds between waves
|
12
13
|
const SLOWEST_SPAWN_INTERVAL_MS = 4000; // Starting spawn rate
|
13
14
|
const FASTEST_SPAWN_INTERVAL_MS = 750; // Fastest spawn rate
|
15
|
+
const GAME_START_COUNTDOWN_S = 45; // 45 seconds delay before game starts
|
14
16
|
const WAVE_SPAWN_INTERVAL_REDUCTION_MS = 300; // Spawn rate reduction per wave
|
15
17
|
const WAVE_DELAY_MS = 10000; // 10s between waves
|
16
18
|
|
@@ -24,7 +26,9 @@ export default class GameManager {
|
|
24
26
|
public world: World | undefined;
|
25
27
|
|
26
28
|
private _enemySpawnTimeout: NodeJS.Timeout | undefined;
|
27
|
-
private
|
29
|
+
private _endGameTimeout: NodeJS.Timeout | undefined;
|
30
|
+
private _startCountdown: number = GAME_START_COUNTDOWN_S;
|
31
|
+
private _startInterval: NodeJS.Timeout | undefined;
|
28
32
|
private _waveTimeout: NodeJS.Timeout | undefined;
|
29
33
|
private _waveStartAudio: Audio;
|
30
34
|
|
@@ -40,6 +44,28 @@ export default class GameManager {
|
|
40
44
|
this.unlockedIds.add(id);
|
41
45
|
}
|
42
46
|
|
47
|
+
public checkEndGame() {
|
48
|
+
clearTimeout(this._endGameTimeout);
|
49
|
+
|
50
|
+
this._endGameTimeout = setTimeout(() => {
|
51
|
+
if (!this.world) return;
|
52
|
+
|
53
|
+
let allPlayersDowned = true;
|
54
|
+
|
55
|
+
this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
|
56
|
+
const gamePlayerEntity = playerEntity as GamePlayerEntity;
|
57
|
+
|
58
|
+
if (!gamePlayerEntity.downed) {
|
59
|
+
allPlayersDowned = false;
|
60
|
+
}
|
61
|
+
});
|
62
|
+
|
63
|
+
if (allPlayersDowned) {
|
64
|
+
this.endGame();
|
65
|
+
}
|
66
|
+
}, 1000);
|
67
|
+
}
|
68
|
+
|
43
69
|
public setupGame(world: World) {
|
44
70
|
this.world = world;
|
45
71
|
|
@@ -58,17 +84,8 @@ export default class GameManager {
|
|
58
84
|
wallCollider.addToSimulation(world.simulation);
|
59
85
|
});
|
60
86
|
|
61
|
-
//
|
62
|
-
|
63
|
-
const purchaseBarrier = new PurchaseBarrierEntity({
|
64
|
-
name: barrier.name,
|
65
|
-
removalPrice: barrier.removalPrice,
|
66
|
-
unlockIds: barrier.unlockIds,
|
67
|
-
width: barrier.width,
|
68
|
-
});
|
69
|
-
|
70
|
-
purchaseBarrier.spawn(world, barrier.position, barrier.rotation);
|
71
|
-
});
|
87
|
+
// Spawn initial purchase barriers
|
88
|
+
this.spawnPurchaseBarriers();
|
72
89
|
|
73
90
|
// Setup weapon crates
|
74
91
|
WEAPON_CRATES.forEach(crate => {
|
@@ -88,14 +105,31 @@ export default class GameManager {
|
|
88
105
|
volume: 0.4,
|
89
106
|
})).play(world);
|
90
107
|
|
91
|
-
|
108
|
+
this.startCountdown();
|
109
|
+
}
|
110
|
+
|
111
|
+
public startCountdown() {
|
112
|
+
clearInterval(this._startInterval);
|
113
|
+
this._startCountdown = GAME_START_COUNTDOWN_S;
|
114
|
+
this._startInterval = setInterval(() => {
|
115
|
+
if (!this.world || !this.world.entityManager.getAllPlayerEntities().length) return;
|
116
|
+
|
117
|
+
this._startCountdown--;
|
118
|
+
|
119
|
+
if (this._startCountdown <= 0) {
|
120
|
+
this.startGame();
|
121
|
+
this.world.chatManager.sendBroadcastMessage('Game starting!', 'FF0000');
|
122
|
+
} else {
|
123
|
+
this.world.chatManager.sendBroadcastMessage(`${this._startCountdown} seconds until the game starts...`, 'FF0000');
|
124
|
+
}
|
125
|
+
}, 1000);
|
92
126
|
}
|
93
127
|
|
94
128
|
public startGame() {
|
95
129
|
if (!this.world || this.isStarted) return; // type guard
|
96
130
|
|
97
131
|
this.isStarted = true;
|
98
|
-
this.
|
132
|
+
clearInterval(this._startInterval);
|
99
133
|
|
100
134
|
GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
|
101
135
|
player.ui.sendData({ type: 'start' });
|
@@ -105,6 +139,64 @@ export default class GameManager {
|
|
105
139
|
this._waveLoop();
|
106
140
|
}
|
107
141
|
|
142
|
+
public endGame() {
|
143
|
+
if (!this.world) return;
|
144
|
+
|
145
|
+
this.world.chatManager.sendBroadcastMessage(`Game Over! Your team made it to wave ${this.waveNumber}!`, '00FF00');
|
146
|
+
|
147
|
+
clearTimeout(this._enemySpawnTimeout);
|
148
|
+
clearTimeout(this._waveTimeout);
|
149
|
+
|
150
|
+
this.isStarted = false;
|
151
|
+
this.unlockedIds = new Set([ 'start' ]);
|
152
|
+
this.waveNumber = 0;
|
153
|
+
this.waveDelay = 0;
|
154
|
+
|
155
|
+
this.world.entityManager.getEntitiesByTag('enemy').forEach(entity => {
|
156
|
+
const enemy = entity as EnemyEntity;
|
157
|
+
enemy.takeDamage(enemy.health); // triggers any UI updates when killed via takedamage
|
158
|
+
});
|
159
|
+
|
160
|
+
this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
|
161
|
+
const player = playerEntity.player;
|
162
|
+
playerEntity.despawn();
|
163
|
+
});
|
164
|
+
|
165
|
+
GameServer.instance.playerManager.getConnectedPlayers().forEach(player => {
|
166
|
+
this.spawnPlayerEntity(player);
|
167
|
+
});
|
168
|
+
|
169
|
+
this.spawnPurchaseBarriers();
|
170
|
+
this.startCountdown();
|
171
|
+
}
|
172
|
+
|
173
|
+
public spawnPlayerEntity(player: Player) {
|
174
|
+
if (!this.world) return;
|
175
|
+
|
176
|
+
const playerEntity = new GamePlayerEntity(player);
|
177
|
+
playerEntity.spawn(this.world, { x: 2, y: 10, z: 19 });
|
178
|
+
player.camera.setAttachedToEntity(playerEntity);
|
179
|
+
}
|
180
|
+
|
181
|
+
public spawnPurchaseBarriers() {
|
182
|
+
if (!this.world) return;
|
183
|
+
|
184
|
+
this.world.entityManager.getEntitiesByTag('purchase-barrier').forEach(entity => {
|
185
|
+
entity.despawn();
|
186
|
+
});
|
187
|
+
|
188
|
+
PURCHASE_BARRIERS.forEach(barrier => {
|
189
|
+
const purchaseBarrier = new PurchaseBarrierEntity({
|
190
|
+
name: barrier.name,
|
191
|
+
removalPrice: barrier.removalPrice,
|
192
|
+
unlockIds: barrier.unlockIds,
|
193
|
+
width: barrier.width,
|
194
|
+
});
|
195
|
+
|
196
|
+
purchaseBarrier.spawn(this.world!, barrier.position, barrier.rotation);
|
197
|
+
});
|
198
|
+
}
|
199
|
+
|
108
200
|
private _spawnLoop() {
|
109
201
|
if (!this.world) return; // type guard
|
110
202
|
|
@@ -21,8 +21,8 @@ import PistolEntity from './guns/PistolEntity';
|
|
21
21
|
|
22
22
|
import InteractableEntity from './InteractableEntity';
|
23
23
|
import type GunEntity from './GunEntity';
|
24
|
-
import type { GunEntityOptions } from './GunEntity';
|
25
24
|
import { INVISIBLE_WALL_COLLISION_GROUP } from '../gameConfig';
|
25
|
+
import GameManager from './GameManager';
|
26
26
|
|
27
27
|
const BASE_HEALTH = 100;
|
28
28
|
const REVIVE_REQUIRED_HEALTH = 50;
|
@@ -137,6 +137,10 @@ export default class GamePlayerEntity extends PlayerEntity {
|
|
137
137
|
|
138
138
|
// Start auto heal ticker
|
139
139
|
this._autoHealTicker();
|
140
|
+
|
141
|
+
// Reset any prior UI from respawn
|
142
|
+
this._updatePlayerUIHealth();
|
143
|
+
this._updatePlayerUIMoney();
|
140
144
|
}
|
141
145
|
|
142
146
|
public addMoney(amount: number) {
|
@@ -263,10 +267,16 @@ export default class GamePlayerEntity extends PlayerEntity {
|
|
263
267
|
this.playerController.walkVelocity = downed ? 1 : 4;
|
264
268
|
this.playerController.jumpVelocity = downed ? 0 : 10;
|
265
269
|
|
270
|
+
if (!downed && this._gun) {
|
271
|
+
this._gun.setParentAnimations();
|
272
|
+
}
|
273
|
+
|
266
274
|
if (downed) {
|
267
275
|
this._downedSceneUI.setState({ progress: 0 })
|
268
276
|
this._downedSceneUI.load(this.world);
|
269
277
|
this.world.chatManager.sendPlayerMessage(this.player, 'You are downed! A teammate can still revive you!', 'FF0000');
|
278
|
+
|
279
|
+
GameManager.instance.checkEndGame();
|
270
280
|
} else {
|
271
281
|
this._downedSceneUI.unload();
|
272
282
|
this.world.chatManager.sendPlayerMessage(this.player, 'You are back up! Thank your team & fight the horde!', '00FF00');
|
@@ -81,7 +81,7 @@ export default abstract class GunEntity extends Entity {
|
|
81
81
|
});
|
82
82
|
|
83
83
|
if (options.parent) {
|
84
|
-
this.
|
84
|
+
this.setParentAnimations();
|
85
85
|
}
|
86
86
|
}
|
87
87
|
|
@@ -168,6 +168,18 @@ export default abstract class GunEntity extends Entity {
|
|
168
168
|
}, this.reloadTimeMs);
|
169
169
|
}
|
170
170
|
|
171
|
+
public setParentAnimations() {
|
172
|
+
if (!this.parent || !this.parent.world) {
|
173
|
+
return;
|
174
|
+
}
|
175
|
+
|
176
|
+
const playerEntityController = this.parent.controller as PlayerEntityController;
|
177
|
+
|
178
|
+
playerEntityController.idleLoopedAnimations = [ this.idleAnimation, 'idle_lower' ];
|
179
|
+
playerEntityController.walkLoopedAnimations = [ this.idleAnimation, 'walk_lower' ];
|
180
|
+
playerEntityController.runLoopedAnimations = [ this.idleAnimation, 'run_lower' ];
|
181
|
+
}
|
182
|
+
|
171
183
|
// override to create specific gun shoot logic
|
172
184
|
public shoot() {
|
173
185
|
if (!this.parent || !this.parent.world) {
|
@@ -221,18 +233,6 @@ export default abstract class GunEntity extends Entity {
|
|
221
233
|
}
|
222
234
|
}
|
223
235
|
|
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
236
|
private _updatePlayerUIAmmo() {
|
237
237
|
if (!this.parent || !this.parent.world) {
|
238
238
|
return;
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { startServer } from 'hytopia';
|
2
|
-
import GamePlayerEntity from './classes/GamePlayerEntity';
|
3
2
|
import worldMap from './assets/maps/terrain.json';
|
4
3
|
|
5
4
|
import GameManager from './classes/GameManager';
|
@@ -8,8 +7,6 @@ startServer(world => {
|
|
8
7
|
// Load map.
|
9
8
|
world.loadMap(worldMap);
|
10
9
|
|
11
|
-
//world.simulation.enableDebugRaycasting(true);
|
12
|
-
|
13
10
|
// Setup lighting
|
14
11
|
world.setAmbientLightIntensity(0.0001);
|
15
12
|
world.setAmbientLightColor({ r: 255, g: 192, b: 192 });
|
@@ -21,12 +18,10 @@ startServer(world => {
|
|
21
18
|
// Spawn a player entity when a player joins the game.
|
22
19
|
world.onPlayerJoin = player => {
|
23
20
|
if (GameManager.instance.isStarted) {
|
24
|
-
return world.chatManager.sendPlayerMessage(player, 'This round has already started,
|
21
|
+
return world.chatManager.sendPlayerMessage(player, 'This round has already started, you will automatically join when the next round starts. While you wait, you can fly around as a spectator by using W, A, S, D.', 'FF0000');
|
25
22
|
}
|
26
23
|
|
27
|
-
|
28
|
-
|
29
|
-
playerEntity.spawn(world, { x: 2, y: 10, z: 19 });
|
24
|
+
GameManager.instance.spawnPlayerEntity(player);
|
30
25
|
};
|
31
26
|
|
32
27
|
// Despawn all player entities when a player leaves the game.
|