hytopia
Version:
The HYTOPIA SDK makes it easy for developers to create massively multiplayer games using JavaScript or TypeScript.
196 lines (164 loc) • 6.6 kB
text/typescript
import { Audio, CollisionGroup, Entity, Quaternion, Vector3Like, QuaternionLike, RigidBodyType, EntityEvent, Vector3, Collider } from 'hytopia';
import GunEntity from '../GunEntity';
import { BEDROCK_BLOCK_ID } from '../../gameConfig';
import type { GunEntityOptions } from '../GunEntity';
import type GamePlayerEntity from '../GamePlayerEntity';
const ROCKET_DESTRUCTION_RADIUS = 4;
const DEFAULT_ROCKET_LAUNCHER_OPTIONS: GunEntityOptions = {
ammo: 1,
damage: 80,
fireRate: 0.8,
heldHand: 'right',
iconImageUri: 'icons/rocket-launcher.png',
idleAnimation: 'idle_gun_right',
mlAnimation: 'shoot_gun_right',
name: 'Rocket Launcher',
maxAmmo: 1,
totalAmmo: 5,
modelUri: 'models/items/rocket-launcher.glb',
modelScale: 1.3,
range: 8,
reloadAudioUri: 'audio/sfx/rocket-launcher-reload.mp3',
reloadTimeMs: 2500,
shootAudioUri: 'audio/sfx/rocket-launcher-shoot.mp3',
};
export default class RocketLauncherEntity extends GunEntity {
public constructor(options: Partial<GunEntityOptions> = {}) {
super({ ...DEFAULT_ROCKET_LAUNCHER_OPTIONS, ...options });
}
public override shoot(): void {
if (!this.parent || !this.processShoot()) return;
super.shoot();
// Cancel input since rocket launcher requires click-to-shoot
// (this.parent as GamePlayerEntity).player.input.ml = false;
}
public override getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike } {
return {
position: { x: 0.03, y: 0.6, z: -1.5 },
rotation: Quaternion.fromEuler(0, 90, 0),
};
}
public override equip(): void {
super.equip();
this.setPosition({ x: 0, y: 0.3, z: 0.4 });
}
public override shootRaycast(origin: Vector3Like, direction: Vector3Like, length: number) {
// Instead of a raycast, we'll spawn a projectile that on collision with a block or entity explodes
// and deals damage and blows up blocks
if (!this.parent?.world) {
return;
}
const rocketMissileEntity = new Entity({
modelUri: 'models/items/rocket-missile.glb',
modelScale: 0.75,
rigidBodyOptions: {
type: RigidBodyType.KINEMATIC_VELOCITY,
colliders: [
{
...Collider.optionsFromModelUri('models/items/rocket-missile.glb', 0.75),
collisionGroups: {
belongsTo: [ CollisionGroup.ENTITY ],
collidesWith: [ CollisionGroup.BLOCK ],
}
},
],
linearVelocity: {
x: direction.x * 30,
y: direction.y * 30,
z: direction.z * 30,
},
}
});
// Create a despawn timer if it doesn't hit
setTimeout(() => {
if (rocketMissileEntity.isSpawned) {
rocketMissileEntity.despawn();
}
}, 3000);
// Convert direction vector to quaternion that faces in the direction vector
const directionQuat = Quaternion.fromEuler(
Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)) * 180 / Math.PI,
Math.atan2(direction.x, direction.z) * 180 / Math.PI,
0
);
rocketMissileEntity.on(EntityEvent.BLOCK_COLLISION, ({ blockType, colliderHandleA, colliderHandleB }) => {
if (!this.parent?.world || !rocketMissileEntity.isSpawned || blockType.isLiquid) {
return;
}
const { world } = this.parent;
const contactManifold = world.simulation.getContactManifolds(colliderHandleA, colliderHandleB)[0];
if (!contactManifold) {
return;
}
const contactPoint = contactManifold.contactPoints[0];
const contactCoordinate = {
x: Math.floor(contactPoint.x),
y: Math.floor(contactPoint.y),
z: Math.floor(contactPoint.z)
};
// Deal damage to nearby players
this.parent.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
const playerPos = Vector3.fromVector3Like(playerEntity.position);
const contactPos = Vector3.fromVector3Like(contactPoint);
const distance = playerPos.distance(contactPos);
if (distance <= ROCKET_DESTRUCTION_RADIUS) {
(playerEntity as GamePlayerEntity).takeDamage(this.damage, direction, this.parent as GamePlayerEntity);
}
});
// Break blocks
for (let dx = -ROCKET_DESTRUCTION_RADIUS; dx <= ROCKET_DESTRUCTION_RADIUS; dx++) {
for (let dy = -ROCKET_DESTRUCTION_RADIUS; dy <= ROCKET_DESTRUCTION_RADIUS; dy++) {
for (let dz = -ROCKET_DESTRUCTION_RADIUS; dz <= ROCKET_DESTRUCTION_RADIUS; dz++) {
// Calculate distance from center of explosion
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
// Only destroy blocks within the spherical radius
if (distance > ROCKET_DESTRUCTION_RADIUS) {
continue;
}
const coordinate = {
x: contactCoordinate.x + dx,
y: contactCoordinate.y + dy,
z: contactCoordinate.z + dz
}
// do not destroy bedrock!
if (world.chunkLattice.getBlockId(coordinate) !== BEDROCK_BLOCK_ID) {
world.chunkLattice.setBlock(coordinate, 0);
}
}
}
}
// Explosion Visual
const explosionEntity = new Entity({
modelUri: 'models/environment/explosion.glb',
modelScale: 0.2,
rigidBodyOptions: { type: RigidBodyType.KINEMATIC_POSITION },
});
const explosionDirectionQuat = Quaternion.fromEuler(
Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)) * 180 / Math.PI + 90,
Math.atan2(direction.x, direction.z) * 180 / Math.PI + 180, // Add 180 degrees to invert direction
0
);
explosionEntity.spawn(world, contactPoint, explosionDirectionQuat);
explosionEntity.setCollisionGroupsForSolidColliders({
belongsTo: [],
collidesWith: [],
})
const explosionEffectInterval = setInterval(() => {
if (explosionEntity.opacity <= 0) {
explosionEntity.despawn();
clearInterval(explosionEffectInterval);
return;
}
explosionEntity.setOpacity(explosionEntity.opacity - 0.1);
}, 100);
// Explosion Audio
(new Audio({
uri: 'audio/sfx/rocket-launcher-explosion.mp3',
referenceDistance: 20,
volume: 0.4,
})).play(world);
rocketMissileEntity.despawn();
});
rocketMissileEntity.spawn(this.parent.world, origin, directionQuat);
}
}