import { RollDataAjustements } from "./rolldata-ajustements.js"; import { RdDUtility } from "./rdd-utility.js"; import { TMRUtility, tmrConstants } from "./tmr-utility.js"; import { RdDResolutionTable } from "./rdd-resolution-table.js"; import { RdDTMRRencontreDialog } from "./rdd-tmr-rencontre-dialog.js"; import { TMRRencontres } from "./tmr-rencontres.js"; import { ChatUtility } from "./chat-utility.js"; import { RdDRoll } from "./rdd-roll.js"; import { Poetique } from "./poetique.js"; import { EffetsDraconiques } from "./tmr/effets-draconiques.js"; import { PixiTMR } from "./tmr/pixi-tmr.js"; import { Draconique } from "./tmr/draconique.js"; import { Grammar } from "./grammar.js"; import { Misc } from "./misc.js"; /* -------------------------------------------- */ export class RdDTMRDialog extends Dialog { static async create(html, actor, tmrData) { if (tmrData.mode != 'visu') { // Notification au MJ ChatMessage.create({ content: actor.name + " est monté dans les TMR en mode : " + tmrData.mode, whisper: ChatMessage.getWhisperRecipients("GM") }); } return new RdDTMRDialog(html, actor, tmrData); } /* -------------------------------------------- */ constructor(html, actor, tmrData) { const dialogConf = { title: "Terres Médianes de Rêve", content: html, buttons: { closeButton: { label: "Fermer", callback: html => this.close(html) } }, default: "closeButton" } const dialogOptions = { classes: ["tmrdialog"], width: 920, height: 980, 'z-index': 20 } super(dialogConf, dialogOptions); this.tmrdata = duplicate(tmrData); this.actor = actor; this.actor.tmrApp = this; // reference this app in the actor structure this.viewOnly = tmrData.mode == "visu" this.fatigueParCase = this.viewOnly ? 0 : this.actor.getTMRFatigue(); this.cumulFatigue = 0; this.loadRencontres(); this.loadSortsReserve(); this.loadCasesSpeciales(); this.allTokens = []; this.rencontreState = 'aucune'; this.pixiApp = new PIXI.Application({ width: 720, height: 860 }); this.pixiTMR = new PixiTMR(this, this.pixiApp); this.callbacksOnAnimate = []; if (!this.viewOnly) { this.actor.setStatusDemiReve(true); this._tellToGM(this.actor.name + " monte dans les terres médianes (" + tmrData.mode + ")"); } // load the texture we need this.pixiTMR.load((loader, resources) => this.createPixiSprites()); } loadCasesSpeciales() { this.casesSpeciales = this.actor.data.items.filter(item => Draconique.isCaseTMR(item)); } loadSortsReserve() { this.sortsReserves = Misc.data(this.actor).data.reve.reserve.list; } loadRencontres() { this.rencontresExistantes = this.actor.getTMRRencontres(); } /* -------------------------------------------- */ createPixiSprites() { EffetsDraconiques.carteTmr.createSprite(this.pixiTMR); this.updateTokens(); this.demiReve = this._tokenDemiReve(); this._updateDemiReve(); } /* -------------------------------------------- */ _createTokens() { let tokens = this._getTokensCasesTmr() .concat(this._getTokensRencontres()) .concat(this._getTokensSortsReserve()); for (let t of tokens) { this._trackToken(t); } } updateTokens() { this._removeTokens(t => true); this.loadRencontres(); this.loadSortsReserve(); this.loadCasesSpeciales(); this._createTokens(); } removeToken(tmr, casetmr) { this._removeTokens(t => t.coordTMR() == tmr.coord && t.caseSpeciale?._id == casetmr._id); this.updateTokens() } /* -------------------------------------------- */ _getTokensCasesTmr() { return this.casesSpeciales.map(c => this._tokenCaseSpeciale(c)).filter(token => token); } _getTokensRencontres() { return this.rencontresExistantes.map(it => this._tokenRencontre(it)); } _getTokensSortsReserve() { return this.sortsReserves.map(it => this._tokenSortEnReserve(it)); } /* -------------------------------------------- */ _tokenRencontre(rencontre) { return EffetsDraconiques.rencontre.token(this.pixiTMR, rencontre, () => rencontre.coord); } _tokenCaseSpeciale(casetmr) { const draconique = Draconique.get(casetmr.data.specific); return draconique?.token(this.pixiTMR, casetmr, () => casetmr.data.coord); } _tokenSortEnReserve(sortEnReserve) { return EffetsDraconiques.sortReserve.token(this.pixiTMR, sortEnReserve.sort, () => sortEnReserve.coord); } _tokenDemiReve() { return EffetsDraconiques.demiReve.token(this.pixiTMR, this.actor, () => Misc.data(this.actor).data.reve.tmrpos.coord); } _updateDemiReve() { this._setTokenPosition(this.demiReve); } /* -------------------------------------------- */ async activateListeners(html) { super.activateListeners(html); document.getElementById("tmrrow1").insertCell(0).append(this.pixiApp.view); if (this.viewOnly) { html.find('#lancer-sort').remove(); } else { // Roll Sort html.find('#lancer-sort').click((event) => { this.actor.rollUnSort(Misc.data(this.actor).data.reve.tmrpos.coord); }); } if (this.viewOnly) { return; } // Gestion du cout de montée en points de rêve let reveCout = ((this.tmrdata.isRapide && !EffetsDraconiques.isDeplacementAccelere(this.actor)) ? -2 : -1) - this.actor.countMonteeLaborieuse(); this.cumulFatigue += this.fatigueParCase; await this.actor.reveActuelIncDec(reveCout); // Le reste... this.updateValuesDisplay(); let tmr = TMRUtility.getTMR(Misc.data(this.actor).data.reve.tmrpos.coord); await this.manageRencontre(tmr, () => { this.postRencontre(tmr); }); } /* -------------------------------------------- */ updateValuesDisplay() { let ptsreve = document.getElementById("tmr-pointsreve-value"); const actorData = Misc.data(this.actor); ptsreve.innerHTML = actorData.data.reve.reve.value; let tmrpos = document.getElementById("tmr-pos"); let tmr = TMRUtility.getTMR(actorData.data.reve.tmrpos.coord); tmrpos.innerHTML = actorData.data.reve.tmrpos.coord + " (" + tmr.label + ")"; let etat = document.getElementById("tmr-etatgeneral-value"); etat.innerHTML = this.actor.getEtatGeneral(); let refoulement = document.getElementById("tmr-refoulement-value"); refoulement.innerHTML = actorData.data.reve.refoulement.value; let fatigueItem = document.getElementById("tmr-fatigue-table"); //console.log("Refresh : ", actorData.data.sante.fatigue.value); fatigueItem.innerHTML = "" + RdDUtility.makeHTMLfatigueMatrix(actorData.data.sante.fatigue.value, actorData.data.sante.endurance.max).html() + "
"; } /* -------------------------------------------- */ close() { this.actor.santeIncDec("fatigue", this.cumulFatigue).then(super.close()); // moving 1 cell costs 1 fatigue this.actor.tmrApp = undefined; // Cleanup reference this.actor.setStatusDemiReve(false); if (!this.viewOnly) { this._tellToGM(this.actor.name + " a quitté les terres médianes"); } } /* -------------------------------------------- */ async derober() { await this.actor.addTMRRencontre(this.currentRencontre); console.log("-> derober", this.currentRencontre); this._tellToGM(this.actor.name + " s'est dérobé et quitte les TMR."); this.close(); } /* -------------------------------------------- */ async refouler() { this._tellToGM(this.actor.name + " a refoulé : " + this.currentRencontre.name); await this.actor.deleteTMRRencontreAtPosition(); // Remove the stored rencontre if necessary await this.actor.ajouterRefoulement(this.currentRencontre.refoulement ?? 1); this.updateTokens(); console.log("-> refouler", this.currentRencontre) this.updateValuesDisplay(); this.nettoyerRencontre(); } /* -------------------------------------------- */ async ignorerRencontre() { this._tellToGM(this.actor.name + " a ignoré : " + this.currentRencontre.name); await this.actor.deleteTMRRencontreAtPosition(); // Remove the stored rencontre if necessary this.updateTokens(); this.updateValuesDisplay(); this.nettoyerRencontre(); } /* -------------------------------------------- */ colorierZoneRencontre(locList) { this.currentRencontre.graphics = []; // Keep track of rectangles to delete it this.currentRencontre.locList = duplicate(locList); // And track of allowed location for (let loc of locList) { let rect = this._getCaseRectangleCoord(loc); var rectDraw = new PIXI.Graphics(); rectDraw.beginFill(0xFFFF00, 0.3); // set the line style to have a width of 5 and set the color to red rectDraw.lineStyle(5, 0xFF0000); // draw a rectangle rectDraw.drawRect(rect.x, rect.y, rect.w, rect.h); this.pixiApp.stage.addChild(rectDraw); this.currentRencontre.graphics.push(rectDraw); // garder les objets pour gestion post-click } } /* -------------------------------------------- */ // garder la trace de l'état en cours setStateRencontre(state) { this.rencontreState = state; } async choisirCasePortee(coord, portee) { // Récupère la liste des cases à portées let locList = TMRUtility.getTMRPortee(coord, portee); this.colorierZoneRencontre(locList); } async choisirCaseType(type) { const locList = TMRUtility.filterTMR(it => it.type == type).map(it => it.coord); this.colorierZoneRencontre(locList); } /* -------------------------------------------- */ checkQuitterTMR() { if (this.actor.isDead()) { this._tellToGM("Vous êtes mort : vous quittez les Terres médianes !"); this.close(); return true; } const resteAvantInconscience = this.actor.getFatigueMax() - this.actor.getFatigueActuelle() - this.cumulFatigue; if (resteAvantInconscience <= 0) { this._tellToGM("Vous vous écroulez de fatigue : vous quittez les Terres médianes !"); this.quitterLesTMRInconscient(); return true; } if (this.actor.getReveActuel() == 0) { this._tellToGM("Vos Points de Rêve sont à 0 : vous quittez les Terres médianes !"); this.quitterLesTMRInconscient(); return true; } return false; } async quitterLesTMRInconscient() { if (this.currentRencontre?.isPersistant) { await this.refouler(); } this.close(); } /* -------------------------------------------- */ async maitriserRencontre() { this.actor.deleteTMRRencontreAtPosition(); this.updateTokens(); let rencontreData = { actor: this.actor, alias: this.actor.name, reveDepart: this.actor.getReveActuel(), competence: this.actor.getBestDraconic(), rencontre: this.currentRencontre, nbRounds: 1, canClose: false, tmr: TMRUtility.getTMR(Misc.data(this.actor).data.reve.tmrpos.coord) } await this._tentativeMaitrise(rencontreData); } /* -------------------------------------------- */ async _tentativeMaitrise(rencData, presentCite) { console.log("-> matriser", rencData); rencData.reve = this.actor.getReveActuel(); rencData.etat = this.actor.getEtatGeneral(); RollDataAjustements.calcul(rencData, this.actor); rencData.rolled = rencData.presentCite ? this._rollPresentCite(rencData) : await RdDResolutionTable.roll(rencData.reve, RollDataAjustements.sum(rencData.ajustements)); let postProcess = await TMRRencontres.gererRencontre(this, rencData); ChatMessage.create({ whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name), content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-rencontre-tmr.html`, rencData) }); if (postProcess) { /** Gère les rencontres avec du post-processing (passeur, messagers, tourbillons, ...) */ await postProcess(this, rencData); } else { this.currentRencontre = undefined; } this.updateValuesDisplay(); if (this.checkQuitterTMR()) { return; } else if (rencData.rolled.isEchec && rencData.rencontre.isPersistant) { setTimeout(() => { rencData.nbRounds++; this.cumulFatigue += this.fatigueParCase; this._tentativeMaitrise(rencData); this._deleteTmrMessages(rencData.actor, rencData.nbRounds); }, 2000); } } _rollPresentCite(rencontreData) { let rolled = RdDResolutionTable.computeChances(rencontreData.reve, 0); mergeObject(rolled, { caracValue: rencontreData.reve, finalLevel: 0, roll: rolled.score }); RdDResolutionTable.succesRequis(rolled); return rolled; } /* -------------------------------------------- */ _deleteTmrMessages(actor, nbRounds = -1) { setTimeout(() => { if (nbRounds < 0) { ChatUtility.removeChatMessageContaining(`

`); } } }, 500); } /* -------------------------------------------- */ _tellToUser(message) { ChatMessage.create({ content: message, user: game.user.id, whisper: [game.user.id] }); } /* -------------------------------------------- */ _tellToGM(message) { ChatMessage.create({ content: message, user: game.user.id, whisper: ChatMessage.getWhisperRecipients("GM") }); } /* -------------------------------------------- */ _tellToUserAndGM(message) { ChatMessage.create({ content: message, user: game.user.id, whisper: [game.user.id].concat(ChatMessage.getWhisperRecipients("GM")) }); } /* -------------------------------------------- */ async manageRencontre(tmr, postRencontre) { if (this.viewOnly) { return; } this.currentRencontre = undefined; if (this._presentCite(tmr, postRencontre)) { return; } let rencontre = await this._jetDeRencontre(tmr); if (rencontre) { // Manages it if (rencontre.rencontre) rencontre = rencontre.rencontre; // Manage stored rencontres console.log("manageRencontre", rencontre); this.currentRencontre = duplicate(rencontre); let dialog = new RdDTMRRencontreDialog("", this, this.currentRencontre, postRencontre); dialog.render(true); } else { postRencontre(); } } _presentCite(tmr, postRencontre) { const presentCite = this.casesSpeciales.find(c => EffetsDraconiques.presentCites.isCase(c, tmr.coord)); if (presentCite) { this.minimize(); EffetsDraconiques.presentCites.choisirUnPresent(presentCite, (type => this._utiliserPresentCite(presentCite, type, tmr, postRencontre))); } return presentCite; } async _utiliserPresentCite(presentCite, typeRencontre, tmr, postRencontre) { this.currentRencontre = TMRRencontres.getRencontre(typeRencontre); await TMRRencontres.evaluerForceRencontre(this.currentRencontre); await EffetsDraconiques.presentCites.ouvrirLePresent(this.actor, presentCite); this.removeToken(tmr, presentCite); // simuler une rencontre let rencontreData = { actor: this.actor, alias: this.actor.name, reveDepart: this.actor.getReveActuel(), competence: this.actor.getBestDraconic(), rencontre: this.currentRencontre, tmr: tmr, presentCite: presentCite }; await this._tentativeMaitrise(rencontreData); this.maximize(); postRencontre(); } /* -------------------------------------------- */ async _jetDeRencontre(tmr) { let rencontre = this.rencontresExistantes.find(prev => prev.coord == tmr.coord); if (rencontre) { return rencontre; } let myRoll = new Roll("1d7").evaluate( { async: false} ).total; if (TMRUtility.isForceRencontre() || myRoll == 7) { return await this.rencontreTMRRoll(tmr, this.actor.isRencontreSpeciale()); } this._tellToUser(myRoll + ": Pas de rencontre en " + tmr.label + " (" + tmr.coord + ")"); } /* -------------------------------------------- */ async rencontreTMRRoll(tmr, isMauvaise = false) { let rencontre = TMRUtility.utiliseForceRencontre() ?? (isMauvaise ? await TMRRencontres.getMauvaiseRencontre() : await TMRRencontres.getRencontreAleatoire(tmr.type)); rencontre.coord = tmr.coord; rencontre.date = game.system.rdd.calendrier.getDateFromIndex(); rencontre.heure = game.system.rdd.calendrier.getCurrentHeure(); return rencontre; } /* -------------------------------------------- */ async manageTmrInnaccessible(tmr) { const caseTmrInnaccessible = this.casesSpeciales.find(c => EffetsDraconiques.isInnaccessible(c, tmr.coord)); if (caseTmrInnaccessible) { return await this.actor.reinsertionAleatoire(caseTmrInnaccessible.name); } return tmr; } /* -------------------------------------------- */ async manageCaseHumide(tmr) { if (this.isCaseHumide(tmr)) { let rollData = { actor: this.actor, competence: duplicate(this.actor.getBestDraconic()), tmr: tmr, canClose: false, diffLibre: -7, forceCarac: { 'reve-actuel': { label: "Rêve Actuel", value: this.actor.getReveActuel() } }, maitrise: { verbe: 'maîtriser', action: 'Maîtriser le fleuve' } } rollData.double = EffetsDraconiques.isDoubleResistanceFleuve(this.actor) ? true : undefined, rollData.competence.data.defaut_carac = 'reve-actuel'; await this._rollMaitriseCaseHumide(rollData); } } /* -------------------------------------------- */ async _rollMaitriseCaseHumide(rollData) { await this._maitriserTMR(rollData, r => this._resultatMaitriseCaseHumide(r)); } async _resultatMaitriseCaseHumide(rollData) { await this.souffleSiEchecTotal(rollData); this.toclose = rollData.rolled.isEchec; if (rollData.rolled.isSuccess && rollData.double) { rollData.previous = { rolled: rollData.rolled, ajustements: rollData.ajustements }; rollData.double = undefined; await this._rollMaitriseCaseHumide(rollData); return; } rollData.poesie = Poetique.getExtrait(); ChatMessage.create({ whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name), content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-resultat-maitrise-tmr.html`, rollData) }); if (rollData.rolled.isEchec) { this.close(); } } async souffleSiEchecTotal(rollData) { if (rollData.rolled.isETotal) { rollData.souffle = await this.actor.ajouterSouffle({ chat: false }); } } /* -------------------------------------------- */ isCaseHumide(tmr) { if (!(TMRUtility.isCaseHumide(tmr) || this.isCaseHumideAdditionelle(tmr))) { return undefined; } if (this.isCaseMaitrisee(tmr.coord)) { ChatMessage.create({ content: tmr.label + ": cette case humide est déja maitrisée grâce à votre Tête Quête des Eaux", whisper: ChatMessage.getWhisperRecipients(game.user.name) }); return undefined; } return -7; } /* -------------------------------------------- */ isCaseHumideAdditionelle(tmr) { if (tmr.type == 'pont' && EffetsDraconiques.isPontImpraticable(this.actor)) { ChatMessage.create({ content: tmr.label + ": Vous êtes sous le coup d'une Impraticabilité des Ponts : ce pont doit être maîtrisé comme une case humide.", whisper: ChatMessage.getWhisperRecipients(game.user.name) }); return true; } if (this.isCaseInondee(tmr.coord)) { ChatMessage.create({ content: tmr.label + ": cette case est inondée, elle doit être maîtrisée comme une case humide.", whisper: ChatMessage.getWhisperRecipients(game.user.name) }); return true; } return false; } /* -------------------------------------------- */ async conquerirCiteFermee(tmr) { if (EffetsDraconiques.fermetureCites.find(this.casesSpeciales, tmr.coord)) { await this._conquerir(tmr, { difficulte: -9, action: 'Conquérir la cité', onConqueteReussie: r => EffetsDraconiques.fermetureCites.onVisiteSupprimer(r.actor, tmr, (casetmr) => this.removeToken(tmr, casetmr)), onConqueteEchec: r => { this.souffleSiEchecTotal(rollData); this.close() }, canClose: false }); } } /* -------------------------------------------- */ async purifierPeriple(tmr) { if (EffetsDraconiques.periple.find(this.casesSpeciales, tmr.coord)) { await this._conquerir(tmr, { difficulte: EffetsDraconiques.periple.getDifficulte(tmr), action: 'Purifier ' + TMRUtility.getTMRDescr(tmr.coord), onConqueteReussie: r => EffetsDraconiques.periple.onVisiteSupprimer(r.actor, tmr, (casetmr) => this.removeToken(tmr, casetmr)), onConqueteEchec: r => { this.souffleSiEchecTotal(rollData); this.close() }, canClose: false }); } } /* -------------------------------------------- */ async conquerirTMR(tmr) { if (EffetsDraconiques.conquete.find(this.casesSpeciales, tmr.coord)) { await this._conquerir(tmr, { difficulte: -7, action: 'Conquérir', onConqueteReussie: r => EffetsDraconiques.conquete.onVisiteSupprimer(r.actor, tmr, (casetmr) => this.removeToken(tmr, casetmr)), onConqueteEchec: r => this.close(), canClose: false }); } } /* -------------------------------------------- */ async _conquerir(tmr, options) { let rollData = { actor: this.actor, competence: duplicate(this.actor.getBestDraconic()), tmr: tmr, canClose: options.canClose ?? false, diffLibre: options.difficulte ?? -7, forceCarac: { 'reve-actuel': { label: "Rêve Actuel", value: this.actor.getReveActuel() } }, maitrise: { verbe: 'conquérir', action: options.action } }; rollData.competence.data.defaut_carac = 'reve-actuel'; await this._maitriserTMR(rollData, r => this._onResultatConquerir(r, options)); } async _onResultatConquerir(rollData, options) { if (rollData.rolled.isETotal) { rollData.souffle = await this.actor.ajouterSouffle({ chat: false }); } this.toclose = rollData.rolled.isEchec; rollData.poesie = Poetique.getExtrait(); ChatMessage.create({ whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name), content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-resultat-maitrise-tmr.html`, rollData) }); if (rollData.rolled.isEchec) { options.onConqueteEchec(rollData, options.effetDraconique); } else { await options.onConqueteReussie(rollData, options.effetDraconique); this.updateTokens(); } } /* -------------------------------------------- */ async _maitriserTMR(rollData, callbackMaitrise) { this.minimize(); // Hide const dialog = await RdDRoll.create(this.actor, rollData, { html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-maitrise-tmr.html', options: { height: 350 }, close: html => { this.maximize(); } // Re-display TMR }, { name: rollData.maitrise.verbe, label: rollData.maitrise.action, callbacks: [ this.actor.createCallbackExperience(), { action: callbackMaitrise } ] } ); dialog.render(true); } async validerVisite(tmr) { await EffetsDraconiques.pelerinage.onVisiteSupprimer(this.actor, tmr, (casetmr) => this.removeToken(tmr, casetmr)); await EffetsDraconiques.urgenceDraconique.onVisiteSupprimer(this.actor, tmr, (casetmr) => this.removeToken(tmr, casetmr)); } /* -------------------------------------------- */ async declencheSortEnReserve(coord) { let sortsEnCoord = TMRUtility.getSortsReserve(this.sortsReserves, coord); if (sortsEnCoord.length > 0) { if (EffetsDraconiques.isSortReserveImpossible(this.actor)) { ui.notifications.error("Une queue ou un souffle vous empèche de déclencher de sort!"); return; } if (!EffetsDraconiques.isUrgenceDraconique(this.actor) && (EffetsDraconiques.isReserveEnSecurite(this.actor) || this.isReserveExtensible(coord))) { let msg = "Vous êtes sur une case avec un Sort en Réserve. Grâce à votre Tête Reserve en Sécurité ou Réserve Exensible, vous pouvez contrôler le déclenchement. Cliquez si vous souhaitez le déclencher :