diff --git a/src/main.mjs b/src/main.mjs index 75bcc926..8cb98829 100644 --- a/src/main.mjs +++ b/src/main.mjs @@ -27,9 +27,7 @@ import {CultureDataModel} from "./module/data/culture.mjs"; import {CultureSheet} from "./module/sheets/CultureSheet.mjs"; import {SpeciesSheet} from "./module/sheets/SpeciesSheet.mjs"; import {ProfessionSheet} from "./module/sheets/ProfessionSheet.mjs"; -import {XmlImport} from "./module/xml-import/xml-import.mjs"; import {XmlImportDialog} from "./module/dialog/xmlImportDialog.mjs"; - async function preloadHandlebarsTemplates() { return foundry.applications.handlebars.loadTemplates([ // ui partials. diff --git a/src/module/data/specialAbility.mjs b/src/module/data/specialAbility.mjs index e49ad641..bfadbd4d 100644 --- a/src/module/data/specialAbility.mjs +++ b/src/module/data/specialAbility.mjs @@ -145,13 +145,13 @@ export class SpecialAbilityDataModel extends BaseItem { if (options?.weapon) { for (const waff of this.waffenLimit) { if (waff.waffe) { - passes = options?.weapon.name === waff.waffe ?? false + passes = options?.weapon?.name === waff.waffe ?? false if (waff.mod) mod = waff.mod if (passes) break } if (waff.gruppe) { - passes = options?.skill.name === waff.gruppe ?? false + passes = options?.skill?.name === waff.gruppe ?? false if (waff.mod) mod = waff.mod if (passes) break diff --git a/src/module/dialog/combatAction.mjs b/src/module/dialog/combatAction.mjs index 5ef5e614..153535c1 100644 --- a/src/module/dialog/combatAction.mjs +++ b/src/module/dialog/combatAction.mjs @@ -53,7 +53,7 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2 this._targetId = null this._skillId = null this._weaponId = null - this._combatManeuverId = null + this._defenseManeuverId = null this._actionManager = new ActionManager(this._actor) } @@ -66,7 +66,7 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2 static async #onSelectManeuver(event, target) { const {maneuverId} = target.dataset - this._combatManeuverId = this._combatManeuverId === maneuverId ? null : maneuverId + this._defenseManeuverId = this._defenseManeuverId === maneuverId ? null : maneuverId this.render({parts: ["form"]}) } @@ -80,7 +80,7 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2 static async #onSubmitForm(event, form, formData) { event.preventDefault() - const maneuver = this.#evaluateManeuvers().find(p => p.id === this._combatManeuverId) + const maneuver = this.#evaluateManeuvers().find(p => p.id === this._defenseManeuverId) const weapon = this._actor.itemTypes["Equipment"].find(p => p._id === this._weaponId) const skill = this._actor.itemTypes["Skill"].find(p => p._id === this._skillId) const target = game.actors.get(game.scenes.current.tokens.find(p => p._id === this._targetId).actorId) @@ -235,7 +235,7 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2 skill }).filter(p => p.type === ActionManager.ATTACK).map(action => { return { - isSelected: this._combatManeuverId === action.name, + isSelected: this._defenseManeuverId === action.name, id: action.name, name: action.name, type: action.type, @@ -265,7 +265,7 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2 if (this._targetId && this._weaponId && this._skillId) { context.maneuver = this.#evaluateManeuvers() } - const maneuver = this._maneuvers?.find(p => p.id === this._combatManeuverId) + const maneuver = this._maneuvers?.find(p => p.id === this._defenseManeuverId) if (maneuver) { context.canMod = maneuver.mod != undefined } @@ -274,7 +274,7 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2 // TODO get W/M of weapon NOW - context.ready = this._targetId && this._weaponId && this._skillId && this._combatManeuverId + context.ready = this._targetId && this._weaponId && this._skillId && this._defenseManeuverId return context } else { ui.notifications.error(`Feature funktioniert nur wenn der Akteur ein Token auf der aktuellen Szene hat`); @@ -286,7 +286,7 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2 const target = this.element.querySelector(".actions button .value") const targetDescription = this.element.querySelector(".modResult") const at = Number(context.targetNumber) - const maneuver = this._maneuvers?.find(p => p.id === this._combatManeuverId) + const maneuver = this._maneuvers?.find(p => p.id === this._defenseManeuverId) const mod = Number(this.element.querySelector('[name="mod"]').value) const penalty = 0 - (maneuver?.penalty ?? 0) + (maneuver?.mod?.(mod) ?? 0) ?? 0 const circumstance = Number(this.element.querySelector('[name="circumstance"]').value) diff --git a/src/module/dialog/defenseAction.mjs b/src/module/dialog/defenseAction.mjs new file mode 100644 index 00000000..6cbb7b83 --- /dev/null +++ b/src/module/dialog/defenseAction.mjs @@ -0,0 +1,303 @@ +import {ActionManager} from "../sheets/actions/action-manager.mjs"; + +const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api + + +/** + * @typedef TokenDistance + * @property {Number} x + * @property {Number} y + * @property {Number} d + * @property {Token} token + */ + +export class DefenseActionDialog extends HandlebarsApplicationMixin(ApplicationV2) { + + static DEFAULT_OPTIONS = { + classes: ['dsa41', 'dialog', 'combat'], + tag: "form", + position: { + width: 480, + height: 640 + }, + window: { + resizable: false, + }, + form: { + submitOnChange: false, + closeOnSubmit: true, + handler: DefenseActionDialog.#onSubmitForm + }, + actions: { + selectWeaponAndSkill: DefenseActionDialog.#onSelectWeaponAndSkill, + selectManeuver: DefenseActionDialog.#onSelectManeuver, + } + } + + static PARTS = { + form: { + template: 'systems/DSA_4-1/templates/dialog/defense-action.hbs', + } + } + + /** + * @type {Actor} + * @private + */ + _actor = null + + constructor(actor, attackData) { + super(); + this._attackData = attackData ?? { + modToDefense: 0, + attacker: null, + weapon: null, // is important to note as weapons like Chain Weapons or Flails can ignore Shields + } + this._actor = actor + this._skillId = null + this._weaponId = null + this._defenseManeuverId = null + this._actionManager = new ActionManager(this._actor) + //if (this._actor) { + // this._actor.prepareDerivedData() + //} + } + + + static async #onSelectTarget(event, target) { + const {targetId} = target.dataset + this._targetId = this._targetId === targetId ? null : targetId + this.render({parts: ["form"]}) + } + + static async #onSelectManeuver(event, target) { + const {maneuverId} = target.dataset + this._defenseManeuverId = this._defenseManeuverId === maneuverId ? null : maneuverId + this.render({parts: ["form"]}) + } + + + static async #onSelectWeaponAndSkill(event, target) { + const {weaponId, skillId} = target.dataset + this._weaponId = this._weaponId === weaponId ? null : weaponId + this._skillId = this._skillId === skillId ? null : skillId + this.render({parts: ["form"]}) + } + + static async #onSubmitForm(event, form, formData) { + event.preventDefault() + const attack = this._attackData + const maneuver = this.#evaluateManeuvers().find(p => p.id === this._defenseManeuverId) + this._actor.rollDefense({ + weapon: this._weaponId, + skill: this._skillId, + attackData: attack, + maneuver, + mod: this._mod, + circumstance: this._circumstance, + penalty: this._penalty, + targetNumber: this._targetNumber, + modDescription: maneuver?.modDescription?.replace("{}", "" + this._mod) ?? "" + }) + } + + _configureRenderOptions(options) { + super._configureRenderOptions(options) + + if (options.window) { + options.window.title = `Gegen einen Angriff verteidigen` + } + return options + } + + #evaluateWeapons() { + + // get equipped weapons and adjust AT/PA values by basis values from actor TODO: and W/M of weapons + const equips = this._actor.system.heldenausruestung[this._actor.system.setEquipped] + const weapons = [] + + const equippedWeapons = ["links", "rechts", "fernkampf"] + + const baseAt = { + links: this._actor.system.at.links.aktuell, // TODO hook Beidhändigerkampf/linkhand + rechts: this._actor.system.at.rechts.aktuell, + fernkampf: this._actor.system.fk.aktuell, + } + const basePa = { + links: this._actor.system.pa.links.aktuell, // TODO hook Beidhändigerkampf/linkhand + rechts: this._actor.system.pa.rechts.aktuell, + fernkampf: 0, + } + + equippedWeapons.forEach(slot => { + const equip = equips[slot] + const weapon = this._actor.itemTypes["Equipment"].find(p => p._id === equip) + if (weapon) { + + const variantWeaponSkills = [...weapon.system.rangedSkills, ...weapon.system.meleeSkills] + + variantWeaponSkills.forEach(weaponSkill => { + + const skill = this._actor.itemTypes["Skill"].find(p => p.name === weaponSkill) + if (skill) { + + const skillAt = skill.system.at + const skillPa = skill.system.pa + + weapons.push({ + isSelected: this._skillId === skill._id && this._weaponId === weapon._id, + weaponId: weapon._id, + skillId: skill._id, + name: weapon.name, + skillName: skill.name, + img: weapon.img, + combatStatistics: { + at: baseAt[slot] + weapon.system.attackModifier + skillAt, + pa: basePa[slot] + weapon.system.parryModifier + skillPa + } + }) + } + }) + } + }) + // TODO: also do this for combatAction + const links = this._actor.system.heldenausruestung[this._actor.system.setEquipped].links + const rechts = this._actor.system.heldenausruestung[this._actor.system.setEquipped].rechts + if (!links && !rechts) { + const unarmedSkills = [this._actor.itemTypes["Skill"].find(p => p.name === "Ringen"), this._actor.itemTypes["Skill"].find(p => p.name === "Raufen")] + + unarmedSkills.forEach(unarmedSkill => { + const [skillAt, skillPa] = [unarmedSkill.system.at, unarmedSkill.system.pa] + + weapons.push({ + isSelected: this._skillId === unarmedSkill._id, + weaponId: "", + skillId: unarmedSkill._id, + name: unarmedSkill.name, + skillName: unarmedSkill.name, + img: "", + combatStatistics: { + at: baseAt["rechts"] + skillAt, + pa: basePa["rechts"] + skillPa + } + }) + }) + } + // Ausweichen as Weapon + weapons.push({ + isSelected: this._weaponId === "Ausweichen", + weaponId: "Ausweichen", + skillId: "Ausweichen", + name: "Ausweichen", + skillName: "Ausweichen", + img: "", + combatStatistics: { + at: 0, + pa: this._actor.system.ausweichen.aktuell + } + }) + + this._weapons = weapons.sort((a, b) => (a.isSelected ? 0 : 1) - (b.isSelected ? 0 : 1)) + return this._weapons + + } + + #evaluateManeuvers() { + const manager = this._actionManager + const weapon = this._weaponId !== "Ausweichen" ? this._actor.itemTypes["Equipment"].find(p => p._id === this._weaponId) : "Ausweichen" + const skill = this._actor.itemTypes["Skill"].find(p => p._id === this._skillId) + //const target = game.actors.get(game.scenes.current.tokens.find(p => p._id === this._targetId).actorId) + this._maneuvers = manager.evaluate({ + //target, + weapon, + skill + }).filter(p => p.type === ActionManager.DEFENSE).map(action => { + return { + isSelected: this._defenseManeuverId === action.name, + id: action.name, + name: action.name, + type: action.type, + source: action.source, + cost: action.cost, + penalty: action.eval?.mod ?? 0, + mod: action.mod, + modDescription: action.modDescription, + cooldown: action.cooldown, + activate: action.activate, + } + }).sort((a, b) => (a.isSelected ? 0 : 1) - (b.isSelected ? 0 : 1)) + return this._maneuvers + } + + async _prepareContext(options) { + const context = await super._prepareContext(options) + + context.actor = this._actor + context.distanceUnit = game.scenes.current.grid.units + + if (this._actor.getActiveTokens()[0]?.id) { + + context.weapons = this.#evaluateWeapons() + + if (this._skillId) { + context.maneuver = this.#evaluateManeuvers() + } + const maneuver = this._maneuvers?.find(p => p.id === this._defenseManeuverId) + if (maneuver) { + context.canMod = maneuver.mod != undefined + } + + context.targetNumber = context.weapons.find(p => ((this._weaponId != "") || p.weaponId === this._weaponId) && p.skillId === this._skillId)?.combatStatistics.pa + + // TODO get W/M of weapon NOW + + context.ready = this._targetId && this._weaponId && this._skillId && this._defenseManeuverId + return context + } else { + ui.notifications.error(`Feature funktioniert nur wenn der Akteur ein Token auf der aktuellen Szene hat`); + } + + } + + #update(context) { + const target = this.element.querySelector(".actions button .value") + const targetDescription = this.element.querySelector(".modResult") + const at = Number(context.targetNumber) + const maneuver = this._maneuvers?.find(p => p.id === this._defenseManeuverId) + const mod = Number(this.element.querySelector('[name="mod"]').value) + const penalty = 0 - (maneuver?.penalty ?? 0) + (maneuver?.mod?.(mod) ?? 0) ?? 0 + const circumstance = Number(this.element.querySelector('[name="circumstance"]').value) + this.element.querySelector('[name="penalty"]').value = penalty + circumstance + const result = (at + circumstance + penalty) + + this._circumstance = circumstance + this._penalty = penalty + this._targetNumber = result + this._mod = mod + this._modDescription = maneuver?.modDescription?.replace("{}", "" + mod) ?? "" + + target.textContent = `(${result})` + targetDescription.textContent = this._modDescription + + if (result <= 0) { + context.ready = false + this.element.querySelector(".actions button").classList.remove("ready") + this.element.querySelector(".actions button").setAttribute("disabled", true) + } else { + context.ready = true + this.element.querySelector(".actions button").classList.add("ready") + this.element.querySelector(".actions button").removeAttribute("disabled") + } + } + + _onRender(context, options) { + this.#update(context) + + this.element.querySelectorAll('[name="mod"], [name="malus"], [name="circumstance"]').forEach(e => e.addEventListener('change', (event) => { + this.#update(context) + })) + + } + + +} \ No newline at end of file diff --git a/src/module/documents/character.mjs b/src/module/documents/character.mjs index 397bff18..29fc92b3 100644 --- a/src/module/documents/character.mjs +++ b/src/module/documents/character.mjs @@ -94,6 +94,10 @@ export class Character extends Actor { systemData.gs.basis = 6; systemData.gs.aktuell = systemData.gs.basis + (systemData.gs.mod ?? 0); // TOOD: get GS from spezien + systemData.ausweichen = {} + systemData.ausweichen.basis = systemData.pa.basis + systemData.ausweichen.aktuell = systemData.ausweichen.basis + (systemData.ausweichen.mod ?? 0) + if (game.settings.get("DSA_4-1", "optional_ruestungzonen")) { systemData.rs = { @@ -190,6 +194,7 @@ export class Character extends Actor { this.prepareEmbeddedDocuments(); } + getRollData() { const data = super.getRollData(); this.prepareDerivedData() @@ -252,6 +257,31 @@ export class Character extends Actor { return false } + + async rollDefense(data) { + const maneuver = data.manuever + const weapon = this.itemTypes["Equipment"].find(p => p._id === data.weapon) + const skill = data.skill !== "Ausweichen" ? this.itemTypes["Skill"].find(p => p._id === data.skill) : "Ausweichen" + //const target = game.actors.get(game.scenes.current.tokens.find(p => p._id === data.target).actorId) + + const roll = new Roll("1d20cs<" + data.targetNumber) + const evaluated1 = (await roll.evaluate()) + + let flavor = '' + + if (skill === "Ausweichen") { + flavor = `Versucht auszuweichen${data.modDescription}` + } else { + flavor = `Verteidigt sich gegen einen Angriff mit ${weapon.name} (${skill.name})${data.modDescription}` + } + + await evaluated1.toMessage({ + speaker: ChatMessage.getSpeaker({actor: this}), + flavor, + rollMode: "publicroll", + }) + } + async rollAttack(data) { const maneuver = data.manuever const weapon = this.itemTypes["Equipment"].find(p => p._id === data.weapon) diff --git a/src/module/sheets/actions/action-manager.mjs b/src/module/sheets/actions/action-manager.mjs index 26ca3da8..ec66f937 100644 --- a/src/module/sheets/actions/action-manager.mjs +++ b/src/module/sheets/actions/action-manager.mjs @@ -23,7 +23,12 @@ export class ActionManager { cost: ActionManager.FREE, type: ActionManager.DEFENSE, source: ActionManager.DEFAULT, - eval: (options) => true + eval: (options) => { + if (options?.weapon && options.weapon === "Ausweichen") { + return true + } + return false + } }, { name: "Rufen", @@ -158,11 +163,16 @@ export class ActionManager { } }, { - name: "Abwehraktion", + name: "Parade", type: ActionManager.DEFENSE, cost: ActionManager.REGULAR, source: ActionManager.DEFAULT, - eval: (options) => true + eval: (options) => { + if (options?.weapon && options.weapon !== "Ausweichen") { + return true + } + return false + } }, { name: "Meisterparade", @@ -170,7 +180,7 @@ export class ActionManager { cost: ActionManager.REGULAR, source: ActionManager.SF, modDescription: "erschwert nächste AT vom Ziel um {}", - mod: (value) => value, + mod: (value) => -value, eval: (options) => { const step1 = this.#hatWaffeinHand(options) && this.#hatSonderfertigkeit("Meisterparade", options) const step2WithBenefits = this.#evalSonderfertigkeitRequirements("Meisterparade", options) diff --git a/src/module/sheets/characterSheet.mjs b/src/module/sheets/characterSheet.mjs index fa0320ab..8d5efd98 100644 --- a/src/module/sheets/characterSheet.mjs +++ b/src/module/sheets/characterSheet.mjs @@ -9,6 +9,7 @@ import Social from "./character/social.mjs"; import Spells from "./character/spells.mjs" import {CombatActionDialog} from "../dialog/combatAction.mjs"; import {ActionManager} from "./actions/action-manager.mjs"; +import {DefenseActionDialog} from "../dialog/defenseAction.mjs"; const {HandlebarsApplicationMixin} = foundry.applications.api const {ActorSheetV2} = foundry.applications.sheets @@ -195,8 +196,15 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) { this.document.itemTypes["Species"]?.[0]?.sheet.render(true) } - static #openCombatAction() { - new CombatActionDialog(this.document).render(true) + static #openCombatAction(event, target) { + switch (target.dataset.mode) { + case "attack": + new CombatActionDialog(this.document).render(true) + break + case "defense": + new DefenseActionDialog(this.document).render(true) + break + } } /** diff --git a/src/templates/dialog/defense-action.hbs b/src/templates/dialog/defense-action.hbs new file mode 100644 index 00000000..90eae4e7 --- /dev/null +++ b/src/templates/dialog/defense-action.hbs @@ -0,0 +1,50 @@ + + + Waffe auswählen + + {{#each weapons}} + + {{#if this.img}}{{else}} + {{/if}} + {{this.name}} + {{#if (ne this.skillName this.name)}}({{this.skillName}}){{/if}} + {{#if this.combatStatistics}} + (AT: {{this.combatStatistics.at}} PA: {{this.combatStatistics.pa}}){{/if}} + {{/each}} + + + + + Manöver auswählen + + {{#each maneuver}} + {{this.name}} + {{/each}} + + + + + Erschwernisse und Ansagen + + Umstände + + + Ansage + + + Erschwernis + + + + + + + + + Verteidigen + + + \ No newline at end of file