584 lines
20 KiB
JavaScript
584 lines
20 KiB
JavaScript
|
|
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 += "<hr/>"
|
|
message += this._activeVariants.map(v => v.name).join(", ")
|
|
}
|
|
if (Object.keys(this._spoMods).length > 0) {
|
|
message += "<hr/>"
|
|
Object.entries(this._spoMods).forEach(([modName, times]) => {
|
|
if (times > 0) {
|
|
message += times + "x" + modName + "<br/>"
|
|
}
|
|
})
|
|
}
|
|
|
|
if (Object.keys({...this._castTimeMutators, ...this._costMutators}).length > 0) {
|
|
message += "<hr/>"
|
|
Object.entries({...this._castTimeMutators, ...this._costMutators}).forEach(([mutatorName, mutatorValue]) => {
|
|
message += mutatorName + ": " + mutatorValue + "<br/>"
|
|
})
|
|
}
|
|
|
|
message += "<hr/>" + this.zfp + " ZfP*<br/>" + 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 => `<em>${v.variantName}</em>—${v.variantText}`).join("<br/><br/>")
|
|
|
|
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 = `<em>${game.i18n.format("SPELL_DIALOG.notReadyReason.title")}</em><ul>`
|
|
|
|
if (this.zfp == null) {
|
|
context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.noZFPDataAvailable")}</li>`
|
|
}
|
|
if (context.zfpModified < 0) {
|
|
context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.overspentZFP")}</li>`
|
|
}
|
|
if (context.spoModCount > context.maxSpoModCount) {
|
|
context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.tooManySpoMods")}</li>`
|
|
}
|
|
if (!this._selectedRepresentation) {
|
|
context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.noRepresentation")}</li>`
|
|
}
|
|
context.notReadyReasons += "</ul>"
|
|
}
|
|
|
|
|
|
return context
|
|
}
|
|
|
|
async _onRender(context, options) {
|
|
|
|
}
|
|
} |