diff --git a/src/api/inventory.ts b/src/api/inventory.ts index 2b9ba79c..1cfedabd 100644 --- a/src/api/inventory.ts +++ b/src/api/inventory.ts @@ -1,16 +1,9 @@ -import { Inventory, InventoryModel } from '@/models/inventory'; +import { Inventory, InventoryModel } from "@/models/inventory"; function dbErr(e) { throw new Error(`Fail in inventory: ${e}`); } -export async function getHarksFromItems(charId: string) { - try { - return await InventoryModel.fullHarks(charId); - } catch (e) { - dbErr(e); - } -} export async function getItems(charId: string) { try { return await InventoryModel.getItems(charId); @@ -22,7 +15,7 @@ export async function getItems(charId: string) { type HandleItemParams = { charId: string; itemId: string; -} +}; export async function putOnItem({ charId, itemId }: HandleItemParams) { return InventoryModel.putOnItem(charId, itemId); @@ -39,7 +32,7 @@ export async function removeItem({ charId, itemId }: HandleItemParams) { type AddItemParams = { charId: string; itemCode: string; -} +}; export async function addItem({ charId, itemCode }: AddItemParams) { return InventoryModel.addItem(charId, itemCode); diff --git a/src/arena/BattleService.ts b/src/arena/BattleService.ts index 5ed1c62a..5cdf5e92 100644 --- a/src/arena/BattleService.ts +++ b/src/arena/BattleService.ts @@ -53,13 +53,13 @@ function getTargetKeyboard(charId: string, game: Game, action: string) { return game.players.alivePlayers .filter((target) => { if (orderType === 'enemy') { - return !game.isPlayersAlly(player, target); + return !player.isAlly(target); } if (orderType === 'team') { - return game.isPlayersAlly(player, target); + return player.isAlly(target); } if (orderType === 'teamExceptSelf') { - return game.isPlayersAlly(player, target) && player.id !== target.id; + return player.isAlly(target, false); } return !orders.some((order) => target.id === order.target && action === order.action); diff --git a/src/arena/CharacterService.js b/src/arena/CharacterService.ts similarity index 55% rename from src/arena/CharacterService.js rename to src/arena/CharacterService.ts index b0cda7b0..105c4afb 100644 --- a/src/arena/CharacterService.js +++ b/src/arena/CharacterService.ts @@ -1,32 +1,13 @@ -const _ = require('lodash'); -const { findCharacter, updateCharacter } = require('../api/character'); -const { - getCollection, getItems, addItem, removeItem, putOnItem, putOffItem, getHarksFromItems, -} = require('../api/inventory'); -const { floatNumber } = require('../utils/floatNumber'); -const { default: { lvlRatio } } = require('./config'); -const { default: arena } = require('./index'); - -/** - * @typedef {import ('../models/clan').Clan} Clan - * @typedef {Object} Statistics - * @property {number} kills - * @property {number} death - * @property {number} games - * @property {number} runs - */ - -const sum = (a, b) => { - if (_.isObject(b)) { - return _.assignWith(a, b, sum); - } - if (_.isNil(a)) { - return +b; - } - return +a + +b; -}; - -const assignWithSum = (a, b) => _.assignWith(a, b, sum); +import _ from 'lodash'; +import type { UpdateQuery } from 'mongoose'; +import { findCharacter, updateCharacter } from '@/api/character'; +import arena from '@/arena'; +import config from '@/arena/config'; +import InventoryService from '@/arena/InventoryService'; +import type { HarksLvl } from '@/data/harks'; +import type { Char } from '@/models/character'; +import { assignWithSum } from '@/utils/assignWithSum'; +import { floatNumber } from '@/utils/floatNumber'; /** * Конструктор персонажа @@ -36,61 +17,60 @@ const assignWithSum = (a, b) => _.assignWith(a, b, sum); */ /** * Возвращает список динамических характеристик - * @param {CharacterService} charObj инстанс Char + * @param charObj инстанс Char * @todo проверить что функция используется при загрузке игрока в игру */ -function getDynHarks(charObj) { - const { harks } = charObj; - const patk = (charObj.prof === 'l') - ? floatNumber(harks.dex + (harks.int * 0.5)) - : floatNumber(harks.dex + (harks.str * 0.4)); - const pdef = floatNumber(((harks.con * 0.6) + (harks.dex * 0.4))); - const maxHp = floatNumber(6 + (harks.con / 3)); +function getDynHarks(charObj: CharacterService) { + const { harks } = charObj.inventory; + const patk = charObj.prof === 'l' + ? floatNumber(harks.dex + harks.int * 0.5) + : floatNumber(harks.dex + harks.str * 0.4); + const pdef = floatNumber(harks.con * 0.6 + harks.dex * 0.4); + const maxHp = floatNumber(6 + harks.con / 3); const maxMp = floatNumber(harks.wis * 1.5); - const maxEn = (charObj.prof === 'l') ? (harks.dex + harks.int * 0.5 - + harks.con * 0.25) : (harks.dex + harks.str * 0.5 + harks.con * 0.25); + const maxEn = charObj.prof === 'l' + ? harks.dex + harks.int * 0.5 + harks.con * 0.25 + : harks.dex + harks.str * 0.5 + harks.con * 0.25; - const mga = floatNumber((harks.wis * 0.6) + (harks.int * 0.4)); - const mgp = floatNumber((harks.wis * 0.6) + (harks.int * 0.4)); + const mga = floatNumber(harks.wis * 0.6 + harks.int * 0.4); + const mgp = floatNumber(harks.wis * 0.6 + harks.int * 0.4); /** @type {import('../models/item').MinMax} */ - const hl = ({ - min: floatNumber(harks.int / 10), max: floatNumber(harks.int / 5), - }); + const hl = { + min: floatNumber(harks.int / 10), + max: floatNumber(harks.int / 5), + }; - const reg_mp = floatNumber((harks.wis * 0.4) + (harks.int * 0.6)); + const reg_mp = floatNumber(harks.wis * 0.4 + harks.int * 0.6); - const reg_en = floatNumber((harks.con * 0.4) + (harks.dex * 0.6)); + const reg_en = floatNumber(harks.con * 0.4 + harks.dex * 0.6); /** * Функция расчета наносимого урона * @return {import('../models/item').MinMax} {min:xx,max:xx} */ function calcHit() { - const h = {}; let dmgFromHarks = 0; - let dmgFromItems = {}; if (charObj.prof === 'l') { dmgFromHarks = (harks.int - 2) / 10; } else { dmgFromHarks = (harks.str - 3) / 10; } - if (!_.isEmpty(charObj.harksFromItems.hit)) { - dmgFromItems = { - max: charObj.harksFromItems.hit.max, - min: charObj.harksFromItems.hit.min, - }; - } else { - dmgFromItems = { min: 0, max: 0 }; - } + const dmgFromItems = { + min: charObj.inventory.harksFromItems.hit?.min ?? 0, + max: charObj.inventory.harksFromItems.hit?.max ?? 0, + }; - h.min = floatNumber(dmgFromHarks + +dmgFromItems.min); - h.max = floatNumber(dmgFromHarks + +dmgFromItems.max); - return h; + return { + min: floatNumber(dmgFromHarks + +dmgFromItems.min), + max: floatNumber(dmgFromHarks + +dmgFromItems.max), + }; } const hit = calcHit(); - const maxTarget = (charObj.prof === 'l') ? Math.round(charObj.lvl + 3 / 2) : 1; - const lspell = (charObj.prof === 'm' || charObj.prof === 'p') ? Math.round((harks.int - 4) / 3) : 0; + const maxTarget = charObj.prof === 'l' ? Math.round(charObj.lvl + 3 / 2) : 1; + const lspell = charObj.prof === 'm' || charObj.prof === 'p' + ? Math.round((harks.int - 4) / 3) + : 0; return { patk, pdef, @@ -111,23 +91,27 @@ function getDynHarks(charObj) { * Класс описывающий персонажа внутри игры */ class CharacterService { - wasLvlUp = false; + tempHarks: HarksLvl & { free: number }; + mm: { status?: string; time?: number }; + inventory: InventoryService; + /** * Конструктор игрока - * @param {import ('../models/character').Char} charObj объект персонажа из базы */ - constructor(charObj) { + constructor(public charObj: Char) { + this.inventory = new InventoryService(charObj, charObj.inventory ?? []); this.charObj = charObj; this.tempHarks = { ...charObj.harks, free: charObj.free, }; this.mm = {}; - this.autoreg = false; - this.modifiers = undefined; this.resetExpLimit(); } + wasLvlUp = false; + autoreg = false; + get id() { return this.charObj.id; } @@ -144,21 +128,21 @@ class CharacterService { get def() { const dynHarks = getDynHarks(this); /** - * Проблематика на подумать: - * характеристики внутри чара имеют имена patk/pdef и т.д, а объект который - * был получен после возвращения updateHarkFromItems, имеет ключи типа: - * atk/prt (models/item). Это не позволяет прозрачно проводить сложение. - */ - _.forEach(this.harksFromItems, (h, i) => { + * Проблематика на подумать: + * характеристики внутри чара имеют имена patk/pdef и т.д, а объект который + * был получен после возвращения updateHarkFromItems, имеет ключи типа: + * atk/prt (models/item). Это не позволяет прозрачно проводить сложение. + */ + _.forEach(this.inventory.harksFromItems, (h, i) => { if (_.isObject(h)) { assignWithSum(dynHarks[i], h); } else { - if (!_.isUndefined(dynHarks[i])) dynHarks[i] += +h; - if (i === 'atc') dynHarks.patk += +h; - if (i === 'prt') dynHarks.pdef += +h; - if (i === 'add_hp') dynHarks.maxHp += +h; - if (i === 'add_mp') dynHarks.maxMp += +h; - if (i === 'add_en') dynHarks.maxEn += +h; + if (!_.isUndefined(dynHarks[i])) dynHarks[i] += Number(h); + if (i === 'atc') dynHarks.patk += Number(h); + if (i === 'prt') dynHarks.pdef += Number(h); + if (i === 'add_hp') dynHarks.maxHp += Number(h); + if (i === 'add_mp') dynHarks.maxMp += Number(h); + if (i === 'add_en') dynHarks.maxEn += Number(h); if (!dynHarks[i]) dynHarks[i] = h; } }); @@ -190,7 +174,7 @@ class CharacterService { this.resetExpLimit(); this.bonus += Math.round(value / 100) - Math.round(this.charObj.exp / 100); this.charObj.exp = value; - this.addLvl(); + void this.addLvl(); } set expEarnedToday(value) { @@ -202,7 +186,7 @@ class CharacterService { } get expLimitToday() { - return this.lvl * lvlRatio * 2000; + return this.lvl * config.lvlRatio * 2000; } get games() { @@ -230,17 +214,9 @@ class CharacterService { this.tempHarks.free = value; } - // Нужно помнить, что this.harks это суммарный объект, с уже полученными от - // вещей характеристиками. + // Базовые harks без учёта надетых вещей get harks() { - const hark = { ...this.charObj.harks }; - if (!_.isEmpty(this.plushark)) { - assignWithSum(hark, this.plushark); - } - if (!_.isUndefined(this.collection.harks)) { - assignWithSum(hark, this.collection.harks); - } - return hark; + return this.charObj.harks; } get magics() { @@ -248,7 +224,7 @@ class CharacterService { } get plushark() { - return this.harksFromItems.plushark; + return this.inventory.harksFromItems.plushark; } get skills() { @@ -263,38 +239,29 @@ class CharacterService { this.charObj.bonus = value; } - get items() { - return this.charObj.inventory; - } - - set items(items) { - this.charObj.inventory = items; - } - get clan() { return this.charObj.clan; } get collection() { - return getCollection(this.getPutonedItems()) || {}; + return this.inventory.collection; } get resists() { - const { resists } = this.collection; - return resists || {}; + return this.collection?.resists; } get chance() { - return this.collection.chance || {}; + return this.collection?.chance; } get statical() { - return this.collection.statical; + return this.collection?.statical; } /** Суммарное количество опыта, требуемое для следующего уровня */ get nextLvlExp() { - return 2 ** (this.lvl - 1) * 1000 * lvlRatio; + return 2 ** (this.lvl - 1) * 1000 * config.lvlRatio; } get favoriteMagicList() { @@ -369,86 +336,6 @@ class CharacterService { } } - async addItem(itemCode) { - const item = await addItem({ charId: this.id, itemCode }); - this.charObj.inventory.push(item); - return this.saveToDb(); - } - - getItem(itemId) { - return this.items.find((item) => item._id.equals(itemId)); - } - - getPutonedItems() { - return this.items.filter((item) => item.putOn); - } - - getPutonedWeapon() { - return this.getPutonedItems().find((item) => /^ab?$/.test(item.wear) && item.putOn); - } - - isCanPutOned(item) { - return !this.items.find((currentItem) => currentItem.putOn - && (item.wear.indexOf(currentItem.wear) !== -1 - || currentItem.wear.indexOf(item.wear) !== -1)); - } - - async removeItem(itemId) { - this.items = this.items.filter((item) => !item._id.equals(itemId)); - await removeItem({ charId: this.id, itemId }); - return this.saveToDb(); - } - - async putOffItem(itemId) { - await putOffItem({ charId: this.id, itemId }); - const inventory = await this.putOffItemsCantPutOned(); - this.charObj.inventory = inventory; - } - - /** @returns {import('../models/inventory').InventoryDocument[]} */ - async putOffItemsCantPutOned() { - const inventory = await getItems(this.id); - if (!inventory) return []; - this.charObj.inventory = inventory; - await this.updateHarkFromItems(); - - const items = this.getPutonedItems().filter((i) => !this.hasRequeredHarks(arena.items[i.code])); - if (items.length) { - const putOffItems = items.map((i) => putOffItem({ charId: this.id, itemId: i._id })); - await Promise.all(putOffItems); - const inventoryFiltered = await this.putOffItemsCantPutOned(); - this.charObj.inventory = inventoryFiltered; - return inventoryFiltered; - } - return inventory; - } - - async putOnItem(itemId) { - const charItem = this.getItem(itemId); - const item = arena.items[charItem.code]; - - if (!this.hasRequeredHarks(item) || !this.isCanPutOned(item)) { - return false; - } - - await putOnItem({ charId: this.id, itemId }); - const inventory = await getItems(this.id); - this.charObj.inventory = inventory; - await this.updateHarkFromItems(); - return true; - } - - /** - * - * @param {import('../models/item').Item} item - */ - hasRequeredHarks(item) { - if (item.hark) { - return _.every(item.hark, (val, hark) => val <= this.harks[hark]); - } - return true; - } - // В функциях прокачки харок следует использоваться this.charObj.harks getIncreaseHarkCount(hark) { const count = this.tempHarks[hark] - this.charObj.harks[hark]; @@ -488,40 +375,26 @@ class CharacterService { } this.gold -= item.price; - await this.addItem(itemCode); + await this.inventory.addItem(itemCode); return this.saveToDb(); } /** - * Продажа предмета. - */ + * Продажа предмета. + */ async sellItem(itemId) { - const charItem = this.getItem(itemId); - const item = arena.items[charItem.code]; + const inventory = this.inventory.getItem(itemId); + if (!inventory) { + return; + } + const item = arena.items[inventory.code]; + + await this.inventory.removeItem(itemId); - await this.removeItem(itemId); this.gold += item.price / 2; - const inventory = await this.putOffItemsCantPutOned(); - this.charObj.inventory = inventory; await this.saveToDb(); } - /** - * Функция пересчитывает все характеристики которые были получены от надетых - * вещей в инвентаре персонажа - * @returns {Promise} - */ - async updateHarkFromItems() { - this.harksFromItems = await getHarksFromItems(this.id); - if (!this.harksFromItems || !Object.keys(this.harksFromItems).length) { - this.harksFromItems = { hit: { min: 0, max: 0 } }; - } - - if (this.statical) { - assignWithSum(this.harksFromItems, this.statical); - } - } - /** * @param {Clan} clan */ @@ -533,13 +406,13 @@ class CharacterService { } async leaveClan() { - this.charObj.clan = null; + this.charObj.clan = undefined; await this.updatePenalty('clan_leave', 5 * 24 * 60); } /** * @desc Получает идентификатор игры из charId участника - * @return {string} gameId идентификатор игры + * @return gameId идентификатор игры */ get gameId() { return this.mm.status || ''; @@ -547,13 +420,12 @@ class CharacterService { /** * Setter gameId - * @param {string} newStatus новый статус mm персонажа + * @param newStatus новый статус mm персонажа */ set gameId(newStatus) { this.mm.status = newStatus; } - /** @return {import ("./GameService").default} */ get currentGame() { return arena.games[this.gameId]; } @@ -561,16 +433,11 @@ class CharacterService { /** * Загрузка чара в память * @param {number} tgId идентификатор пользователя в TG (tgId) - * @return {Promise} */ - static async getCharacter(tgId) { + static async getCharacter(tgId: number) { const charFromDb = await findCharacter({ tgId }); - if (!charFromDb) { - return null; - } const char = new CharacterService(charFromDb); - await char.updateHarkFromItems(); arena.characters[char.id] = char; return char; } @@ -578,21 +445,16 @@ class CharacterService { /** * Загрузка чара в память * @param {string} id идентификатор пользователя - * @return {Promise} */ - static async getCharacterById(id) { + static async getCharacterById(id: string) { const cachedChar = arena.characters[id]; if (cachedChar) { return cachedChar; } const charFromDb = await findCharacter({ _id: id }); - if (!charFromDb) { - return null; - } const char = new CharacterService(charFromDb); - await char.updateHarkFromItems(); arena.characters[id] = char; return char; } @@ -607,7 +469,8 @@ class CharacterService { const { gameId } = arena.characters[charId]; if (arena.games[gameId]) { return arena.games[gameId]; - } throw Error('gameId_error'); + } + throw Error('gameId_error'); } /** @@ -627,7 +490,7 @@ class CharacterService { * @param {number} lvl уровень проученного умения */ async learnSkill(skillId, lvl) { - this.charObj.skills[skillId] = lvl; + this.skills[skillId] = lvl; await this.saveToDb(); } @@ -635,7 +498,7 @@ class CharacterService { * Сохраняет состояние чара в базу * @param {import('mongoose').UpdateQuery} [query] */ - async save(query) { + async save(query: UpdateQuery) { console.log('Saving char :: id', this.id); try { return await updateCharacter(this.id, query); @@ -653,7 +516,7 @@ class CharacterService { try { console.log('Saving char :: id', this.id); const { - gold, exp, magics, bonus, items, skills, lvl, clan, free, + gold, exp, magics, bonus, skills, lvl, clan, free, } = this; return await updateCharacter(this.id, { gold, @@ -666,7 +529,6 @@ class CharacterService { penalty: this.charObj.penalty, free, expLimit: this.charObj.expLimit, - inventory: items, statistics: this.charObj.statistics, favoriteMagicList: this.charObj.favoriteMagicList, }); @@ -676,4 +538,4 @@ class CharacterService { } } -module.exports = CharacterService; +export default CharacterService; diff --git a/src/arena/ClanService/ClanService.ts b/src/arena/ClanService/ClanService.ts index 16ac9da2..9331a28e 100644 --- a/src/arena/ClanService/ClanService.ts +++ b/src/arena/ClanService/ClanService.ts @@ -80,7 +80,9 @@ export class ClanService { if (char) { return char.leaveClan(); } - return CharacterService.getCharacterById(player.id).then((char) => char.leaveClan()); + return CharacterService.getCharacterById(player.id).then((char) => + char?.leaveClan() + ); }); await Promise.all(promises); @@ -200,7 +202,7 @@ export class ClanService { $pull: { requests: { $in: [charId] } }, }); - await char.joinClan(clan); + await char?.joinClan(clan); } /** @@ -230,6 +232,6 @@ export class ClanService { $pull: { players: { $in: [charId] } }, }); const char = await CharacterService.getCharacterById(charId); - await char.leaveClan(); + await char?.leaveClan(); } } diff --git a/src/arena/Constuructors/HealMagicConstructor.ts b/src/arena/Constuructors/HealMagicConstructor.ts index c896a0c1..6034ab13 100644 --- a/src/arena/Constuructors/HealMagicConstructor.ts +++ b/src/arena/Constuructors/HealMagicConstructor.ts @@ -47,7 +47,7 @@ export abstract class Heal extends AffectableAction { // Получение экспы за хил следует вынести в отдельный action следующий // за самим хилом, дабы выдать exp всем хиллерам после формирования // общего массива хила - this.getExp(initiator, target, game); + this.getExp(initiator, target); // this.backToLife(); this.next(); } catch (e) { @@ -80,8 +80,8 @@ export abstract class Heal extends AffectableAction { return floatNumber(healEffect); } - getExp(initiator: Player, target: Player, game: Game): void { - if (game.isPlayersAlly(initiator, target)) { + getExp(initiator: Player, target: Player): void { + if (initiator.isAlly(target)) { const healEffect = this.status.effect; const exp = Math.round(healEffect * 10); initiator.stats.up('exp', exp); diff --git a/src/arena/Constuructors/PhysConstructor.ts b/src/arena/Constuructors/PhysConstructor.ts index 41e75acf..84768cc1 100644 --- a/src/arena/Constuructors/PhysConstructor.ts +++ b/src/arena/Constuructors/PhysConstructor.ts @@ -4,9 +4,7 @@ import type GameService from '../GameService'; import MiscService from '../MiscService'; import type { Player } from '../PlayersService'; import { AffectableAction } from './AffectableAction'; -import type { - ActionType, DamageType, OrderType, -} from './types'; +import type { ActionType, DamageType, OrderType } from './types'; /** * Конструктор физической атаки @@ -41,9 +39,7 @@ export default abstract class PhysConstructor extends AffectableAction { * @param game Объект игры */ cast(initiator: Player, target: Player, game: GameService) { - this.params = { - initiator, target, game, - }; + this.params = { initiator, target, game }; this.reset(); try { @@ -130,8 +126,8 @@ export default abstract class PhysConstructor extends AffectableAction { /** * Рассчитываем полученный exp */ - getExp({ initiator, target, game } = this.params) { - if (game.isPlayersAlly(initiator, target) && !initiator.flags.isGlitched) { + getExp({ initiator, target } = this.params) { + if (initiator.isAlly(target) && !initiator.flags.isGlitched) { this.status.exp = 0; } else { const exp = this.status.effect * 8; diff --git a/src/arena/GameService.ts b/src/arena/GameService.ts index bf956013..cf23ffda 100644 --- a/src/arena/GameService.ts +++ b/src/arena/GameService.ts @@ -90,19 +90,6 @@ export default class GameService { return !!this.history.hasDamageForRound(this.round.count); } - /** - * Проверяет являются ли игроки союзниками - * @param player - * @param target - */ - isPlayersAlly(player: Player, target: Player): boolean { - const allies = this.players.getMyTeam(player.id); - if (!allies.length) { - return player.id === target.id; - } - return allies.some((ally) => ally.id === target.id); - } - /** * Предзагрузка игры */ @@ -299,6 +286,7 @@ export default class GameService { this.orders.reset(); this.handleEndGameFlags(); if (this.isGameEnd) { + this.round.unsubscribe(); this.endGame(); } else { this.refreshRoundFlags(); diff --git a/src/arena/InventoryService.ts b/src/arena/InventoryService.ts new file mode 100644 index 00000000..c08fce7c --- /dev/null +++ b/src/arena/InventoryService.ts @@ -0,0 +1,183 @@ +import _ from 'lodash'; +import { + getCollection, + getItems, + addItem, + removeItem, + putOnItem, + putOffItem, +} from '@/api/inventory'; +import arena from '@/arena'; +import ValidationError from '@/arena/errors/ValidationError'; +import type { Char } from '@/models/character'; +import type { Inventory } from '@/models/inventory'; +import type { Item, ParseAttrItem } from '@/models/item'; +import { ItemModel } from '@/models/item'; +import { assignWithSum } from '@/utils/assignWithSum'; + +class InventoryService { + harksFromItems: Partial; + + constructor(private char: Char, public inventory: Inventory[]) { + this.updateHarkFromItems(); + } + + /** + * Нужно помнить, что this.harks это суммарный объект, + * с уже полученными от вещей характеристиками + * */ + get harks() { + const charHarks = { ...this.char.harks }; + const plusHarks = this.plushark; + const collectionHarks = this.collection?.harks; + + if (!_.isEmpty(plusHarks)) { + assignWithSum(charHarks, plusHarks); + } + if (!_.isUndefined(collectionHarks)) { + assignWithSum(charHarks, collectionHarks); + } + return charHarks; + } + + get plushark() { + return this.harksFromItems.plushark; + } + + get collection() { + return getCollection(this.getEquippedItems()); + } + + get resists() { + return this.collection?.resists; + } + + get chance() { + return this.collection?.chance; + } + + get statical() { + return this.collection?.statical; + } + + getItem(itemId) { + return this.inventory.find((item) => item._id.equals(itemId)); + } + + getEquippedItems() { + return this.inventory.filter((item) => item.putOn); + } + + getEquippedWeapon() { + return this.getEquippedItems().find( + (item) => /^ab?$/.test(item.wear) && item.putOn, + ); + } + + canEquip(itemToEquip: Item) { + return !this.getEquippedItems().some( + (item) => item.wear.includes(itemToEquip.wear) + || itemToEquip.wear.includes(item.wear), + ); + } + + async addItem(itemCode: string) { + const item = await addItem({ charId: this.char.id, itemCode }); + this.inventory.push(item); + } + + async removeItem(itemId: string) { + await removeItem({ charId: this.char.id, itemId }); + await this.unEquipNonEquippableItems(); + } + + async equipItem(itemId: string) { + const charItem = this.getItem(itemId); + if (!charItem) { + throw new ValidationError('Этого предмета больше не существует'); + } + + const item = arena.items[charItem.code]; + + if (!this.hasRequiredHarks(item)) { + throw new ValidationError('Недостаточно характеристик'); + } + + if (!this.canEquip(item)) { + throw new ValidationError('На этом месте уже надет другой предмет'); + } + + await putOnItem({ charId: this.char.id, itemId }); + const inventory = await getItems(this.char.id); + this.inventory = inventory ?? []; + await this.updateHarkFromItems(); + } + + async unEquipItem(itemId) { + await putOffItem({ charId: this.char.id, itemId }); + await this.unEquipNonEquippableItems(); + } + + async unEquipNonEquippableItems() { + this.inventory = (await getItems(this.char.id)) ?? []; + + await this.updateHarkFromItems(); + + const items = this.getEquippedItems().filter( + (i) => !this.hasRequiredHarks(arena.items[i.code]), + ); + if (items.length) { + const putOffItems = items.map((i) => putOffItem({ charId: this.char.id, itemId: i.id })); + await Promise.all(putOffItems); + await this.unEquipNonEquippableItems(); + } + } + + hasRequiredHarks(item: Item) { + if (item.hark) { + return Object.entries(item.hark).every( + ([hark, val]) => val <= this.char.harks[hark], + ); + } + return true; + } + + /** + * Функция пересчитывает все характеристики, + * которые были получены от надетых вещей в инвентаре персонажа + */ + updateHarkFromItems() { + const harksFromItems = this.getEquippedItems().reduce((sum, { code }) => { + // берем характеристики вещи + const attributes = ItemModel.getHarks(code); + // делаем слияние общего объекта и объекта вещи + return _.assignInWith(sum, attributes, (objValue, srcValue) => { + // Если в общем обтекте в этом ключе НЕ пустое значине + if (!_.isEmpty(objValue) || _.isNumber(objValue)) { + // и если этот ключ объекта является Объектом + if (_.isObject(objValue)) { + // то складываем два этих объекта + _.assignInWith(objValue, srcValue, (o, s) => +o + +s); + return objValue; + } + // если ключ не является Объектом, складываем значения + return +objValue + +srcValue; + } + // Если в общем объекте пустое значение, то берем значение вещи + return srcValue; + }); + }, {} as ParseAttrItem); + + if (!harksFromItems || !Object.keys(harksFromItems).length) { + this.harksFromItems = { hit: { min: 0, max: 0 } }; + } else { + this.harksFromItems = harksFromItems; + } + + if (this.statical) { + assignWithSum(this.harksFromItems, this.statical); + } + } +} + +export default InventoryService; diff --git a/src/arena/ItemService.js b/src/arena/ItemService.js index 31531ab1..f9179c66 100644 --- a/src/arena/ItemService.js +++ b/src/arena/ItemService.js @@ -61,7 +61,7 @@ const attrNames = { const getRequiredHark = (char, value, hark) => { const name = attrNames.hark[hark]; - const pointToPutOn = char.harks[hark] - value; + const pointToPutOn = char.inventory.harks[hark] - value; const canPutOn = pointToPutOn >= 0; return `\t\t${canPutOn ? '✅' : '❗️'} ${name}: ${value} ${canPutOn ? '' : `(${pointToPutOn})`}`; }; @@ -144,7 +144,7 @@ module.exports = { /** * - * @param {import('./CharacterService')} char + * @param {import('./CharacterService').default} char * @param {import('../models/item').Item} item */ itemDescription(char, item) { diff --git a/src/arena/PlayersService/Player.ts b/src/arena/PlayersService/Player.ts index e9483928..2633be3c 100644 --- a/src/arena/PlayersService/Player.ts +++ b/src/arena/PlayersService/Player.ts @@ -87,7 +87,6 @@ export default class Player { }, }, castChance: 0, - ...params.modifiers, }; // Объект // модификаторов this.resists = params.resists || {}; // Объект резистов @@ -96,7 +95,7 @@ export default class Player { this.statical = params.statical || {}; // статически реген this.alive = true; this.proc = 100; - this.weapon = new PlayerWeapon(params.getPutonedWeapon()); + this.weapon = new PlayerWeapon(params.inventory.getEquippedWeapon()); } /** @@ -180,9 +179,9 @@ export default class Player { return this.skills[skill] ?? 0; } - isAlly(player: Player) { + isAlly(player: Player, includeSelf = true) { if (player.id === this.id) { - return true; + return includeSelf; } if (!this.clan || !player.clan) { diff --git a/src/arena/RoundService.ts b/src/arena/RoundService.ts index 0f6d4bfd..84d005f3 100644 --- a/src/arena/RoundService.ts +++ b/src/arena/RoundService.ts @@ -48,6 +48,10 @@ export class RoundService { this.emitter.on(ROUND_SERVICE_EVENT, listener); } + unsubscribe() { + this.emitter.removeAllListeners(); + } + /** * Иницилизируем начало раунда */ diff --git a/src/models/inventory.ts b/src/models/inventory.ts index 705f5a98..4495158f 100644 --- a/src/models/inventory.ts +++ b/src/models/inventory.ts @@ -1,14 +1,10 @@ -import _ from 'lodash'; -import mongoose, { - Schema, Model, Query, Types, -} from 'mongoose'; -import arena from '../arena'; -import config from '../arena/config'; -import { Collections, Profs } from '../data'; -import { CharModel, Char } from './character'; -import { - ItemModel, ParseAttrItem, Item, -} from './item'; +import _ from "lodash"; +import mongoose, { Schema, Model, Query, Types } from "mongoose"; +import arena from "../arena"; +import config from "../arena/config"; +import { Collections, Profs } from "../data"; +import { CharModel, Char } from "./character"; +import { Item } from "./item"; /** * getDefaultItem @@ -34,44 +30,6 @@ export interface Inventory { export type InventoryModel = Model & typeof Inventory; export class Inventory { - /** - * fullHarks - * - * @desc Возвращает суммарный объект всех суммирующихся характеристик от - * одетых вещей внутри инвентаря чара - * @param charId Идентификатор чара чей inventory нужно пропарсить - * @todo нужна фунция которая выбирает коды всех одеты вещей в инвентаре - * а затем суммирует все полученные данны в единый объект. - */ - static async fullHarks( - this: InventoryModel, - charId: string, - ): Promise { - // берем из базы все надетые вещи - const allItems = await this.getPutOned(charId); - // Складываем все характеристики от вещей в одоин общий объект - return _.reduce(allItems, (ob, i) => { - // берем характеристики вещи - const f = ItemModel.getHarks(i.code); - // делаем слияние общего объекта и объекта вещи - return _.assignInWith(ob, f, (objValue, srcValue) => { - // Если в общем обтекте в этом ключе НЕ пустое значине - if (!_.isEmpty(objValue) || _.isNumber(objValue)) { - // и если этот ключ объекта является Объектом - if (_.isObject(objValue)) { - // то складываем два этих объекта - _.assignInWith(objValue, srcValue, (o, s) => +o + +s); - return objValue; - } - // если ключ не является Объектом, складываем значения - return +objValue + +srcValue; - } - // Если в общем объекте пустое значение, то берем значение вещи - return srcValue; - }); - }, {} as ParseAttrItem); - } - /** * Возвращает массив одетых вещей в инвентаре * @param charId diff --git a/src/scenes/battle.ts b/src/scenes/battle.ts index 366b08cd..d0cd84ac 100644 --- a/src/scenes/battle.ts +++ b/src/scenes/battle.ts @@ -79,7 +79,10 @@ battleScene.action('search', async (ctx) => { id, mm, nickname, lvl, prof, clan, } = ctx.session.character; if (!checkCancelFindCount(ctx.session.character)) { - const remainingTime = ((penaltyTime - (Date.now() - mm.time)) / 1000).toFixed(); + const remainingTime = ( + (penaltyTime - (Date.now() - Number(mm.time))) / + 1000 + ).toFixed(); await ctx.editMessageText( `Слишком много жмёшь кнопку, жди ${remainingTime} секунд до следующей попытки`, Markup.inlineKeyboard([ @@ -224,7 +227,7 @@ if (process.env.NODE_ENV === 'development') { battleScene.command('debug', async (ctx) => { // test players: tgId: 123456789 const char = await loginHelper.getChar(123456789); - const searchObject = { charId: char.id, psr: 1000, startTime: Date.now() }; + const searchObject = { charId: char?.id, psr: 1000, startTime: Date.now() }; arena.mm.push(searchObject); await ctx.reply('ok'); }); diff --git a/src/scenes/create/create.ts b/src/scenes/create/create.ts index d9ad4aad..9ff3a847 100644 --- a/src/scenes/create/create.ts +++ b/src/scenes/create/create.ts @@ -82,9 +82,12 @@ create.on('text', async (ctx) => { nickname, sex: 'm', }); - ctx.session.character = await loginHelper.getChar(ctx.from?.id); - await ctx.scene.enter('lobby'); - ctx.session.hearNick = false; + const char = await loginHelper.getChar(ctx.from?.id); + if (char) { + ctx.session.character = char; + await ctx.scene.enter("lobby"); + ctx.session.hearNick = false; + } } catch (e) { await ctx.reply(e.message); } diff --git a/src/scenes/inventory.ts b/src/scenes/inventory.ts index b3e6a84c..92aec3ec 100644 --- a/src/scenes/inventory.ts +++ b/src/scenes/inventory.ts @@ -1,109 +1,104 @@ -import { Scenes, Markup } from 'telegraf'; -import arena from '../arena'; -import ItemService from '../arena/ItemService'; -import type { BotContext } from '../fwo'; -import { InventoryModel } from '../models/inventory'; - -export const inventoryScene = new Scenes.BaseScene('inventory'); - -const getInventoryItems = (items) => items.map((item) => [Markup.button.callback( - `${item.putOn ? '✔️' : ''} ${InventoryModel.getItemName(item.code)}`, - `itemInfo_${item._id}`, -)]); +import { Scenes, Markup } from "telegraf"; +import arena from "../arena"; +import ItemService from "../arena/ItemService"; +import type { BotContext } from "../fwo"; +import { Inventory, InventoryModel } from "@/models/inventory"; + +export const inventoryScene = new Scenes.BaseScene("inventory"); + +const getInventoryItems = (items: Inventory[]) => + items.map((item) => [ + Markup.button.callback( + `${item.putOn ? "✔️" : ""} ${InventoryModel.getItemName(item.code)}`, + `itemInfo_${item._id}` + ), + ]); inventoryScene.enter(async (ctx) => { - const { items } = ctx.session.character; + const { inventory } = ctx.session.character.inventory; await ctx.replyWithMarkdown( - '*Инвентарь*', - Markup.keyboard([ - ['🔙 В лобби'], - ]).resize(), + "*Инвентарь*", + Markup.keyboard([["🔙 В лобби"]]).resize() ); await ctx.reply( - 'Список вещей', - Markup.inlineKeyboard(getInventoryItems(items)), + "Список вещей", + Markup.inlineKeyboard(getInventoryItems(inventory)) ); }); -inventoryScene.action('inventoryBack', async (ctx) => { - const { items } = ctx.session.character; +inventoryScene.action("inventoryBack", async (ctx) => { + const { inventory } = ctx.session.character.inventory; await ctx.editMessageText( - 'Список вещей', - Markup.inlineKeyboard(getInventoryItems(items)), + "Список вещей", + Markup.inlineKeyboard(getInventoryItems(inventory)) ); }); inventoryScene.action(/itemInfo(?=_)/, async (ctx) => { - const [, itemId] = ctx.match.input.split('_'); - const item = ctx.session.character.getItem(itemId); + const [, itemId] = ctx.match.input.split("_"); + const item = ctx.session.character.inventory.getItem(itemId); if (!item) return; const itemDescription = ItemService.itemDescription( ctx.session.character, - arena.items[item.code], + arena.items[item.code] ); const itemAction = item.putOn - ? Markup.button.callback( - 'Снять', - `putOff_${itemId}`, - ) : Markup.button.callback( - 'Надеть', - `putOn_${itemId}`, - ); - - await ctx.editMessageText( - `${itemDescription}`, - { - ...Markup.inlineKeyboard([ - [ - itemAction, - Markup.button.callback('Продать', `sellConfirm_${itemId}`), - Markup.button.callback('Назад', 'back'), - ], - ]), - parse_mode: 'Markdown', - }, - ); + ? Markup.button.callback("Снять", `putOff_${itemId}`) + : Markup.button.callback("Надеть", `putOn_${itemId}`); + + await ctx.editMessageText(`${itemDescription}`, { + ...Markup.inlineKeyboard([ + [ + itemAction, + Markup.button.callback("Продать", `sellConfirm_${itemId}`), + Markup.button.callback("Назад", "back"), + ], + ]), + parse_mode: "Markdown", + }); }); inventoryScene.action(/putOff(?=_)/, async (ctx) => { - const [, itemId] = ctx.match.input.split('_'); - await ctx.session.character.putOffItem(itemId); + const [, itemId] = ctx.match.input.split("_"); + await ctx.session.character.inventory.unEquipItem(itemId); + await ctx.answerCbQuery("Предмет успешно снят!"); - await ctx.editMessageText( - 'Предмет успешно снят!', + await ctx.editMessageReplyMarkup( Markup.inlineKeyboard([ - Markup.button.callback('Назад', 'inventoryBack'), - ]), + [ + Markup.button.callback("Надеть", `putOn_${itemId}`), + Markup.button.callback("Продать", `sellConfirm_${itemId}`), + Markup.button.callback("Назад", "back"), + ], + ]).reply_markup ); }); inventoryScene.action(/putOn(?=_)/, async (ctx) => { - const [, itemId] = ctx.match.input.split('_'); - - const result = await ctx.session.character.putOnItem(itemId); + const [, itemId] = ctx.match.input.split("_"); - if (result) { - await ctx.editMessageText( - 'Предмет успешно надет!', - Markup.inlineKeyboard([ - Markup.button.callback('Назад', 'inventoryBack'), - ]), - ); - } else { - await ctx.editMessageText( - 'Недостаточно характеристик либо на этом место уже надет предмет', + try { + await ctx.session.character.inventory.equipItem(itemId); + await ctx.answerCbQuery("Предмет успешно надет!"); + await ctx.editMessageReplyMarkup( Markup.inlineKeyboard([ - Markup.button.callback('Назад', 'inventoryBack'), - ]), + [ + Markup.button.callback("Снять", `putOff_${itemId}`), + Markup.button.callback("Продать", `sellConfirm_${itemId}`), + Markup.button.callback("Назад", "back"), + ], + ]).reply_markup ); + } catch (e) { + await ctx.answerCbQuery(e.message); } }); inventoryScene.action(/sellConfirm(?=_)/, async (ctx) => { - const [, itemId] = ctx.match.input.split('_'); - const item = ctx.session.character.getItem(itemId); + const [, itemId] = ctx.match.input.split("_"); + const item = ctx.session.character.inventory.getItem(itemId); if (!item) return; const { name, price } = arena.items[item.code]; @@ -113,33 +108,31 @@ inventoryScene.action(/sellConfirm(?=_)/, async (ctx) => { { ...Markup.inlineKeyboard([ [ - Markup.button.callback('Да', `sell_${itemId}`), - Markup.button.callback('Нет', `itemInfo_${itemId}`), + Markup.button.callback("Да", `sell_${itemId}`), + Markup.button.callback("Нет", `itemInfo_${itemId}`), ], ]), - parse_mode: 'Markdown', - }, + parse_mode: "Markdown", + } ); }); inventoryScene.action(/sell(?=_)/, async (ctx) => { - const [, itemId] = ctx.match.input.split('_'); + const [, itemId] = ctx.match.input.split("_"); await ctx.session.character.sellItem(itemId); await ctx.editMessageText( - 'Предмет успешно продан!', - Markup.inlineKeyboard([ - Markup.button.callback('Назад', 'inventoryBack'), - ]), + "Предмет успешно продан!", + Markup.inlineKeyboard([Markup.button.callback("Назад", "inventoryBack")]) ); }); -inventoryScene.action('back', async (ctx) => { +inventoryScene.action("back", async (ctx) => { await ctx.scene.reenter(); }); -inventoryScene.hears('🔙 В лобби', async (ctx) => { +inventoryScene.hears("🔙 В лобби", async (ctx) => { await ctx.scene.leave(); - await ctx.scene.enter('lobby'); + await ctx.scene.enter("lobby"); }); diff --git a/src/utils/assignWithSum.ts b/src/utils/assignWithSum.ts new file mode 100644 index 00000000..a0d3c281 --- /dev/null +++ b/src/utils/assignWithSum.ts @@ -0,0 +1,13 @@ +import _ from 'lodash'; + +const sum = (a, b) => { + if (_.isObject(b)) { + return _.assignWith(a, b, sum); + } + if (_.isNil(a)) { + return +b; + } + return +a + +b; +}; + +export const assignWithSum = (a, b) => _.assignWith(a, b, sum);