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.
- 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/potion-consume.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.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/auto-sniper.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/gravity-potion.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/revolver.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/icons/submachine-gun.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/auto-sniper/auto-sniper-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb.md5 +1 -0
- package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb.md5 +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/revolver/revolver-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.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/.optimized/submachine-gun/submachine-gun-named-nodes.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.glb +0 -0
- package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.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/auto-sniper.glb +0 -0
- package/examples/hygrounds/assets/models/items/bolt-action-sniper.glb +0 -0
- package/examples/hygrounds/assets/models/items/gravity-potion.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/revolver.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/items/submachine-gun.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 +1122 -0
- package/examples/hygrounds/bun.lock +503 -0
- package/examples/hygrounds/classes/ChestEntity.ts +133 -0
- package/examples/hygrounds/classes/GameManager.ts +422 -0
- package/examples/hygrounds/classes/GamePlayerEntity.ts +595 -0
- package/examples/hygrounds/classes/GunEntity.ts +259 -0
- package/examples/hygrounds/classes/ItemEntity.ts +225 -0
- package/examples/hygrounds/classes/ItemFactory.ts +61 -0
- package/examples/hygrounds/classes/MeleeWeaponEntity.ts +138 -0
- package/examples/hygrounds/classes/TerrainDamageManager.ts +56 -0
- package/examples/hygrounds/classes/items/GravityPotionEntity.ts +48 -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/AutoSniperEntity.ts +43 -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/RevolverEntity.ts +46 -0
- package/examples/hygrounds/classes/weapons/RocketLauncherEntity.ts +195 -0
- package/examples/hygrounds/classes/weapons/ShotgunEntity.ts +84 -0
- package/examples/hygrounds/classes/weapons/SubmachineGunEntity.ts +43 -0
- package/examples/hygrounds/gameConfig.ts +430 -0
- package/examples/hygrounds/index.ts +41 -0
- package/examples/hygrounds/package.json +16 -0
- package/examples/player-persistence/README.md +3 -0
- package/examples/player-persistence/assets/map.json +2623 -0
- package/examples/player-persistence/dev/persistence/player-player-1.json +6 -0
- package/examples/player-persistence/dev/persistence/test.json +3 -0
- package/examples/player-persistence/index.ts +126 -0
- package/examples/player-persistence/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,133 @@
|
|
1
|
+
import {
|
2
|
+
Audio,
|
3
|
+
Collider,
|
4
|
+
Entity,
|
5
|
+
EntityOptions,
|
6
|
+
QuaternionLike,
|
7
|
+
SceneUI,
|
8
|
+
Vector3Like,
|
9
|
+
World,
|
10
|
+
} from 'hytopia';
|
11
|
+
|
12
|
+
import { CHEST_DROP_ITEMS, CHEST_MAX_DROP_ITEMS, CHEST_OPEN_DESPAWN_MS } from '../gameConfig';
|
13
|
+
import ItemFactory from './ItemFactory';
|
14
|
+
|
15
|
+
export default class ChestEntity extends Entity {
|
16
|
+
private _labelSceneUI: SceneUI;
|
17
|
+
private _openAudio: Audio;
|
18
|
+
private _opened: boolean = false;
|
19
|
+
|
20
|
+
public constructor(options: EntityOptions = {}) {
|
21
|
+
super({
|
22
|
+
modelUri: 'models/environment/chest.gltf',
|
23
|
+
modelScale: 1,
|
24
|
+
name: 'Item Chest',
|
25
|
+
rigidBodyOptions: {
|
26
|
+
additionalMass: 10000,
|
27
|
+
enabledPositions: { x: false, y: true, z: false },
|
28
|
+
enabledRotations: { x: false, y: false, z: false },
|
29
|
+
ccdEnabled: true,
|
30
|
+
gravityScale: 0.3, // we want it to drop slow when spawned mid-game in the sky randomly.
|
31
|
+
colliders: [
|
32
|
+
{
|
33
|
+
...Collider.optionsFromModelUri('models/environment/chest.gltf'),
|
34
|
+
radius: 0.45, // collider isn't calculating perfect because of the coin positions in the model.
|
35
|
+
bounciness: 0.25,
|
36
|
+
}
|
37
|
+
]
|
38
|
+
},
|
39
|
+
...options,
|
40
|
+
});
|
41
|
+
|
42
|
+
this._labelSceneUI = this._createLabelUI();
|
43
|
+
|
44
|
+
this._openAudio = new Audio({
|
45
|
+
attachedToEntity: this,
|
46
|
+
uri: 'audio/sfx/chest-open-2.mp3',
|
47
|
+
volume: 0.7,
|
48
|
+
referenceDistance: 8,
|
49
|
+
});
|
50
|
+
}
|
51
|
+
|
52
|
+
public open(): void {
|
53
|
+
if (this._opened || !this.world) return;
|
54
|
+
|
55
|
+
this._opened = true;
|
56
|
+
this._openAudio.play(this.world, true);
|
57
|
+
this._labelSceneUI.unload();
|
58
|
+
|
59
|
+
this.startModelOneshotAnimations(['opening']);
|
60
|
+
|
61
|
+
setTimeout(() => {
|
62
|
+
this.startModelLoopedAnimations([ 'open' ]);
|
63
|
+
|
64
|
+
const numItems = Math.floor(Math.random() * CHEST_MAX_DROP_ITEMS) + 1;
|
65
|
+
|
66
|
+
for (let i = 0; i < numItems; i++) {
|
67
|
+
this._spawnRandomChestItem();
|
68
|
+
}
|
69
|
+
}, 600);
|
70
|
+
|
71
|
+
// despawn chest after 20 seconds
|
72
|
+
setTimeout(() => {
|
73
|
+
if (this.isSpawned) {
|
74
|
+
this.despawn();
|
75
|
+
}
|
76
|
+
}, CHEST_OPEN_DESPAWN_MS);
|
77
|
+
}
|
78
|
+
|
79
|
+
public override spawn(world: World, position: Vector3Like, rotation?: QuaternionLike): void {
|
80
|
+
super.spawn(world, position, rotation);
|
81
|
+
this._labelSceneUI.load(world);
|
82
|
+
}
|
83
|
+
|
84
|
+
private _createLabelUI(): SceneUI {
|
85
|
+
return new SceneUI({
|
86
|
+
attachedToEntity: this,
|
87
|
+
templateId: 'chest-label',
|
88
|
+
state: { name: this.name },
|
89
|
+
viewDistance: 8,
|
90
|
+
offset: { x: 0, y: 0.85, z: 0 },
|
91
|
+
});
|
92
|
+
}
|
93
|
+
|
94
|
+
private async _spawnRandomChestItem(): Promise<void> {
|
95
|
+
if (!this.world) return;
|
96
|
+
|
97
|
+
// Calculate total weight
|
98
|
+
const totalWeight = CHEST_DROP_ITEMS.reduce((sum, item) => sum + item.pickWeight, 0);
|
99
|
+
|
100
|
+
// Get random value between 0 and total weight
|
101
|
+
let random = Math.random() * totalWeight;
|
102
|
+
|
103
|
+
// Find the selected item
|
104
|
+
let selectedItem = CHEST_DROP_ITEMS[0];
|
105
|
+
for (const item of CHEST_DROP_ITEMS) {
|
106
|
+
random -= item.pickWeight;
|
107
|
+
if (random <= 0) {
|
108
|
+
selectedItem = item;
|
109
|
+
break;
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
const item = await ItemFactory.createItem(selectedItem.itemId);
|
114
|
+
|
115
|
+
if (item) {
|
116
|
+
item.spawn(this.world, {
|
117
|
+
x: this.position.x,
|
118
|
+
y: this.position.y + 2,
|
119
|
+
z: this.position.z,
|
120
|
+
});
|
121
|
+
|
122
|
+
item.startDespawnTimer();
|
123
|
+
|
124
|
+
item.applyImpulse({ // apply an impulse in a random x/z direction
|
125
|
+
x: (Math.random() - 0.5) * 10 * item.mass,
|
126
|
+
y: 5 * item.mass,
|
127
|
+
z: (Math.random() - 0.5) * 10 * item.mass,
|
128
|
+
});
|
129
|
+
} else {
|
130
|
+
console.error(`Failed to create item: ${selectedItem.itemId}`);
|
131
|
+
}
|
132
|
+
}
|
133
|
+
}
|
@@ -0,0 +1,422 @@
|
|
1
|
+
import {
|
2
|
+
GameServer,
|
3
|
+
Player,
|
4
|
+
Quaternion,
|
5
|
+
Vector3Like,
|
6
|
+
World,
|
7
|
+
} from 'hytopia';
|
8
|
+
|
9
|
+
import worldMap from '../assets/map.json';
|
10
|
+
|
11
|
+
import {
|
12
|
+
BEDROCK_BLOCK_ID,
|
13
|
+
CHEST_SPAWNS,
|
14
|
+
CHEST_SPAWNS_AT_START,
|
15
|
+
CHEST_DROP_INTERVAL_MS,
|
16
|
+
CHEST_DROP_REGION_AABB,
|
17
|
+
GAME_DURATION_MS,
|
18
|
+
ITEM_SPAWNS,
|
19
|
+
ITEM_SPAWNS_AT_START,
|
20
|
+
ITEM_SPAWN_ITEMS,
|
21
|
+
MINIMUM_PLAYERS_TO_START,
|
22
|
+
SPAWN_REGION_AABB,
|
23
|
+
} from '../gameConfig';
|
24
|
+
|
25
|
+
import GamePlayerEntity from './GamePlayerEntity';
|
26
|
+
import ChestEntity from './ChestEntity';
|
27
|
+
import ItemFactory from './ItemFactory';
|
28
|
+
|
29
|
+
export default class GameManager {
|
30
|
+
public static readonly instance = new GameManager();
|
31
|
+
|
32
|
+
public world: World | undefined;
|
33
|
+
private _chestDropInterval: NodeJS.Timeout | undefined;
|
34
|
+
private _gameStartAt: number = 0;
|
35
|
+
private _gameTimer: NodeJS.Timeout | undefined;
|
36
|
+
private _playerCount: number = 0;
|
37
|
+
private _restartTimer: NodeJS.Timeout | undefined;
|
38
|
+
private _killCounter: Map<string, number> = new Map();
|
39
|
+
private _gameActive: boolean = false;
|
40
|
+
|
41
|
+
public get isGameActive(): boolean { return this._gameActive; }
|
42
|
+
|
43
|
+
public get playerCount(): number { return this._playerCount; }
|
44
|
+
public set playerCount(value: number) {
|
45
|
+
this._playerCount = value;
|
46
|
+
this._updatePlayerCountUI();
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* Sets up the game world and waits for players to join
|
51
|
+
*/
|
52
|
+
public setupGame(world: World) {
|
53
|
+
this.world = world;
|
54
|
+
this._spawnBedrock(world);
|
55
|
+
this._waitForPlayersToStart();
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Starts a new game round
|
60
|
+
*/
|
61
|
+
public startGame() {
|
62
|
+
if (!this.world) return;
|
63
|
+
|
64
|
+
// Clean up any previous game state
|
65
|
+
this._cleanup();
|
66
|
+
|
67
|
+
// Set game as active
|
68
|
+
this._gameActive = true;
|
69
|
+
this._gameStartAt = Date.now();
|
70
|
+
|
71
|
+
// Spawn initial game elements
|
72
|
+
this._spawnStartingChests();
|
73
|
+
this._spawnStartingItems();
|
74
|
+
this._startChestDropInterval();
|
75
|
+
|
76
|
+
// Move all players to random spawn positions
|
77
|
+
this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
|
78
|
+
playerEntity.setPosition(this.getRandomSpawnPosition());
|
79
|
+
playerEntity.player.ui.sendData({ type: 'game-start' });
|
80
|
+
this._sendGameStartAnnouncements(playerEntity.player);
|
81
|
+
});
|
82
|
+
|
83
|
+
// Set game timer
|
84
|
+
this._gameTimer = setTimeout(() => this.endGame(), GAME_DURATION_MS);
|
85
|
+
|
86
|
+
// Sync UI for all players
|
87
|
+
this._syncAllPlayersUI();
|
88
|
+
}
|
89
|
+
|
90
|
+
/**
|
91
|
+
* Ends the current game round and schedules the next one
|
92
|
+
*/
|
93
|
+
public endGame() {
|
94
|
+
if (!this.world || !this._gameActive) return;
|
95
|
+
|
96
|
+
this._gameActive = false;
|
97
|
+
this.world.chatManager.sendBroadcastMessage('Game over! Starting the next round in 10 seconds...', 'FF0000');
|
98
|
+
|
99
|
+
this._focusWinningPlayer();
|
100
|
+
|
101
|
+
// Clear any existing restart timer
|
102
|
+
if (this._restartTimer) {
|
103
|
+
clearTimeout(this._restartTimer);
|
104
|
+
}
|
105
|
+
|
106
|
+
this._restartTimer = setTimeout(() => this.startGame(), 10 * 1000);
|
107
|
+
}
|
108
|
+
|
109
|
+
/**
|
110
|
+
* Spawns a player entity in the world
|
111
|
+
*/
|
112
|
+
public spawnPlayerEntity(player: Player) {
|
113
|
+
if (!this.world) return;
|
114
|
+
|
115
|
+
const playerEntity = new GamePlayerEntity(player);
|
116
|
+
playerEntity.spawn(this.world, this.getRandomSpawnPosition());
|
117
|
+
|
118
|
+
// Sync UI for the new player
|
119
|
+
this.syncTimer(player);
|
120
|
+
this.syncLeaderboard(player);
|
121
|
+
|
122
|
+
// Send start announcement if game is active
|
123
|
+
if (this._gameActive) {
|
124
|
+
player.ui.sendData({ type: 'game-start' });
|
125
|
+
this._sendGameStartAnnouncements(player);
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
/**
|
130
|
+
* Increments kill count for a player and updates the leaderboard
|
131
|
+
*/
|
132
|
+
public addKill(playerId: string): void {
|
133
|
+
const killCount = this._killCounter.get(playerId) ?? 0;
|
134
|
+
const newKillCount = killCount + 1;
|
135
|
+
|
136
|
+
this._killCounter.set(playerId, newKillCount);
|
137
|
+
this._updateLeaderboardUI(playerId, newKillCount);
|
138
|
+
}
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Gets a random spawn position within the defined spawn region
|
142
|
+
*/
|
143
|
+
public getRandomSpawnPosition(): Vector3Like {
|
144
|
+
return {
|
145
|
+
x: SPAWN_REGION_AABB.min.x + Math.random() * (SPAWN_REGION_AABB.max.x - SPAWN_REGION_AABB.min.x),
|
146
|
+
y: SPAWN_REGION_AABB.min.y + Math.random() * (SPAWN_REGION_AABB.max.y - SPAWN_REGION_AABB.min.y),
|
147
|
+
z: SPAWN_REGION_AABB.min.z + Math.random() * (SPAWN_REGION_AABB.max.z - SPAWN_REGION_AABB.min.z),
|
148
|
+
};
|
149
|
+
}
|
150
|
+
|
151
|
+
/**
|
152
|
+
* Returns the current kill counts for all players
|
153
|
+
*/
|
154
|
+
public getKillCounts(): Record<string, number> {
|
155
|
+
return Object.fromEntries(this._killCounter);
|
156
|
+
}
|
157
|
+
|
158
|
+
/**
|
159
|
+
* Syncs the leaderboard UI for a specific player
|
160
|
+
*/
|
161
|
+
public syncLeaderboard(player: Player) {
|
162
|
+
if (!this.world) return;
|
163
|
+
|
164
|
+
player.ui.sendData({
|
165
|
+
type: 'leaderboard-sync',
|
166
|
+
killCounts: this.getKillCounts(),
|
167
|
+
});
|
168
|
+
}
|
169
|
+
|
170
|
+
/**
|
171
|
+
* Syncs the game timer UI for a specific player
|
172
|
+
*/
|
173
|
+
public syncTimer(player: Player) {
|
174
|
+
if (!this.world || !this._gameStartAt) return;
|
175
|
+
|
176
|
+
player.ui.sendData({
|
177
|
+
type: 'timer-sync',
|
178
|
+
startedAt: this._gameStartAt,
|
179
|
+
endsAt: this._gameStartAt + GAME_DURATION_MS,
|
180
|
+
});
|
181
|
+
}
|
182
|
+
|
183
|
+
/**
|
184
|
+
* Resets the leaderboard and syncs it for all players
|
185
|
+
*/
|
186
|
+
public resetLeaderboard() {
|
187
|
+
if (!this.world) return;
|
188
|
+
|
189
|
+
this._killCounter.clear();
|
190
|
+
|
191
|
+
GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
|
192
|
+
this.syncLeaderboard(player);
|
193
|
+
});
|
194
|
+
}
|
195
|
+
|
196
|
+
/**
|
197
|
+
* Cleans up the game state for a new round
|
198
|
+
*/
|
199
|
+
private _cleanup() {
|
200
|
+
if (!this.world) return;
|
201
|
+
|
202
|
+
// Reset map to initial state
|
203
|
+
this.world.loadMap(worldMap);
|
204
|
+
|
205
|
+
// Reset player state
|
206
|
+
this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
|
207
|
+
if (playerEntity instanceof GamePlayerEntity) {
|
208
|
+
playerEntity.setActiveInventorySlotIndex(0); // reset to pickaxe at slot 0
|
209
|
+
playerEntity.dropAllInventoryItems();
|
210
|
+
playerEntity.resetCamera();
|
211
|
+
playerEntity.resetMaterials();
|
212
|
+
playerEntity.health = 100;
|
213
|
+
playerEntity.shield = 0;
|
214
|
+
}
|
215
|
+
});
|
216
|
+
|
217
|
+
// Remove non-player entities except pickaxes
|
218
|
+
this.world.entityManager.getAllEntities().forEach(entity => {
|
219
|
+
if (!(entity instanceof GamePlayerEntity) && entity.tag !== 'pickaxe') {
|
220
|
+
// allow 1 event loop for drop to resolve, there's some
|
221
|
+
// weird bug here otherwise we need to investigate later.
|
222
|
+
setTimeout(() => {
|
223
|
+
if (entity.isSpawned) {
|
224
|
+
entity.despawn();
|
225
|
+
}
|
226
|
+
}, 0);
|
227
|
+
}
|
228
|
+
});
|
229
|
+
|
230
|
+
// Clear timers
|
231
|
+
if (this._gameTimer) {
|
232
|
+
clearTimeout(this._gameTimer);
|
233
|
+
this._gameTimer = undefined;
|
234
|
+
}
|
235
|
+
|
236
|
+
if (this._chestDropInterval) {
|
237
|
+
clearInterval(this._chestDropInterval);
|
238
|
+
this._chestDropInterval = undefined;
|
239
|
+
}
|
240
|
+
|
241
|
+
// Reset leaderboard
|
242
|
+
this.resetLeaderboard();
|
243
|
+
}
|
244
|
+
|
245
|
+
public _focusWinningPlayer() {
|
246
|
+
if (!this.world) return;
|
247
|
+
|
248
|
+
// Find player with most kills
|
249
|
+
let highestKills = 0;
|
250
|
+
let winningPlayer = '';
|
251
|
+
|
252
|
+
this._killCounter.forEach((kills, player) => {
|
253
|
+
if (kills > highestKills) {
|
254
|
+
highestKills = kills;
|
255
|
+
winningPlayer = player;
|
256
|
+
}
|
257
|
+
});
|
258
|
+
|
259
|
+
// Get winning player entity
|
260
|
+
const winningPlayerEntity = this.world.entityManager
|
261
|
+
.getAllPlayerEntities()
|
262
|
+
.find(entity => entity.player.username === winningPlayer);
|
263
|
+
|
264
|
+
if (!winningPlayerEntity) return;
|
265
|
+
|
266
|
+
this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
|
267
|
+
if (playerEntity instanceof GamePlayerEntity) {
|
268
|
+
if (playerEntity.player.username !== winningPlayer) { // don't change camera for the winner
|
269
|
+
playerEntity.focusCameraOnPlayer(winningPlayerEntity as GamePlayerEntity);
|
270
|
+
}
|
271
|
+
|
272
|
+
playerEntity.player.ui.sendData({
|
273
|
+
type: 'announce-winner',
|
274
|
+
username: winningPlayer,
|
275
|
+
});
|
276
|
+
}
|
277
|
+
});
|
278
|
+
}
|
279
|
+
|
280
|
+
/**
|
281
|
+
* Syncs UI for all connected players
|
282
|
+
*/
|
283
|
+
private _syncAllPlayersUI() {
|
284
|
+
if (!this.world) return;
|
285
|
+
|
286
|
+
const players = GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world);
|
287
|
+
players.forEach(player => {
|
288
|
+
this.syncTimer(player);
|
289
|
+
this.syncLeaderboard(player);
|
290
|
+
});
|
291
|
+
}
|
292
|
+
|
293
|
+
/**
|
294
|
+
* Sends game start announcements to a specific player
|
295
|
+
*/
|
296
|
+
private _sendGameStartAnnouncements(player: Player) {
|
297
|
+
if (!this.world) return;
|
298
|
+
|
299
|
+
this.world.chatManager.sendPlayerMessage(player, 'Game started - most kills wins!', '00FF00');
|
300
|
+
this.world.chatManager.sendPlayerMessage(player, '- Search for chests and weapons to survive');
|
301
|
+
this.world.chatManager.sendPlayerMessage(player, '- Break blocks with your pickaxe to gain materials');
|
302
|
+
this.world.chatManager.sendPlayerMessage(player, '- Right click to spend 3 materials to place a block');
|
303
|
+
this.world.chatManager.sendPlayerMessage(player, '- Some weapons zoom with "Z". Drop items with "Q"');
|
304
|
+
}
|
305
|
+
|
306
|
+
/**
|
307
|
+
* Creates bedrock floor for the game world
|
308
|
+
*/
|
309
|
+
private _spawnBedrock(world: World) {
|
310
|
+
for (let x = -50; x <= 50; x++) {
|
311
|
+
for (let z = -50; z <= 50; z++) {
|
312
|
+
world.chunkLattice.setBlock({ x, y: -1, z }, BEDROCK_BLOCK_ID);
|
313
|
+
}
|
314
|
+
}
|
315
|
+
}
|
316
|
+
|
317
|
+
/**
|
318
|
+
* Spawns initial chests at random positions
|
319
|
+
*/
|
320
|
+
private _spawnStartingChests() {
|
321
|
+
if (!this.world) return;
|
322
|
+
|
323
|
+
const shuffledChestSpawns = [...CHEST_SPAWNS].sort(() => Math.random() - 0.5);
|
324
|
+
const selectedChestSpawns = shuffledChestSpawns.slice(0, CHEST_SPAWNS_AT_START);
|
325
|
+
|
326
|
+
for (const spawn of selectedChestSpawns) {
|
327
|
+
const chest = new ChestEntity();
|
328
|
+
chest.spawn(this.world, spawn.position, spawn.rotation);
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
/**
|
333
|
+
* Spawns initial items at random positions
|
334
|
+
*/
|
335
|
+
private _spawnStartingItems() {
|
336
|
+
if (!this.world) return;
|
337
|
+
|
338
|
+
const shuffledItemSpawns = [...ITEM_SPAWNS].sort(() => Math.random() - 0.5);
|
339
|
+
const selectedItemSpawns = shuffledItemSpawns.slice(0, ITEM_SPAWNS_AT_START);
|
340
|
+
const totalWeight = ITEM_SPAWN_ITEMS.reduce((sum, item) => sum + item.pickWeight, 0);
|
341
|
+
|
342
|
+
selectedItemSpawns.forEach(async spawn => {
|
343
|
+
// Select random item based on weight
|
344
|
+
let random = Math.random() * totalWeight;
|
345
|
+
let selectedItem = ITEM_SPAWN_ITEMS[0];
|
346
|
+
|
347
|
+
for (const item of ITEM_SPAWN_ITEMS) {
|
348
|
+
random -= item.pickWeight;
|
349
|
+
if (random <= 0) {
|
350
|
+
selectedItem = item;
|
351
|
+
break;
|
352
|
+
}
|
353
|
+
}
|
354
|
+
|
355
|
+
const item = await ItemFactory.createItem(selectedItem.itemId);
|
356
|
+
item.spawn(this.world!, spawn.position, Quaternion.fromEuler(0, Math.random() * 360 - 180, 0));
|
357
|
+
});
|
358
|
+
}
|
359
|
+
|
360
|
+
/**
|
361
|
+
* Starts the interval for dropping chests during the game
|
362
|
+
*/
|
363
|
+
private _startChestDropInterval() {
|
364
|
+
if (this._chestDropInterval) {
|
365
|
+
clearInterval(this._chestDropInterval);
|
366
|
+
}
|
367
|
+
|
368
|
+
this._chestDropInterval = setInterval(() => {
|
369
|
+
if (!this.world || !this._gameActive) return;
|
370
|
+
|
371
|
+
const randomPosition = {
|
372
|
+
x: Math.floor(Math.random() * (CHEST_DROP_REGION_AABB.max.x - CHEST_DROP_REGION_AABB.min.x + 1)) + CHEST_DROP_REGION_AABB.min.x,
|
373
|
+
y: CHEST_DROP_REGION_AABB.min.y,
|
374
|
+
z: Math.floor(Math.random() * (CHEST_DROP_REGION_AABB.max.z - CHEST_DROP_REGION_AABB.min.z + 1)) + CHEST_DROP_REGION_AABB.min.z,
|
375
|
+
};
|
376
|
+
|
377
|
+
const chest = new ChestEntity();
|
378
|
+
const randomRotation = Quaternion.fromEuler(0, [0, 90, -90, 180][Math.floor(Math.random() * 4)], 0);
|
379
|
+
chest.spawn(this.world, randomPosition, randomRotation);
|
380
|
+
}, CHEST_DROP_INTERVAL_MS);
|
381
|
+
}
|
382
|
+
|
383
|
+
/**
|
384
|
+
* Updates the leaderboard UI for all players
|
385
|
+
*/
|
386
|
+
private _updateLeaderboardUI(username: string, killCount: number) {
|
387
|
+
if (!this.world) return;
|
388
|
+
|
389
|
+
GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
|
390
|
+
player.ui.sendData({
|
391
|
+
type: 'leaderboard-update',
|
392
|
+
username,
|
393
|
+
killCount,
|
394
|
+
});
|
395
|
+
});
|
396
|
+
}
|
397
|
+
|
398
|
+
private _updatePlayerCountUI() {
|
399
|
+
setTimeout(() => { // have to wait 1 tick, we need to figure out this race condition later
|
400
|
+
if (!this.world) return;
|
401
|
+
|
402
|
+
GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).forEach(player => {
|
403
|
+
player.ui.sendData({ type: 'players-count', count: this._playerCount });
|
404
|
+
});
|
405
|
+
}, 25);
|
406
|
+
}
|
407
|
+
|
408
|
+
/**
|
409
|
+
* Waits for enough players to join before starting the game
|
410
|
+
*/
|
411
|
+
private _waitForPlayersToStart() {
|
412
|
+
if (!this.world) return;
|
413
|
+
|
414
|
+
const connectedPlayers = GameServer.instance.playerManager.getConnectedPlayersByWorld(this.world).length;
|
415
|
+
|
416
|
+
if (connectedPlayers >= MINIMUM_PLAYERS_TO_START) {
|
417
|
+
this.startGame();
|
418
|
+
} else {
|
419
|
+
setTimeout(() => this._waitForPlayersToStart(), 1000);
|
420
|
+
}
|
421
|
+
}
|
422
|
+
}
|