207 lines
7.4 KiB
TypeScript
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
|