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 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' /** Players names */ friends: string[] /** Targeted entities names */ mobs: string[] /** Targeted entities ids (override mobs) */ mobsIds?: number[] } 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 life!: Life private entities!: Entities private connection!: Connection public umount() { this.conf.fight = false this.conf.arrows = 'ignore' } protected mount() { this.state = this.load(State) this.life = this.load(Life) this.entities = this.load(Entities) this.connection = this.load(Connection) this.fight() this.arrows() 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.life.respawn() } } else { this.logger.info({ msg: 'Fight end', packet }) } } }) } protected getConf(): IConf { return { respawn: true, fight: true, weapon: 'always', 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', ], } } private fight() { if (this.conf.fight) { //TODO: pick weapon if change wait //TODO: pick shield if (!this.conf.mobsIds) { if (this.connection.data?.ready) { this.conf.mobsIds = this.conf.mobs .map(name => this.connection.data!.entities![name].id) } else { this.logger.warn('Waiting data') setTimeout(this.fight.bind(this), SAFE_DELAY) } } // 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 }); (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 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 }