Inventory
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
2dc150c38b
commit
095c5c8ce2
|
@ -35,7 +35,7 @@ yarn run test
|
|||
```
|
||||
|
||||
### Note
|
||||
`minecraft-data` package isn't updated for 1.15+. As a workaround, data will be downloader for `https://raw.githubusercontent.com/Pokechu22/Burger/gh-pages/{version}.json` and cached in `data` folder
|
||||
`minecraft-data` package isn't updated for 1.15+. As a workaround, data will be downloader from `https://pokechu22.github.io/Burger/{version}.json` and cached in `data` folder
|
||||
|
||||
## Tests
|
||||
```
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
},
|
||||
"Combat": {},
|
||||
"Chat": {},
|
||||
"Meal": {},
|
||||
"Time": {
|
||||
"log": "debug"
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ 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 Inventory from './Inventory'
|
||||
import Spawn from './Spawn'
|
||||
import State from './State'
|
||||
|
||||
interface IConf {
|
||||
|
@ -28,6 +29,10 @@ interface IConf {
|
|||
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 {
|
||||
|
@ -40,9 +45,8 @@ const SAFE_DELAY = SERVER_TICK_RATE * 2
|
|||
export default class Combat extends Module<IConf> {
|
||||
|
||||
private state!: State
|
||||
private life!: Life
|
||||
private spawn!: Spawn
|
||||
private entities!: Entities
|
||||
private connection!: Connection
|
||||
|
||||
public umount() {
|
||||
this.conf.fight = false
|
||||
|
@ -51,13 +55,27 @@ export default class Combat extends Module<IConf> {
|
|||
|
||||
protected mount() {
|
||||
this.state = this.load<State>(State)
|
||||
this.life = this.load<Life>(Life)
|
||||
this.spawn = this.load<Spawn>(Spawn)
|
||||
this.entities = this.load<Entities>(Entities)
|
||||
this.connection = this.load<Connection>(Connection)
|
||||
|
||||
this.fight()
|
||||
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:
|
||||
|
@ -72,7 +90,7 @@ export default class Combat extends Module<IConf> {
|
|||
})
|
||||
// MAYBE: chat packet.message
|
||||
if (this.conf.respawn) {
|
||||
this.life.respawn()
|
||||
this.spawn.respawn()
|
||||
}
|
||||
} else {
|
||||
this.logger.info({ msg: 'Fight end', packet })
|
||||
|
@ -85,7 +103,7 @@ export default class Combat extends Module<IConf> {
|
|||
return {
|
||||
respawn: true,
|
||||
fight: true,
|
||||
weapon: 'always',
|
||||
weapon: 'needed',
|
||||
arrows: 'dodge',
|
||||
delay: 'safe',
|
||||
multiaura: false,
|
||||
|
@ -98,22 +116,18 @@ export default class Combat extends Module<IConf> {
|
|||
/*'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) {
|
||||
//TODO: pick weapon if change wait
|
||||
if (this.conf.weapon == 'always') {
|
||||
this.equipWeapon()
|
||||
}
|
||||
//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[])
|
||||
|
@ -139,6 +153,10 @@ export default class Combat extends Module<IConf> {
|
|||
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)
|
||||
|
@ -155,6 +173,14 @@ export default class Combat extends Module<IConf> {
|
|||
}
|
||||
}
|
||||
|
||||
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]
|
||||
|
@ -176,3 +202,6 @@ export default class Combat extends Module<IConf> {
|
|||
//TODO: respawn or disconnect
|
||||
|
||||
}
|
||||
|
||||
// TODO: look and swing
|
||||
// TODO: critical
|
|
@ -3,6 +3,7 @@ import Cubbot from '../Cubbot'
|
|||
import Data from '../utils/Data'
|
||||
import Module from '../utils/Module'
|
||||
import Chat, { IMessage } from './Chat'
|
||||
import Spawn from './Spawn'
|
||||
|
||||
type DisconnectReason = 'authservers_down' | 'banned' | 'banned.reason' | 'banned.expiration' | 'banned_ip.reason' |
|
||||
'banned_ip.expiration' | 'duplicate_login' | 'flying' | 'generic' | 'idling' | 'illegal_characters' |
|
||||
|
@ -37,7 +38,7 @@ export default class Connection extends Module<IConfig> {
|
|||
return this._oldData
|
||||
}
|
||||
|
||||
private _data?: Data
|
||||
private _data = new Data()
|
||||
public get data() {
|
||||
return this._data
|
||||
}
|
||||
|
@ -48,9 +49,13 @@ export default class Connection extends Module<IConfig> {
|
|||
}
|
||||
|
||||
protected mount() {
|
||||
const spawn = this.load<Spawn>(Spawn)
|
||||
spawn.addCheck('data')
|
||||
|
||||
this.client.on('connect', () => {
|
||||
this._connected = true
|
||||
this.logger.trace('Connected')
|
||||
this._data.onReady(() => spawn.validateCheck('data'))
|
||||
})
|
||||
this.client.on('disconnect', ({ reason }) => {
|
||||
this._connected = false
|
||||
|
@ -66,7 +71,7 @@ export default class Connection extends Module<IConfig> {
|
|||
})
|
||||
this.client.on('login', () => {
|
||||
this.logger.trace('Logged')
|
||||
this._data = new Data(this.conf.dataSource, this.conf.dataDir, (this.client as any).version,
|
||||
this._data.load(this.conf.dataSource, this.conf.dataDir, (this.client as any).version,
|
||||
this.logger.child({ name: 'Data', level: 'info' }))
|
||||
// FIXME: use 'minecraft-data' when it include 1.15+
|
||||
})
|
||||
|
|
|
@ -254,8 +254,6 @@ export default class Entities extends Module<{}> {
|
|||
|
||||
}
|
||||
|
||||
// TODO: player info
|
||||
|
||||
// MAYBE: animation
|
||||
// MAYBE: attach_entity
|
||||
// MAYBE: entity_sound_effect
|
||||
|
|
|
@ -1,9 +1,200 @@
|
|||
//TODO: window_items
|
||||
//TODO: set_slot
|
||||
import { HOTBAR_END_SLOT, HOTBAR_START_SLOT, OFF_HAND_SLOT } from '../utils/constants'
|
||||
import Module from '../utils/Module'
|
||||
import { ISlot } from '../utils/types'
|
||||
|
||||
//TODO: declare_recipes
|
||||
type Ingredient = ISlot[]
|
||||
|
||||
type IRecipe = IRecipeCraftingShapeless | IRecipeCraftingShaped | IRecipeSpecial | IRecipeSmelting | IRecipeStoneCutting
|
||||
interface IRecipeCraftingShapeless {
|
||||
type: 'minecraft:crafting_shapeless'
|
||||
recipeId: string
|
||||
data: {
|
||||
group: string,
|
||||
ingredients: Ingredient[],
|
||||
result: ISlot,
|
||||
}
|
||||
}
|
||||
interface IRecipeCraftingShaped {
|
||||
type: 'minecraft:crafting_shaped'
|
||||
recipeId: string
|
||||
data: {
|
||||
group: string,
|
||||
width: number,
|
||||
height: number,
|
||||
ingredients: Ingredient[][],
|
||||
result: ISlot,
|
||||
}
|
||||
}
|
||||
interface IRecipeSpecial {
|
||||
type: string
|
||||
recipeId: string
|
||||
}
|
||||
interface IRecipeSmelting {
|
||||
type: 'minecraft:smelting' | 'minecraft:blasting' | 'minecraft:smoking' | 'minecraft:campfire_cooking'
|
||||
recipeId: string
|
||||
data: {
|
||||
group: string,
|
||||
ingredient: Ingredient,
|
||||
result: ISlot,
|
||||
experience: number,
|
||||
cookTime: number,
|
||||
}
|
||||
}
|
||||
interface IRecipeStoneCutting {
|
||||
type: 'minecraft:stonecutting'
|
||||
recipeId: string
|
||||
data: {
|
||||
group: string,
|
||||
ingredient: Ingredient,
|
||||
result: ISlot,
|
||||
}
|
||||
}
|
||||
|
||||
interface IWindow {
|
||||
items?: ISlot[]
|
||||
closed?: true
|
||||
properties?: number[]
|
||||
}
|
||||
|
||||
const NULL_SLOT: ISlot = { present: false, itemId: -1, itemCount: 0, nbtData: [] }
|
||||
|
||||
export default class Inventory extends Module<{}> {
|
||||
|
||||
private _recipes!: IRecipe[]
|
||||
public get recipes() {
|
||||
return this._recipes
|
||||
}
|
||||
|
||||
private _windows = new Map<number, IWindow>()
|
||||
public get inventory() {
|
||||
return this._windows.get(0)?.items || []
|
||||
}
|
||||
public get armor() {
|
||||
return {
|
||||
head: this.inventory[5],
|
||||
chest: this.inventory[6],
|
||||
legs: this.inventory[7],
|
||||
feet: this.inventory[8],
|
||||
}
|
||||
}
|
||||
private get hotbar() {
|
||||
return this.inventory.slice(HOTBAR_START_SLOT, HOTBAR_END_SLOT)
|
||||
}
|
||||
private get offItem() {
|
||||
return this.inventory[OFF_HAND_SLOT]
|
||||
}
|
||||
|
||||
private _held = 0
|
||||
public get mainHand() {
|
||||
return this._held
|
||||
}
|
||||
public get mainItem() {
|
||||
return this.hotbar[this.mainHand]
|
||||
}
|
||||
|
||||
private _actionCounter = 0
|
||||
public click(windowId: number, slot: number, mouseButton: number, mode: number, item: ISlot) {
|
||||
this.client.write('window_click', { windowId, slot, mouseButton, action: this._actionCounter++, mode, item })
|
||||
}
|
||||
public toHotbar(to: number, from: number, windowId = 0) {
|
||||
const inv = this._windows.get(windowId)?.items
|
||||
const item = inv ? inv[from] : NULL_SLOT
|
||||
this.click(windowId, from, to, 2, item)
|
||||
}
|
||||
public drop(from: number, windowId = 0, stack = false) {
|
||||
this.click(windowId, from, stack ? 1 : 0, 4, NULL_SLOT)
|
||||
}
|
||||
|
||||
public setHeld(slotId: number) {
|
||||
this.client.write('held_item_slot', { slotId })
|
||||
this._held = slotId
|
||||
}
|
||||
|
||||
public swapOffItem() {
|
||||
this.client.write('block_dig', { status: 6, location: { x: 0, y: 0, z: 0 }, face: 0 })
|
||||
}
|
||||
|
||||
public selectItem(slot: number, allowOff: boolean) {
|
||||
let hand = -1
|
||||
if (slot === OFF_HAND_SLOT && allowOff) {
|
||||
hand = 1
|
||||
} else {
|
||||
let hotSlot = slot
|
||||
if (hotSlot < HOTBAR_START_SLOT || hotSlot > HOTBAR_END_SLOT) {
|
||||
hotSlot = this.hotbar.findIndex(s => !s.present)
|
||||
if (hotSlot < 0) {
|
||||
hotSlot = this.mainHand + 1 % (HOTBAR_END_SLOT - HOTBAR_START_SLOT)
|
||||
}
|
||||
this.toHotbar(hotSlot, slot)
|
||||
} else {
|
||||
hotSlot -= HOTBAR_START_SLOT
|
||||
}
|
||||
if (hotSlot !== this.mainHand || Math.random() > 0.9) { // Force held randomlys
|
||||
this.setHeld(hotSlot)
|
||||
}
|
||||
hand = 0
|
||||
}
|
||||
return hand
|
||||
}
|
||||
public useItem(slot: number, allowOff = true) {
|
||||
this.client.write('use_item', { hand: this.selectItem(slot, allowOff) })
|
||||
}
|
||||
|
||||
public findFirst(order: number[]) {
|
||||
return this.inventory.reduce((prev, slot, pos) => {
|
||||
const idx = order.indexOf(slot.itemId)
|
||||
return idx < 0 || (prev.value > -1 && prev.value <= idx) ? prev : { slot: pos, value: idx }
|
||||
}, { slot: -1, value: -1 })
|
||||
}
|
||||
|
||||
protected mount(): void {
|
||||
this.client.on('declare_recipes', packet => this._recipes = packet.recipes)
|
||||
|
||||
this.client.on('window_items', packet => this.updateWindow(packet.windowId, { items: packet.items}))
|
||||
this.client.on('close_window', packet => this.updateWindow(packet.windowId, { closed: true }))
|
||||
this.client.on('craft_progress_bar', packet => {
|
||||
const window = this._windows.get(packet.windowId) || {}
|
||||
if (!window.properties) {
|
||||
window.properties = []
|
||||
}
|
||||
window.properties[packet.property] = packet.value
|
||||
this._windows.set(packet.windowId, window)
|
||||
})
|
||||
this.client.on('transaction', packet => {
|
||||
// TODO: action list
|
||||
if (!packet.accepted) {
|
||||
this.logger.warn('Conflict')
|
||||
this.client.write('transaction', packet)
|
||||
}
|
||||
})
|
||||
this.client.on('set_slot', packet => {
|
||||
const window = this._windows.get(packet.windowId) || {}
|
||||
if (!window.items) {
|
||||
window.items = []
|
||||
}
|
||||
window.items[packet.slot] = packet.item
|
||||
this._windows.set(packet.windowId, window)
|
||||
})
|
||||
this.client.on('held_item_slot', packet => this._held = packet.slot)
|
||||
}
|
||||
|
||||
protected getConf() {
|
||||
return {}
|
||||
}
|
||||
|
||||
private updateWindow(windowId: number, data: IWindow) {
|
||||
const window = this._windows.get(windowId) || {}
|
||||
this._windows.set(windowId, {...window, ...data})
|
||||
}
|
||||
|
||||
// TODO: autodrop
|
||||
|
||||
}
|
||||
|
||||
// MAYBE: horse_window
|
||||
|
||||
/* Action */
|
||||
// TODO: Pick Item
|
||||
//TDOO: craft
|
||||
//TODO: Held Item Change
|
||||
// TODO: craft
|
||||
// TODO: Log actions
|
||||
// TODO: Move inventory (equip armor)
|
||||
|
|
|
@ -1,36 +1,34 @@
|
|||
import { LOW_HEALTH, MAX_HEALTH, REGENERATE_FOOD, SPRINT_FOOD } from '../utils/constants'
|
||||
import { EventEmitter } from 'events'
|
||||
import { LOW_HEALTH, MAX_FOOD, MAX_HEALTH, MAX_SATURATION, SPRINT_FOOD } from '../utils/constants'
|
||||
import { round } from '../utils/func'
|
||||
import Module from '../utils/Module'
|
||||
import Spawn from './Spawn'
|
||||
|
||||
interface IConf {
|
||||
/** Respawn on world join */
|
||||
spawn: boolean,
|
||||
/** Warn if health is lower */
|
||||
lowHealth: number,
|
||||
/** Life to regenerate */
|
||||
targetHealth: number,
|
||||
/** Log health info */
|
||||
showHealth: boolean,
|
||||
/** Use food if needed */
|
||||
eat: boolean,
|
||||
/** Prefer saturation over food power */
|
||||
preferSaturation: boolean,
|
||||
/** Warn and eat if food level is lower */
|
||||
/** Warn if food level is lower */
|
||||
lowFood: number,
|
||||
/** Food level needed to regenerate */
|
||||
regenerateFood: number,
|
||||
/** Log food level info */
|
||||
showFood: boolean,
|
||||
}
|
||||
|
||||
/** Monitor health, food and xp */
|
||||
export default class Life extends Module<IConf> {
|
||||
|
||||
private _health = MAX_HEALTH
|
||||
public get health() {
|
||||
return this._health
|
||||
}
|
||||
|
||||
private _food = MAX_FOOD
|
||||
public get food() {
|
||||
return this._food
|
||||
}
|
||||
|
||||
private _saturation = MAX_SATURATION
|
||||
public get saturation() {
|
||||
return this._saturation
|
||||
}
|
||||
|
@ -39,49 +37,37 @@ export default class Life extends Module<IConf> {
|
|||
return this.health > 0
|
||||
}
|
||||
|
||||
public get mustEat() {
|
||||
return this.food <= this.conf.lowFood ||
|
||||
(this.health <= this.conf.targetHealth && this.food <= this.conf.regenerateFood)
|
||||
}
|
||||
private _experienceBar = 0
|
||||
public get experienceBar() {
|
||||
return this._experienceBar
|
||||
}
|
||||
|
||||
private _level = 0
|
||||
public get level() {
|
||||
return this._level
|
||||
}
|
||||
|
||||
private _totalExperience = 0
|
||||
public get totalExperience() {
|
||||
return this._totalExperience
|
||||
}
|
||||
|
||||
private _health!: number
|
||||
|
||||
private _food!: number
|
||||
|
||||
private _saturation!: number
|
||||
|
||||
private _experienceBar!: number
|
||||
|
||||
private _level!: number
|
||||
|
||||
private _totalExperience!: number
|
||||
|
||||
public respawn() {
|
||||
this.logger.info('respawn')
|
||||
this.client.write('client_command', { action: 1 })
|
||||
private _events = new EventEmitter()
|
||||
public get events() {
|
||||
return this._events
|
||||
}
|
||||
|
||||
protected mount() {
|
||||
//TODO: const inventory = this.conf.eat ? this.load('inventory') : undefined
|
||||
const spawn = this.load<Spawn>(Spawn)
|
||||
spawn.addCheck('health')
|
||||
|
||||
this.client.on('update_health', data => {
|
||||
if (this.conf.spawn && this._health == null) {
|
||||
setTimeout(this.respawn.bind(this), 500)
|
||||
}
|
||||
|
||||
this._health = round(data.health)
|
||||
this._food = data.food
|
||||
this._saturation = data.foodSaturation
|
||||
|
||||
spawn.validateCheck('health')
|
||||
|
||||
if (this.health <= this.conf.lowHealth) {
|
||||
this.logger.warn({ msg: this.alive ? 'Low health' : 'Dead', type: 'health', value: this.health })
|
||||
} else if (this.conf.showHealth) {
|
||||
|
@ -94,10 +80,7 @@ export default class Life extends Module<IConf> {
|
|||
this.logger.info({ msg: 'Replete', type: 'food', value: this.food })
|
||||
}
|
||||
|
||||
if (this.conf.eat && this.mustEat) {
|
||||
this.logger.warn({ msg: 'TODO: Must eat' })
|
||||
// TODO: inventory.items.filter(food).orderBy(this.conf.preferSaturation)
|
||||
}
|
||||
this._events.emit('update')
|
||||
})
|
||||
|
||||
this.client.on('experience', data => {
|
||||
|
@ -107,18 +90,13 @@ export default class Life extends Module<IConf> {
|
|||
})
|
||||
}
|
||||
|
||||
//TODO: send client settings
|
||||
// MAYBE: send client settings
|
||||
|
||||
protected getConf(): IConf {
|
||||
return {
|
||||
spawn: true,
|
||||
lowHealth: LOW_HEALTH,
|
||||
targetHealth: MAX_HEALTH,
|
||||
showHealth: false,
|
||||
eat: true,
|
||||
preferSaturation: true,
|
||||
lowFood: SPRINT_FOOD,
|
||||
regenerateFood: REGENERATE_FOOD,
|
||||
showFood: false,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import { MAX_HEALTH, REGENERATE_FOOD, SPRINT_FOOD } from '../utils/constants'
|
||||
import Module from '../utils/Module'
|
||||
import Connection from './Connection'
|
||||
import Inventory from './Inventory'
|
||||
import Life from './Life'
|
||||
import State from './State'
|
||||
|
||||
interface IConf {
|
||||
/** Life to regenerate */
|
||||
targetHealth: number,
|
||||
/** Minimal food level */
|
||||
minFood: number,
|
||||
/** Food level needed to regenerate */
|
||||
regenerateFood: number,
|
||||
/** Food names by priority */
|
||||
foods: string[]
|
||||
/** Food ids by priority (override foods) */
|
||||
foodsIds?: number[]
|
||||
}
|
||||
|
||||
export default class Meal extends Module<IConf> {
|
||||
|
||||
private life!: Life
|
||||
private inv!: Inventory
|
||||
private state!: State
|
||||
|
||||
private _eating?: NodeJS.Timeout
|
||||
|
||||
public get mustEat() {
|
||||
return this.life.food <= this.conf.minFood ||
|
||||
(this.life.health <= this.conf.targetHealth && this.life.food <= this.conf.regenerateFood)
|
||||
}
|
||||
|
||||
protected mount() {
|
||||
this.life = this.load<Life>(Life)
|
||||
this.inv = this.load<Inventory>(Inventory)
|
||||
this.state = this.load<State>(State)
|
||||
|
||||
const ready = () => {
|
||||
this.life.events.on('update', this.eat.bind(this))
|
||||
this.eat()
|
||||
}
|
||||
|
||||
if (this.conf.foodsIds) {
|
||||
ready()
|
||||
} else {
|
||||
this.load<Connection>(Connection).data.onReady(data => {
|
||||
this.conf.foodsIds = this.conf.foods
|
||||
.map(name => data.items![name].numeric_id)
|
||||
|
||||
ready()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
protected getConf() {
|
||||
return {
|
||||
minFood: SPRINT_FOOD,
|
||||
targetHealth: MAX_HEALTH,
|
||||
regenerateFood: REGENERATE_FOOD,
|
||||
foods: [
|
||||
'golden_carrot', 'cooked_porkchop', 'cooked_beef', 'rabbit_stew', 'cooked_mutton', 'cooked_salmon',
|
||||
// 'golden_apple', 'enchanted_golden_apple',
|
||||
'beetroot_soup', 'cooked_chicken', 'mushroom_stem', 'baked_potato', 'bread', 'cooked_cod',
|
||||
'cooked_rabbit', 'pumpkin_pie', 'carrot', 'apple', 'beef', 'porkchop', 'rabbit', 'honey_bottle',
|
||||
'melon_slice', 'mutton', 'beetroot', 'dried_kelp', 'potato', 'cookie', 'cod', 'salmon', 'tropical_fish',
|
||||
'chicken', 'rotten_flesh',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
private eat() {
|
||||
if (this.mustEat && !this._eating) {
|
||||
const food = this.inv.findFirst(this.conf.foodsIds!)
|
||||
|
||||
if (food.slot >= 0) {
|
||||
this.client.on('entity_status', packet => {
|
||||
if (this._eating && packet.entityId === this.state.state.entityId && packet.entityStatus === 9) {
|
||||
clearTimeout(this._eating)
|
||||
this._eating = undefined
|
||||
}
|
||||
})
|
||||
this.logger.info('Eating %s', this.conf.foods[food.value])
|
||||
this.inv.useItem(food.slot)
|
||||
this._eating = setTimeout(() => {
|
||||
this.logger.error('Fail eating')
|
||||
this._eating = undefined
|
||||
this.eat()
|
||||
}, 3000)
|
||||
} else {
|
||||
this.logger.warn('No food available')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import Module from '../utils/Module'
|
||||
|
||||
export default class Spawn extends Module<{ spawn: boolean }> {
|
||||
|
||||
private _checkList: string[] = []
|
||||
|
||||
public addCheck(name: string) {
|
||||
this._checkList.push(name)
|
||||
}
|
||||
public validateCheck(name: string) {
|
||||
const idx = this._checkList.indexOf(name)
|
||||
if (idx > -1) {
|
||||
this._checkList.splice(this._checkList.indexOf(name), 1)
|
||||
this.logger.trace({ msg: 'Spawn ready', value: name, remain: this._checkList.length })
|
||||
if (this._checkList.length === 0 && this.conf.spawn) {
|
||||
this.respawn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public respawn() {
|
||||
this.logger.info('Respawn')
|
||||
this.client.write('client_command', { action: 1 })
|
||||
}
|
||||
|
||||
protected mount() {
|
||||
// passive
|
||||
}
|
||||
|
||||
protected getConf() {
|
||||
return { spawn: true }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import Module from '../utils/Module'
|
||||
import { IDelta, IPosition, IState, gameMode } from '../utils/types'
|
||||
import Spawn from './Spawn'
|
||||
|
||||
// MAYBE: split
|
||||
|
||||
|
@ -65,11 +66,22 @@ export default class State extends Module<{ positionConfirm: boolean }> {
|
|||
}
|
||||
|
||||
protected mount() {
|
||||
this.client.on('login', data => this._state = data)
|
||||
const spawn = this.load<Spawn>(Spawn)
|
||||
spawn.addCheck('state')
|
||||
spawn.addCheck('abilities')
|
||||
spawn.addCheck('position')
|
||||
|
||||
this.client.on('login', data => {
|
||||
this._state = data
|
||||
spawn.validateCheck('state')
|
||||
})
|
||||
this.client.on('difficulty', data => this._difficulty = {
|
||||
level: data.difficulty, locked: data.difficultyLocked,
|
||||
})
|
||||
this.client.on('abilities', data => this._abilities = data)
|
||||
this.client.on('abilities', data => {
|
||||
this._abilities = data
|
||||
spawn.validateCheck('abilities')
|
||||
})
|
||||
|
||||
this.client.on('position', (data: IPositionPacket) => {
|
||||
if (this._position == null) {
|
||||
|
@ -94,6 +106,7 @@ export default class State extends Module<{ positionConfirm: boolean }> {
|
|||
this.client.write('teleport_confirm', { teleportId: data.teleportId })
|
||||
}
|
||||
this.logger.debug({ msg: 'Teleported', type: 'position', value: this.position })
|
||||
spawn.validateCheck('position')
|
||||
})
|
||||
|
||||
this.client.on('player_info', (packet: { action: number, data: IPlayerInfo[] }) => {
|
||||
|
@ -103,13 +116,22 @@ export default class State extends Module<{ positionConfirm: boolean }> {
|
|||
this._players.set(player.UUID, player)
|
||||
break
|
||||
case 1:
|
||||
this._players.get(player.UUID)!.gamemode = player.gamemode
|
||||
const p1 = this._players.get(player.UUID)
|
||||
if (p1) {
|
||||
p1.gamemode = player.gamemode
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
this._players.get(player.UUID)!.ping = player.ping
|
||||
const p2 = this._players.get(player.UUID)
|
||||
if (p2) {
|
||||
p2.ping = player.ping
|
||||
}
|
||||
break
|
||||
case 3:
|
||||
this._players.get(player.UUID)!.name = player.name
|
||||
const p3 = this._players.get(player.UUID)
|
||||
if (p3) {
|
||||
p3.name = player.name
|
||||
}
|
||||
break
|
||||
case 4:
|
||||
this._players.delete(player.UUID)
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { IModuleType } from '../utils/Module'
|
||||
import Chat from './Chat'
|
||||
import Client from './Connection'
|
||||
import Combat from './Combat'
|
||||
import Client from './Connection'
|
||||
import Entities from './Entities'
|
||||
import Inventory from './Inventory'
|
||||
import Life from './Life'
|
||||
import Meal from './Meal'
|
||||
import Spawn from './Spawn'
|
||||
import State from './State'
|
||||
import Time from './Time'
|
||||
|
||||
|
@ -14,6 +17,9 @@ export default [
|
|||
Combat,
|
||||
Entities,
|
||||
Life,
|
||||
Meal,
|
||||
State,
|
||||
Time,
|
||||
Inventory,
|
||||
Spawn,
|
||||
] as IModuleType[]
|
||||
|
|
|
@ -11,20 +11,42 @@ export interface IEntityData {
|
|||
metadata: Array<{ entity?: string }>
|
||||
}
|
||||
|
||||
export interface IItemData {
|
||||
display_name: string
|
||||
max_stack_size: number
|
||||
numeric_id: number
|
||||
}
|
||||
|
||||
/** Load game data */
|
||||
export default class Data {
|
||||
|
||||
private _ready = false
|
||||
public get ready() {
|
||||
return this._ready
|
||||
}
|
||||
private _onReady: Array<(data: Data) => void> = []
|
||||
|
||||
private _entities?: { [name: string]: IEntityData }
|
||||
public get entities() {
|
||||
return this._entities
|
||||
}
|
||||
|
||||
constructor(source: string, directory: string, version: string, logger: Logger) {
|
||||
private _items?: { [name: string]: IItemData }
|
||||
public get items() {
|
||||
return this._items
|
||||
}
|
||||
|
||||
public onReady(cb: ((data: Data) => void)) {
|
||||
this._onReady.push(cb)
|
||||
}
|
||||
|
||||
public getEntityById(entityId: number) {
|
||||
if (this._entities) {
|
||||
for (const name in (this._entities as object)) {
|
||||
if (this._entities[name].id === entityId) {
|
||||
return this._entities[name]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public load(source: string, directory: string, version: string, logger: Logger) {
|
||||
logger.debug('Loading data file')
|
||||
const path = directory + version + '.json'
|
||||
if (!fs.existsSync(directory)) {
|
||||
|
@ -39,29 +61,20 @@ export default class Data {
|
|||
res.on('error', error => console.error({ msg: 'Downloading error', error }))
|
||||
res.on('end', () => {
|
||||
out.close()
|
||||
setTimeout(() => this.load(path), 500)
|
||||
setTimeout(() => this.read(path), 500)
|
||||
})
|
||||
}).on('error', error => console.error({ msg: 'Downloading error', error }))
|
||||
} else {
|
||||
this.load(path)
|
||||
this.read(path)
|
||||
}
|
||||
}
|
||||
|
||||
public getEntityById(entityId: number) {
|
||||
if (this._entities) {
|
||||
for (const name in (this._entities as object)) {
|
||||
if (this._entities[name].id === entityId) {
|
||||
return this._entities[name]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private load(path: string) {
|
||||
private read(path: string) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(path).toString())[0] // NOTE: memory ???
|
||||
this._entities = data.entities.entity
|
||||
this._ready = true
|
||||
this._items = data.items.item
|
||||
this._onReady.forEach(cb => cb(this))
|
||||
} catch (error) {
|
||||
console.error('/!\\ =( Please remove ' + path, error)
|
||||
process.exit(1)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export const SPRINT_FOOD = 6
|
||||
export const REGENERATE_FOOD = 18
|
||||
export const MAX_FOOD = 20
|
||||
export const MAX_SATURATION = 20
|
||||
export const MAX_HEALTH = 20
|
||||
export const LOW_HEALTH = 4
|
||||
|
||||
|
@ -15,3 +16,7 @@ export const DELTA_RATIO = 128 * 32
|
|||
|
||||
export const SERVER_TICK_RATE = 50
|
||||
export const SERVER_TPS = 1 / SERVER_TICK_RATE * 1000
|
||||
|
||||
export const OFF_HAND_SLOT = 45
|
||||
export const HOTBAR_START_SLOT = 36
|
||||
export const HOTBAR_END_SLOT = 44
|
||||
|
|
Loading…
Reference in New Issue