hytopia
Version:
The HYTOPIA SDK makes it easy for developers to create massively multiplayer games using JavaScript or TypeScript.
139 lines (108 loc) • 4.24 kB
text/typescript
import {
Audio,
Entity,
Quaternion,
RaycastHit,
Vector3Like,
} from 'hytopia';
import GamePlayerEntity from './GamePlayerEntity';
import ItemEntity from './ItemEntity';
import TerrainDamageManager from './TerrainDamageManager';
import type { ItemEntityOptions } from './ItemEntity';
export type MeleeWeaponEntityOptions = {
damage: number; // The damage dealt by the weapon
attackRate: number; // Attacks per second
range: number; // The range of the melee attack
attackAudioUri: string; // The audio played when attacking
hitAudioUri: string; // The audio played when hitting an entity or block
minesMaterials: boolean; // Whether the weapon mines materials when it hits a block
} & ItemEntityOptions;
export default abstract class MeleeWeaponEntity extends ItemEntity {
protected readonly damage: number;
protected readonly attackRate: number;
protected readonly range: number;
protected readonly minesMaterials: boolean;
private _lastAttackTime: number = 0;
private _attackAudio: Audio;
private _hitAudio: Audio;
public constructor(options: MeleeWeaponEntityOptions) {
super(options);
this.damage = options.damage;
this.attackRate = options.attackRate;
this.range = options.range;
this.minesMaterials = options.minesMaterials;
this._attackAudio = new Audio({
attachedToEntity: this,
uri: options.attackAudioUri,
volume: 0.3,
referenceDistance: 3,
});
this._hitAudio = new Audio({
attachedToEntity: this,
uri: options.hitAudioUri,
volume: 0.3,
referenceDistance: 3,
});
}
public override equip(): void {
if (!this.world) return;
super.equip();
this.setRotation(Quaternion.fromEuler(-90, 0, 0));
}
public attack(): void {
if (!this.parent?.world) return;
const player = this.parent as GamePlayerEntity;
const { origin, direction } = this.getAttackOriginDirection();
this._performAttackEffects(player);
this.attackRaycast(origin, direction, this.range);
}
protected getAttackOriginDirection(): { origin: Vector3Like, direction: Vector3Like } {
const player = this.parent as GamePlayerEntity;
const { x, y, z } = player.position;
const cameraYOffset = player.player.camera.offset.y;
const direction = player.player.camera.facingDirection;
return {
origin: { x, y: y + cameraYOffset, z },
direction
};
}
protected processAttack(): boolean {
const now = performance.now();
if (this._lastAttackTime && now - this._lastAttackTime < 1000 / this.attackRate) return false;
this._lastAttackTime = now;
return true;
}
protected attackRaycast(origin: Vector3Like, direction: Vector3Like, length: number): RaycastHit | null | undefined {
if (!this.parent?.world) return;
const { world } = this.parent;
const raycastHit = world.simulation.raycast(origin, direction, length, {
filterExcludeRigidBody: this.parent.rawRigidBody,
});
if (raycastHit?.hitBlock) {
const brokeBlock = TerrainDamageManager.instance.damageBlock(world, raycastHit.hitBlock, this.damage);
if (this.minesMaterials && brokeBlock) {
const player = this.parent as GamePlayerEntity;
const blockId = raycastHit.hitBlock.blockType.id;
const materialCount = TerrainDamageManager.getBreakMaterialCount(blockId);
player.addMaterial(materialCount);
}
}
if (raycastHit?.hitEntity) {
this._handleHitEntity(raycastHit.hitEntity, direction);
}
if (raycastHit?.hitBlock || raycastHit?.hitEntity) {
this._hitAudio.play(world, true);
}
return raycastHit;
}
private _performAttackEffects(player: GamePlayerEntity): void {
player.startModelOneshotAnimations([ this.mlAnimation ]);
this._attackAudio.play(this.parent!.world!, true);
}
protected _handleHitEntity(hitEntity: Entity, hitDirection: Vector3Like): void {
if (!(hitEntity instanceof GamePlayerEntity) || hitEntity.isDead) return;
const attacker = this.parent as GamePlayerEntity;
attacker.dealtDamage(this.damage);
hitEntity.takeDamage(this.damage, hitDirection, attacker);
}
}