import { ChatUtility } from "../chat-utility.js"; import { SYSTEM_SOCKET_ID } from "../constants.js"; import { Monnaie } from "../item-monnaie.js"; import { Misc } from "../misc.js"; import { RdDAudio } from "../rdd-audio.js"; import { RdDConfirm } from "../rdd-confirm.js"; import { RdDUtility } from "../rdd-utility.js"; import { SystemCompendiums } from "../settings/system-compendiums.js"; import { APP_ASTROLOGIE_REFRESH } from "../sommeil/app-astrologie.js"; export class RdDBaseActor extends Actor { static getDefaultImg(itemType) { return game.system.rdd.actorClasses[itemType]?.defaultIcon ?? defaultItemImg[itemType]; } /* -------------------------------------------- */ static init() { Hooks.on("preUpdateItem", (item, change, options, id) => RdDBaseActor.getParentActor(item)?.onPreUpdateItem(item, change, options, id)); Hooks.on("createItem", (item, options, id) => RdDBaseActor.getParentActor(item)?.onCreateItem(item, options, id)); Hooks.on("deleteItem", (item, options, id) => RdDBaseActor.getParentActor(item)?.onDeleteItem(item, options, id)); Hooks.on("updateActor", (actor, change, options, actorId) => actor.onUpdateActor(change, options, actorId)); } static onSocketMessage(sockmsg) { switch (sockmsg.msg) { case "msg_remote_actor_call": return RdDBaseActor.onRemoteActorCall(sockmsg.data, sockmsg.userId); case "msg_reset_nombre_astral": game.user.character.resetNombresAstraux(); game.system.rdd.calendrier.notifyChangeNombresAstraux(); return; case "msg_refresh_nombre_astral": Hooks.callAll(APP_ASTROLOGIE_REFRESH); return; } } static remoteActorCall(callData, userId = undefined) { userId = userId ?? Misc.firstConnectedGMId(); if (userId == game.user.id) { RdDBaseActor.onRemoteActorCall(callData, userId); return false; } else { game.socket.emit(SYSTEM_SOCKET_ID, { msg: "msg_remote_actor_call", data: callData, userId: userId }); return true; } } static onRemoteActorCall(callData, userId) { if (userId == game.user.id) { const actor = game.actors.get(callData?.actorId); if (Misc.isOwnerPlayerOrUniqueConnectedGM(actor)) { // Seul le joueur choisi effectue l'appel: le joueur courant si propriétaire de l'actor, ou le MJ sinon const args = callData.args; console.info(`RdDBaseActor.onRemoteActorCall: pour l'Actor ${callData.actorId}, appel de RdDBaseActor.${callData.method}(`, ...args, ')'); actor[callData.method](...args); } } } static getParentActor(document) { return document?.parent instanceof Actor ? document.parent : undefined } /** * Cet methode surcharge Actor.create() pour ajouter si besoin des Items par défaut: * compétences et monnaies. * * @param {Object} actorData template d'acteur auquel ajouter des informations. * @param {Object} options optionspour customiser la création */ static async create(actorData, options) { // import depuis un compendium if (actorData instanceof Array) { return super.create(actorData, options); } // Création d'un acteur avec des items (uniquement en cas de duplication): pas besoin d'ajouter d'items if (actorData.items) { return await super.create(actorData, options); } actorData.items = []; if (actorData.type == "personnage") { const competences = await SystemCompendiums.getCompetences(actorData.type); actorData.items = actorData.items.concat(competences.map(i => i.toObject())) .concat(Monnaie.monnaiesStandard()); } else if (actorData.type == "commerce") { actorData.items = actorData.items.concat(Monnaie.monnaiesStandard()); } return super.create(actorData, options); } constructor(docData, context = {}) { if (!context.rdd?.ready) { mergeObject(context, { rdd: { ready: true } }); const ActorConstructor = game.system.rdd.actorClasses[docData.type]; if (ActorConstructor) { if (!docData.img) { docData.img = ActorConstructor.defaultIcon; } return new ActorConstructor(docData, context); } } super(docData, context); } isCreatureEntite() { return this.type == 'creature' || this.type == 'entite'; } isCreature() { return this.type == 'creature'; } isEntite() { return this.type == 'entite'; } isPersonnage() { return this.type == 'personnage'; } isVehicule() { return this.type == 'vehicule'; } getItem(id, type = undefined) { const item = this.items.get(id); if (type == undefined || (item?.type == type)) { return item; } return undefined; } listItems(type = undefined) { return (type ? this.itemTypes[type] : this.items); } filterItems(filter, type = undefined) { return type ? this.itemTypes[type]?.filter(filter) ?? [] : []; } findItemLike(idOrName, type) { return this.getItem(idOrName, type) ?? Misc.findFirstLike(idOrName, this.listItems(type), { description: Misc.typeName('Item', type) }); } getMonnaie(id) { return this.findItemLike(id, 'monnaie'); } recompute() { } /* -------------------------------------------- */ async onPreUpdateItem(item, change, options, id) { } async onCreateItem(item, options, id) { } async onDeleteItem(item, options, id) { } async onUpdateActor(update, options, actorId) { } async onTimeChanging(oldTimestamp, newTimestamp) { this.items.filter(it => it.isFinPeriode(oldTimestamp, newTimestamp)) .forEach(async it => await it.onFinPeriodeTemporel(oldTimestamp, newTimestamp)) } /* -------------------------------------------- */ getFortune() { return Monnaie.getFortune(this.itemTypes['monnaie']); } /* -------------------------------------------- */ async itemQuantiteIncDec(id, value) { let item = this.getItem(id); if (item && item.isInventaire()) { const quantite = Math.max(0, item.system.quantite + value); await this.updateEmbeddedDocuments('Item', [{ _id: item.id, 'system.quantite': quantite }]); } } computePrixTotalEquipement() { return this.items.filter(it => it.isInventaire()) .filter(it => !it.isMonnaie()) .map(it => it.valeurTotale()) .reduce(Misc.sum(), 0); } async payerSols(depense) { depense = Number(depense); if (depense == 0) { return; } let fortune = this.getFortune(); console.log("payer", game.user.character, depense, fortune); let msg = ""; if (fortune >= depense) { await Monnaie.optimiserFortune(this, fortune - depense); msg = `Vous avez payé ${depense} Sols, qui ont été soustraits de votre argent.`; RdDAudio.PlayContextAudio("argent"); // Petit son } else { msg = "Vous n'avez pas assez d'argent pour payer cette somme !"; } let message = { whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name), content: msg }; ChatMessage.create(message); } async depenserSols(sols) { let reste = this.getFortune() - Number(sols); if (reste >= 0) { await Monnaie.optimiserFortune(this, reste); } return reste; } async ajouterSols(sols, fromActorId = undefined) { sols = Number(sols); if (sols == 0) { return; } if (sols < 0) { ui.notifications.error(`Impossible d'ajouter un gain de ${sols} <0`); return; } if (fromActorId && !game.user.isGM) { RdDBaseActor.remoteActorCall({ userId: Misc.connectedGMOrUser(), actorId: this.id, method: 'ajouterSols', args: [sols, fromActorId] }); } else { const fromActor = game.actors.get(fromActorId) await Monnaie.optimiserFortune(this, sols + this.getFortune()); RdDAudio.PlayContextAudio("argent"); // Petit son ChatMessage.create({ whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name), content: `Vous avez reçu ${sols} Sols ${fromActor ? " de " + fromActor.name : ''}, qui ont été ajoutés à votre argent.` }); } } /* -------------------------------------------- */ getQuantiteDisponible(item) { return item?.isService() ? undefined : item?.getQuantite(); } /* -------------------------------------------- */ async achatVente(achat) { if (achat.vendeurId == achat.acheteurId) { ui.notifications.info("Inutile de se vendre à soi-même"); return; } if (!Misc.isUniqueConnectedGM()) { RdDBaseActor.remoteActorCall({ actorId: achat.vendeurId ?? achat.acheteurId, method: 'achatVente', args: [achat] }); return; } const cout = Number(achat.prixTotal ?? 0); const vendeur = achat.vendeurId ? game.actors.get(achat.vendeurId) : undefined; const acheteur = achat.acheteurId ? game.actors.get(achat.acheteurId) : undefined; const quantite = (achat.choix.nombreLots ?? 1) * (achat.vente.tailleLot); const itemVendu = vendeur?.getItem(achat.vente.item._id) ?? game.items.get(achat.vente.item._id) ?? achat.vente.item; if (!itemVendu) { ui.notifications.warn("Erreur sur achat: rien à acheter
Si possible, transmettez les logs de la console aux développeurs"); console.log('Erreur sur achat: rien à acheter', achat); return; } if (!this.verifierQuantite(vendeur, itemVendu, quantite)) { ChatUtility.notifyUser(achat.userId, 'warn', `Le vendeur n'a pas assez de ${itemVendu.name} !`); return } if (acheteur && !acheteur.verifierFortune(cout)) { ChatUtility.notifyUser(achat.userId, 'warn', `Vous n'avez pas assez d'argent pour payer ${Math.ceil(cout / 100)} sols !`); return; } await this.decrementerVente(vendeur, itemVendu, quantite, cout); if (acheteur) { await acheteur.depenserSols(cout); const createdItemId = await acheteur.creerQuantiteItem(achat.vente.item, quantite); await acheteur.consommerNourritureAchetee(achat, achat.vente, createdItemId); } if (cout > 0) { RdDAudio.PlayContextAudio("argent"); } const chatAchatItem = duplicate(achat.vente); chatAchatItem.quantiteTotal = quantite; ChatMessage.create({ user: achat.userId, speaker: { alias: (acheteur ?? vendeur).name }, whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name), content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-achat-item.html', chatAchatItem) }); if (!achat.vente.quantiteIllimite) { if (achat.vente.quantiteNbLots <= achat.choix.nombreLots) { ChatUtility.removeChatMessageId(achat.chatMessageIdVente); } else if (achat.chatMessageIdVente) { achat.vente.properties = itemVendu.getProprietes(); achat.vente.quantiteNbLots -= achat.choix.nombreLots; achat.vente.jsondata = JSON.stringify(achat.vente.item); const messageVente = game.messages.get(achat.chatMessageIdVente); messageVente.update({ content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-vente-item.html', achat.vente) }); messageVente.render(true); } } } async decrementerVente(vendeur, itemVendu, quantite, cout) { if (vendeur) { await vendeur.ajouterSols(cout); await vendeur.decrementerQuantiteItem(itemVendu, quantite); } } verifierFortune(cout) { return this.getFortune() >= cout; } verifierQuantite(vendeur, item, quantiteTotal) { const disponible = vendeur?.getQuantiteDisponible(item); return disponible == undefined || disponible >= quantiteTotal; } async consommerNourritureAchetee(achat, vente, createdItemId) { if (achat.choix.consommer && vente.item.type == 'nourritureboisson' && createdItemId != undefined) { achat.choix.doses = achat.choix.nombreLots; await this.consommerNourritureboisson(createdItemId, achat.choix, vente.actingUserId); } } async decrementerQuantiteItem(item, quantite, options = { supprimerSiZero: true }) { if (item.isService()) { return; } let resteQuantite = (item.system.quantite ?? 1) - quantite; if (resteQuantite <= 0) { if (options.supprimerSiZero) { await this.deleteEmbeddedDocuments("Item", [item.id]); } else { await this.updateEmbeddedDocuments("Item", [{ _id: item.id, 'system.quantite': 0 }]); } if (resteQuantite < 0) { ui.notifications.warn(`La quantité de ${item.name} était insuffisante, l'objet a donc été supprimé`) } } else if (resteQuantite > 0) { await this.updateEmbeddedDocuments("Item", [{ _id: item.id, 'system.quantite': resteQuantite }]); } } async creerQuantiteItem(item, quantite) { if (this.canReceive(item)) { const isItemEmpilable = "quantite" in item.system; const baseItem = { type: item.type, img: item.img, name: item.name, system: mergeObject(item.system, { quantite: isItemEmpilable ? quantite : undefined }) }; const newItems = isItemEmpilable ? [baseItem] : Array.from({ length: quantite }, (_, i) => baseItem); const items = await this.createEmbeddedDocuments("Item", newItems); return items.length > 0 ? items[0].id : undefined; } } /* -------------------------------------------- */ computeMalusSurEncombrement() { return 0; } getEncombrementMax() { return 0; } async computeEncTotal() { if (!this.pack) { this.encTotal = this.items.map(it => it.getEncTotal()).reduce(Misc.sum(), 0); return this.encTotal; } return 0; } async createItem(type, name = undefined) { if (!name) { name = 'Nouveau ' + Misc.typeName('Item', type); } await this.createEmbeddedDocuments('Item', [{ name: name, type: type }], { renderSheet: true }); } canReceive(item) { return false; } async processDropItem(params) { const targetActorId = this.id; const sourceActorId = params.sourceActorId; const itemId = params.itemId; const destId = params.destId; const srcId = params.srcId; if (sourceActorId && sourceActorId != targetActorId) { console.log("Moving objects", sourceActorId, targetActorId, itemId); this.moveItemsBetweenActors(itemId, sourceActorId); return false; } let result = true; const item = this.getItem(itemId); if (item?.isInventaire('all') && sourceActorId == targetActorId) { // rangement if (srcId != destId && itemId != destId) { // déplacement de l'objet const src = this.getItem(srcId); const dest = this.getItem(destId); const cible = this.getContenantOrParent(dest); const [empilable, message] = item.isInventaireEmpilable(dest); if (empilable) { await dest.empiler(item) result = false; } // changer de conteneur else if (!cible || this.conteneurPeutContenir(cible, item)) { await this.enleverDeConteneur(item, src, params.onEnleverConteneur); await this.ajouterDansConteneur(item, cible, params.onAjouterDansConteneur); if (message && !dest.isConteneur()) { ui.notifications.info(cible ? `${message}
${item.name} a été déplacé dans: ${cible.name}` : `${message}
${item.name} a été sorti du conteneur`); } } } } await this.computeEncTotal(); return result; } getContenantOrParent(dest) { if (!dest || dest.isConteneur()) { return dest; } return this.getContenant(dest); } getContenant(item) { return this.itemTypes['conteneur'].find(it => it.system.contenu.includes(item.id)); } /* -------------------------------------------- */ conteneurPeutContenir(dest, item) { if (!dest) { return true; } if (!dest.isConteneur()) { return false; } const destData = dest if (this._isConteneurContenu(item, dest)) { ui.notifications.warn(`Impossible de déplacer un conteneur parent (${item.name}) dans un de ses contenus ${destData.name} !`); return false; // Loop detected ! } // Calculer le total actuel des contenus let encContenu = this.getRecursiveEnc(dest) - Number(destData.system.encombrement); let newEnc = this.getRecursiveEnc(item); // Calculer le total actuel du nouvel objet // Teste si le conteneur de destination a suffisament de capacité pour recevoir le nouvel objet if (Number(destData.system.capacite) < encContenu + newEnc) { ui.notifications.warn( `Le conteneur ${dest.name} a une capacité de ${destData.system.capacite}, et contient déjà ${encContenu}. Impossible d'y ranger: ${item.name} d'encombrement ${newEnc}!`); return false; } return true; } /* -------------------------------------------- */ _isConteneurContenu(item, conteneur) { if (item?.isConteneur()) { // Si c'est un conteneur, il faut vérifier qu'on ne le déplace pas vers un sous-conteneur lui appartenant for (let id of item.system.contenu) { let subObjet = this.getItem(id); if (subObjet?.id == conteneur.id) { return true; // Loop detected ! } if (subObjet?.isConteneur()) { return this._isConteneurContenu(subObjet, conteneur); } } } return false; } /* -------------------------------------------- */ getRecursiveEnc(objet) { if (!objet) { return 0; } const tplData = objet.system; if (objet.type != 'conteneur') { return Number(tplData.encombrement) * Number(tplData.quantite); } const encContenus = tplData.contenu.map(idContenu => this.getRecursiveEnc(this.getItem(idContenu))); return encContenus.reduce(Misc.sum(), 0) + Number(tplData.encombrement) /* TODO? Number(tplData.quantite) -- on pourrait avoir plusieurs conteneurs...*/ } /* -------------------------------------------- */ /** Ajoute un item dans un conteneur, sur la base * de leurs ID */ async ajouterDansConteneur(item, conteneur, onAjouterDansConteneur) { if (!conteneur) { // TODO: afficher item.estContenu = false; } else if (conteneur.isConteneur()) { item.estContenu = true; await this.updateEmbeddedDocuments('Item', [{ _id: conteneur.id, 'system.contenu': [...conteneur.system.contenu, item.id] }]); onAjouterDansConteneur(item.id, conteneur.id); } } /* -------------------------------------------- */ /** Fonction de remise à plat de l'équipement (ie vide les champs 'contenu') */ async nettoyerConteneurs() { RdDConfirm.confirmer({ settingConfirmer: "confirmation-vider", content: `

Etes vous certain de vouloir vider tous les conteneurs ?

`, title: 'Vider les conteneurs', buttonLabel: 'Vider', onAction: async () => { const corrections = []; for (let item of this.items) { if (item.estContenu) { item.estContenu = undefined; } if (item.type == 'conteneur' && item.system.contenu.length > 0) { corrections.push({ _id: item.id, 'system.contenu': [] }); } } if (corrections.length > 0) { await this.updateEmbeddedDocuments('Item', corrections); } } }); } /* -------------------------------------------- */ buildSubConteneurObjetList(conteneurId, deleteList) { let conteneur = this.getItem(conteneurId); if (conteneur?.type == 'conteneur') { // Si c'est un conteneur for (let subId of conteneur.system.contenu) { let subObj = this.getItem(subId); if (subObj) { if (subObj.type == 'conteneur') { this.buildSubConteneurObjetList(subId, deleteList); } deleteList.push({ id: subId, conteneurId: conteneurId }); } } } } /* -------------------------------------------- */ async deleteAllConteneur(itemId, options) { let list = []; list.push({ id: itemId, conteneurId: undefined }); // Init list this.buildSubConteneurObjetList(itemId, list); await this.deleteEmbeddedDocuments('Item', list.map(it => it.id), options); } /* -------------------------------------------- */ /** Supprime un item d'un conteneur, sur la base * de leurs ID */ async enleverDeConteneur(item, conteneur, onEnleverDeConteneur) { if (conteneur?.isConteneur()) { item.estContenu = false; await this.updateEmbeddedDocuments('Item', [{ _id: conteneur.id, 'system.contenu': conteneur.system.contenu.filter(id => id != item.id) }]); onEnleverDeConteneur(); } } /* -------------------------------------------- */ async moveItemsBetweenActors(itemId, sourceActorId) { let itemsList = [] let sourceActor = game.actors.get(sourceActorId); itemsList.push({ id: itemId, conteneurId: undefined }); // Init list sourceActor.buildSubConteneurObjetList(itemId, itemsList); // Get itemId list const itemsDataToCreate = itemsList.map(it => sourceActor.getItem(it.id)) .map(it => duplicate(it)) .map(it => { it.system.contenu = []; return it; }); let newItems = await this.createEmbeddedDocuments('Item', itemsDataToCreate); let itemMap = this._buildMapOldNewId(itemsList, newItems); for (let item of itemsList) { // Second boucle pour traiter la remise en conteneurs // gestion conteneur/contenu if (item.conteneurId) { // l'Objet était dans un conteneur let newConteneurId = itemMap[item.conteneurId]; // Get conteneur let newConteneur = this.getItem(newConteneurId); let newItemId = itemMap[item.id]; // Get newItem console.log('New conteneur filling!', newConteneur, newItemId, item); let contenu = duplicate(newConteneur.system.contenu); contenu.push(newItemId); await this.updateEmbeddedDocuments('Item', [{ _id: newConteneurId, 'system.contenu': contenu }]); } } for (let item of itemsList) { await sourceActor.deleteEmbeddedDocuments('Item', [item.id]); } } _buildMapOldNewId(itemsList, newItems) { let itemMap = {}; for (let i = 0; i < itemsList.length; i++) { itemMap[itemsList[i].id] = newItems[i].id; // Pour garder le lien ancien / nouveau } return itemMap; } /* -------------------------------------------- */ async postActorToChat(modeOverride) { let chatData = { doctype: 'Actor', id: this.id, type: this.type, img: this.img, pack: this.pack, name: this.name, system: { description: this.system.description } } renderTemplate('systems/foundryvtt-reve-de-dragon/templates/post-actor.html', chatData) .then(html => ChatMessage.create(RdDUtility.chatDataSetup(html, modeOverride))); } }