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,56 @@
|
|
1
|
+
import { Block, Vector3Like, World } from 'hytopia';
|
2
|
+
import {
|
3
|
+
BEDROCK_BLOCK_ID,
|
4
|
+
BLOCK_ID_BREAK_DAMAGE,
|
5
|
+
BLOCK_ID_MATERIALS,
|
6
|
+
} from '../gameConfig';
|
7
|
+
|
8
|
+
interface BlockDamage {
|
9
|
+
blockId: number;
|
10
|
+
totalDamage: number;
|
11
|
+
}
|
12
|
+
|
13
|
+
export default class TerrainDamageManager {
|
14
|
+
public static instance: TerrainDamageManager = new TerrainDamageManager();
|
15
|
+
|
16
|
+
private _blockDamages: Map<string, BlockDamage> = new Map();
|
17
|
+
|
18
|
+
private constructor() {}
|
19
|
+
|
20
|
+
public static getBreakMaterialCount(blockId: number): number {
|
21
|
+
return BLOCK_ID_MATERIALS[blockId] ?? BLOCK_ID_MATERIALS.default;
|
22
|
+
}
|
23
|
+
|
24
|
+
public damageBlock(world: World, block: Block, damage: number): boolean {
|
25
|
+
const coordinateKey = this._coordinateToKey(block.globalCoordinate);
|
26
|
+
let blockDamage = this._blockDamages.get(coordinateKey);
|
27
|
+
|
28
|
+
if (!blockDamage) {
|
29
|
+
const blockId = block.blockType.id;
|
30
|
+
|
31
|
+
if (block.blockType.isLiquid || blockId === BEDROCK_BLOCK_ID) {
|
32
|
+
return false;
|
33
|
+
}
|
34
|
+
|
35
|
+
blockDamage = { blockId, totalDamage: 0 };
|
36
|
+
this._blockDamages.set(coordinateKey, blockDamage);
|
37
|
+
}
|
38
|
+
|
39
|
+
blockDamage.totalDamage += damage;
|
40
|
+
|
41
|
+
const requiredBreakDamage = BLOCK_ID_BREAK_DAMAGE[blockDamage.blockId] ?? BLOCK_ID_BREAK_DAMAGE.default;
|
42
|
+
|
43
|
+
if (blockDamage.totalDamage >= requiredBreakDamage) {
|
44
|
+
world.chunkLattice.setBlock(block.globalCoordinate, 0);
|
45
|
+
this._blockDamages.delete(coordinateKey);
|
46
|
+
|
47
|
+
return true;
|
48
|
+
}
|
49
|
+
|
50
|
+
return false;
|
51
|
+
}
|
52
|
+
|
53
|
+
private _coordinateToKey(coordinate: Vector3Like): string {
|
54
|
+
return `${coordinate.x},${coordinate.y},${coordinate.z}`;
|
55
|
+
}
|
56
|
+
}
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import { Quaternion } from 'hytopia';
|
2
|
+
import ItemEntity from "../ItemEntity";
|
3
|
+
import GamePlayerEntity from '../GamePlayerEntity';
|
4
|
+
import type { ItemEntityOptions } from "../ItemEntity";
|
5
|
+
|
6
|
+
const ADD_HEALTH_AMOUNT = 50;
|
7
|
+
|
8
|
+
const DEFAULT_MEDPACK_OPTIONS: ItemEntityOptions = {
|
9
|
+
heldHand: 'right',
|
10
|
+
iconImageUri: 'icons/medpack.png',
|
11
|
+
idleAnimation: 'idle_gun_right',
|
12
|
+
mlAnimation: 'shoot_gun_right',
|
13
|
+
modelUri: 'models/items/medpack.glb',
|
14
|
+
modelScale: 0.4,
|
15
|
+
name: 'Med Pack',
|
16
|
+
consumable: true,
|
17
|
+
consumeAudioUri: 'audio/sfx/medpack-consume.mp3',
|
18
|
+
consumeTimeMs: 1000,
|
19
|
+
quantity: 1,
|
20
|
+
}
|
21
|
+
|
22
|
+
export default class MedPackEntity extends ItemEntity {
|
23
|
+
public constructor(options: Partial<ItemEntityOptions> = {}) {
|
24
|
+
super({ ...DEFAULT_MEDPACK_OPTIONS, ...options });
|
25
|
+
}
|
26
|
+
|
27
|
+
public override consume(): void {
|
28
|
+
if (!(this.parent instanceof GamePlayerEntity) || this.parent.health >= this.parent.maxHealth) {
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
|
32
|
+
this.parent.updateHealth(ADD_HEALTH_AMOUNT);
|
33
|
+
|
34
|
+
super.consume();
|
35
|
+
}
|
36
|
+
|
37
|
+
public override equip(): void {
|
38
|
+
super.equip();
|
39
|
+
|
40
|
+
this.setPosition({ x: 0, y: 0.15, z: 0.3 });
|
41
|
+
this.setRotation(Quaternion.fromEuler(-90, 0, 270));
|
42
|
+
}
|
43
|
+
}
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import { Quaternion } from 'hytopia';
|
2
|
+
import ItemEntity from "../ItemEntity";
|
3
|
+
import GamePlayerEntity from '../GamePlayerEntity';
|
4
|
+
import type { ItemEntityOptions } from "../ItemEntity";
|
5
|
+
|
6
|
+
const ADD_SHIELD_AMOUNT = 25;
|
7
|
+
|
8
|
+
const DEFAULT_SHIELD_POTION_OPTIONS: ItemEntityOptions = {
|
9
|
+
heldHand: 'right',
|
10
|
+
iconImageUri: 'icons/shield-potion.png',
|
11
|
+
idleAnimation: 'idle_gun_right',
|
12
|
+
mlAnimation: 'shoot_gun_right',
|
13
|
+
modelUri: 'models/items/shield-potion.glb',
|
14
|
+
modelScale: 0.4,
|
15
|
+
name: 'Shield Potion',
|
16
|
+
consumable: true,
|
17
|
+
consumeAudioUri: 'audio/sfx/shield-potion-consume.mp3',
|
18
|
+
consumeTimeMs: 1000,
|
19
|
+
quantity: 1,
|
20
|
+
}
|
21
|
+
|
22
|
+
export default class ShieldPotionEntity extends ItemEntity {
|
23
|
+
public constructor(options: Partial<ItemEntityOptions> = {}) {
|
24
|
+
super({ ...DEFAULT_SHIELD_POTION_OPTIONS, ...options });
|
25
|
+
}
|
26
|
+
|
27
|
+
public override consume(): void {
|
28
|
+
if (!(this.parent instanceof GamePlayerEntity) || this.parent.shield >= this.parent.maxShield) {
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
|
32
|
+
this.parent.updateShield(ADD_SHIELD_AMOUNT);
|
33
|
+
|
34
|
+
super.consume();
|
35
|
+
}
|
36
|
+
|
37
|
+
public override equip(): void {
|
38
|
+
super.equip();
|
39
|
+
|
40
|
+
this.setPosition({ x: 0, y: 0.15, z: -0.2 });
|
41
|
+
this.setRotation(Quaternion.fromEuler(-90, 0, 0));
|
42
|
+
}
|
43
|
+
}
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import { Quaternion, Vector3Like, QuaternionLike } from 'hytopia';
|
2
|
+
import GunEntity from '../GunEntity';
|
3
|
+
import type { GunEntityOptions } from '../GunEntity';
|
4
|
+
|
5
|
+
const DEFAULT_AK47_OPTIONS: GunEntityOptions = {
|
6
|
+
ammo: 25,
|
7
|
+
damage: 22,
|
8
|
+
fireRate: 5,
|
9
|
+
heldHand: 'both',
|
10
|
+
iconImageUri: 'icons/ak-47.png',
|
11
|
+
idleAnimation: 'idle_gun_both',
|
12
|
+
mlAnimation: 'shoot_gun_both',
|
13
|
+
name: 'AK-47',
|
14
|
+
maxAmmo: 25,
|
15
|
+
totalAmmo: 150,
|
16
|
+
scopeZoom: 2,
|
17
|
+
modelUri: 'models/items/ak-47.glb',
|
18
|
+
modelScale: 1.3,
|
19
|
+
range: 70,
|
20
|
+
reloadAudioUri: 'audio/sfx/rifle-reload.mp3',
|
21
|
+
reloadTimeMs: 2200,
|
22
|
+
shootAudioUri: 'audio/sfx/rifle-shoot.mp3',
|
23
|
+
};
|
24
|
+
|
25
|
+
export default class AK47Entity extends GunEntity {
|
26
|
+
public constructor(options: Partial<GunEntityOptions> = {}) {
|
27
|
+
super({ ...DEFAULT_AK47_OPTIONS, ...options });
|
28
|
+
}
|
29
|
+
|
30
|
+
public override shoot(): void {
|
31
|
+
if (!this.parent || !this.processShoot()) return;
|
32
|
+
|
33
|
+
super.shoot();
|
34
|
+
}
|
35
|
+
|
36
|
+
public override getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike } {
|
37
|
+
return {
|
38
|
+
position: { x: 0, y: 0.01, z: -1.25 },
|
39
|
+
rotation: Quaternion.fromEuler(0, 90, 0),
|
40
|
+
};
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import { Quaternion, Vector3Like, QuaternionLike } from 'hytopia';
|
2
|
+
import GunEntity from '../GunEntity';
|
3
|
+
import type { GunEntityOptions } from '../GunEntity';
|
4
|
+
|
5
|
+
const DEFAULT_AUTO_SHOTGUN_OPTIONS: GunEntityOptions = {
|
6
|
+
ammo: 6,
|
7
|
+
damage: 10, // Per pellet (7 pellets = 70 max damage)
|
8
|
+
fireRate: 1.5,
|
9
|
+
heldHand: 'both',
|
10
|
+
iconImageUri: 'icons/auto-shotgun.png',
|
11
|
+
idleAnimation: 'idle_gun_both',
|
12
|
+
mlAnimation: 'shoot_gun_both',
|
13
|
+
name: 'Auto Shotgun',
|
14
|
+
maxAmmo: 6,
|
15
|
+
totalAmmo: 30,
|
16
|
+
modelUri: 'models/items/auto-shotgun.glb',
|
17
|
+
modelScale: 1.2,
|
18
|
+
range: 8,
|
19
|
+
reloadAudioUri: 'audio/sfx/shotgun-reload.mp3',
|
20
|
+
reloadTimeMs: 3500,
|
21
|
+
shootAudioUri: 'audio/sfx/shotgun-shoot.mp3',
|
22
|
+
};
|
23
|
+
|
24
|
+
export default class AutoShotgunEntity extends GunEntity {
|
25
|
+
public constructor(options: Partial<GunEntityOptions> = {}) {
|
26
|
+
super({ ...DEFAULT_AUTO_SHOTGUN_OPTIONS, ...options });
|
27
|
+
}
|
28
|
+
|
29
|
+
public override shoot(): void {
|
30
|
+
if (!this.parent || !this.processShoot()) return;
|
31
|
+
|
32
|
+
super.shoot();
|
33
|
+
}
|
34
|
+
|
35
|
+
public override getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike } {
|
36
|
+
return {
|
37
|
+
position: { x: 0.015, y: 0, z: -1 },
|
38
|
+
rotation: Quaternion.fromEuler(0, 90, 0),
|
39
|
+
};
|
40
|
+
}
|
41
|
+
|
42
|
+
public override shootRaycast(origin: Vector3Like, direction: Vector3Like, length: number) {
|
43
|
+
// Create spread pattern for auto-shotgun pellets using angles relative to direction
|
44
|
+
const spreadAngles = [
|
45
|
+
{ x: 0, y: 0 }, // Center
|
46
|
+
{ x: 0.05, y: 0.05 }, // Upper right
|
47
|
+
{ x: -0.05, y: 0.05 }, // Upper left
|
48
|
+
{ x: 0.07, y: 0 }, // Right
|
49
|
+
{ x: -0.07, y: 0 }, // Left
|
50
|
+
{ x: 0.05, y: -0.05 }, // Lower right
|
51
|
+
{ x: -0.05, y: -0.05 } // Lower left
|
52
|
+
];
|
53
|
+
|
54
|
+
// Fire each pellet with spread applied to original direction
|
55
|
+
for (const angle of spreadAngles) {
|
56
|
+
// Calculate spread direction relative to original direction
|
57
|
+
const spreadDirection = {
|
58
|
+
x: direction.x + (direction.z * angle.x), // Add horizontal spread
|
59
|
+
y: direction.y + angle.y, // Add vertical spread
|
60
|
+
z: direction.z - (direction.x * angle.x) // Maintain direction magnitude
|
61
|
+
};
|
62
|
+
|
63
|
+
// Normalize the spread direction to maintain consistent range
|
64
|
+
const magnitude = Math.sqrt(
|
65
|
+
spreadDirection.x * spreadDirection.x +
|
66
|
+
spreadDirection.y * spreadDirection.y +
|
67
|
+
spreadDirection.z * spreadDirection.z
|
68
|
+
);
|
69
|
+
|
70
|
+
const normalizedDirection = {
|
71
|
+
x: spreadDirection.x / magnitude,
|
72
|
+
y: spreadDirection.y / magnitude,
|
73
|
+
z: spreadDirection.z / magnitude
|
74
|
+
};
|
75
|
+
|
76
|
+
super.shootRaycast(origin, normalizedDirection, length);
|
77
|
+
}
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { Quaternion, Vector3Like, QuaternionLike } from 'hytopia';
|
2
|
+
import GunEntity from '../GunEntity';
|
3
|
+
import type { GunEntityOptions } from '../GunEntity';
|
4
|
+
|
5
|
+
const DEFAULT_BOLT_ACTION_SNIPER_OPTIONS: GunEntityOptions = {
|
6
|
+
ammo: 1,
|
7
|
+
damage: 75,
|
8
|
+
fireRate: 0.5,
|
9
|
+
heldHand: 'both',
|
10
|
+
iconImageUri: 'icons/bolt-action-sniper.png',
|
11
|
+
idleAnimation: 'idle_gun_both',
|
12
|
+
mlAnimation: 'shoot_gun_both',
|
13
|
+
name: 'Bolt Action Sniper',
|
14
|
+
maxAmmo: 1,
|
15
|
+
totalAmmo: 12,
|
16
|
+
scopeZoom: 5,
|
17
|
+
modelUri: 'models/items/bolt-action-sniper.glb',
|
18
|
+
modelScale: 1.3,
|
19
|
+
range: 100,
|
20
|
+
reloadAudioUri: 'audio/sfx/sniper-reload.mp3',
|
21
|
+
reloadTimeMs: 2200,
|
22
|
+
shootAudioUri: 'audio/sfx/sniper-shoot.mp3',
|
23
|
+
};
|
24
|
+
|
25
|
+
export default class BoltActionSniperEntity extends GunEntity {
|
26
|
+
public constructor(options: Partial<GunEntityOptions> = {}) {
|
27
|
+
super({ ...DEFAULT_BOLT_ACTION_SNIPER_OPTIONS, ...options });
|
28
|
+
}
|
29
|
+
|
30
|
+
public override shoot(): void {
|
31
|
+
if (!this.parent || !this.processShoot()) return;
|
32
|
+
|
33
|
+
super.shoot();
|
34
|
+
|
35
|
+
// It's bolt action, auto reload it 300ms after a shot.
|
36
|
+
setTimeout(() => { this.reload() }, 300);
|
37
|
+
}
|
38
|
+
|
39
|
+
public override getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike } {
|
40
|
+
return { // TODO: FIX MUZZLE FLASH POSITION
|
41
|
+
position: { x: 0, y: 0.01, z: -1.25 },
|
42
|
+
rotation: Quaternion.fromEuler(0, 90, 0),
|
43
|
+
};
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import { Quaternion, Vector3Like, QuaternionLike } from 'hytopia';
|
2
|
+
import GunEntity from '../GunEntity';
|
3
|
+
import type { GunEntityOptions } from '../GunEntity';
|
4
|
+
|
5
|
+
const DEFAULT_LIGHT_MACHINE_GUN_OPTIONS: GunEntityOptions = {
|
6
|
+
ammo: 50,
|
7
|
+
damage: 15,
|
8
|
+
fireRate: 10,
|
9
|
+
heldHand: 'both',
|
10
|
+
iconImageUri: 'icons/light-machine-gun.png',
|
11
|
+
idleAnimation: 'idle_gun_both',
|
12
|
+
mlAnimation: 'shoot_gun_both',
|
13
|
+
name: 'Light Machine Gun',
|
14
|
+
maxAmmo: 50,
|
15
|
+
totalAmmo: 300,
|
16
|
+
scopeZoom: 1.5,
|
17
|
+
modelUri: 'models/items/light-machine-gun.glb',
|
18
|
+
modelScale: 1.3,
|
19
|
+
range: 50,
|
20
|
+
reloadAudioUri: 'audio/sfx/machine-gun-reload.mp3',
|
21
|
+
reloadTimeMs: 4200,
|
22
|
+
shootAudioUri: 'audio/sfx/machine-gun-shoot.mp3',
|
23
|
+
};
|
24
|
+
|
25
|
+
export default class LightMachineGunEntity extends GunEntity {
|
26
|
+
public constructor(options: Partial<GunEntityOptions> = {}) {
|
27
|
+
super({ ...DEFAULT_LIGHT_MACHINE_GUN_OPTIONS, ...options });
|
28
|
+
}
|
29
|
+
|
30
|
+
public override shoot(): void {
|
31
|
+
if (!this.parent || !this.processShoot()) return;
|
32
|
+
|
33
|
+
super.shoot();
|
34
|
+
}
|
35
|
+
|
36
|
+
public override getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike } {
|
37
|
+
return {
|
38
|
+
position: { x: 0, y: 0.05, z: -1.7 },
|
39
|
+
rotation: Quaternion.fromEuler(0, 90, 0),
|
40
|
+
};
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { Quaternion } from 'hytopia';
|
2
|
+
import MeleeWeaponEntity from '../MeleeWeaponEntity';
|
3
|
+
import type { MeleeWeaponEntityOptions } from '../MeleeWeaponEntity';
|
4
|
+
|
5
|
+
const DEFAULT_MINING_DRILL_OPTIONS: MeleeWeaponEntityOptions = {
|
6
|
+
damage: 40,
|
7
|
+
attackRate: 5,
|
8
|
+
heldHand: 'right',
|
9
|
+
iconImageUri: 'icons/mining-drill.png',
|
10
|
+
idleAnimation: 'idle_gun_right',
|
11
|
+
mlAnimation: 'shoot_gun_right',
|
12
|
+
name: 'Mining Drill',
|
13
|
+
modelUri: 'models/items/mining-drill.glb',
|
14
|
+
modelScale: 0.05,
|
15
|
+
range: 1.5,
|
16
|
+
minesMaterials: true,
|
17
|
+
attackAudioUri: 'audio/sfx/mining-drill-drilling.mp3',
|
18
|
+
hitAudioUri: 'audio/sfx/dig/dig-stone.mp3',
|
19
|
+
};
|
20
|
+
|
21
|
+
export default class MiningDrillEntity extends MeleeWeaponEntity {
|
22
|
+
public constructor(options: Partial<MeleeWeaponEntityOptions> = {}) {
|
23
|
+
super({ ...DEFAULT_MINING_DRILL_OPTIONS, ...options, tag: 'mining-drill' });
|
24
|
+
}
|
25
|
+
|
26
|
+
public override attack(): void {
|
27
|
+
if (!this.parent || !this.processAttack()) return;
|
28
|
+
|
29
|
+
super.attack();
|
30
|
+
}
|
31
|
+
|
32
|
+
public override equip(): void {
|
33
|
+
super.equip();
|
34
|
+
|
35
|
+
this.setPosition({ x: -0.3, y: 0.5, z: -0.1 });
|
36
|
+
this.setRotation(Quaternion.fromEuler(180, 0, -90));
|
37
|
+
}
|
38
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { Quaternion } from 'hytopia';
|
2
|
+
import MeleeWeaponEntity from '../MeleeWeaponEntity';
|
3
|
+
import type { MeleeWeaponEntityOptions } from '../MeleeWeaponEntity';
|
4
|
+
|
5
|
+
const DEFAULT_PICKAXE_OPTIONS: MeleeWeaponEntityOptions = {
|
6
|
+
damage: 10, // 10 hits to kill unshielded
|
7
|
+
attackRate: 4.5, // Slower attack rate to prevent spam
|
8
|
+
heldHand: 'right',
|
9
|
+
iconImageUri: 'icons/pickaxe.png',
|
10
|
+
idleAnimation: 'idle_gun_right',
|
11
|
+
mlAnimation: 'simple_interact',
|
12
|
+
name: 'Pickaxe',
|
13
|
+
modelUri: 'models/items/pickaxe.gltf',
|
14
|
+
modelScale: 1.25,
|
15
|
+
range: 2,
|
16
|
+
minesMaterials: true,
|
17
|
+
attackAudioUri: 'audio/sfx/player/player-swing-woosh.mp3',
|
18
|
+
hitAudioUri: 'audio/sfx/dig/dig-stone.mp3',
|
19
|
+
};
|
20
|
+
|
21
|
+
export default class PickaxeEntity extends MeleeWeaponEntity {
|
22
|
+
public constructor(options: Partial<MeleeWeaponEntityOptions> = {}) {
|
23
|
+
super({ ...DEFAULT_PICKAXE_OPTIONS, ...options, tag: 'pickaxe' });
|
24
|
+
}
|
25
|
+
|
26
|
+
public override attack(): void {
|
27
|
+
if (!this.parent || !this.processAttack()) return;
|
28
|
+
|
29
|
+
super.attack();
|
30
|
+
}
|
31
|
+
|
32
|
+
public override equip(): void {
|
33
|
+
super.equip();
|
34
|
+
|
35
|
+
this.setPosition({ x: 0, y: 0.2, z: 0 });
|
36
|
+
this.setRotation(Quaternion.fromEuler(-90, 0, 90));
|
37
|
+
}
|
38
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { Quaternion, Vector3Like, QuaternionLike } from 'hytopia';
|
2
|
+
import GunEntity from '../GunEntity';
|
3
|
+
import type { GunEntityOptions } from '../GunEntity';
|
4
|
+
import type GamePlayerEntity from '../GamePlayerEntity';
|
5
|
+
|
6
|
+
const DEFAULT_PISTOL_OPTIONS: GunEntityOptions = {
|
7
|
+
ammo: 15,
|
8
|
+
damage: 18,
|
9
|
+
fireRate: 6,
|
10
|
+
heldHand: 'right',
|
11
|
+
iconImageUri: 'icons/pistol.png',
|
12
|
+
idleAnimation: 'idle_gun_right',
|
13
|
+
mlAnimation: 'shoot_gun_right',
|
14
|
+
name: 'Pistol',
|
15
|
+
maxAmmo: 15,
|
16
|
+
totalAmmo: 75,
|
17
|
+
modelUri: 'models/items/pistol.glb',
|
18
|
+
modelScale: 1.3,
|
19
|
+
range: 30,
|
20
|
+
reloadAudioUri: 'audio/sfx/pistol-reload.mp3',
|
21
|
+
reloadTimeMs: 1500,
|
22
|
+
shootAudioUri: 'audio/sfx/pistol-shoot.mp3',
|
23
|
+
};
|
24
|
+
|
25
|
+
export default class PistolEntity extends GunEntity {
|
26
|
+
public constructor(options: Partial<GunEntityOptions> = {}) {
|
27
|
+
super({ ...DEFAULT_PISTOL_OPTIONS, ...options });
|
28
|
+
}
|
29
|
+
|
30
|
+
public override shoot(): void {
|
31
|
+
if (!this.parent || !this.processShoot()) return;
|
32
|
+
|
33
|
+
super.shoot();
|
34
|
+
|
35
|
+
// Cancel input since pistol requires click-to-shoot
|
36
|
+
(this.parent as GamePlayerEntity).player.input.ml = false;
|
37
|
+
}
|
38
|
+
|
39
|
+
public override getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike } {
|
40
|
+
return {
|
41
|
+
position: { x: 0.03, y: 0.1, z: -0.5 },
|
42
|
+
rotation: Quaternion.fromEuler(0, 90, 0),
|
43
|
+
};
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
@@ -0,0 +1,186 @@
|
|
1
|
+
import { Audio, Entity, Quaternion, Vector3Like, QuaternionLike, RigidBodyType, EntityEvent, Vector3 } from 'hytopia';
|
2
|
+
import GunEntity from '../GunEntity';
|
3
|
+
import { BEDROCK_BLOCK_ID } from '../../gameConfig';
|
4
|
+
import type { GunEntityOptions } from '../GunEntity';
|
5
|
+
import type GamePlayerEntity from '../GamePlayerEntity';
|
6
|
+
|
7
|
+
const ROCKET_DESTRUCTION_RADIUS = 4;
|
8
|
+
|
9
|
+
const DEFAULT_ROCKET_LAUNCHER_OPTIONS: GunEntityOptions = {
|
10
|
+
ammo: 1,
|
11
|
+
damage: 80,
|
12
|
+
fireRate: 0.8,
|
13
|
+
heldHand: 'right',
|
14
|
+
iconImageUri: 'icons/rocket-launcher.png',
|
15
|
+
idleAnimation: 'idle_gun_right',
|
16
|
+
mlAnimation: 'shoot_gun_right',
|
17
|
+
name: 'Rocket Launcher',
|
18
|
+
maxAmmo: 1,
|
19
|
+
totalAmmo: 5,
|
20
|
+
modelUri: 'models/items/rocket-launcher.glb',
|
21
|
+
modelScale: 1.3,
|
22
|
+
range: 8,
|
23
|
+
reloadAudioUri: 'audio/sfx/rocket-launcher-reload.mp3',
|
24
|
+
reloadTimeMs: 2500,
|
25
|
+
shootAudioUri: 'audio/sfx/rocket-launcher-shoot.mp3',
|
26
|
+
};
|
27
|
+
|
28
|
+
export default class RocketLauncherEntity extends GunEntity {
|
29
|
+
public constructor(options: Partial<GunEntityOptions> = {}) {
|
30
|
+
super({ ...DEFAULT_ROCKET_LAUNCHER_OPTIONS, ...options });
|
31
|
+
}
|
32
|
+
|
33
|
+
public override shoot(): void {
|
34
|
+
if (!this.parent || !this.processShoot()) return;
|
35
|
+
|
36
|
+
super.shoot();
|
37
|
+
|
38
|
+
// Cancel input since rocket launcher requires click-to-shoot
|
39
|
+
(this.parent as GamePlayerEntity).player.input.ml = false;
|
40
|
+
}
|
41
|
+
|
42
|
+
public override getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike } {
|
43
|
+
return {
|
44
|
+
position: { x: 0.03, y: 0.6, z: -1.5 },
|
45
|
+
rotation: Quaternion.fromEuler(0, 90, 0),
|
46
|
+
};
|
47
|
+
}
|
48
|
+
|
49
|
+
public override equip(): void {
|
50
|
+
super.equip();
|
51
|
+
|
52
|
+
this.setPosition({ x: 0, y: 0.3, z: 0.4 });
|
53
|
+
}
|
54
|
+
|
55
|
+
public override shootRaycast(origin: Vector3Like, direction: Vector3Like, length: number) {
|
56
|
+
// Instead of a raycast, we'll spawn a projectile that on collision with a block or entity explodes
|
57
|
+
// and deals damage and blows up blocks
|
58
|
+
if (!this.parent?.world) {
|
59
|
+
return;
|
60
|
+
}
|
61
|
+
|
62
|
+
const rocketMissileEntity = new Entity({
|
63
|
+
modelUri: 'models/items/rocket-missile.glb',
|
64
|
+
modelScale: 0.75,
|
65
|
+
rigidBodyOptions: {
|
66
|
+
type: RigidBodyType.KINEMATIC_VELOCITY,
|
67
|
+
linearVelocity: {
|
68
|
+
x: direction.x * 30,
|
69
|
+
y: direction.y * 30,
|
70
|
+
z: direction.z * 30,
|
71
|
+
},
|
72
|
+
}
|
73
|
+
});
|
74
|
+
|
75
|
+
// Create a despawn timer if it doesn't hit
|
76
|
+
setTimeout(() => {
|
77
|
+
if (rocketMissileEntity.isSpawned) {
|
78
|
+
rocketMissileEntity.despawn();
|
79
|
+
}
|
80
|
+
}, 3000);
|
81
|
+
|
82
|
+
// Convert direction vector to quaternion that faces in the direction vector
|
83
|
+
const directionQuat = Quaternion.fromEuler(
|
84
|
+
Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)) * 180 / Math.PI,
|
85
|
+
Math.atan2(direction.x, direction.z) * 180 / Math.PI,
|
86
|
+
0
|
87
|
+
);
|
88
|
+
|
89
|
+
rocketMissileEntity.on(EntityEvent.BLOCK_COLLISION, ({ blockType, colliderHandleA, colliderHandleB }) => {
|
90
|
+
if (!this.parent?.world || !rocketMissileEntity.isSpawned || blockType.isLiquid) {
|
91
|
+
return;
|
92
|
+
}
|
93
|
+
|
94
|
+
const { world } = this.parent;
|
95
|
+
const contactManifold = world.simulation.getContactManifolds(colliderHandleA, colliderHandleB)[0];
|
96
|
+
|
97
|
+
if (!contactManifold) {
|
98
|
+
return;
|
99
|
+
}
|
100
|
+
|
101
|
+
const contactPoint = contactManifold.contactPoints[0];
|
102
|
+
const contactCoordinate = {
|
103
|
+
x: Math.floor(contactPoint.x),
|
104
|
+
y: Math.floor(contactPoint.y),
|
105
|
+
z: Math.floor(contactPoint.z)
|
106
|
+
};
|
107
|
+
|
108
|
+
// Deal damage to nearby players
|
109
|
+
this.parent.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
|
110
|
+
const playerPos = Vector3.fromVector3Like(playerEntity.position);
|
111
|
+
const contactPos = Vector3.fromVector3Like(contactPoint);
|
112
|
+
const distance = playerPos.distance(contactPos);
|
113
|
+
|
114
|
+
if (distance <= ROCKET_DESTRUCTION_RADIUS) {
|
115
|
+
(playerEntity as GamePlayerEntity).takeDamage(this.damage, direction, this.parent as GamePlayerEntity);
|
116
|
+
}
|
117
|
+
});
|
118
|
+
|
119
|
+
// Break blocks
|
120
|
+
for (let dx = -ROCKET_DESTRUCTION_RADIUS; dx <= ROCKET_DESTRUCTION_RADIUS; dx++) {
|
121
|
+
for (let dy = -ROCKET_DESTRUCTION_RADIUS; dy <= ROCKET_DESTRUCTION_RADIUS; dy++) {
|
122
|
+
for (let dz = -ROCKET_DESTRUCTION_RADIUS; dz <= ROCKET_DESTRUCTION_RADIUS; dz++) {
|
123
|
+
// Calculate distance from center of explosion
|
124
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
125
|
+
|
126
|
+
// Only destroy blocks within the spherical radius
|
127
|
+
if (distance > ROCKET_DESTRUCTION_RADIUS) {
|
128
|
+
continue;
|
129
|
+
}
|
130
|
+
|
131
|
+
const coordinate = {
|
132
|
+
x: contactCoordinate.x + dx,
|
133
|
+
y: contactCoordinate.y + dy,
|
134
|
+
z: contactCoordinate.z + dz
|
135
|
+
}
|
136
|
+
|
137
|
+
// do not destroy bedrock!
|
138
|
+
if (world.chunkLattice.getBlockId(coordinate) !== BEDROCK_BLOCK_ID) {
|
139
|
+
world.chunkLattice.setBlock(coordinate, 0);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
// Explosion Visual
|
146
|
+
const explosionEntity = new Entity({
|
147
|
+
modelUri: 'models/environment/explosion.glb',
|
148
|
+
modelScale: 0.2,
|
149
|
+
rigidBodyOptions: { type: RigidBodyType.KINEMATIC_POSITION },
|
150
|
+
});
|
151
|
+
|
152
|
+
const explosionDirectionQuat = Quaternion.fromEuler(
|
153
|
+
Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)) * 180 / Math.PI + 90,
|
154
|
+
Math.atan2(direction.x, direction.z) * 180 / Math.PI + 180, // Add 180 degrees to invert direction
|
155
|
+
0
|
156
|
+
);
|
157
|
+
|
158
|
+
explosionEntity.spawn(world, contactPoint, explosionDirectionQuat);
|
159
|
+
explosionEntity.setCollisionGroupsForSolidColliders({
|
160
|
+
belongsTo: [],
|
161
|
+
collidesWith: [],
|
162
|
+
})
|
163
|
+
|
164
|
+
const explosionEffectInterval = setInterval(() => {
|
165
|
+
if (explosionEntity.opacity <= 0) {
|
166
|
+
explosionEntity.despawn();
|
167
|
+
clearInterval(explosionEffectInterval);
|
168
|
+
return;
|
169
|
+
}
|
170
|
+
|
171
|
+
explosionEntity.setOpacity(explosionEntity.opacity - 0.1);
|
172
|
+
}, 100);
|
173
|
+
|
174
|
+
// Explosion Audio
|
175
|
+
(new Audio({
|
176
|
+
uri: 'audio/sfx/rocket-launcher-explosion.mp3',
|
177
|
+
referenceDistance: 20,
|
178
|
+
volume: 0.4,
|
179
|
+
})).play(world);
|
180
|
+
|
181
|
+
rocketMissileEntity.despawn();
|
182
|
+
});
|
183
|
+
|
184
|
+
rocketMissileEntity.spawn(this.parent.world, origin, directionQuat);
|
185
|
+
}
|
186
|
+
}
|