diff --git a/icons/items/services.webp b/icons/items/services.webp new file mode 100644 index 00000000..a55077d6 Binary files /dev/null and b/icons/items/services.webp differ diff --git a/lang/fr.json b/lang/fr.json index 3cc1bf86..36819273 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -27,6 +27,7 @@ "TypeArmure": "Armure", "TypeConteneur": "Conteneur", "TypeNourritureboisson": "Nourriture & boisson", + "TypeService": "Services/Boutique", "TypeChant": "Chant", "TypeDanse": "Danse", "TypeMusique": "Musique", diff --git a/module/actor-sheet.js b/module/actor-sheet.js index a8ef1ab5..0108d369 100644 --- a/module/actor-sheet.js +++ b/module/actor-sheet.js @@ -57,7 +57,7 @@ export class RdDActorSheet extends ActorSheet { notes: await TextEditor.enrichHTML(this.object.system.notes, { async: true }), notesmj: await TextEditor.enrichHTML(this.object.system.notesmj, { async: true }), calc: { - fortune: Monnaie.getFortune(this.actor), + fortune: Monnaie.getFortuneSolsDeniers(this.actor), encTotal: await this.actor.computeEncombrementTotalEtMalusArmure(), surenc: this.actor.computeMalusSurEncombrement(), prixTotalEquipement: this.actor.computePrixTotalEquipement(), @@ -137,7 +137,7 @@ export class RdDActorSheet extends ActorSheet { RdDSheetUtility.splitItem(item, this.actor); }); this.html.find('.item-edit').click(async event => RdDSheetUtility.getItem(event, this.actor)?.sheet.render(true)) - this.html.find('.item-delete').click(async event => RdDUtility.confirmerSuppressionItem(this, RdDSheetUtility.getItem(event, this.actor))); + this.html.find('.item-delete').click(async event => RdDUtility.confirmActorItemDelete(this, RdDSheetUtility.getItem(event, this.actor))); this.html.find('.item-vendre').click(async event => RdDSheetUtility.getItem(event, this.actor)?.proposerVente()); this.html.find('.item-montrer').click(async event => RdDSheetUtility.getItem(event, this.actor)?.postItem()); this.html.find('.item-action').click(async event => RdDSheetUtility.getItem(event, this.actor)?.actionPrincipale(this.actor)); diff --git a/module/actor.js b/module/actor.js index 4dd54eed..40d77e64 100644 --- a/module/actor.js +++ b/module/actor.js @@ -188,17 +188,17 @@ export class RdDActor extends Actor { canReceive(item) { if (this.isCreature()) { - return item.type == 'competencecreature' || RdDItem.isItemInventaire(item); + return item.type == 'competencecreature' || item.isInventaire(); } if (this.isEntite()) { return item.type == 'competencecreature'; } if (this.isVehicule()) { - return RdDItem.isItemInventaire(item); + return item.isInventaire(); } if (this.isPersonnage()) { switch (item.type) { - case 'competencecreature': case 'tarot': + case 'competencecreature': case 'tarot': case 'service': return false; } return true; @@ -461,7 +461,7 @@ export class RdDActor extends Actor { selectedCaracName: 'apparence', competences: this.itemTypes['competence'] }; - + const dialog = await RdDRoll.create(this, rollData, { html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll.html' }, { @@ -1406,6 +1406,7 @@ export class RdDActor extends Actor { /* -------------------------------------------- */ computePrixTotalEquipement() { const valeur = this.items.filter(it => it.isInventaire()) + .filter(it => !it.isMonnaie()) .map(it => it.valeurTotale()) .reduce(Misc.sum(), 0); return valeur; @@ -3769,35 +3770,32 @@ export class RdDActor extends Actor { } const cout = Number(achat.prixTotal ?? 0); - const vente = achat.vente; - const acheteur = achat.acheteurId ? game.actors.get(achat.acheteurId) : undefined; const vendeur = achat.vendeurId ? game.actors.get(achat.vendeurId) : undefined; - let itemVendu = vendeur?.getObjet(vente.item._id); - - if (vendeur && (itemVendu?.getQuantite() ?? 0) < achat.quantiteTotal) { - ChatUtility.notifyUser(achat.userId, 'warn', `Le vendeur n'a plus assez de ${vente.item.name} !`); - return; + const service = achat.serviceId ? (vendeur?.getObjet(achat.serviceId) ?? game.items.get(achat.serviceId)) : undefined; + const acheteur = achat.acheteurId ? game.actors.get(achat.acheteurId) : undefined; + const vente = achat.vente; + const quantite = (achat.choix.nombreLots ?? 1) * (vente.tailleLot); + const itemVendu = vendeur?.getObjet(vente.item._id) ?? (await RdDItem.getCorrespondingItem(vente.item)); + if (!this.verifierQuantite(service, vendeur, itemVendu, quantite)) { + ChatUtility.notifyUser(achat.userId, 'warn', `Le vendeur n'a pas assez de ${itemVendu.name} !`); + return } + if (Monnaie.getFortune(acheteur) < Number(cout)) { ChatUtility.notifyUser(achat.userId, 'warn', `Vous n'avez pas assez d'argent pour payer ${Math.ceil(cout / 100)} sols !`); return; } - - achat.quantiteTotal = (achat.choix.nombreLots ?? 1) * (vente.tailleLot); - if (vendeur) { - await vendeur.ajouterSols(cout); - await vendeur.decrementerQuantiteItem(itemVendu, achat.quantiteTotal,); - } + await this.decrementerVente(service, vendeur, itemVendu, quantite, cout); if (acheteur) { await acheteur.depenserSols(cout); - let createdItemId = await acheteur.creerQuantiteItem(vente.item, achat.quantiteTotal); + let createdItemId = await acheteur.creerQuantiteItem(vente.item, quantite); await acheteur.consommerNourritureAchetee(achat, vente, createdItemId); } if (cout > 0) { RdDAudio.PlayContextAudio("argent"); } const chatAchatItem = duplicate(vente); - chatAchatItem.quantiteTotal = achat.quantiteTotal; + chatAchatItem.quantiteTotal = quantite; ChatMessage.create({ user: achat.userId, speaker: { alias: (acheteur ?? vendeur).name }, @@ -3809,8 +3807,8 @@ export class RdDActor extends Actor { if (vente.quantiteNbLots <= achat.choix.nombreLots) { ChatUtility.removeChatMessageId(achat.chatMessageIdVente); } - else { - vente["properties"] = new RdDItem(vente.item).getProprietes(); + else if (!service) { + vente["properties"] = itemVendu.getProprietes(); vente.quantiteNbLots -= achat.choix.nombreLots; vente.jsondata = JSON.stringify(vente.item); const messageVente = game.messages.get(achat.chatMessageIdVente); @@ -3820,6 +3818,21 @@ export class RdDActor extends Actor { } } + async decrementerVente(service, vendeur, itemVendu, quantite, cout) { + if (service) { + await service.venteRefItem(itemVendu, quantite, cout) + } + else if (vendeur) { + await vendeur.ajouterSols(cout); + await vendeur.decrementerQuantiteItem(itemVendu, quantite); + } + } + + verifierQuantite(service, vendeur, item, quantiteTotal) { + const disponible = service ? service.getQuantiteDisponible(item, quantiteTotal) : (vendeur ? (item?.getQuantite() ?? 0) : quantiteTotal); + return disponible >= quantiteTotal; + } + async consommerNourritureAchetee(achat, vente, createdItemId) { if (achat.choix.consommer && vente.item.type == 'nourritureboisson' && createdItemId != undefined) { achat.choix.doses = achat.choix.nombreLots; diff --git a/module/dialog-item-achat.js b/module/dialog-item-achat.js index 947e18e7..5509b178 100644 --- a/module/dialog-item-achat.js +++ b/module/dialog-item-achat.js @@ -1,12 +1,13 @@ +import { Misc } from "./misc.js"; import { RdDUtility } from "./rdd-utility.js"; export class DialogItemAchat extends Dialog { - static venteData(button) { - const vendeurId = button.attributes['data-vendeurId']?.value; + static preparerAchat(chatButton) { + const vendeurId = chatButton.attributes['data-vendeurId']?.value; const vendeur = vendeurId ? game.actors.get(vendeurId) : undefined; const acheteur = RdDUtility.getSelectedActor(); - const json = button.attributes['data-jsondata']?.value; + const json = chatButton.attributes['data-jsondata']?.value; if (!acheteur && !vendeur) { ui.notifications.info("Pas d'acheteur ni de vendeur, aucun changement"); return undefined; @@ -16,46 +17,68 @@ export class DialogItemAchat extends Dialog { return undefined; } - const prixLot = Number(button.attributes['data-prixLot']?.value ?? 0); return { - item: json ? JSON.parse(json) : undefined, - actingUserId: game.user.id, - vendeurId: vendeurId, - vendeur: vendeur, - acheteur: acheteur, - tailleLot: parseInt(button.attributes['data-tailleLot']?.value ?? 1), - quantiteIllimite: button.attributes['data-quantiteIllimite']?.value == 'true', - quantiteNbLots: parseInt(button.attributes['data-quantiteNbLots']?.value), - choix: { - nombreLots: 1, - seForcer: false, - supprimerSiZero: true - }, - prixLot: prixLot, - prixTotal: prixLot, - isVente: prixLot > 0, - chatMessageIdVente: RdDUtility.findChatMessageId(button) + item: (json ? JSON.parse(json) : undefined), + vendeur, + acheteur, + nbLots: parseInt(chatButton.attributes['data-quantiteNbLots']?.value), + tailleLot: parseInt(chatButton.attributes['data-tailleLot']?.value ?? 1), + prixLot: Number(chatButton.attributes['data-prixLot']?.value ?? 0), + quantiteIllimite: chatButton.attributes['data-quantiteIllimite']?.value == 'true', + chatMessageIdVente: RdDUtility.findChatMessageId(chatButton), }; } - static async onAcheter(venteData) { + + static async onAcheter({ item, vendeur, acheteur, service, tailleLot, prixLot, nbLots, quantiteIllimite, chatMessageIdVente }) { + const venteData = { + item, + actingUserId: game.user.id, + vendeurId: vendeur?.id, + vendeur, + acheteur, + service, + tailleLot, + quantiteIllimite, + quantiteNbLots: nbLots, + choix: { seForcer: false, supprimerSiZero: true }, + prixLot, + isVente: prixLot > 0, + isConsommable: item.type == 'nourritureboisson' && acheteur?.isPersonnage(), + chatMessageIdVente + }; + + DialogItemAchat.changeNombreLots(venteData, 1); const html = await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/dialog-item-achat.html`, venteData); new DialogItemAchat(html, venteData).render(true); } + static changeNombreLots(venteData, nombreLots) { + venteData.choix.nombreLots = nombreLots; + venteData.prixTotal = (nombreLots * venteData.prixLot).toFixed(2); + if (venteData.isConsommable) { + const doses = nombreLots * venteData.tailleLot; + venteData.totalSust = Misc.keepDecimals(doses * (venteData.item.system.sust ?? 0), 2); + venteData.totalDesaltere = venteData.item.system.boisson + ? Misc.keepDecimals(doses * (venteData.item.system.desaltere ?? 0), 2) + : 0; + } + } + constructor(html, venteData) { - const isConsommable = venteData.item.type == 'nourritureboisson' && venteData.acheteur?.isPersonnage(); let options = { classes: ["dialogachat"], width: 400, height: 'fit-content', 'z-index': 99999 }; const actionAchat = venteData.prixLot > 0 ? "Acheter" : "Prendre"; const buttons = {}; - if (isConsommable) { + if (venteData.isConsommable) { buttons["consommer"] = { label: venteData.item.system.boisson ? "Boire" : "Manger", callback: it => this.onAchatConsommer() } } buttons[actionAchat] = { label: actionAchat, callback: it => { this.onAchat(); } }; buttons["decliner"] = { label: "Décliner", callback: it => { } }; + const acheteur = venteData.acheteur?.name ?? 'Un acheteur'; + const vendeur = (venteData.service ?? venteData.vendeur)?.name ?? 'Un vendeur'; let conf = { - title: venteData.acheteur ? venteData.acheteur.name + " - " + actionAchat : actionAchat, + title: `${acheteur} - ${actionAchat} à ${vendeur}`, content: html, default: actionAchat, buttons: buttons @@ -69,6 +92,7 @@ export class DialogItemAchat extends Dialog { await this.html.find(".nombreLots").change(); (this.venteData.vendeur ?? this.venteData.acheteur).achatVente({ userId: game.user.id, + serviceId: this.venteData.service?.id, vendeurId: this.venteData.vendeur?.id, acheteurId: this.venteData.acheteur?.id, prixTotal: this.venteData.prixTotal, @@ -96,13 +120,21 @@ export class DialogItemAchat extends Dialog { } setNombreLots(nombreLots) { - if (nombreLots > this.venteData.quantiteNbLots) { - ui.notifications.warn(`Seulement ${this.venteData.quantiteNbLots} lots disponibles, vous ne pouvez pas en prendre ${nombreLots}`) + + if (!this.venteData.quantiteIllimite) { + if (!this.venteData.quantiteIllimite && nombreLots > this.venteData.quantiteNbLots) { + ui.notifications.warn(`Seulement ${this.venteData.quantiteNbLots} lots disponibles, vous ne pouvez pas en prendre ${nombreLots}`) + } + nombreLots = Math.min(nombreLots, this.venteData.quantiteNbLots); } - this.venteData.choix.nombreLots = Math.min(nombreLots, this.venteData.quantiteNbLots); - this.venteData.prixTotal = (nombreLots * this.venteData.prixLot).toFixed(2); - this.html.find(".nombreLots").val(this.venteData.choix.nombreLots); + + DialogItemAchat.changeNombreLots(this.venteData, nombreLots); + + this.html.find(".nombreLots").val(nombreLots); this.html.find(".prixTotal").text(this.venteData.prixTotal); + this.html.find("span.total-sust").text(this.venteData.totalSust); + this.html.find("span.total-desaltere").text(this.venteData.totalDesaltere); } + } \ No newline at end of file diff --git a/module/dialog-item-vente.js b/module/dialog-item-vente.js index 2b09b60b..4441456f 100644 --- a/module/dialog-item-vente.js +++ b/module/dialog-item-vente.js @@ -2,21 +2,24 @@ import { HtmlUtility } from "./html-utility.js"; export class DialogItemVente extends Dialog { - static async display(item, callback) { - const quantite = item.isConteneur() ? 1 : item.system.quantite; + static async display({ item, callback, service = undefined, quantiteMax = undefined }) { + const quantite = quantiteMax ?? item.getQuantite(); + const isOwned = item.isOwned; + // const isOwned = item.isOwned || service?.actor; const venteData = { item: item, - alias: item.actor?.name ?? game.user.name, - vendeurId: item.actor?.id, + alias: item.actor?.name ?? service?.name ?? game.user.name, + serviceId: service?.id, + vendeurId: item.actor?.id ?? service?.actor?.id, prixOrigine: item.system.cout, prixUnitaire: item.system.cout, prixLot: item.system.cout, tailleLot: 1, quantiteNbLots: quantite, quantiteMaxLots: quantite, - quantiteMax: quantite , - quantiteIllimite: !item.isOwned, - isOwned: item.isOwned, + quantiteMax: quantite, + quantiteIllimite: service? service.system.illimite : !isOwned, + isOwned: isOwned, }; const html = await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/dialog-item-vente.html`, venteData); return new DialogItemVente(venteData, html, callback).render(true); @@ -54,7 +57,7 @@ export class DialogItemVente extends Dialog { await this.html.find(".quantiteIllimite").change(); await this.html.find(".prixLot").change(); this.callback(this.venteData); - } + } /* -------------------------------------------- */ setPrixLot(prixLot) { @@ -68,13 +71,11 @@ export class DialogItemVente extends Dialog { this.html.find(".prixLot").val(this.venteData.prixLot); } this.venteData.tailleLot = tailleLot; - if (this.venteData.isOwned) { - // recalculer le nombre de lots max - this.venteData.quantiteMaxLots = Math.floor(this.venteData.quantiteMax / tailleLot); - this.venteData.quantiteNbLots = Math.min(this.venteData.quantiteMaxLots, this.venteData.quantiteNbLots); - this.html.find(".quantiteNbLots").val(this.venteData.quantiteNbLots); - this.html.find(".quantiteNbLots").attr("max", this.venteData.quantiteMaxLots) - } + // recalculer le nombre de lots max + this.venteData.quantiteMaxLots = Math.floor(this.venteData.quantiteMax / tailleLot); + this.venteData.quantiteNbLots = Math.min(this.venteData.quantiteMaxLots, this.venteData.quantiteNbLots); + this.html.find(".quantiteNbLots").val(this.venteData.quantiteNbLots); + this.html.find(".quantiteNbLots").attr("max", this.venteData.quantiteMaxLots) } setNbLots(nbLots) { diff --git a/module/item-monnaie.js b/module/item-monnaie.js index be030cbd..d766c0af 100644 --- a/module/item-monnaie.js +++ b/module/item-monnaie.js @@ -72,6 +72,14 @@ export class Monnaie { return 0; } + static getFortuneSolsDeniers(actor) { + const fortune = Monnaie.getFortune(actor); + return { + sols: Math.floor(fortune), + deniers: Math.round(100 * (fortune - Math.floor(fortune))) + }; + } + static async optimiserFortune(actor, fortune) { let resteEnDeniers = Math.round(fortune * 100); let monnaies = actor.itemTypes['monnaie']; diff --git a/module/item-service-sheet.js b/module/item-service-sheet.js new file mode 100644 index 00000000..97488a57 --- /dev/null +++ b/module/item-service-sheet.js @@ -0,0 +1,81 @@ +import { RdDItemSheet } from "./item-sheet.js"; +import { Misc } from "./misc.js"; +import { RdDUtility } from "./rdd-utility.js"; +import { SystemCompendiums } from "./settings/system-compendiums.js"; +import { DialogItemAchat } from "./dialog-item-achat.js"; +import { RdDItem } from "./item.js"; +import { RdDItemService } from "./item-service.js"; + +export class RdDServiceItemSheet extends RdDItemSheet { + + static get ITEM_TYPE() { return "service" }; + + async getData() { + const formData = await super.getData(); + formData.disabled = formData.isGM || formData.isOwned ? '' : 'disabled'; + return formData; + } + + activateListeners(html) { + super.activateListeners(html); + + this.html.find('a.rdd-world-content-link').click(async event => { + const itemRef = this.getItemRef(event); + game.items.get(itemRef.id)?.sheet.render(true) + }); + + this.html.find('a.sub-item-acheter').click(async event => { + const subItem = this.item.findRefItem(this.getItemRef(event)); + await this.item.acheter(RdDUtility.getSelectedActor(), subItem); + }); + + if (!this.options.editable) return; + + this.html.find('a.sub-item-vendre').click(async event => { + const subItem = this.item.findRefItem(this.getItemRef(event)); + await this.item.vendre(subItem); + }); + + this.html.find('a.sub-item-delete').click(async event => { + await this.item.removeRefItem(this.getItemRef(event)); + }); + + this.html.find('a.sub-item-quantite-moins').click(async event => await this.item.increaseRefItemQuantite(this.getItemRef(event), -1)) + this.html.find('a.sub-item-quantite-plus').click(async event => await this.item.increaseRefItemQuantite(this.getItemRef(event), 1)) + this.html.find('input.sub-item-quantite').change(async event => { + const newQuantite = Math.max(0, Number.parseInt(this.html.find(event.currentTarget).val())); + await this.item.updateRefItem(this.getItemRef(event), it => it.system.quantite = newQuantite); + }) + this.html.find('input.sub-item-cout').change(async event => { + const newCout = Math.max(0, Number(this.html.find(event.currentTarget).val())); + await this.item.updateRefItem(this.getItemRef(event), it => it.system.cout = newCout); + }) + this.html.find('a.sub-item-info-add').click(__ => + ui.notifications.info(`Utiliser le glisser-déposer pour ajouter des objets depuis un compendium ou les objets du monde`) + ); + + } + + async _onDropItem(event, dragData) { + let linkedItem = fromUuidSync(dragData.uuid); + const existing = this.item.system.items.find(it => it.pack == linkedItem.pack && it.id == linkedItem.id && it.type == linkedItem.type); + if (existing) { + ui.notifications.warn(`${this.item.name} contient déjà un ${existing.name}`); + return; + } + if (linkedItem.pack) { + linkedItem = await SystemCompendiums.loadDocument(linkedItem); + } + if (linkedItem.isInventaire()) { + await this.item.addRefItem(RdDServiceItemSheet.createSubItem(linkedItem)); + } + else { + ui.notifications.warn(`${this.item.name} ne peut pas proposer à la vente de ${Misc.typeName('Item', linkedItem.type)}: ${linkedItem.name}`); + } + } + + getItemRef(event) { + const itemRow = this.html.find(event.currentTarget)?.parents('.item.service-item'); + return { id: itemRow?.data("item-id"), pack: itemRow?.data("pack") ?? undefined } + } +} diff --git a/module/item-service.js b/module/item-service.js new file mode 100644 index 00000000..17fc6ab5 --- /dev/null +++ b/module/item-service.js @@ -0,0 +1,140 @@ +import { DialogItemAchat } from "./dialog-item-achat.js"; +import { RdDItem } from "./item.js"; +import { Misc } from "./misc.js"; + +export class RdDItemService extends RdDItem { + + static get defaultIcon() { + return "systems/foundryvtt-reve-de-dragon/icons/items/services.webp"; + } + + /** @override*/ + getUserLevel(user) { + const level = super.getUserLevel(user); + if (level == CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) { + // si quelqu'un a accès au lien d'un service, il peut le voir + return CONST.DOCUMENT_OWNERSHIP_LEVELS.LIMITED; + } + return level; + } + + isService() { return true; } + getChatItemTemplate() { return 'systems/foundryvtt-reve-de-dragon/templates/post-item-service.html'; } + getProprietes() { return []; } + + getServiceItem(itemRef) { + if (this.isService()) { + return this.system.items.find(it => it.id == itemRef.id && it.pack == itemRef.pack); + } + return undefined; + } + + getQuantiteDisponible(itemRef, max) { + if (this.system.illimite) { + return max; + } + const subItem = this.getServiceItem(itemRef); + return subItem?.system.quantite ?? 0; + } + + async venteRefItem(ref, quantite, cout) { + if (this.actor) { + await this.actor.ajouterSols(cout); + } + await this.increaseRefItemQuantite(ref, -quantite); + } + + async vendre(subItem) { + const item = await RdDItem.getCorrespondingItem(subItem); + const quantiteMax = this.system.illimite ? undefined : subItem.system.quantite; + await item.proposerVente({ service: this, quantiteMax }); + } + + async acheter(acheteur, subItem) { + if (!acheteur) { + ui.notifications.warn(`Pas d'acheteur sélectionné`); + return; + } + const nbLots = this.system.illimite ? 1 : subItem.system.quantite; + if (nbLots <= 0) { + ui.notifications.warn(`${this.name} n'a plus de ${subItem.name} en vente`); + return; + } + + await DialogItemAchat.onAcheter({ + item: await RdDItem.getCorrespondingItem(subItem), + acheteur, + service: this, + quantiteIllimite: this.system.illimite, + nbLots, + tailleLot: 1, + prixLot: subItem.system.cout + }); + } + + static createSubItem(linkedItem) { + return { + id: linkedItem.id, + pack: linkedItem.pack, + name: linkedItem.name, + img: linkedItem.img, + system: { + quantite: 1, + cout: linkedItem.system.cout ?? 0 + } + }; + } + + static matchRefItem({ id, pack }) { + return it => it.id == id && (pack ? (it.pack == pack) : (!it.pack)); + } + + findRefItem(ref) { + return this.system.items.find(RdDItemService.matchRefItem(ref)); + } + + async increaseRefItemQuantite(ref, quantite) { + await this.updateRefItem(ref, + it => it.system.quantite = Math.max(0, it.system.quantite + quantite) + ); + } + + async updateRefItem(ref, update = it => { }) { + await this.updateRefItems(RdDItemService.matchRefItem(ref), update); + } + + async addRefItem(newItem) { + if (!newItem.id) { + ui.notifications.warn(`${newItem?.name ?? '??'} n'a pas d'identifiant`); + return; + } + if (this.system.items.find(RdDItemService.matchRefItem(newItem))) { + ui.notifications.warn(`${newItem?.name ?? newItem.id} est déjà présent ici`); + return; + } + await this.setRefItems([...this.system.items, newItem]); + } + + async removeRefItem(ref) { + await this.removeRefItems(RdDItemService.matchRefItem(ref)); + } + + async removeRefItems(matcher = it => false) { + await this.setRefItems(this.system.items.filter(it => !matcher(it))); + } + + async updateRefItems(matcher = it => false, update = it => { }) { + const updatedList = this.system.items.map(it => { + if (matcher(it)) { + update(it); + } + return it; + }); + await this.setRefItems(updatedList); + } + + async setRefItems(newItems) { + await this.update({ 'system.items': newItems.sort(Misc.ascending(it => it.type + ':' + it.name)) }); + } + +} \ No newline at end of file diff --git a/module/item-sheet.js b/module/item-sheet.js index 90ad5eb7..27c1ebb8 100644 --- a/module/item-sheet.js +++ b/module/item-sheet.js @@ -209,7 +209,7 @@ export class RdDItemSheet extends ItemSheet { this.html.find('.item-split').click(async event => RdDSheetUtility.splitItem(RdDSheetUtility.getItem(event, this.actor), this.actor, async () => this.render(true))); this.html.find('.item-edit').click(async event => RdDSheetUtility.getItem(event, this.actor)?.sheet.render(true)); - this.html.find('.item-delete').click(async event => RdDUtility.confirmerSuppressionItem(this, RdDSheetUtility.getItem(event, this.actor))); + this.html.find('.item-delete').click(async event => RdDUtility.confirmActorItemDelete(this, RdDSheetUtility.getItem(event, this.actor))); this.html.find('.item-vendre').click(async event => RdDSheetUtility.getItem(event, this.actor)?.proposerVente()); this.html.find('.item-montrer').click(async event => RdDSheetUtility.getItem(event, this.actor)?.postItem()); this.html.find('.item-action').click(async event => RdDSheetUtility.getItem(event, this.actor)?.actionPrincipale(this.actor, async () => this.render(true))); diff --git a/module/item.js b/module/item.js index 175bfaa8..6a2b5fed 100644 --- a/module/item.js +++ b/module/item.js @@ -3,6 +3,7 @@ import { Grammar } from "./grammar.js"; import { Misc } from "./misc.js"; import { RdDHerbes } from "./rdd-herbes.js"; import { RdDUtility } from "./rdd-utility.js"; +import { SystemCompendiums } from "./settings/system-compendiums.js"; const typesObjetsInventaire = [ "arme", @@ -57,6 +58,7 @@ export const defaultItemImg = { poison: "systems/foundryvtt-reve-de-dragon/icons/maladies_venins/venin.webp", oeuvre: "systems/foundryvtt-reve-de-dragon/icons/competence_comedie.webp", nourritureboisson: "systems/foundryvtt-reve-de-dragon/icons/objets/provision_crue.webp", + service: "systems/foundryvtt-reve-de-dragon/icons/items/services.webp", signedraconique: "systems/foundryvtt-reve-de-dragon/icons/tmr/signe_draconique.webp", gemme: "systems/foundryvtt-reve-de-dragon/icons/gemmes/almaze.webp", possession: "systems/foundryvtt-reve-de-dragon/icons/entites/possession2.webp", @@ -69,11 +71,7 @@ export const defaultItemImg = { export class RdDItem extends Item { static getDefaultImg(itemType) { - return defaultItemImg[itemType]; - } - - static isItemInventaire(newLocal) { - return typesObjetsInventaire.includes(newLocal.type); + return game.system.rdd.itemClasses[itemType]?.defaultIcon ?? defaultItemImg[itemType]; } static isFieldInventaireModifiable(type, field) { @@ -92,6 +90,42 @@ export class RdDItem extends Item { return true; } + static async getCorrespondingItem(itemRef) { + if (itemRef.pack) { + return await SystemCompendiums.loadDocument(itemRef) + } + return game.items.get(itemRef.id ?? itemRef._id); + } + + static getItemTypesInventaire() { + return typesObjetsInventaire + } + + static getTypesOeuvres() { + return typesObjetsOeuvres + } + + constructor(docData, context = {}) { + if (!context.rdd?.ready) { + mergeObject(context, { rdd: { ready: true } }); + const ItemConstructor = game.system.rdd.itemClasses[docData.type]; + if (ItemConstructor) { + if (!docData.img) { + docData.img = ItemConstructor.defaultIcon; + } + return new ItemConstructor(docData, context); + } + } + if (!docData.img) { + docData.img = RdDItem.getDefaultImg(docData.type); + } + super(docData, context); + } + + static get defaultIcon() { + return undefined; + } + getUniteQuantite() { switch (this.type) { case "monnaie": return "(Pièces)" @@ -107,32 +141,17 @@ export class RdDItem extends Item { return ''; } - constructor(itemData, context) { - if (!itemData.img) { - itemData.img = RdDItem.getDefaultImg(itemData.type); - } - super(itemData, context); - } - - static getItemTypesInventaire() { - return typesObjetsInventaire - } - - static getTypesOeuvres() { - return typesObjetsOeuvres - } - - isCompetencePersonnage() { - return this.type == 'competence' - } - isCompetenceCreature() { - return this.type == 'competencecreature' - } + isCompetencePersonnage() { return this.type == 'competence' } + isCompetenceCreature() { return this.type == 'competencecreature' } + isConteneur() { return this.type == 'conteneur'; } + isMonnaie() { return this.type == 'monnaie'; } + isNourritureBoisson() { return this.type == 'nourritureboisson'; } + isService() { return this.type == 'service'; } isCompetence() { return typesObjetsCompetence.includes(this.type) } isInventaire() { - return RdDItem.isItemInventaire(this) + return typesObjetsInventaire.includes(this.type); } isOeuvre() { return typesObjetsOeuvres.includes(this.type) @@ -146,12 +165,7 @@ export class RdDItem extends Item { isConnaissance() { return typesObjetsConnaissance.includes(this.type) } - isConteneur() { - return this.type == 'conteneur'; - } - isMonnaie() { - return this.type == 'monnaie'; - } + getItemGroup() { if (this.isInventaire()) return "equipement"; @@ -175,10 +189,6 @@ export class RdDItem extends Item { return !this.isConteneur() || (this.system.contenu?.length ?? 0) == 0; } - isNourritureBoisson() { - return this.type == 'nourritureboisson'; - } - isComestible() { switch (this.type) { case 'nourritureboisson': return 'pret'; @@ -209,7 +219,7 @@ export class RdDItem extends Item { } getQuantite() { - return Math.round(this.isConteneur() ? 1 : (this.system.quantite ?? 0)) + return Math.round(this.system.quantite ?? 0) } getEncTotal() { @@ -284,7 +294,7 @@ export class RdDItem extends Item { } return undefined; } - + /* -------------------------------------------- */ async actionPrincipale(actor, onActionItem = async () => { }) { if (!this.getActionPrincipale()) { @@ -410,31 +420,38 @@ export class RdDItem extends Item { return [true, undefined]; } - async proposerVente() { + async proposerVente({ service = undefined, quantiteMax = undefined }) { console.log(this); if (this.isConteneurNonVide()) { ui.notifications.warn(`Votre ${this.name} n'est pas vide, pas possible de le proposer`); return; } - await DialogItemVente.display(this, async (vente) => { - vente["properties"] = this.getProprietes(); - if (vente.isOwned) { - if (vente.quantiteNbLots * vente.tailleLot > vente.quantiteMax) { - ui.notifications.warn(`Vous avez ${vente.quantiteMax} ${vente.item.name}, ce n'est pas suffisant pour vendre ${vente.quantiteNbLots} de ${vente.tailleLot}`) - return; + await DialogItemVente.display({ + item: this, + service, + quantiteMax, + callback: async (vente) => { + vente["properties"] = this.getProprietes(); + if (vente.isOwned) { + if (vente.quantiteNbLots * vente.tailleLot > vente.quantiteMax) { + ui.notifications.warn(`Vous avez ${vente.quantiteMax} ${vente.item.name}, ce n'est pas suffisant pour vendre ${vente.quantiteNbLots} de ${vente.tailleLot}`) + return; + } } - } - vente.jsondata = JSON.stringify(vente.item); + vente.jsondata = JSON.stringify(vente.item); - console.log(vente); - let html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-vente-item.html', vente); - ChatMessage.create(RdDUtility.chatDataSetup(html)); + let html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-vente-item.html', vente); + ChatMessage.create(RdDUtility.chatDataSetup(html)); + } }); } /* -------------------------------------------- */ getProprietes() { - return this[`_${this.type}ChatData`]().filter(it => it != undefined); + if (this[`_${this.type}ChatData`]) { + return this[`_${this.type}ChatData`]().filter(it => it != undefined); + } + return []; } /* -------------------------------------------- */ @@ -452,12 +469,19 @@ export class RdDItem extends Item { payload: chatData, }); - renderTemplate('systems/foundryvtt-reve-de-dragon/templates/post-item.html', chatData).then(html => { + renderTemplate(this.getChatItemTemplate(), chatData).then(html => { let chatOptions = RdDUtility.chatDataSetup(html, modeOverride); ChatMessage.create(chatOptions) }); } + getChatItemTemplate() { + switch (this.type) { + case 'service': return 'systems/foundryvtt-reve-de-dragon/templates/post-item-service.html'; + } + return 'systems/foundryvtt-reve-de-dragon/templates/post-item.html'; + } + static propertyIfDefined(name, val, condition = true) { return condition ? `${name}: ${val}` : undefined; } @@ -706,5 +730,4 @@ export class RdDItem extends Item { ...this._inventaireTemplateChatData() ] } - } diff --git a/module/migrations.js b/module/migrations.js index 5f2e8d36..f2765567 100644 --- a/module/migrations.js +++ b/module/migrations.js @@ -29,6 +29,39 @@ class Migration { } +class _1_5_34_migrationPngWebp { + get code() { return "migrationPngWebp"; } + get version() { return "1.5.34"; } + async migrate() { + + const regexOldPngJpg = /(systems\/foundryvtt-reve-de-dragon\/icons\/.*)\.(png|jpg)/; + const replaceWithWebp = '$1.webp'; + function convertImgToWebp(img) { + return img.replace(regexOldPngJpg, replaceWithWebp); + } + function prepareDocumentsImgUpdate(documents) { + return documents.filter(it => it.img && it.img.match(regexOldPngJpg)) + .map(it => { + return { _id: it.id, img: convertImgToWebp(it.img) } + }); + } + + const itemsUpdates = prepareDocumentsImgUpdate(game.items); + const actorsUpdates = prepareDocumentsImgUpdate(game.actors); + //Migrate system png to webp + await Item.updateDocuments(itemsUpdates); + await Actor.updateDocuments(actorsUpdates); + game.actors.forEach(actor => { + if (actor.token?.img && actor.token.img.match(regexOldPngJpg)) { + actor.update({ "token.img": convertImgToWebp(actor.token.img) }); + } + const actorItemsToUpdate = prepareDocumentsImgUpdate(actor.items); + actor.updateEmbeddedDocuments('Item', actorItemsToUpdate); + }); + } +} + + class _10_0_16_MigrationSortsReserve extends Migration { get code() { return "creation-item-sort-reserve"; } get version() { return "10.0.16"; } @@ -296,6 +329,7 @@ class _10_3_17_Monnaies extends Migration { export class Migrations { static getMigrations() { return [ + new _1_5_34_migrationPngWebp(), new _10_0_16_MigrationSortsReserve(), new _10_0_17_MigrationCompetenceCreature(), new _10_0_21_VehiculeStructureResistanceMax(), diff --git a/module/rdd-calendrier-editeur.js b/module/rdd-calendrier-editeur.js index 936dee45..d097a1d3 100644 --- a/module/rdd-calendrier-editeur.js +++ b/module/rdd-calendrier-editeur.js @@ -29,7 +29,7 @@ export class RdDCalendrierEditeur extends Dialog { this.html.find("input[name='nomMois']").val(this.calendrierData.moisKey); this.html.find("select[name='nomHeure']").val(this.calendrierData.heureKey); this.html.find("select[name='jourMois']").val(this.calendrierData.jourMois); - this.html.find("select[name='minutesRelative']").val(calendrierData.minutesRelative); + this.html.find("select[name='minutesRelative']").val(this.calendrierData.minutesRelative); this.html.find("select[name='annee']").val(this.calendrierData.annee); } diff --git a/module/rdd-combat.js b/module/rdd-combat.js index 9dd406f2..b9186d1d 100644 --- a/module/rdd-combat.js +++ b/module/rdd-combat.js @@ -162,7 +162,6 @@ export class RdDCombatManager extends Combat { if (arme.system.unemain && arme.system.deuxmains && !dommages.includes("/")) { ui.notifications.info("Les dommages de l'arme à 1/2 mains " + arme.name + " ne sont pas corrects (ie sous la forme X/Y)"); } - console.log(">>>>", arme) if ((arme.system.unemain && arme.system.competence) || (arme.system.competence.toLowerCase().includes("corps à corps"))) { actions.push(RdDCombatManager.$prepareAttaqueArme({ diff --git a/module/rdd-hotbar-drop.js b/module/rdd-hotbar-drop.js index 4bef344f..e2963f64 100644 --- a/module/rdd-hotbar-drop.js +++ b/module/rdd-hotbar-drop.js @@ -1,4 +1,3 @@ -import { Misc } from "./misc.js"; export class RdDHotbar { diff --git a/module/rdd-main.js b/module/rdd-main.js index 912d6209..e3d456d9 100644 --- a/module/rdd-main.js +++ b/module/rdd-main.js @@ -1,13 +1,3 @@ -/** - * RdD system - * Author: LeRatierBretonnien - * Software License: GNU GPLv3 - */ - -/* -------------------------------------------- */ - -/* -------------------------------------------- */ -// Import Modules import { SYSTEM_RDD, SYSTEM_SOCKET_ID } from "./constants.js"; import { RdDActor } from "./actor.js"; import { RdDItemSheet } from "./item-sheet.js"; @@ -44,302 +34,281 @@ import { Environnement } from "./environnement.js"; import { RdDIngredientItemSheet } from "./item-ingredient-sheet.js"; import { RdDFauneItemSheet } from "./item-faune-sheet.js"; import { RdDConteneurItemSheet } from "./item-conteneur-sheet.js"; +import { RdDServiceItemSheet } from "./item-service-sheet.js"; +import { RdDItemService } from "./item-service.js"; -/* -------------------------------------------- */ -/* Foundry VTT Initialization */ -/* -------------------------------------------- */ +/** + * RdD system + * Author: LeRatierBretonnien + * Software License: GNU GPLv3 + */ +export class SystemReveDeDragon { - - -/************************************************************************************/ -Hooks.once("init", async function () { - console.log(`Initializing Reve de Dragon System`); - - // preload handlebars templates - RdDUtility.preloadHandlebarsTemplates(); - // Create useful storage space - game.system.rdd = { - TMRUtility, - RdDUtility, - RdDHotbar, - RdDPossession, + static start() { + const system = new SystemReveDeDragon(); + Hooks.once('init', async () => await system.onInit()); + Hooks.once('diceSoNiceReady', (dice3d) => RdDDice.diceSoNiceReady(dice3d)); } - /* -------------------------------------------- */ - game.settings.register(SYSTEM_RDD, "accorder-entite-cauchemar", { - name: "Accorder le rêve aux entités", - hint: "A quel moment les personnages doivent accorder leur rêve aux entités de cauchemar", - scope: "world", - config: true, - type: String, - choices: { // If choices are defined, the resulting setting will be a select menu - "avant-attaque": "Avant l'attaque", - "avant-defense": "Avant la défense", - "avant-encaissement": "Avant l'encaissement", - }, - default: "avant-encaissement" - }); - - /* -------------------------------------------- */ - game.settings.register(SYSTEM_RDD, "calendrier", { - name: "calendrier", - scope: "world", - config: false, - default: RdDCalendrier.createCalendrierInitial(), - type: Object - }); - - /* -------------------------------------------- */ - game.settings.register(SYSTEM_RDD, "migration-png-webp-1.5.34", { - name: "calendrier", - scope: "world", - config: false, - default: false, - type: Boolean - }); - - /* -------------------------------------------- */ - game.settings.register(SYSTEM_RDD, "liste-nombre-astral", { - name: "liste-nombre-astral", - scope: "world", - config: false, - default: [], - type: Object - }); - - /* -------------------------------------------- */ - game.settings.register(SYSTEM_RDD, "calendrier-pos", { - name: "calendrierPos", - scope: "client", - config: false, - default: RdDCalendrier.createCalendrierPos(), - type: Object - }); - - - /* -------------------------------------------- */ - game.settings.register(SYSTEM_RDD, "supprimer-dialogues-combat-chat", { - name: "Supprimer les dialogues de combat", - hint: "Si désactivée, tous les dialogues de combat sont conservés dans la conversation", - scope: "world", - config: true, - default: true, - type: Boolean - }); - /* -------------------------------------------- */ - game.settings.register(SYSTEM_RDD, "activer-sons-audio", { - name: "Activer les bruitages intégrés", - hint: "Si activé, certaines actions en jeu déclenchent un son d'ambiance", - scope: "world", - config: true, - default: true, - type: Boolean - }); - /* -------------------------------------------- */ - game.settings.register(SYSTEM_RDD, "appliquer-famine-soif", { - name: "Notifier de la famine et la soif pour", - hint: "Indique si les cas de famine et de soif seront indiqués durant Château Dormant", - scope: "world", - config: true, - type: String, - choices: { - "aucun": "ni la famine, ni la soif", - "famine": "seulement la famine", - "famine-soif": "la famine et la soif", - }, - default: "aucun" - }); - - /* -------------------------------------------- */ - // Set an initiative formula for the system - CONFIG.Combat.initiative = { - formula: "1+(1d6/10)", - decimals: 2 - }; - - /* -------------------------------------------- */ - game.socket.on(SYSTEM_SOCKET_ID, async (sockmsg) => { - console.log(">>>>> MSG RECV", sockmsg); - try { - RdDUtility.onSocketMessage(sockmsg); - RdDCombat.onSocketMessage(sockmsg); - ChatUtility.onSocketMessage(sockmsg); - RdDActor.onSocketMessage(sockmsg); - } catch (e) { - console.error('game.socket.on(SYSTEM_SOCKET_ID) Exception: ', sockmsg, ' => ', e) + constructor() { + this.RdDUtility = RdDUtility; + this.RdDHotbar = RdDHotbar; + this.itemClasses = { + service: RdDItemService + } + this.actorClasses = { } - }); - - /* -------------------------------------------- */ - // Define custom Entity classes - CONFIG.Actor.documentClass = RdDActor; - CONFIG.Item.documentClass = RdDItem; - CONFIG.RDD = { - resolutionTable: RdDResolutionTable.resolutionTable, - carac_array: RdDUtility.getCaracArray(), - ajustementsConditions: RdDUtility.getAjustementsConditions(), - difficultesLibres: RdDUtility.getDifficultesLibres() } /* -------------------------------------------- */ - // Register sheet application classes - Actors.unregisterSheet("core", ActorSheet); - Actors.registerSheet(SYSTEM_RDD, RdDActorSheet, { types: ["personnage"], makeDefault: true }); - Actors.registerSheet(SYSTEM_RDD, RdDActorCreatureSheet, { types: ["creature"], makeDefault: true }); - Actors.registerSheet(SYSTEM_RDD, RdDActorVehiculeSheet, { types: ["vehicule"], makeDefault: true }); - Actors.registerSheet(SYSTEM_RDD, RdDActorEntiteSheet, { types: ["entite"], makeDefault: true }); - Items.unregisterSheet("core", ItemSheet); + /* Foundry VTT Initialization */ + /* -------------------------------------------- */ + async onInit() { + game.system.rdd = this; - RdDItemSheet.register(RdDSigneDraconiqueItemSheet); - RdDItemSheet.register(RdDRencontreItemSheet); - RdDItemSheet.register(RdDConteneurItemSheet); - RdDItemSheet.register(RdDHerbeItemSheet); - RdDItemSheet.register(RdDFauneItemSheet); - RdDItemSheet.register(RdDIngredientItemSheet); + console.log(`Initializing Reve de Dragon System`); - Items.registerSheet(SYSTEM_RDD, RdDItemSheet, { - types: [ - "competence", "competencecreature", - "recettealchimique", "musique", "chant", "danse", "jeu", "recettecuisine", "oeuvre", - "objet", "arme", "armure", "livre", "potion", "munition", - "monnaie", "nourritureboisson", "gemme", - "meditation", "queue", "ombre", "souffle", "tete", "casetmr", "sort", "sortreserve", - "nombreastral", "tache", "maladie", "poison", "possession", - "tarot", "extraitpoetique" - ], makeDefault: true - }); - CONFIG.Combat.documentClass = RdDCombatManager; + // preload handlebars templates + RdDUtility.preloadHandlebarsTemplates(); - // préparation des différents modules - SystemCompendiums.init(); - DialogChronologie.init(); - ReglesOptionelles.init(); - RdDUtility.init(); - RdDDice.init(); - RdDCommands.init(); - RdDCombat.init(); - RdDCombatManager.init(); - RdDTokenHud.init(); - RdDActor.init(); - RddCompendiumOrganiser.init(); - EffetsDraconiques.init() - TMRUtility.init(); - RdDHotbar.initDropbar(); - RdDPossession.init(); - TMRRencontres.init(); - Environnement.init(); -}); + /* -------------------------------------------- */ + game.settings.register(SYSTEM_RDD, "accorder-entite-cauchemar", { + name: "Accorder le rêve aux entités", + hint: "A quel moment les personnages doivent accorder leur rêve aux entités de cauchemar", + scope: "world", + config: true, + type: String, + choices: { // If choices are defined, the resulting setting will be a select menu + "avant-attaque": "Avant l'attaque", + "avant-defense": "Avant la défense", + "avant-encaissement": "Avant l'encaissement", + }, + default: "avant-encaissement" + }); -/* -------------------------------------------- */ -function messageDeBienvenue() { - if (game.user.isGM) { - ChatUtility.removeChatMessageContaining('
/aide
dans le chat permet de voir les commandes spécifiques à Rêve de Dragon.{{> 'systems/foundryvtt-reve-de-dragon/templates/common/compendium-link.hbs' pack=pack id=_id name=name}}
+{{{system.description}}}
+ +