138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
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<IConf> {
|
|
|
|
private state!: State
|
|
private entities!: Entities
|
|
|
|
public umount() {
|
|
this.conf.fight = false
|
|
this.conf.arrows = 'ignore'
|
|
}
|
|
|
|
protected mount() {
|
|
this.state = this.load<State>(State)
|
|
this.entities = this.load<Entities>(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>(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
|
|
|
|
}
|