cubbot/src/modules/Combat.ts

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
}