UNPKG

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
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); } }