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")}`
if (this.zfp == null) {
context.notReadyReasons += `- ${game.i18n.format("SPELL_DIALOG.notReadyReason.noZFPDataAvailable")}
`
}
if (context.zfpModified < 0) {
context.notReadyReasons += `- ${game.i18n.format("SPELL_DIALOG.notReadyReason.overspentZFP")}
`
}
if (context.spoModCount > context.maxSpoModCount) {
context.notReadyReasons += `- ${game.i18n.format("SPELL_DIALOG.notReadyReason.tooManySpoMods")}
`
}
if (!this._selectedRepresentation) {
context.notReadyReasons += `- ${game.i18n.format("SPELL_DIALOG.notReadyReason.noRepresentation")}
`
}
context.notReadyReasons += "
"
}
return context
}
async _onRender(context, options) {
}
}