cubbot/src/modules/Combat.ts

207 lines
7.4 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 Connection from './Connection'
import Entities, { IAlive, ILiving, IPlayer } from './Entities'
import Inventory from './Inventory'
import Spawn from './Spawn'
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'
/** Players names */
friends: string[]
/** Targeted entities names */
mobs: string[]
/** Targeted entities ids (override mobs) */
mobsIds?: number[]
/** Weapons names by priority */
weapons: string[]
/** Weapons ids by priority (override weapons) */
weaponsIds?: number[]
}
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 spawn!: Spawn
private entities!: Entities
public umount() {
this.conf.fight = false
this.conf.arrows = 'ignore'
}
protected mount() {
this.state = this.load<State>(State)
this.spawn = this.load<Spawn>(Spawn)
this.entities = this.load<Entities>(Entities)
this.arrows()
if (this.conf.mobsIds && (this.conf.weaponsIds || this.conf.weapon === 'none')) {
this.fight()
} else {
this.load<Connection>(Connection).data.onReady(data => {
if (!this.conf.mobsIds) {
this.conf.mobsIds = this.conf.mobs
.map(name => data.entities![name].id)
}
if (!this.conf.weaponsIds) {
this.conf.weaponsIds = this.conf.weapons
.map(name => data.items![name].numeric_id)
}
this.fight()
})
}
this.client.on('combat_event', packet => {
switch (packet.event) {
case 0:
this.logger.warn({ msg: 'Fighting' })
break
case 2:
if (packet.playerId === this.state.state.entityId) {
this.logger.error({
msg: 'Killed', type: 'entity',
value: this.entities.getAlive(packet.entityId),
})
// MAYBE: chat packet.message
if (this.conf.respawn) {
this.spawn.respawn()
}
} else {
this.logger.info({ msg: 'Fight end', packet })
}
}
})
}
protected getConf(): IConf {
return {
respawn: true,
fight: true,
weapon: 'needed',
arrows: 'dodge',
delay: 'safe',
multiaura: false,
priority: 'nearest',
friends: [],
mobs: [ // TODO: find if aggressive
'blaze', 'cave_spider', 'creeper', 'drowned', 'elder_guardian', 'ender_dragon', 'enderman', 'endermite',
'evoker', 'evoker_fangs', 'ghast', 'giant', 'guardian', 'husk', 'illusioner', /*'iron_golem',*/
'magma_cube', 'phantom', 'pillager', 'player', 'ravager', 'shulker', 'silverfish', 'skeleton', 'slime',
/*'snow_golem',*/ 'spider', 'stray', 'vex', 'vindicator', 'witch', 'wither', 'wither_skeleton',
/*'wolf',*/ 'zombie', /*'zombie_pigman',*/ 'zombie_villager',
],
weapons: [
'diamond_sword', 'diamond_axe', 'iron_sword', 'iron_axe', 'stone_sword', 'stone_axe',
],
}
}
private fight() {
if (this.conf.fight) {
if (this.conf.weapon == 'always') {
this.equipWeapon()
}
//TODO: pick shield
// Find dangerous entities
const dangers: IEnemy[] = ([...this.entities.players, ...this.entities.livings] as IAlive[])
.filter(e => {
if ((e as ILiving).type !== undefined) { //TODO: add type to players
return this.conf.mobsIds!.includes((e as ILiving).type)
}
if ((e as IPlayer).playerUUID !== undefined && !(e as IPlayer).me) {
const data = this.state.players.get((e as IPlayer).playerUUID)
if (!data) {
return false
}
return (data.gamemode % 2 === 0) && !this.conf.friends.includes(data.name)
}
return false
})
.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({ msg: 'Fighting', type: 'count', value: dangers.length });
if (this.conf.weapon === 'needed') {
this.equipWeapon()
}
(this.conf.multiaura ? dangers : dangers.slice(0, 1)).forEach(({ entityId }) => {
this.logger.debug({ msg: 'Attack', type: 'entityId', value: 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 equipWeapon() {
const inv = this.load<Inventory>(Inventory)
const weapon = inv.findFirst(this.conf.weaponsIds!)
if (weapon.slot >= 0) {
inv.selectItem(weapon.slot, false)
}
}
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({ msg: 'Arrows', type: 'count', value: 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
}
// TODO: look and swing
// TODO: critical