Files
foundryvtt-reve-de-dragon/module/roll/roll-dialog.mjs

451 lines
16 KiB
JavaScript

import { Misc } from "../misc.js";
import { RollTypeComp } from "./roll-type-comp.mjs";
import { RollTypeTache } from "./roll-type-tache.mjs";
import { RollTypeAttaque } from "./roll-type-attaque.mjs";
import { RollTypeDefense } from "./roll-type-defense.mjs";
import { RollTypeMeditation } from "./roll-type-meditation.mjs";
import { RollTypeSort } from "./roll-type-sort.mjs";
import { RollTypeOeuvre } from "./roll-type-oeuvre.mjs";
import { RollTypeJeu } from "./roll-type-jeu.mjs";
import { RollPartAction } from "./roll-part-action.mjs";
import { RollPartActor } from "./roll-part-actor.mjs";
import { RollPartAppelMoral } from "./roll-part-appelmoral.mjs";
import { RollPartAstrologique } from "./roll-part-astrologique.mjs";
import { PART_CARAC, RollPartCarac } from "./roll-part-carac.mjs";
import { RollPartCoeur } from "./roll-part-coeur.mjs";
import { PART_COMP, RollPartComp } from "./roll-part-comp.mjs";
import { RollPartConditions } from "./roll-part-conditions.mjs";
import { RollPartDiff } from "./roll-part-diff.mjs";
import { RollPartEncTotal } from "./roll-part-enctotal.mjs";
import { RollPartEtat } from "./roll-part-etat.mjs";
import { RollPartEthylisme } from "./roll-part-ethylisme.mjs";
import { RollPartMalusArmure } from "./roll-part-malusarmure.mjs";
import { RollPartMeditation } from "./roll-part-meditation.mjs";
import { RollPartMoral } from "./roll-part-moral.mjs";
import { RollPartOpponent } from "./roll-part-opponent.mjs";
import { RollPartSurEnc } from "./roll-part-surenc.mjs";
import { PART_TRICHER, RollPartTricher } from "./roll-part-tricher.mjs";
import { RollPartTache } from "./roll-part-tache.mjs";
import { RollPartOeuvre } from "./roll-part-oeuvre.mjs";
import { RollPartSort } from "./roll-part-sort.mjs";
import { RollBasicParts } from "./roll-basic-parts.mjs";
import { RollPartRollMode } from "./roll-part-rollmode.mjs";
import { RollPartJeu } from "./roll-part-jeu.mjs";
import { RollPartSign } from "./roll-part-sign.mjs";
import { RollPartAttaque } from "./roll-part-attaque.mjs";
import { RollPartDefense } from "./roll-part-defense.mjs";
import { RollDialogAdapter } from "./roll-dialog-adapter.mjs";
import { ROLLDIALOG_SECTION } from "./roll-part.mjs";
import { ROLL_TYPE_ATTAQUE, ROLL_TYPE_COMP } from "./roll-constants.mjs";
import ChatRollResult from "./chat-roll-result.mjs";
import { renderTemplate } from "../constants.js";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
const doNothing = (dialog) => { }
const ALL_ROLL_TYPES = [
new RollTypeComp(),
new RollTypeTache(),
new RollTypeAttaque(),
new RollTypeDefense(),
new RollTypeSort(),
new RollTypeMeditation(),
new RollTypeOeuvre(),
new RollTypeJeu(),
// new RollTypeResistance ??
// new RollTypeFixedCarac ??
]
const ROLL_PARTS = [
new RollPartActor(),
new RollPartAction(),
new RollPartOpponent(),
new RollPartCarac(),
new RollPartComp(),
new RollPartDiff(),
new RollPartAttaque(),
new RollPartDefense(),
new RollPartMeditation(),
new RollPartSort(),
new RollPartTache(),
new RollPartOeuvre(),
new RollPartJeu(),
new RollPartSign(),
new RollPartEtat(),
new RollPartConditions(),
new RollPartEthylisme(),
new RollPartMalusArmure(),
new RollPartEncTotal(),
new RollPartSurEnc(),
new RollPartAppelMoral(),
new RollPartMoral(),
new RollPartCoeur(),
new RollPartAstrologique(),
new RollPartTricher(),
new RollPartRollMode(),
]
/**
* Extend the base Dialog entity to select roll parameters
* @extends {Dialog}
* # Principes
* - une seule fenêtre de dialogue (classe RollDialog)
* - plusieurs "types"s de fonctionnement (classe RollType)
* - gestion uniforme des modificateurs (classe RollPart)
* - un objet rollData contient les informations liées à un jet de dés
* - un rollData doit pouvoir être "réduit" pour fournir les informations significatives
* d'un jet de dés
* - un rollData réduit doit pouvoir être complété pour afficher la même fenêtre
* - un rollData réduit sera utilisé pour piloter l'ouverture de la fenêtre
*
* - TODO: une classe de base RollChatMessage gerera les messages correspondant aux résultats du dés
* - TODO: réfléchir aux messages supplémentaires gérés par RdDCombat ?
*
* ## Types de fonctionnement - RollType
*
* Un type de fonctionnement (RollType) détermine quelles parties (RollPart) de la
* fenêtre RollDialog sont actives, mais aussi quels sont les effets du jet.
*
* - chaque type de fonctionnement peut impacter les RollPart utilisés, les données
* attendues et ajoutées au rollData.
* - chaque type de fonctionnement peut définir le template de ChatMessage correspondant
* - Le type de fonctionnement détermine aussi quelles sont les effets du jet:
* - quelle ChatMessage afficher dans le tchat?
* - en cas d'attaque/de défense, quelles sont les suites à donner?
* - en cas de lancement de sort, réduire les points de rêve
* - en cas de méditation, créer le signe draconique
* - en cas de tâche, ajuster les points de tâche
*
*
* ## Modificateurs - RollPart
* - Chaque modificateur a:
* - un code (comp, carac, diff, ...)
* - une partie dédiée pour sauvegarder son contexte
* - le contexte d'un RollPart est stocké dans le rollData de la fenêtre RollDialog,
* dans des parties dédiés:
* - `rollData.refs[code]` pour les données de référentiel (liste de compétences, ...)
* - `rollData.current[code]` pour les informations d'état courante (la compétence sélectionnée, ...)
* - `rollData.selected[code]` pour les informations à sauvegarder, et utilisées pour paramétrer l'ouverture
* - Chaque RollPart gère ses données dans cet espace dédié.
* - Chaque RollPart a un sous-template dédié, et indique où il doit s'afficher dans le RollDialog
* - Chaque RollPart peut enregistrer ses propres events handlers pour mettre à jour son contexte (et généralement réafficher le RollDialo)
* - Chaque RollPart fournit les informations contextuelles associées au jet
* - TODO: chaque RollPart peut fournir un sous-template pour le ChatMessage correspondant au résultat du dé.
*
* ## boucle de rétroaction
* Lors de l'affichage, chaque RollPart peut fournir un filtre pour les autres RollParts.
* Ce filtre sert principalement à filtrer les caractéristiques/compétense.
*
* Une fois ce filtrage effectué, chaque RollPart va pouvoir modifier sa partie du contexte
* de la fenêtre, permettant à son template hbs d'avoir les donnéers à afficher.
*
* Enfin, lors de l'affichage (vu que les contrêles sont réaffichés), il peut
* enregistrer les listeners appropriés.
*
* ## Utilisation des informations sélectionnées
*
* Le rollData est la structure de stockage, et sert à préparer le jet de dé.
* Le résultat du jet est stocké dans le noeud `rollData.rolled` (comme pour
* la première version de jets de dés)
*
*
* # TODO
* - intégration pour un jet (oeuvres / tâches / méditation / compétence)
* - RdDRollResult V2 (affichage avec templates basés sur roll-dialog)
* - Extraction de jet résumé (pour appel chance)
* - gestion significative
* - Attaque
* - Défense
* - intégration rdd-combat
* - combat rencontres
*
*/
/* -------------------------------------------- */
export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2)
{
static init() {
}
static onReady() {
foundry.applications.handlebars.loadTemplates({
'roll-section': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-section.hbs',
'roll-type': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-type.hbs',
'roll-table': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-table.hbs',
'roll-ajustements': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-ajustements.hbs',
'roll-chances': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-chances.hbs',
'roll-button': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-button.hbs',
})
ChatRollResult.onReady()
foundry.applications.handlebars.loadTemplates(ALL_ROLL_TYPES.map(m => m.template))
foundry.applications.handlebars.loadTemplates(ROLL_PARTS.map(p => p.template))
ROLL_PARTS.forEach(p => p.onReady())
Handlebars.registerHelper('roll-centered-array', (base, show) => {
show = Math.abs(show)
const start = base - show
return [...Array(2 * show + 1).keys()].map(it => start + it)
})
Handlebars.registerHelper('roll-list-item-value', (list, key, path = undefined) => {
const selected = list.find(p => p.key == key)
if (selected && path && path != '') {
return foundry.utils.getProperty(selected, path)
}
return selected
})
Handlebars.registerHelper('roll-part-context', (rollData, code) => {
const rollPart = ROLL_PARTS.find(it => it.code == code)
if (rollPart == undefined) {
return {}
}
return {
code: code,
name: rollPart.name,
template: rollPart.template,
rollData: rollData,
refs: rollPart.getRefs(rollData),
current: rollPart.getCurrent(rollData)
}
})
}
static async create(rollData, rollOptions = {}) {
const rollDialog = new RollDialog(rollData, rollOptions)
rollDialog.render(true)
}
static get PARTS() {
return { form: { template: 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-dialog.hbs', } }
}
static get DEFAULT_OPTIONS() {
const default_options = {
tag: "form",
form: {
handler: RollDialog.handler,
submitOnChange: false,
closeOnSubmit: false
},
position: {
width: 600,
height: "auto",
},
}
return default_options
}
static async handler(event, form, formData) {
// rien pour l'instant
}
/** pre-configure les paramètres des différentes parties de la fenêtre (par exemple, prépare les listes de caractéristiques/compétences */
static $prepareRollData(rollData) {
rollData.current = rollData.current ?? {}
rollData.selected = rollData.selected ?? {}
rollData.type = rollData.type ?? {}
rollData.type.retry = rollData.type.retry ?? false
RollBasicParts.restore(rollData)
const potential = ALL_ROLL_TYPES.find(m => m.code == rollData.type.current)?.code
const allowed = rollData.type.retry && potential
? [potential]
: (rollData.type.allowed ?? ALL_ROLL_TYPES.filter(m => m.isAllowed(rollData) && m.visible(rollData)).map(m => m.code))
const rollType = allowed.find(c => c == rollData.type.current) ?? (allowed.length > 0 ? allowed[0].code : ROLL_TYPE_COMP);
rollData.type.allowed = allowed
rollData.type.current = rollType
ALL_ROLL_TYPES.find(m => m.code == rollType).setRollDataType(rollData)
rollData.refs = foundry.utils.mergeObject(rollData.refs ?? {}, Object.fromEntries(ROLL_PARTS.map(p => [p.code, {}])));
rollData.options = rollData.options ?? { rollMode: game.settings.get("core", "rollMode") }
ROLL_PARTS.forEach(p => p.initialize(rollData))
ROLL_PARTS.forEach(p => p.restore(rollData))
ROLL_PARTS.filter(p => p.isValid(rollData))
.forEach(p => {
p.loadRefs(rollData)
p.prepareContext(rollData)
})
return rollData
}
static saveParts(rollData) {
const target = RollBasicParts.initFrom(rollData)
ROLL_PARTS.filter(p => p.isActive(rollData))
.forEach(p => p.storeClean(rollData, target))
target.attackerRoll = rollData.attackerRoll
target.rolled = rollData.rolled
target.result = rollData.result
target.done = rollData.done ?? {}
target.dmg = rollData.dmg
return target
}
constructor(rollData, rollOptions) {
super()
this.rollData = RollDialog.$prepareRollData(rollData)
this.rollOptions = {
callbacks: [
async r => await r.active.actor.appliquerAjoutExperience(r),
async r => await r.active.actor.appliquerAppelMoral(r),
...(rollOptions.callbacks ?? [])
],
customChatMessage: rollOptions.customChatMessage,
onRollDone: rollOptions.onRollDone ?? doNothing
}
this.chatRollResult = new ChatRollResult();
this.selectType()
}
selectType() {
const selectedType = this.getSelectedType();
this.rollData.type.label = selectedType.title(this.rollData)
selectedType.setRollDataType(this.rollData)
selectedType.onSelect(this.rollData)
ROLL_PARTS.find(it => it.code == PART_CARAC).filterCaracs(this.rollData)
ROLL_PARTS.find(it => it.code == PART_COMP).filterComps(this.rollData)
}
static getActiveParts(rollData) {
return ROLL_PARTS.filter(p => p.isActive(rollData))
}
// get title() {
// return this.rollData.title ?? `Jet de dés de ${this.rollData.active.actor.name}`
// }
rollTitle(rollData) {
return rollData.label ?? ROLL_PARTS
.filter(it => it.section == ROLLDIALOG_SECTION.ACTION)
.filter(it => it.isActive(rollData))
.map(it => it.title(rollData))
.reduce(Misc.joining(' '))
}
async _onRender(context, options) {
this.window.title.innerText = this.rollTitle(this.rollData)
const buttonRoll = this.element.querySelector(`button[name="roll-dialog-button"]`)
buttonRoll?.addEventListener(
"click", e => {
e.preventDefault()
this.roll()
}
)
const buttonsType = this.element.querySelectorAll(`button[name="roll-type"]`)
buttonsType?.forEach(it => it.addEventListener(
"click", e => {
e.preventDefault()
this.rollData.type.current = e.currentTarget.dataset.type
this.selectType()
this.render()
}
))
Promise.all(
RollDialog.getActiveParts(this.rollData).map(async p => await p._onRender(this, context, options))
)
}
static getAjustements(rollData) {
return RollDialog.getActiveParts(rollData)
.map(p => p.getAjustements(rollData))
.reduce((a, b) => a.concat(b))
.sort((a, b) => a.diff == undefined ? 1 : b.diff == undefined ? -1 : 0)
}
async buildHTMLTable(carac, diff) {
return await renderTemplate('roll-table', { carac, diff })
}
async _prepareContext() {
const rollData = this.rollData
const types = ALL_ROLL_TYPES.filter(m => m.isAllowed(rollData) && m.visible(rollData))
.map(m => m.toTypeData(rollData))
RollBasicParts.loadSurprises(rollData, this.getSelectedType().code)
rollData.type.label = this.getSelectedType()?.title(rollData)
//TOCHECK: set type.label ?
const visibleRollParts = RollDialog.getActiveParts(rollData)
visibleRollParts.forEach(p => p.applyExternalImpacts(visibleRollParts, rollData))
this.setSpecialComp(visibleRollParts);
visibleRollParts.forEach(p => p.prepareContext(rollData))
RollDialog.calculAjustements(rollData)
const templates = RollDialog.getActiveParts(rollData).map(p => p.toTemplateData())
const context = await super._prepareContext()
return foundry.utils.mergeObject(
{
types: types,
templates: templates,
rollData: rollData,
}, context)
}
setSpecialComp(visibleRollParts) {
const specialComp = visibleRollParts.map(p => p.getSpecialComp(this.rollData))
.reduce((a, b) => a.concat(b))
if (specialComp.length > 0) {
const rollPartComp = RollDialog.getActiveParts(this.rollData)
.find(it => it.code == PART_COMP);
rollPartComp?.setSpecialComp(this.rollData, specialComp)
}
}
static calculAjustements(rollData) {
rollData.ajustements = RollDialog.getAjustements(rollData)
rollData.ajustements.forEach(it => it.isDiff = it.diff != undefined)
rollData.current.totaldiff = rollData.ajustements
.map(adj => adj.diff)
.filter(d => d != undefined)
.reduce(Misc.sum(), 0)
}
getSelectedType() {
return ALL_ROLL_TYPES.find(m => m.code == this.rollData.type.current)
}
async roll() {
const roll = RollDialog.saveParts(this.rollData)
RollDialog.loadRollData(roll)
roll.current.resultat = this.rollData.current[PART_TRICHER]?.resultat ?? -1
roll.choix = {}
roll.rolled = await RollDialogAdapter.rollDice(roll, this.rollTitle(roll))
roll.result = this.getSelectedType(roll).getResult(roll)
console.info('RollDialog.roll:', roll)
await this.chatRollResult.display(roll)
await Promise.all(this.rollOptions.callbacks.map(async callback => await callback(roll)))
this.rollOptions.onRollDone(this)
}
static loadRollData(roll) {
RollDialog.$prepareRollData(roll)
RollDialog.calculAjustements(roll)
roll.v2 = true
}
async defaultCallback(roll, rolled) {
await roll.active.actor.appliquerAjoutExperience(roll)
await roll.active.actor.appliquerAppelMoral(roll)
}
}