import { ChatVente } from "../achat-vente/chat-vente.js"; import { ChatUtility } from "../chat-utility.js"; import { SYSTEM_SOCKET_ID } from "../constants.js"; import { Grammar } from "../grammar.js"; import { Monnaie } from "../item-monnaie.js"; import { ITEM_TYPES } from "../item.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"; export class RdDBaseActor extends Actor { static _findCaracNode(carac, name) { return Object.entries(carac) .filter(it => Grammar.equalsInsensitive(it[1].label, name)) .map(it => it[0]) .find(it => it) } static $findCaracByName(carac, name) { const caracList = Object.entries(carac); let entry = Misc.findFirstLike(name, caracList, { mapper: it => it[0], description: 'caractéristique' }); if (!entry || entry.length == 0) { entry = Misc.findFirstLike(name, caracList, { mapper: it => it[1].label, description: 'caractéristique' }); } return entry && entry.length > 0 ? carac[entry[0]] : undefined; } static getDefaultImg(itemType) { return game.system.rdd.actorClasses[itemType]?.defaultIcon ?? defaultItemImg[itemType]; } static init() { Hooks.on("preUpdateItem", (item, change, options, id) => Misc.documentIfResponsible(item.parent)?.onPreUpdateItem(item, change, options, id)) Hooks.on("createItem", (item, options, id) => Misc.documentIfResponsible(item.parent)?.onCreateItem(item, options, id)) Hooks.on("deleteItem", (item, options, id) => Misc.documentIfResponsible(item.parent)?.onDeleteItem(item, options, id)) Hooks.on("updateActor", (actor, change, options, actorId) => Misc.documentIfResponsible(actor)?.onUpdateActor(change, options, actorId)) } static onSocketMessage(sockmsg) { switch (sockmsg.msg) { case "msg_remote_actor_call": return RdDBaseActor.onRemoteActorCall(sockmsg.data, sockmsg.userId); } } 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 = RdDBaseActor.getRealActor(callData?.actorId, callData?.tokenId); 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 getRealActor(actorId, tokenId) { if (tokenId) { let token = canvas.tokens.get(tokenId) if (token) { return token.actor } } return game.actors.get(actorId) } getAlias() { if (this.token?.name != null && this.token != this.prototypeToken) { return this.token.name } return this.name } isPersonnageJoueur() { return false } static extractActorMin = (actor) => { return { id: actor?.id, type: actor?.type, name: actor?.name, img: actor?.img }; }; /** * Cette 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) { foundry.utils.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); } } context.rdd = undefined super(docData, context); } findCaracByName(name) { name = Grammar.toLowerCaseNoAccent(name) switch (name) { case 'reve-actuel': case 'reve actuel': return this.system.carac.reve case 'chance-actuelle': case 'chance actuelle': return this.system.carac.chance case 'vie': return this.system.sante.vie } const carac = this.system.carac; return RdDBaseActor.$findCaracByName(carac, name); } getCaracByName(name) { switch (Grammar.toLowerCaseNoAccent(name)) { case 'reve-actuel': case 'reve actuel': return this.getCaracReveActuel(); case 'chance-actuelle': case 'chance-actuelle': return this.getCaracChanceActuelle(); } return this.findCaracByName(name); } /* -------------------------------------------- */ async _preCreate(data, options, user) { await super._preCreate(data, options, user); // Configure prototype token settings const prototypeToken = {}; if (this.type === "personnage") Object.assign(prototypeToken, { sight: { enabled: true }, actorLink: true, disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY }); this.updateSource({ prototypeToken }); } /* -------------------------------------------- */ prepareData() { super.prepareData() this.prepareActorData() this.cleanupConteneurs() this.computeEtatGeneral() this.computeEncTotal() } async prepareActorData() { } async computeEtatGeneral() { } /* -------------------------------------------- */ findPlayer() { return game.users.players.find(player => player.active && player.character?.id == this.id); } isCreatureEntite() { return this.isCreature() || this.isEntite() } isCreature() { return false } isEntite(typeentite = []) { return false } isVehicule() { return false } isPersonnage() { return false } getItem(id, type = undefined) { const item = this.items.get(id); if (type == undefined || (item?.type == type)) { return item; } return undefined; } listeSuivants(filter = suivant => true) { return [] } listeSuivants(filter = suivant => true) { return [] } listItems(type = undefined) { return (type ? this.itemTypes[type] : this.items); } filterItems(filter, type = undefined) { return (type ? this.itemTypes[type] : this.items)?.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'); } getEncombrementMax() { return 0 } /* -------------------------------------------- */ async onPreUpdateItem(item, change, options, id) { } async onCreateItem(item, options, id) { } async onUpdateActor(update, options, actorId) { } async onDeleteItem(item, options, id) { if (item.isInventaire()) { this._removeItemFromConteneur(item) } } _removeItemFromConteneur(item) { this.items.filter(it => it.isConteneur() && it.system.contenu.includes(item.id)) .forEach(conteneur => { const nouveauContenu = conteneur.system.contenu.filter(id => id != item.id); conteneur.update({ 'system.contenu': nouveauContenu }); }); } async onTimeChanging(oldTimestamp, newTimestamp) { this.items.filter(it => it.isFinPeriode(oldTimestamp, newTimestamp)) .forEach(async it => await it.onFinPeriodeTemporel(oldTimestamp, newTimestamp)) } async creerObjetParMJ(object) { if (!Misc.isFirstConnectedGM()) { RdDBaseActor.remoteActorCall({ tokenId: this.token?.id, actorId: this.id, method: 'creerObjetParMJ', args: [object] }); return; } await this.createEmbeddedDocuments('Item', [object]) } /* -------------------------------------------- */ async cleanupConteneurs() { if (Misc.isOwnerPlayerOrUniqueConnectedGM(this)) { let updates = this.itemTypes['conteneur'] .filter(c => c.system.contenu.filter(id => this.getItem(id) == undefined).length > 0) .map(c => { return { _id: c._id, 'system.contenu': c.system.contenu.filter(id => this.getItem(id) != undefined) } }); if (updates.length > 0) { await this.updateEmbeddedDocuments("Item", updates) } } } /* -------------------------------------------- */ 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 item.update({ '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); // TODO: passer en handlebars 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 !"; } ChatMessage.create({ whisper: ChatUtility.getOwners(this), content: msg }) } 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(), tokenId: this.token?.id, 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.getOwners(this), 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.isFirstConnectedGM()) { 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); if (!itemVendu) { ChatUtility.notifyUser(achat.userId, 'warn', vendeur ? `Le vendeur n'a pas plus de ${achat.vente.item.name} !` : `Impossible de retrouver: ${achat.vente.item.name} !`); return; } if (vendeur && !vendeur.verifierQuantite(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 vendeur?.vendre(itemVendu, quantite, cout); await acheteur?.acheter(itemVendu, quantite, cout, achat) if (cout > 0) { RdDAudio.PlayContextAudio("argent"); } const chatAchatItem = foundry.utils.duplicate(achat.vente); chatAchatItem.quantiteTotal = quantite; ChatMessage.create({ user: achat.userId, speaker: { alias: (acheteur ?? vendeur).getAlias() }, whisper: ChatUtility.getOwners(this), content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-achat-item.html', chatAchatItem) }); if (!achat.vente.quantiteIllimite) { if (achat.vente.nbLots <= achat.choix.nombreLots) { ChatUtility.removeChatMessageId(achat.chatMessageIdVente); } else if (achat.chatMessageIdVente) { await ChatVente.diminuerQuantiteAchatVente(achat.chatMessageIdVente, achat.choix.nombreLots) } } } async vendre(item, quantite, cout) { await this.ajouterSols(cout); await this.decrementerQuantiteItem(item, quantite); } async acheter(item, quantite, cout, achat) { await this.depenserSols(cout) const createdItemId = await this.creerQuantiteItem(item, quantite) if (achat.choix.consommer && item.type == 'nourritureboisson' && createdItemId != undefined) { achat.choix.doses = achat.choix.nombreLots; await this.consommerNourritureboisson(createdItemId, achat.choix, achat.vente.actingUserId); } } verifierFortune(cout) { return this.getFortune() >= cout; } verifierQuantite(item, quantiteDemande) { const disponible = this.getQuantiteDisponible(item); return disponible == undefined || disponible >= quantiteDemande; } async consommerNourritureboisson(itemId, choix, userId) { } async decrementerQuantiteItem(item, quantite, options = { supprimerSiZero: true }) { if (item.isService()) { return; } const itemId = item.id; 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: itemId, '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) { const realItem = this.getItem(item.id) realItem.update({ 'system.quantite': resteQuantite }); 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: foundry.utils.mergeObject(item.system, { quantite: isItemEmpilable ? quantite : undefined }, { inplace: false }) }; 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; } } /* -------------------------------------------- */ async computeEncTotal() { if (!this.pack) { this.encTotal = this.items.map(it => it.getEncTotal()).reduce(Misc.sum(), 0); return this.encTotal; } return 0; } getEncTotal() { return Math.floor(this.encTotal ?? 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 sourceTokenId = params.sourceTokenId const itemId = params.itemId const destId = params.destId const srcId = params.srcId if (sourceActorId && sourceActorId != targetActorId) { console.log("Moving objects", sourceActorId, sourceTokenId, targetActorId, itemId); this.moveItemsBetweenActors(itemId, sourceActorId, sourceTokenId); 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, moved) { if (!dest) { return true; } if (!dest.isConteneur()) { return false; } if (moved.isConteneurContenu(dest)) { ui.notifications.warn(`Impossible de déplacer un conteneur parent (${moved.name}) dans un de ses contenus ${dest.name} !`); return false; } // Calculer le total actuel des contenus const encContenu = dest.getEncContenu(); const newEnc = moved.getEncTotal(); // Calculer le total actuel du nouvel objet const placeDisponible = Misc.keepDecimals(dest.system.capacite - encContenu - newEnc, 4) // Teste si le conteneur de destination a suffisament de capacité pour recevoir le nouvel objet if (placeDisponible < 0) { ui.notifications.warn( `Le conteneur ${dest.name} a une capacité de ${dest.system.capacite}, et contient déjà ${encContenu}. Impossible d'y ranger: ${moved.name} d'encombrement ${newEnc}!`); return false; } return true; } /* -------------------------------------------- */ /** Ajoute un item dans un conteneur, sur la base de leurs ID */ async ajouterDansConteneur(item, conteneur, onAjouterDansConteneur) { if (conteneur?.isConteneur()) { item.estContenu = true; const nouveauContenu = [...conteneur.system.contenu, item.id]; await conteneur.update({ 'system.contenu': nouveauContenu }); onAjouterDansConteneur(item.id, conteneur.id) } else { item.estContenu = false; await conteneur?.update({ 'system.-=contenu': undefined }) } } /* -------------------------------------------- */ /** 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.system.contenu != undefined) { if (item.type == 'conteneur') { corrections.push({ _id: item.id, 'system.contenu': [] }); } else { corrections.push({ _id: item.id, 'system.-=contenu': undefined }); } } } 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) { if (conteneur.isConteneur()) { const contenu = conteneur.system.contenu.filter(id => id != item.id); await conteneur.update({ 'system.contenu': contenu }); onEnleverDeConteneur(); } else { await conteneur.update({ 'system.-=contenu': undefined }) } } item.estContenu = false; } /* -------------------------------------------- */ async moveItemsBetweenActors(itemId, sourceActorId, sourceTokenId) { let sourceActor = RdDBaseActor.getRealActor(sourceActorId, sourceTokenId) let itemsList = [{ id: itemId, conteneurId: undefined }] sourceActor.buildSubConteneurObjetList(itemId, itemsList); // Get itemId list const itemsDataToCreate = itemsList.map(it => sourceActor.getItem(it.id)) .map(it => foundry.utils.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 const newConteneurId = itemMap[item.conteneurId]; const newConteneur = this.getItem(newConteneurId); const newItemId = itemMap[item.id]; // Get newItem console.log('New conteneur filling!', newConteneur, newItemId, item); const nouveauContenu = [...newConteneur.system.contenu, newItemId] await newConteneur.update({ 'system.contenu': nouveauContenu }) } } const deletedItemIds = itemsList.map(it => it.id) await sourceActor.deleteEmbeddedDocuments('Item', deletedItemIds); } _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.getAlias(), system: { description: this.system.description } } renderTemplate('systems/foundryvtt-reve-de-dragon/templates/post-actor.html', chatData) .then(html => ChatMessage.create(RdDUtility.chatDataSetup(html, modeOverride))); } actionImpossible(action) { ui.notifications.info(`${this.getAlias()} ne peut pas faire cette action: ${action}`) } async jetEthylisme() { this.actionImpossible("jet d'éthylisme") } async rollAppelChance() { this.actionImpossible("appel à la chance") } async jetDeMoral() { this.actionImpossible("jet de moral") } async actionPrincipale(item, onActionItem = async () => { }) { switch (item.type) { case ITEM_TYPES.conteneur: return await item.sheet.render(true); } return undefined } async resetItemUse() { } async incDecItemUse(itemId, inc = 1) { } getItemUse(itemId) { return 0; } async finDeRound(options = { terminer: false }) { } isActorCombat() { return false } getCaracInit(competence) { return 0 } listActionsCombat() { return [] } listActionsPossessions() { return this.itemTypes[ITEM_TYPES.possession] .map(p => { return { name: p.name, action: 'possession', system: { competence: p.name, possessionid: p.system.possessionid, } } }) } }