/** * Extend the base Dialog entity by defining a custom window to perform spell. * @extends {Dialog} */ import { RdDUtility } from "./rdd-utility.js"; import { TMRUtility } from "./tmr-utility.js"; import { RdDRollTables } from "./rdd-rolltables.js"; import { RdDResolutionTable } from "./rdd-resolution-table.js"; /* -------------------------------------------- */ const tmrConstants = { col1_y: 30, col2_y: 55, cellw: 55, cellh: 55, gridx: 28, gridy: 28 } /* -------------------------------------------- */ export class RdDTMRDialog extends Dialog { /* -------------------------------------------- */ constructor(html, actor, tmrData, viewOnly) { 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 = viewOnly this.nbFatigue = this.viewOnly ? 0 : 1; // 1 premier point de fatigue du à la montée this.rencontresExistantes = duplicate(this.actor.data.data.reve.rencontre.list); this.sortReserves = duplicate(this.actor.data.data.reve.reserve.list); this.allTokens = []; this.rencontreState = 'aucune'; this.pixiApp = new PIXI.Application({ width: 720, height: 860 }); } /* -------------------------------------------- */ close() { this.actor.santeIncDec("fatigue", this.nbFatigue).then(super.close()); // moving 1 cell costs 1 fatigue this.actor.tmrApp = undefined; // Cleanup reference } /* -------------------------------------------- */ displaySortReserve() { console.debug("displaySortReserve", this.sortReserves); for (let sort of this.sortReserves) { this._trackToken(this._tokenSortEnReserve(sort)); } } /* -------------------------------------------- */ displayPreviousRencontres() { console.debug("displayPreviousRencontres", this.rencontresExistantes); for (let rencontre of this.rencontresExistantes) { this._trackToken(this._tokenRencontre(rencontre)); } } /* -------------------------------------------- */ updatePreviousRencontres() { this._removeTokens(t => t.rencontre != undefined); this.rencontresExistantes = duplicate(this.actor.data.data.reve.rencontre.list); this.displayPreviousRencontres(); } /* -------------------------------------------- */ updateSortReserve() { this._removeTokens(t => t.sort != undefined); this.sortReserves = duplicate(this.actor.data.data.reve.reserve.list); this.displaySortReserve(); } /* -------------------------------------------- */ 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(data) { this._tellToGM(this.actor.name + " a refoulé une rencontre."); await this.actor.deleteTMRRencontreAtPosition(); // Remove the stored rencontre if necessary let result = await this.actor.ajouterRefoulement(1); this.updatePreviousRencontres(); console.log("-> refouler", this.currentRencontre) 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 } } /* -------------------------------------------- */ /** Gère les rencontres avec du post-processing graphique (passeur, messagers, tourbillons, ...) */ rencontrePostProcess( rencontreData) { if (!rencontreData) return; // Sanity check this.rencontreState = rencontreData.state; // garder la trace de l'état en cours if ( this.rencontreState == 'passeur') { console.log("Processing passeur"); // Récupère la liste des cases à portées let locList = TMRUtility.getTMRArea(this.actor.data.data.reve.tmrpos.coord, this.currentRencontre.force, tmrConstants ); this.colorierZoneRencontre( locList ); } else if ( this.rencontreState == 'messager') { console.log("Processing messager"); // Récupère la liste des cases à portées let locList = TMRUtility.getTMRArea(this.actor.data.data.reve.tmrpos.coord, this.currentRencontre.force, tmrConstants ); this.colorierZoneRencontre( locList ); } else { this.currentRencontre = undefined; // Cleanup, not used anymore } } /* -------------------------------------------- */ async maitriser(data) { this.actor.deleteTMRRencontreAtPosition(); // Remove the stored rencontre if necessary this.updatePreviousRencontres(); const draconic = this.actor.getBestDraconic(); const carac = this.actor.getReveActuel(); // TODO: ajouter l'état général? const etatGeneral = this.actor.data.data.compteurs.etat.value const difficulte = draconic.data.niveau - this.currentRencontre.force; console.log("Maitriser", carac, draconic.data.niveau, this.currentRencontre.force); let rolled = RdDResolutionTable.roll(carac, difficulte); let message = "
Test : Rêve actuel / " + draconic.name + " / " + this.currentRencontre.name + "" + "
" + RdDResolutionTable.explain(rolled); let rencontreData if (rolled.isEchec) { rencontreData = await TMRUtility.processRencontreEchec(this.actor, this.currentRencontre, rolled, this); message += rencontreData.message; this._tellToUser("Vous avez échoué à maîtriser un " + this.currentRencontre.name + " de force " + this.currentRencontre.force + message); if (this.currentRencontre.data.quitterTMR) // Selon les rencontres, quitter TMR ou pas this.close(); } else { rencontreData = await TMRUtility.processRencontreReussite(this.actor, this.currentRencontre, rolled); message += rencontreData.message; this._tellToUser("Vous avez réussi à maîtriser un " + this.currentRencontre.name + " de force " + this.currentRencontre.force + message); } this.rencontrePostProcess( rencontreData ); console.log("-> matriser", this.currentRencontre); this.updateValuesDisplay(); } /* -------------------------------------------- */ _tellToUser(message) { ChatMessage.create({ title: "TMR", content: message, user: game.user._id }); } /* -------------------------------------------- */ _tellToGM(message) { ChatMessage.create({ title: "TMR", content: message, user: game.user._id, whisper: ChatMessage.getWhisperRecipients("GM") }); } /* -------------------------------------------- */ async manageRencontre(coordTMR, cellDescr) { if (this.viewOnly) { return; } this.currentRencontre = undefined; let rencontre = this.rencontresExistantes.find(prev => prev.coord == coordTMR); if (rencontre == undefined) { let myRoll = new Roll("d7").roll(); if (myRoll.total == 7) { rencontre = await TMRUtility.rencontreTMRRoll(coordTMR, cellDescr); } } if ( TMRUtility.isForceRencontre() ) rencontre = await TMRUtility.rencontreTMRRoll(coordTMR, cellDescr); if (rencontre) { // Manages it if (rencontre.rencontre) rencontre = rencontre.rencontre; // Manage stored rencontres console.log("manageRencontre", rencontre) this.currentRencontre = duplicate(rencontre); let dialog = new Dialog({ title: "Rencontre en TMR!", content: "Vous recontrez un " + rencontre.name + " de force " + rencontre.force + "
", buttons: { derober: { icon: '', label: "Se dérober", callback: () => this.derober() }, refouler: { icon: '', label: "Refouler", callback: () => this.refouler() }, maitiser: { icon: '', label: "Maîtriser", callback: () => this.maitriser() } }, default: "derober" }); dialog.render(true); } } /* -------------------------------------------- */ performRoll(html) { if (this.viewOnly) { return; } this.actor.performRoll(this.rollData); } /* -------------------------------------------- */ updateValuesDisplay() { let ptsreve = document.getElementById("tmr-pointsreve-value"); ptsreve.innerHTML = this.actor.data.data.reve.reve.value; let tmrpos = document.getElementById("tmr-pos"); let tmr = TMRUtility.getTMRDescription(this.actor.data.data.reve.tmrpos.coord); tmrpos.innerHTML = this.actor.data.data.reve.tmrpos.coord + " (" + tmr.label + ")"; let etat = document.getElementById("tmr-etatgeneral-value"); etat.innerHTML = this.actor.data.data.compteurs.etat.value; let refoulement = document.getElementById("tmr-refoulement-value"); refoulement.innerHTML = this.actor.data.data.reve.refoulement.value; let fatigueItem = document.getElementById("tmr-fatigue-table"); fatigueItem.innerHTML = "" + RdDUtility.makeHTMLfatigueMatrix(this.actor.data.data.sante.fatigue.value, this.actor.data.data.sante.endurance.max).html() + "
"; } /* -------------------------------------------- */ manageCaseHumideResult() { if (this.toclose) this.close(); } /* -------------------------------------------- */ async manageCaseHumide(cellDescr) { if (this.viewOnly) { return; } if (cellDescr.type == "lac" || cellDescr.type == "fleuve" || cellDescr.type == "marais") { let draconic = this.actor.getBestDraconic(); let carac = this.actor.getReveActuel(); // TODO: ajouter l'état général? const etatGeneral = this.actor.data.data.compteurs.etat.value let difficulte = draconic.data.niveau - 7; let rolled = RdDResolutionTable.roll(carac, difficulte); console.log("manageCaseHumide >>", rolled); let explication = ""; this.toclose = rolled.isEchec; if (rolled.isEchec) { explication += "Vous êtes entré sur une case humide, et vous avez raté votre maîtrise ! Vous quittez les Terres Médianes !" } else { explication += "Vous êtes entré sur une case humide, et vous avez réussi votre maîtrise !" } explication += "
Test : Rêve actuel / " + draconic.name + " / " + cellDescr.type + "" + RdDResolutionTable.explain(rolled); if (rolled.isETotal) { let souffle = RdDRollTables.getSouffle(); explication += "
Vous avez fait un Echec Total. Vous subissez un Souffle de Dragon : " + souffle.name; this.actor.createOwnedItem(souffle); } if (rolled.isPart) { explication += "
Vous avez fait une Réussite Particulière"; explication += RdDResolutionTable.buildXpMessage(rolled, difficulte) } let humideDiag = new Dialog({ title: "Case humide", content: explication, buttons: { choice: { icon: '', label: "Fermer", callback: () => this.manageCaseHumideResult() } } } ); humideDiag.render(true); } } /* -------------------------------------------- */ async declencheSortEnReserve(coordTMR) { if (this.viewOnly) { return; } let sortReserve = this.sortReserves.find(it => it.coord == coordTMR) if (sortReserve != undefined) { await this.actor.deleteSortReserve(sortReserve.coord); this.updateSortReserve(); console.log("declencheSortEnReserve", sortReserve) const declenchementSort = "Vous avez déclenché le sort " + sortReserve.sort.name + " en réserve en " + sortReserve.coord + " (" + TMRUtility.getTMRDescription(sortReserve.coord).label + ") avec " + sortReserve.sort.ptreve_reel + " points de Rêve"; this._tellToUser(declenchementSort); this.close(); } } /* -------------------------------------------- */ nettoyerRencontre( ) { if ( !this.currentRencontre) return; // Sanity check if ( this.currentRencontre.graphics) { for (let drawRect of this.currentRencontre.graphics) { // Suppression des dessins des zones possibles this.pixiApp.stage.removeChild( drawRect ); } } this.currentRencontre = undefined; // Nettoyage de la structure this.rencontreState = 'aucune'; // Et de l'état } /* -------------------------------------------- */ processClickPostRencontre( coord ) { let deplacementType = "erreur"; if (this.rencontreState == 'passeur' || this.rencontreState == 'messager') { console.log("Searching", this.currentRencontre.locList, coord); let isInArea = this.currentRencontre.locList.find(locCoord => locCoord == coord ); if ( isInArea ) { // OK ! deplacementType = (this.rencontreState == 'messager') ? 'messager' : 'saut'; } } return deplacementType; } /* -------------------------------------------- */ async deplacerDemiReve(event) { if (this.viewOnly) { return; } let origEvent = event.data.originalEvent; let myself = event.target.tmrObject; let eventCoord = RdDTMRDialog._computeEventCoord(origEvent); let cellx = eventCoord.cellx; let celly = eventCoord.celly; console.log("deplacerDemiReve >>>>", cellx, celly); let currentPos = TMRUtility.convertToCellCoord(myself.actor.data.data.reve.tmrpos.coord); let coordTMR = TMRUtility.convertToTMRCoord(cellx, celly); // Validation de la case de destination (gestion du cas des rencontres qui peuvent téléporter) let deplacementType = 'erreur'; if ( myself.rencontreState == 'aucune') { // Pas de recontre en post-processing, donc deplacement normal if ( !RdDTMRDialog._horsDePortee(currentPos, cellx, celly) ) { deplacementType = 'normal'; } } else { deplacementType = myself.processClickPostRencontre( coordTMR ); } // Si le deplacement est valide if ( deplacementType == 'normal' || deplacementType == 'saut') { if ( myself.currentRencontre != 'normal' ) myself.nettoyerRencontre(); let cellDescr = TMRUtility.getTMRDescription(coordTMR); console.log("deplacerDemiReve: TMR column is", coordTMR, cellx, celly, cellDescr, this); let tmrPos = duplicate(myself.actor.data.data.reve.tmrpos); tmrPos.coord = coordTMR; await myself.actor.update({ "data.reve.tmrpos": tmrPos }); myself._updateDemiReve(myself); myself.nbFatigue += 1; myself.updateValuesDisplay(); if ( deplacementType == 'normal') { // Pas de rencontres après un saut de type passeur/changeur/... myself.manageRencontre(coordTMR, cellDescr); } myself.manageCaseHumide(cellDescr); await myself.declencheSortEnReserve(coordTMR); } else if (deplacementType == 'messager') { // Dans ce cas, ouverture du lancement de sort sur la case visée myself.actor.rollUnSort( coordTMR ); myself.nettoyerRencontre(); } else { ui.notifications.error("Vous ne pouvez vous déplacer que sur des cases adjacentes à votre position ou valides dans le cas d'une rencontre"); console.log("STATUS :", myself.rencontreState, myself.currentRencontre); } } /* -------------------------------------------- */ async forceDemiRevePosition( coordTMR ) { await this.actor.updateCoordTMR(coordTMR); this._updateDemiReve(this); let cellDescr = TMRUtility.getTMRDescription(coordTMR); this.manageRencontre(coordTMR, cellDescr); this.manageCaseHumide(cellDescr); await this.declencheSortEnReserve(coordTMR); } /* -------------------------------------------- */ async activateListeners(html) { super.activateListeners(html); var row = document.getElementById("tmrrow1"); var cell1 = row.insertCell(1); cell1.append(this.pixiApp.view); if (this.viewOnly) { html.find('#lancer-sort').remove(); } else { // Roll Sort html.find('#lancer-sort').click((event) => { this.actor.rollUnSort(this.actor.data.data.reve.tmrpos.coord); }); } // load the texture we need await this.pixiApp.loader .add('tmr', 'systems/foundryvtt-reve-de-dragon/styles/img/ui/tmp_main_r1.webp') .add('demi-reve', "icons/svg/sun.svg") .load((loader, resources) => { // This creates a texture from a TMR image const mytmr = new PIXI.Sprite(resources.tmr.texture); // Setup the position of the TMR mytmr.x = 0; mytmr.y = 0; mytmr.width = 720; mytmr.height = 860; // Rotate around the center mytmr.anchor.x = 0; mytmr.anchor.y = 0; mytmr.interactive = true; mytmr.buttonMode = true; mytmr.tmrObject = this; if (!this.viewOnly) { mytmr.on('pointerdown', this.deplacerDemiReve); } this.pixiApp.stage.addChild(mytmr); this._addDemiReve(); this.displayPreviousRencontres(); this.displaySortReserve(); }); if (this.viewOnly) { return; } await this.actor.updatePointsDeReve((this.tmrdata.isRapide) ? -2 : -1); // 1 point defatigue this.updateValuesDisplay(); let cellDescr = TMRUtility.getTMRDescription(this.actor.data.data.reve.tmrpos.coord); this.manageRencontre(this.actor.data.data.reve.tmrpos.coord, cellDescr); this.manageCaseHumide(cellDescr); } /* -------------------------------------------- */ static _computeEventCoord(origEvent) { let canvasRect = origEvent.target.getBoundingClientRect(); let x = origEvent.clientX - canvasRect.left; let y = origEvent.clientY - canvasRect.top; let cellx = Math.floor(x / tmrConstants.cellw); // [From 0 -> 12] y -= (cellx % 2 == 0) ? tmrConstants.col1_y : tmrConstants.col2_y; let celly = Math.floor(y / tmrConstants.cellh); // [From 0 -> 14] return { cellx, celly }; } /* -------------------------------------------- */ static _horsDePortee(pos, cellx, celly) { return Math.abs(cellx - pos.x) > 1 || Math.abs(celly - pos.y) > 1 || (pos.y == 0 && celly > pos.y && cellx != pos.x && pos.x % 2 == 0) || (celly == 0 && celly < pos.y && cellx != pos.x && pos.x % 2 == 1); } /* -------------------------------------------- */ _tokenRencontre(rencontre) { let sprite = new PIXI.Graphics(); sprite.beginFill(0x767610, 0.6); sprite.drawCircle(0, 0, 6); sprite.endFill(); sprite.decallage = { x: (tmrConstants.cellw / 2) - 16, y: 16 - (tmrConstants.cellh / 2) }; return { sprite: sprite, rencontre: rencontre, coordTMR: () => rencontre.coord }; } /* -------------------------------------------- */ _tokenSortEnReserve(sort) { let sprite = new PIXI.Graphics(); sprite.beginFill(0x101010, 0.8); sprite.drawCircle(0, 0, 6); sprite.endFill(); sprite.decallage = { x: 16 - (tmrConstants.cellw / 2), y: 16 - (tmrConstants.cellh / 2) } return { sprite: sprite, sort: sort, coordTMR: () => sort.coord } } /* -------------------------------------------- */ _tokenDemiReve() { let texture = PIXI.utils.TextureCache['demi-reve']; let sprite = new PIXI.Sprite(texture); sprite.width = tmrConstants.cellw * 0.7; sprite.height = tmrConstants.cellh * 0.7; sprite.anchor.set(0.5); sprite.tint = 0x00FFEE; return { sprite: sprite, actor: this.actor, coordTMR: () => this.actor.data.data.reve.tmrpos.coord } } /* -------------------------------------------- */ _addDemiReve() { this.demiReve = this._tokenDemiReve(); this._setTokenPosition(this.demiReve); this.pixiApp.stage.addChild(this.demiReve.sprite); } /* -------------------------------------------- */ _updateDemiReve(myself) { myself._setTokenPosition(myself.demiReve); } /* -------------------------------------------- */ /** Retourne les coordonnées x, h, w, h du rectangle d'une case donnée */ _getCaseRectangleCoord( coord ) { let coordXY = TMRUtility.convertToCellCoord( coord ); let decallagePairImpair = (coordXY.x % 2 == 0) ? tmrConstants.col1_y : tmrConstants.col2_y; let x = tmrConstants.gridx + (coordXY.x * tmrConstants.cellw) - (tmrConstants.cellw /2); let y = tmrConstants.gridy + (coordXY.y * tmrConstants.cellh) - (tmrConstants.cellh /2) + decallagePairImpair; return {x: x, y: y, w: tmrConstants.cellw, h: tmrConstants.cellh} } /* -------------------------------------------- */ _setTokenPosition(token) { let coordXY = TMRUtility.convertToCellCoord(token.coordTMR()); let decallagePairImpair = (coordXY.x % 2 == 0) ? tmrConstants.col1_y : tmrConstants.col2_y; let dx = (token.sprite.decallage == undefined) ? 0 : token.sprite.decallage.x; let dy = (token.sprite.decallage == undefined) ? 0 : token.sprite.decallage.y; token.sprite.x = tmrConstants.gridx + (coordXY.x * tmrConstants.cellw) + dx; token.sprite.y = tmrConstants.gridy + (coordXY.y * tmrConstants.cellh) + dy + decallagePairImpair; } /* -------------------------------------------- */ _removeTokens(filter) { const tokensToRemove = this.allTokens.filter(filter); for (let token of tokensToRemove) { this.pixiApp.stage.removeChild(token.sprite); } } /* -------------------------------------------- */ _trackToken(token) { this.allTokens.push(token) this._setTokenPosition(token); this.pixiApp.stage.addChild(token.sprite); } }