implements defense screen

pull/61/head
macniel 2025-10-24 09:57:11 +02:00
parent f67560a2b1
commit 3f6b4a0303
8 changed files with 416 additions and 17 deletions

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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)
}))
}
}

View File

@ -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<br/>${data.modDescription}`
} else {
flavor = `Verteidigt sich gegen einen Angriff mit ${weapon.name} (${skill.name})<br/>${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)

View File

@ -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)

View File

@ -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() {
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
}
}
/**

View File

@ -0,0 +1,50 @@
<section>
<fieldset>
<legend>Waffe auswählen</legend>
<ul>
{{#each weapons}}
<li class="{{#if isSelected}}selected{{/if}}" data-action="selectWeaponAndSkill"
data-weapon-id="{{this.weaponId}}" data-skill-id="{{this.skillId}}">
{{#if this.img}}<img src="{{this.img}}" style="width: 32px; height: 32px"/>{{else}}
<span style="width: 32px"></span> {{/if}}
<span>{{this.name}}
{{#if (ne this.skillName this.name)}}({{this.skillName}}){{/if}}</span>
<span>{{#if this.combatStatistics}}
(AT: {{this.combatStatistics.at}} PA: {{this.combatStatistics.pa}}){{/if}}</span></li>
{{/each}}
</ul>
</fieldset>
<fieldset>
<legend>Manöver auswählen</legend>
<ul>
{{#each maneuver}}
<li class="{{#if isSelected}}selected{{/if}} name-only" data-action="selectManeuver"
data-maneuver-id="{{this.id}}" class="{{this.source}}">{{this.name}}</li>
{{/each}}
</ul>
</fieldset>
<fieldset class="modding">
<legend>Erschwernisse und Ansagen</legend>
<div class="malus-and-mod">
<label><span>Umstände</span>
<input name="circumstance" type="number">
</label>
<label><span>Ansage</span>
<input name="mod" type="number" {{disabled (not canMod)}}>
</label>
<label><span>Erschwernis</span>
<input name="penalty" type="number" {{disabled true}}>
</label>
</div>
<output class="modResult"></output>
</fieldset>
<div class="actions">
<button {{#if ready}}class="ready"{{/if}} type="submit"><i class="fa-solid fa-shield"></i>Verteidigen <span
class="value"></span></button>
</div>
</section>