import { ATTACK_DIST2, SERVER_TICK_RATE } from '../utils/constants' import { dist2 } from '../utils/func' import Module from '../utils/Module' import { IDelta } from '../utils/types' import Entities, { IAlive } from './Entities' import Life from './Life' import State from './State' interface IConf { /** Respawn after death */ respawn: boolean, /** Attack dangerous entities */ fight: boolean, /** Must change weapon */ weapon: 'none' | 'needed' | 'always', /** Must react to arrows */ arrows: 'ignore' | 'dodge' | 'shield' /** Time between attacks */ delay: number | 'item' | 'safe', /** Attacks multiple entities at ones */ multiaura: boolean, /** Entity to target first */ priority: 'nearest' } interface IEnemy extends IAlive { reach2: number } const SAFE_DELAY = SERVER_TICK_RATE * 2 /** Protect and fight */ export default class Combat extends Module { private state!: State private entities!: Entities public umount() { this.conf.fight = false this.conf.arrows = 'ignore' } protected mount() { this.state = this.load(State) this.entities = this.load(Entities) this.fight() this.arrows() this.client.on('combat_event', packet => { switch (packet.event) { case 0: this.logger.warn('fighting') break case 2: if (packet.playerId === this.state.state.entityId) { this.logger.error('%o killed me at %o', this.entities.getAlive(packet.entityId), this.state.position) // MAYBE: chat packet.message if (this.conf.respawn) { this.load(Life).respawn() } } else { this.logger.info('killed %o', packet) } } }) } protected getConf(): IConf { return { respawn: true, fight: true, weapon: 'always', arrows: 'dodge', delay: 'safe', multiaura: false, priority: 'nearest', } } private fight() { if (this.conf.fight) { //TODO: pick weapon if change wait //TODO: pick shield // Find dangerous entities const dangers: IEnemy[] = ([...this.entities.players, ...this.entities.livings] as IAlive[]) .filter(({ entityId }) => entityId !== this.state.state.entityId) //TODO: filter friends, creative, passives .map(e => ({ reach2: dist2(this.state.position, e), ...e })) // TODO: include box .filter(({ reach2 }) => reach2 <= ATTACK_DIST2) // MAYBE: as option .sort((a, b) => a.reach2 - b.reach2) // MAYBE: switch this.conf.priority // MAYBE: include entity speed (for tiny zombie or phantom) if (dangers.length > 0) { this.logger.info('fighting %s target(s)', dangers.length); (this.conf.multiaura ? dangers : dangers.slice(0, 1)).forEach(({ entityId }) => { this.logger.debug('attack %s', entityId) this.entities.attack(entityId) }) } const attackSpeed = 1.6 //TODO: get from inventory const delay = typeof this.conf.delay === 'number' ? this.conf.delay : ( this.conf.delay === 'safe' && dangers.length == 0 ? SAFE_DELAY : // TODO: exponential back-off 1 / attackSpeed * 20 * SERVER_TICK_RATE) setTimeout(this.fight.bind(this), delay) } } private arrows() { if (this.conf.arrows !== 'ignore') { const arrows = [...this.entities.objects] .filter(e => e.type === 2 && // arrow dist2(this.state.position, e) <= 99) if (arrows.length > 0) { this.logger.warn('%s arrows', arrows.length) this.state.translate(arrows.reduce((p, arrow) => Math.abs(arrow.velocityX) < Math.abs(arrow.velocityZ) ? { ...p, dX: p.dX + Math.sign(arrow.velocityX) } : { ...p, dZ: p.dZ + Math.sign(arrow.velocityZ) }, { dX: 0, dY: 0, dZ: 0 } as IDelta)) // MAYBE: do some trigo } setTimeout(this.arrows.bind(this), SAFE_DELAY) } } //TODO: respawn or disconnect }