import {ATTRIBUTE} from "../data/attribute.mjs"; import {spoModData, leadingAttribute} from "../data/spellData/spellData.mjs"; import {evaluateRoll} from "../globals/DSARoll.mjs"; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api export class SpellDialog extends HandlebarsApplicationMixin(ApplicationV2) { static DEFAULT_OPTIONS = { classes: ['dsa41', 'dialog', 'spell'], tag: "form", position: { width: 480, height: 800 }, window: { resizable: false, title: "Zauber wirken" }, form: { submitOnChange: true, closeOnSubmit: false, handler: SpellDialog.#onSubmitForm }, actions: { cast: SpellDialog.#cast, diceRoll: SpellDialog.#diceRoll } } static PARTS = { form: { template: 'systems/DSA_4-1/templates/dialog/spell-dialog.hbs', } } static data = {} /** * * @type {Actor} * @private */ _actor = null _costMutators = {} _castTimeMutators = {} _variants = {} _costModel = {} _castTimeModel = {} _spoMods = {} displayModResult = 0 constructor(actor, spellId) { super() this._actor = actor this._spell = this._actor.itemTypes["Spell"].find(p => p._id === spellId) this._circumstance = 0 this._mods = [] this.mod = 0 this._costMutators = {} this._castTimeMutators = {} this._selectedRepresentation = this._spell.getFlag("DSA_4-1", "representation") this._spellDie = null this._variants = {} this._costModel = this._spell.system.kosten.find(c => c.repräsentation === context.selectedRepresentation) ?? this._spell.system.kosten.find(c => c.repräsentation === "") this._castTimeModel = this._spell.system.zauberdauer this._castTimeMutators = {} this._costMutators = {} this._costModel.variables.forEach(v => this._costMutators[v] = 0) this._castTimeModel.variables.forEach(v => this._castTimeMutators[v] = 0) this.cost = this.normalizeCastingCost() ?? 0 this.castingTime = this.#normalizeCastingTime(this._spell) this.zfp = null this.zfpDetermined = false if (this._selectedRepresentation) { this._costModel = this._spell.system.kosten.find(c => c.repräsentation === context.selectedRepresentation) ?? this._spell.system.kosten.find(c => c.repräsentation === "") this._castTimeModel = this._spell.system.zauberdauer this._castTimeMutators = {} this._costMutators = {} this._costModel.variables.forEach(v => this._costMutators[v] = 0) this._castTimeModel.variables.forEach(v => this._castTimeMutators[v] = 0) } } /** * @typedef FormulaData * @property {String} additionalFormula mathematical expression that can be eval'd by replacing the variables with the user added input * @property {[String]} variables contains all tokens which will be replaceable inside the formula * @property {[String: Number]} substitutions user input with which the variables with the same key will be replaced in the formula text * @property {"Aktionen"|"SR"} unit gives the evaluated formula its appropriate time unit */ static async #onSubmitForm(event, form, formData) { event.preventDefault() // handle changes in variable Inputs this._selectedRepresentation = formData.object.representation ?? this._selectedRepresentation this._variants = foundry.utils.expandObject(formData.object)["variants"] ?? this._variants if (this._spell.system.probe.includes("*")) { // ATTRIBUTO if (this._variants["Mut"]) { this._spellDie = "MU" } else if (this._variants["Klugheit"]) { this._spellDie = "KL" } else if (this._variants["Intuition"]) { this._spellDie = "IN" } else if (this._variants["Charisma"]) { this._spellDie = "CH" } else if (this._variants["Fingerfertigkeit"]) { this._spellDie = "FF" } else if (this._variants["Gewandtheit"]) { this._spellDie = "GE" } else if (this._variants["Konstitution"]) { this._spellDie = "KO" } else if (this._variants["Konstitution"]) { this._spellDie = "KK" } else { this._spellDie = null } } let costMutators = foundry.utils.expandObject(formData.object)["costMutators"] ?? this._costMutators if (costMutators) { this._costMutators = costMutators } this.cost = this.normalizeCastingCost() let castTimeMutators = foundry.utils.expandObject(formData.object)["castTimeMutators"] ?? this._castTimeMutators this._castTimeMutators = castTimeMutators this.mod = 0 this._activeVariants = Object.entries(this._variants) .filter(([key, truthiness]) => truthiness) .map(([key, truthiness]) => this._spell.system.varianten.find(v => v.name === key)) this._activeVariants.forEach(variant => { if (variant.mod) { this.mod += Number(variant.mod) } }) this.castingTime = this.#normalizeCastingTime(this._spell, this._castTimeMutators) // eval probeMod if (formData.object["checkMod"]) { this.mod -= formData.object["checkMod"] this._checkModValue = formData.object["checkMod"] } // eval spomods this._spoMods = foundry.utils.expandObject(formData.object)["spoMods"] ?? {} let totalMod = this.mod let totalCost = this.cost let totalCastingTime = Number(this.castingTime) Object.entries(this._spoMods).forEach(([modName, times]) => { const actualMod = spoModData[modName] for (let i = 0; i < times; i++) { const ctfn = new Function("castTime", "return " + actualMod.castTimeModFn) totalCastingTime = ctfn(totalCastingTime) const cfn = new Function("cost", "return " + actualMod.costModFn) totalCost = cfn(totalCost) const zmfn = new Function("mod", "return " + actualMod.modFn) totalMod = zmfn(totalMod) } }) this.mod = totalMod this.cost = totalCost this.castingTime = totalCastingTime this.render({parts: ["form"]}) } static async #cast(event, target) { ChatMessage.create({ user: game.user._id, speaker: {actor: this._actor}, content: `beginnt ${this._spell.name} zu wirken`, type: CONST.CHAT_MESSAGE_TYPES.IC }) const cooldowns = this._actor.system.cooldowns let m = (queue, data) => { ChatMessage.create({ user: game.user._id, speaker: {actor: this._actor}, content: data.message, type: CONST.CHAT_MESSAGE_TYPES.IC }) } let message = this._spell.system.wirkung if (this._activeVariants.length > 0) { message += "
" message += this._activeVariants.map(v => v.name).join(", ") } if (Object.keys(this._spoMods).length > 0) { message += "
" Object.entries(this._spoMods).forEach(([modName, times]) => { if (times > 0) { message += times + "x" + modName + "
" } }) } if (Object.keys({...this._castTimeMutators, ...this._costMutators}).length > 0) { message += "
" Object.entries({...this._castTimeMutators, ...this._costMutators}).forEach(([mutatorName, mutatorValue]) => { message += mutatorName + ": " + mutatorValue + "
" }) } message += "
" + this.zfp + " ZfP*
" + this._spell.system.zfw + " ZfW" cooldowns.push({ start: this.castingTime, current: 0, data: { cssClass: "Magisch", title: this._spell.name, taw: this.zfp, mod: 0, actorId: this._actor._id, spellId: this._spell._id, message, maneuver: m.toString() } }) await this._actor.update({"system.cooldowns": cooldowns}) } static async #diceRoll(event, target) { const result = await evaluateRoll( "3d20", { value: this._spell.system.zfw + this.mod, werte: this.#getProbenWerte(), owner: this._actor } ) if (result.tap >= 0) { // erfolg await result.evaluated.toMessage({ speaker: ChatMessage.getSpeaker({actor: this._actor}), flavor: ` ${result.meisterlich ? 'Meisterlich geschafft' : 'Geschafft'} mit ${result.tap} Punkten übrig`, }) } else { // misserfolg await result.evaluated.toMessage({ speaker: ChatMessage.getSpeaker({actor: this._actor}), flavor: ` ${result.meisterlich ? 'Gepatzt' : ''} mit ${Math.abs(result.tap)} Punkten daneben`, }) } this.zfp = result.tap this.zfpDetermined = true this.render({parts: ["form"]}) } normalizeCastingCost() { let costFormula = this._costModel.additionalFormula if (costFormula) { this._costModel.variables.forEach(v => { costFormula = costFormula.replace(v, this._costMutators[v]) }) costFormula = Number(eval(costFormula)) + Number(this._costModel.cost) } else { costFormula = this._costModel.cost } if (costFormula <= this._costModel.min) { costFormula = this._costModel.min } return costFormula } /** * * @param spell * @param {FormulaData} additionalFormulaData * @returns {number|*} */ #normalizeCastingTime(spell, additionalFormulaData) { // min: Wenn ein Zauber eine mindest dauer hat kann diese nachdem diese abgelaufen ist jederzeit abgebrochen werden // normal: Standard Zauberzeit eines Zaubers // additionalFormulaData: enthält die zur Normalzeit zusätzlichen Zauberdauer const castingTime = spell.system.zauberdauer.normal ?? 0 let baseCastTime = 0 const minCastingTime = spell.system.zauberdauer.min ?? 0 let baseMinCastTime = 0 if (castingTime) { baseCastTime = castingTime.replace(/(.*) Aktionen/g, (_, aktionen) => { return aktionen }) baseCastTime = baseCastTime.replace(/(.*) SR/g, (_, aktionen) => { return aktionen * 20 }) } if (minCastingTime) { baseMinCastTime = minCastingTime.replace(/(.*) Aktionen/g, (_, aktionen) => { return aktionen }) baseMinCastTime = baseMinCastTime.replace(/(.*) SR/g, (_, aktionen) => { return aktionen * 20 }) } let actualCastingTime = 0 let formula = spell.system.zauberdauer.additionalFormula if (formula) { Object.entries(additionalFormulaData).forEach(([variableName, variableValue]) => { formula = formula.replaceAll(variableName, variableValue) }) if (spell.system.zauberdauer.additionalFormulaTimeUnit == "Aktionen") { actualCastingTime = (Number(baseCastTime) + Number(eval(formula)) ?? 0) } else { actualCastingTime = (Number(baseCastTime) + (Number(eval(formula)) * 20) ?? 0) } } else { actualCastingTime = baseCastTime } if (Number(actualCastingTime) <= Number(baseMinCastTime)) { actualCastingTime = baseMinCastTime } return actualCastingTime } _configureRenderOptions(options) { super._configureRenderOptions(options) if (options.window) { if (this._spell) { options.window.title = `${this._spell.name} [${this._spell.system.zfw}]` } } return options } #getProbenWerte() { let dice = [] this._spell.system.probe.map(p => { if (p === "*") { return this._spellDie ?? null } else { return p } }).forEach(p => { if (p !== null) { dice.push( this._actor.system.attribute[p.toLowerCase()].aktuell ) } else { dice.push( "??" ) } }) return dice } async _prepareContext(options) { const context = await super._prepareContext(options) context.actor = this._actor context.spell = this._spell context.representationOptions = {} context.selectedRepresentation = this._selectedRepresentation context.text = this._spell.system.wirkung context.dice = [] context.colorfulDice = game.settings.get('DSA_4-1', 'optional_colorfuldice') context.modResult = this._spell.system.zfw + this.mod context.penalty = (this.mod > 0 ? "+" : "") + this.mod context.displayModResult = (context.modResult > 0 ? "+" : "") + context.modResult context.castingTime = this.castingTime context.ready = true context.zfpDetermined = this.zfpDetermined // variable probe (should consider Achaz as they can replace one KL in a KL/KL/* spell with IN this._spell.system.probe.map(p => { if (p === "*") { return this._spellDie ?? null } else { return p } }).forEach(p => { if (p !== null) { context.dice.push({ wert: this._actor.system.attribute[p.toLowerCase()].aktuell, name: p, tooltip: ATTRIBUTE[p.toLowerCase()], }) } else { context.dice.push({ wert: "??", name: "??", tooltip: ATTRIBUTE["UNKNOWN"], }) } }) context.variants = this._spell.system.varianten.map(v => { return { variantText: v.description, variantName: v.name, variantPenalty: v.mod ?? "0", variantChecked: this._variants[v.name] } }) // Repräsentation context.representationOptions[""] = "" Object.entries(this._spell.system.repräsentation).forEach(([key, value]) => { context.representationOptions[key] = key }) if (!this._selectedRepresentation) { context.ready = false } // Costs and Mutators context.castingCosts = this.cost // set probe to current held probe variables or take from _spell context.costMutators = this._costMutators if (this._costModel) { context.costVariables = this._costModel.variables } else { context.costVariables = [] } // probeMod if (this._spell.system.probeMod) { context.checkModTest = this._spell.system.probeMod context.checkModValue = this._checkModValue } // SpoMods context.spoModCount = Object.values(this._spoMods).reduce((previousValue, currentValue) => previousValue + currentValue, 0) context.maxSpoModCount = 0 if (this._selectedRepresentation) { const leadingAttributKey = leadingAttribute[this._selectedRepresentation] context.maxSpoModCount = (this._actor.system.attribute[leadingAttributKey.toLowerCase()].aktuell ?? 0) - 12 if (context.maxSpoModCount < 0) { context.maxSpoModCount = 0 } } if (context.spoModCount > context.maxSpoModCount) { context.ready = false } const mapper = (spoModName) => { let data = spoModData[spoModName] let value = this._spoMods[data.name] ?? 0 let totalModValue = data.mod * value return { ...data, value, totalModValue } } context.spoMods = [] if (this._spell.system.modifikationen) { this._spell.system.modifikationen.split(",").forEach(spoMod => { switch (spoMod.trim()) { case "Zauberdauer": context.spoMods.push(mapper("Halbierte Zauberdauer")) context.spoMods.push(mapper("Verdoppelte Zauberdauer")) break; case "Kosten": context.spoMods.push(mapper("Kosten einsparen")) break; case "Reichweite": context.spoMods.push(mapper("Verkleinerung von Reichweite oder Wirkungsradius")) context.spoMods.push(mapper("Vergrößerung von Reichweite oder Wirkungsradius")) break; } }) } // if this.zfp is null then we are in the first step pre dice roll if (this.zfp == null) { context.ready = false context.diceRoll = true } else { if (this.zfp === 0) { this.zfp = 1 } let zfpMod = 0 Object.entries(this._spoMods).forEach(([modName, times]) => { const actualMod = spoModData[modName] for (let i = 0; i < times; i++) { const zmfn = new Function("mod", "return " + actualMod.modFn) zfpMod = zmfn(zfpMod) } }) if (this.zfp + zfpMod > this._spell.system.zfw) { // cant be higher than the learnt level context.zfpModified = this._spell.system.zfw } else { context.zfpModified = this.zfp + zfpMod } context.spellName = this._spell.system.name context.variant = context.variants.filter(v => v.variantChecked).map(v => `${v.variantName}—${v.variantText}`).join("

") if (context.zfpModified < 0) { context.ready = false } } if (!context.ready) { // rules have changed, it cant be cast when zfp - selected mutators is below 0 context.notReadyReasons = `${game.i18n.format("SPELL_DIALOG.notReadyReason.title")}" } return context } async _onRender(context, options) { } }