reldens
Version:
Reldens - MMORPG Platform
364 lines (347 loc) • 14.3 kB
JavaScript
/**
*
* Reldens - TraderObject
*
* Trading NPC with inventory that can buy and sell items to players.
*
*/
const { NpcObject } = require('./npc-object');
const { ObjectsConst } = require('../../../constants');
const { Processor } = require('../../../../inventory/server/exchange/processor');
const { GameConst } = require('../../../../game/constants');
const { ItemsFactory } = require('../../../../inventory/server/items-factory');
const {
ItemsManager,
ItemsEvents,
RequirementsCollection,
RewardsCollection,
ItemsConst
} = require('@reldens/items-system');
const { Logger, sc } = require('@reldens/utils');
/**
* @typedef {import('../../../../users/server/player').Player} Player
*/
class TraderObject extends NpcObject
{
/**
* @param {Object} props
*/
constructor(props)
{
super(props);
this.type = ObjectsConst.TYPE_TRADER;
this.eventsPrefix = this.uid+'.'+ObjectsConst.EVENT_PREFIX.TRADER;
this.clientParams.type = ObjectsConst.TYPE_TRADER;
this.sendInvalidOptionMessage = true;
this.inventory = false;
this.exchangeRequirementsA = new RequirementsCollection();
this.exchangeRewardsB = new RewardsCollection();
this.tradesInProgress = {};
this.configuredItemClasses = this.config.getWithoutLogs('server/customClasses/inventory/items', {});
this.configuredGroupClasses = this.config.getWithoutLogs('server/customClasses/inventory/groups', {});
this.dataServer = false;
this.content = sc.get(this.clientParams, 'content', ObjectsConst.SNIPPETS.TRADER.CONTENT);
this.options = sc.get(this.clientParams, 'options', {
buy: {
label: ObjectsConst.SNIPPETS.TRADER.OPTIONS.BUY,
value: ObjectsConst.DEFAULTS.TRADER_OBJECT.OPTIONS.BUY
},
sell: {
label: ObjectsConst.SNIPPETS.TRADER.OPTIONS.SELL,
value: ObjectsConst.DEFAULTS.TRADER_OBJECT.OPTIONS.SELL
}
});
}
/**
* @param {Object} props
* @returns {Promise<void>}
*/
async runAdditionalSetup(props)
{
this.dataServer = sc.get(props.objectsManager, 'dataServer', false);
if(false === this.dataServer){
Logger.error('Data Server was not specified.');
return;
}
await this.createObjectInventory();
}
/**
* @returns {Promise<void>}
*/
async createObjectInventory()
{
let objectsInventoryRepository = this.dataServer.getEntity('objectsItemsInventory');
let itemsModelsList = await objectsInventoryRepository.loadByWithRelations(
'owner_id',
this.id,
['related_items_item.related_items_item_modifiers']
);
if(0 === itemsModelsList.length){
Logger.error('Object does not have any items assigned.');
return;
}
await this.enrichWithLoadedRequirements();
await this.enrichWithLoadedRewards();
// @NOTE: here we create an ItemsManager and not an ItemsServer because the object is not connected to any
// specific client, so we need to send the object items "manually" to the current client on each message.
this.inventory = new ItemsManager({
owner: this,
eventsManager: this.events,
itemClasses: this.configuredItemClasses,
groupClasses: this.configuredGroupClasses,
itemsModelData: this.config.inventory.items
});
let itemsInstances = await ItemsFactory.fromModelsList(itemsModelsList, this.inventory);
if(false === itemsInstances){
Logger.error('Item instances could not be created.');
return;
}
await this.inventory.fireEvent(ItemsEvents.LOADED_OWNER_ITEMS, this, itemsInstances, itemsModelsList);
await this.inventory.setItems(itemsInstances);
}
/**
* @returns {Promise<void>}
*/
async enrichWithLoadedRequirements()
{
let objectsItemsRequirementsRepository = this.dataServer.getEntity('objectsItemsRequirements');
let exchangeRequirementsModelsList = await objectsItemsRequirementsRepository.loadBy('object_id', this.id);
if(0 === exchangeRequirementsModelsList.length){
return;
}
for(let exchangeRequirement of exchangeRequirementsModelsList){
this.exchangeRequirementsA.add(
// @Note: uid is used for the requirement key but for our "buy" process we don't need different items
// with the same key for different requirements.
exchangeRequirement.item_key,
exchangeRequirement.item_key,
exchangeRequirement.required_item_key,
exchangeRequirement.required_quantity,
exchangeRequirement.auto_remove_requirement
);
}
}
/**
* @returns {Promise<void>}
*/
async enrichWithLoadedRewards()
{
let objectsItemsRewardsRepository = this.dataServer.getEntity('objectsItemsRewards');
let exchangeRewardsModelsList = await objectsItemsRewardsRepository.loadBy('object_id', this.id);
if(0 === exchangeRewardsModelsList.length){
return;
}
for(let exchangeReward of exchangeRewardsModelsList){
this.exchangeRewardsB.add(
// @Note: uid is used for the reward key but for our "sell" process we don't need different items with
// the same key for different rewards.
exchangeReward.item_key,
exchangeReward.item_key,
exchangeReward.reward_item_key,
exchangeReward.reward_quantity,
exchangeReward.reward_item_is_required
);
}
}
/**
* @param {Object} client
* @param {Object} data
* @param {Object} room
* @param {Player} playerSchema
* @returns {Promise<boolean>}
*/
async executeMessageActions(client, data, room, playerSchema)
{
let tradeKey = playerSchema.player_id;
if(this.shouldCancelExchange(data, tradeKey)){
this.tradesInProgress[tradeKey].cancelExchange();
return false;
}
let superResult = await super.executeMessageActions(client, data, room, playerSchema);
if(false === superResult){
return false;
}
let tradeAction = sc.get(data, 'value', 'init');
let inventoryKey = this.mapInventoryKeyFromAction(tradeAction);
if(false === inventoryKey){
if('init' !== tradeAction){
Logger.error('Undefined inventory key for action: "'+tradeAction+'".');
}
return false;
}
let subActionParam = sc.get(data, ObjectsConst.TRADE_ACTIONS.SUB_ACTION, false);
let mappedSubAction = this.mapSubAction(subActionParam);
if(false !== mappedSubAction && sc.isFunction(Processor[mappedSubAction])){
return await this.processSubAction(mappedSubAction, tradeKey, data, playerSchema, inventoryKey, tradeAction, client);
}
return await this.initializeTransaction(tradeKey, data, playerSchema, inventoryKey, tradeAction, client);
}
/**
* @param {string|number} tradeKey
* @param {Object} data
* @param {Player} playerSchema
* @param {string} inventoryKey
* @param {string} tradeAction
* @param {Object} client
* @returns {Promise<boolean>}
*/
async initializeTransaction(tradeKey, data, playerSchema, inventoryKey, tradeAction, client)
{
let exchangeRequirementsA = sc.get(this, 'exchangeRequirementsA', new RequirementsCollection());
let exchangeRewardsB = sc.get(this, 'exchangeRewardsB', new RewardsCollection());
// @TODO - BETA - Transaction initialization and requirements could be sent only once to each client.
let params = {
data,
from: this.inventory,
to: playerSchema.inventory.manager,
exchangeRequirementsA,
exchangeRewardsB
};
params['dropExchangeA'] = true;
if(ItemsConst.TRADE_ACTIONS.BUY === tradeAction){
params['avoidExchangeDecreaseA'] = true;
}
this.tradesInProgress[tradeKey] = await Processor.init(params);
if(false === this.tradesInProgress[tradeKey]){
return this.transactionError(playerSchema, 'Transaction could not be initialized.');
}
// on "sell" we will send the items of the player, on "buy" we will send this object items:
let inventory = this.tradesInProgress[tradeKey].inventories[inventoryKey];
let inventoryItems = inventory.items;
// @TODO - BETA - Refactor when include false conditions in the shortcuts.
if(ItemsConst.TRADE_ACTIONS.SELL === tradeAction){
inventoryItems = [
...(inventory.findItemsByPropertyValue('equipped', false) || []),
...(inventory.findItemsByPropertyValue('equipped', undefined) || [])];
}
let sendData = {
act: GameConst.UI,
id: this.id,
result: {
action: tradeAction,
items: playerSchema.inventory.client.extractItemsDataForSend(inventoryItems),
exchangeRequirementsA: exchangeRequirementsA.requirements,
exchangeRewardsB: exchangeRewardsB.rewards
},
listener: 'traderObject'
};
client.send('*', sendData);
return true;
}
/**
* @param {string} mappedSubAction
* @param {string|number} tradeKey
* @param {Object} data
* @param {Player} playerSchema
* @param {string} inventoryKey
* @param {string} tradeAction
* @param {Object} client
* @returns {Promise<boolean>}
*/
async processSubAction(mappedSubAction, tradeKey, data, playerSchema, inventoryKey, tradeAction, client)
{
if(false === sc.get(this.tradesInProgress, tradeKey, false)){
let result = await this.initializeTransaction(
tradeKey,
data,
playerSchema,
inventoryKey,
tradeAction,
client
);
if(false === result){
Logger.error(
'Transaction could not be initialized on sub-action process.',
data,
tradeKey,
tradeAction
);
return false;
}
}
let subActionResult = await Processor[mappedSubAction]({
transaction: this.tradesInProgress[tradeKey],
data,
inventoryKey
});
let inventory = this.tradesInProgress[tradeKey].inventories[inventoryKey];
let inventoryItems = inventory.items;
// @TODO - BETA - Refactor when include false conditions in the shortcuts and a new property "canBeTraded".
if(ItemsConst.TRADE_ACTIONS.SELL === tradeAction){
inventoryItems = [
...(inventory.findItemsByPropertyValue('equipped', false) || []),
...(inventory.findItemsByPropertyValue('equipped', undefined) || [])
];
}
let sendData = {
act: GameConst.UI,
id: this.id,
result: {
action: tradeAction,
subAction: mappedSubAction,
subActionResult: Boolean(subActionResult),
lastErrorMessage: this.tradesInProgress[tradeKey].lastError.code,
lastErrorData: this.tradesInProgress[tradeKey].lastError.data,
exchangeData: this.tradesInProgress[tradeKey].exchangeBetween,
items: playerSchema.inventory.client.extractItemsDataForSend(inventoryItems),
exchangeRequirementsA: this.exchangeRequirementsA?.requirements || [],
exchangeRewardsB: this.exchangeRewardsB?.rewards || []
},
listener: 'traderObject'
};
client.send('*', sendData);
if(true === subActionResult && ObjectsConst.TRADE_ACTIONS_FUNCTION_NAME.CONFIRM === mappedSubAction){
delete this.tradesInProgress[tradeKey];
}
return true;
}
/**
* @param {Object} data
* @param {string|number} tradeKey
* @returns {boolean}
*/
shouldCancelExchange(data, tradeKey)
{
return GameConst.CLOSE_UI_ACTION === data.act
&& this.id === data.id
&& this.tradesInProgress[tradeKey];
}
/**
* @param {Player} playerSchema
* @param {string} message
* @returns {boolean}
*/
transactionError(playerSchema, message)
{
Logger.error(message, {
'Player ID': sc.get(playerSchema, 'player_id', 'Undefined'),
'Trade in progress': sc.get(this.tradesInProgress, playerSchema.player_id, 'None')
});
return false;
}
/**
* @param {string} action
* @returns {string|false}
*/
mapInventoryKeyFromAction(action)
{
return sc.get(ObjectsConst.DEFAULTS.TRADER_OBJECT.INVENTORY_MAP, action, false);
}
/**
* @param {string|false} subAction
* @returns {string|false}
*/
mapSubAction(subAction)
{
if(false === subAction || '' === subAction){
return false;
}
let map = {
[ObjectsConst.TRADE_ACTIONS.ADD]: ObjectsConst.TRADE_ACTIONS_FUNCTION_NAME.ADD,
[ObjectsConst.TRADE_ACTIONS.REMOVE]: ObjectsConst.TRADE_ACTIONS_FUNCTION_NAME.REMOVE,
[ObjectsConst.TRADE_ACTIONS.CONFIRM]: ObjectsConst.TRADE_ACTIONS_FUNCTION_NAME.CONFIRM
}
return sc.get(map, subAction, false);
}
}
module.exports.TraderObject = TraderObject;