diff --git a/src/module/data/specialAbility.mjs b/src/module/data/specialAbility.mjs index f26024cb..8341214b 100644 --- a/src/module/data/specialAbility.mjs +++ b/src/module/data/specialAbility.mjs @@ -48,7 +48,11 @@ export class SpecialAbilityDataModel extends BaseItem { }) ), waffenLimit: new ArrayField( - new StringField(), + new SchemaField({ + waffe: new StringField(), + gruppe: new StringField(), + mod: new NumberField(), + }) ), mod: new ArrayField(new SchemaField({ name: new StringField(), @@ -67,11 +71,13 @@ export class SpecialAbilityDataModel extends BaseItem { } } - isActive() { // TODO also handle Waffenlimit + isActive(options) { // TODO also handle Waffenlimit const requirements = this.#getRequirements() let passes = false + let mod = 0 + const flatActor = foundry.utils.flattenObject(this.parent.actor.system) for (let requirement of requirements) { @@ -90,11 +96,55 @@ export class SpecialAbilityDataModel extends BaseItem { passes = targetField <= requirement.maxValue } + if (requirement["compare"]) { + const {ownAttribute, operation, targetAttribute} = requirement["compare"] + if (options.target) { + const flatTarget = foundry.utils.flattenObject(options.target.system) + const foreignTargetField = flatTarget[targetAttribute] + const ourTargetField = flatActor[ownAttribute] + switch (operation) { + case "lt": + passes = ourTargetField < foreignTargetField; + break; + case "lte": + passes = ourTargetField <= foreignTargetField; + break; + case "eq": + passes = ourTargetField == foreignTargetField; + break; + case "neq": + passes = ourTargetField != foreignTargetField; + break; + case "gte": + passes = ourTargetField >= foreignTargetField; + break; + case "gt": + passes = ourTargetField > foreignTargetField; + break; + } + } + passes = false + } + if (!passes) { break } } - return passes + if (passes) { // TODO: how are we going to communicate the malus? + this.system.waffenLimit.forEach(waff => { + if (waff.waffe) { + passes = options.weapon.name === waff.waffe + if (waff.mod) mod = waff.mod + } else if (waff.gruppe) { + passes = options.skill.name === waff.gruppe + if (waff.mod) mod = waff.mod + } + + + }) + } + + return {passes, mod} } } diff --git a/src/module/dialog/combatAction.mjs b/src/module/dialog/combatAction.mjs new file mode 100644 index 00000000..81730f5c --- /dev/null +++ b/src/module/dialog/combatAction.mjs @@ -0,0 +1,255 @@ +import {XmlImport} from "../xml-import/xml-import.mjs"; +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 CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2) { + + static DEFAULT_OPTIONS = { + classes: ['dsa41', 'dialog', 'combat'], + tag: "form", + position: { + width: 320, + height: 540 + }, + window: { + resizable: false, + }, + form: { + submitOnChange: false, + closeOnSubmit: true, + handler: CombatActionDialog.#onSubmitForm + }, + actions: { + selectTarget: CombatActionDialog.#onSelectTarget, + selectWeaponAndSkill: CombatActionDialog.#onSelectWeaponAndSkill, + selectManeuver: CombatActionDialog.#onSelectManeuver, + } + } + + static PARTS = { + form: { + template: 'systems/DSA_4-1/templates/dialog/combat-action.hbs', + } + } + + /** + * @type {Actor} + * @private + */ + _actor = null + + constructor(actor) { + super(); + this._actor = actor + this._targetId = null + this._skillId = null + this._weaponId = null + this._combatManeuverId = null + this._actionManager = new ActionManager(this._actor) + } + + + 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._combatManeuverId = this._combatManeuverId === 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() + } + + _configureRenderOptions(options) { + super._configureRenderOptions(options) + if (options.window) { + options.window.title = `Mit ${this._actor.name} angreifen` + } + return options + } + + #getDistanceBetween(originToken, targetToken) { + const distance = game.scenes.current.dimensions.distancePixels + + // get distances between of tokens and thisTokenRepresentative + const actorOfToken = game.actors.get(targetToken.actorId) + return { + id: targetToken._id, + x: targetToken.x, + y: targetToken.y, + actor: actorOfToken, + token: targetToken, + d: ((Math.sqrt(Math.pow(targetToken.x - originToken.x, 2) + Math.pow(targetToken.y - originToken.y, 2))) / distance).toFixed(2), + } + } + + #evaluateDistances() { + + // get tokens in range on the current scene + const scene = game.scenes.current + const tokens = scene.tokens + const tokenId = this._actor.getActiveTokens()[0].id + const thisTokenRepresentative = tokens.get(tokenId) + + + /** + * + * @type {[TokenDistance]} + */ + return tokens.map(token => { + return { + isSelected: this._targetId === token.id, + ...this.#getDistanceBetween(thisTokenRepresentative, token) + } + }) + } + + #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 + } + }) + } + }) + } + }) + + this._weapons = weapons + return this._weapons + + } + + #evaluateManeuvers() { + const manager = this._actionManager + 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(this._targetId) + this._maneuvers = manager.evaluate({ + target, + weapon, + skill + }).filter(p => p.type === ActionManager.ATTACK).map(action => { + return { + isSelected: this._combatManeuverId === action.name, + id: action.name, + name: action.name, + type: action.type, + source: action.source, + cost: action.cost, + mod: action.mod, + } + }) + 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.tokenDistances = this.#evaluateDistances() + context.weapons = this.#evaluateWeapons() + + if (this._targetId && this._weaponId && this._skillId) { + context.maneuver = this.#evaluateManeuvers() + } + const maneuver = this._maneuvers?.find(p => p.id === this._combatManeuverId) + if (maneuver) { + context.canMod = maneuver.mod != undefined + } + + context.targetNumber = context.weapons.find(p => p.weaponId === this._weaponId && p.skillId === this._skillId)?.combatStatistics.at + + // TODO get W/M of weapon NOW + + context.ready = this._targetId && this._weaponId && this._skillId && this._combatManeuverId + return context + } else { + ui.notifications.error(`Feature funktioniert nur wenn der Akteur ein Token auf der aktuellen Szene hat`); + } + + } + + _onRender(context, options) { + const target = this.element.querySelector(".actions button .value") + this.element.querySelectorAll('[name="mod"], [name="malus"]').forEach(e => e.addEventListener('change', (event) => { + + const at = Number(context.targetNumber) + const malus = Number(this.element.querySelector('[name="malus"]').value) + const mod = Number(this.element.querySelector('[name="mod"]').value) + const maneuver = this._maneuvers?.find(p => p.id === this._combatManeuverId) + const result = at + (maneuver.mod?.(mod) ?? 0) + malus + target.textContent = `(${result})` + })) + + } + + +} \ No newline at end of file diff --git a/src/module/documents/specialAbility.mjs b/src/module/documents/specialAbility.mjs index e60847e5..454b3329 100644 --- a/src/module/documents/specialAbility.mjs +++ b/src/module/documents/specialAbility.mjs @@ -16,8 +16,4 @@ export class SpecialAbility extends Item { } } - isActive() { - return true - } - } diff --git a/src/module/sheets/actions/action-manager.mjs b/src/module/sheets/actions/action-manager.mjs index b4ffc765..f2e01d6a 100644 --- a/src/module/sheets/actions/action-manager.mjs +++ b/src/module/sheets/actions/action-manager.mjs @@ -23,97 +23,121 @@ export class ActionManager { cost: ActionManager.FREE, type: ActionManager.DEFENSE, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Rufen", cost: ActionManager.FREE, type: ActionManager.INTERACTION, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Sich zu Boden fallen lassen", cost: ActionManager.FREE, type: ActionManager.MOVEMENT, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Waffe oder Gegenstand fallen lassen", cost: ActionManager.FREE, type: ActionManager.INTERACTION, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "getragenes Artefakt aktivieren", cost: ActionManager.FREE, type: ActionManager.INTERACTION, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Schnellziehen", cost: ActionManager.FREE, type: ActionManager.INTERACTION, source: ActionManager.SF, - eval: () => this.#hatSonderfertigkeit("Schnellziehen") && this.#evalSonderfertigkeitRequirements("Schnellziehen") + eval: (options) => this.#hatSonderfertigkeit("Schnellziehen", options) && this.#evalSonderfertigkeitRequirements("Schnellziehen", options) } ] #regularActions = [ { - name: "Angriffsaktion", + name: "Nahkampfangriff", type: ActionManager.ATTACK, cost: ActionManager.REGULAR, source: ActionManager.DEFAULT, - eval: () => this.#hatWaffeinHand() + eval: (options) => this.#hatWaffeinHand() && !this.#hatFernkampfWaffeinHand() + }, + { + name: "Fernkampfangriff", + type: ActionManager.ATTACK, + cost: ActionManager.REGULAR, + source: ActionManager.DEFAULT, + eval: (options) => this.#hatFernkampfWaffeinHand(), + }, + { + name: "Angesagter Fernkampfangriff", + type: ActionManager.ATTACK, + cost: ActionManager.CONTINUING, + source: ActionManager.DEFAULT, + eval: (options) => this.#hatFernkampfWaffeinHand(), + mod: (value) => -value * 2 + }, + { + name: "Scharfer Schuss", + type: ActionManager.ATTACK, + cost: ActionManager.CONTINUING, + source: ActionManager.SF, + eval: (options) => this.#hatFernkampfWaffeinHand() && this.#hatSonderfertigkeit("Scharfschütze", options) && this.#evalSonderfertigkeitRequirements("Scharfschütze", options), + mod: (value) => -value }, { name: "Schnellschuss", type: ActionManager.INTERACTION, cost: ActionManager.CONTINUING, source: ActionManager.DEFAULT, - eval: () => this.#hatFernkampfWaffeinHand() + eval: (options) => this.#hatFernkampfWaffeinHand() }, { name: "Schnellschuss (Scharfschütze)", type: ActionManager.INTERACTION, cost: ActionManager.CONTINUING, source: ActionManager.SF, - eval: () => this.#hatFernkampfWaffeinHand() && this.#hatSonderfertigkeit("Scharfschütze") && this.#evalSonderfertigkeitRequirements("Scharfschütze") + eval: (options) => this.#hatFernkampfWaffeinHand() && this.#hatSonderfertigkeit("Scharfschütze", options) && this.#evalSonderfertigkeitRequirements("Scharfschütze", options) }, { name: "Abwehraktion", type: ActionManager.DEFENSE, cost: ActionManager.REGULAR, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Bewegen", type: ActionManager.MOVEMENT, cost: ActionManager.REGULAR, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Position", type: ActionManager.MOVEMENT, cost: ActionManager.REGULAR, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Finte", type: ActionManager.ATTACK, cost: ActionManager.REGULAR, source: ActionManager.SF, - eval: () => + mod: (value) => value, + eval: (options) => this.#hatWaffeinHand() && - this.#hatSonderfertigkeit("Finte") && - this.#evalSonderfertigkeitRequirements("Finte") + this.#hatSonderfertigkeit("Finte", options) && + this.#evalSonderfertigkeitRequirements("Finte", options) }, { @@ -121,23 +145,25 @@ export class ActionManager { type: ActionManager.ATTACK, cost: ActionManager.REGULAR, source: ActionManager.DEFAULT, - eval: () => true + mod: (value) => -(value), + eval: (options) => !this.#hatFernkampfWaffeinHand() }, { name: "Wuchtschlag", type: ActionManager.ATTACK, cost: ActionManager.REGULAR, source: ActionManager.SF, - eval: () => this.#hatSonderfertigkeit("Wuchtschlag") - && this.#evalSonderfertigkeitRequirements("Wuchtschlag") + mod: (value) => -(value), + eval: (options) => !this.#hatFernkampfWaffeinHand() && this.#hatSonderfertigkeit("Wuchtschlag", options) + && this.#evalSonderfertigkeitRequirements("Wuchtschlag", options) }, { name: "Betäubungsschlag", type: ActionManager.ATTACK, cost: ActionManager.REGULAR, source: ActionManager.SF, - eval: () => this.#hatSonderfertigkeit("Betäubungsschlag") - && this.#evalSonderfertigkeitRequirements("Betäubungsschlag") + eval: (options) => !this.#hatFernkampfWaffeinHand() && this.#hatSonderfertigkeit("Betäubungsschlag", options) + && this.#evalSonderfertigkeitRequirements("Betäubungsschlag", options) } ] @@ -147,76 +173,76 @@ export class ActionManager { type: ActionManager.TALENT, cost: ActionManager.CONTINUING, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Waffe ziehen", type: ActionManager.INTERACTION, cost: ActionManager.CONTINUING, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Sprinten", type: ActionManager.MOVEMENT, cost: ActionManager.CONTINUING, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Gegenstand benutzen", type: ActionManager.INTERACTION, cost: ActionManager.CONTINUING, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Schnellladen (Bogen)", type: ActionManager.INTERACTION, cost: ActionManager.CONTINUING, source: ActionManager.SF, - eval: () => this.#hatMunition() - && this.#hatFernkampfWaffeinHand("Bogen") - && this.#hatSonderfertigkeit("Schnellladen (Bogen)") - && this.#evalSonderfertigkeitRequirements("Schnellladen (Bogen)") + eval: (options) => this.#hatMunition() + && this.#hatFernkampfWaffeinHand("Bogen", options) + && this.#hatSonderfertigkeit("Schnellladen (Bogen)", options) + && this.#evalSonderfertigkeitRequirements("Schnellladen (Bogen)", options) }, { name: "Schnellladen (Armbrust)", type: ActionManager.INTERACTION, cost: ActionManager.CONTINUING, source: ActionManager.SF, - eval: () => this.#hatMunition() - && this.#hatFernkampfWaffeinHand("Armbrust") - && this.#hatSonderfertigkeit("Schnellladen (Armbrust)") - && this.#evalSonderfertigkeitRequirements("Schnellladen (Armbrust)") + eval: (options) => this.#hatMunition() + && this.#hatFernkampfWaffeinHand("Armbrust", options) + && this.#hatSonderfertigkeit("Schnellladen (Armbrust)", options) + && this.#evalSonderfertigkeitRequirements("Schnellladen (Armbrust)", options) }, { name: "Nachladen", type: ActionManager.INTERACTION, cost: ActionManager.CONTINUING, source: ActionManager.DEFAULT, - eval: () => this.#hatMunition() + eval: (options) => this.#hatMunition() }, { name: "Talenteinsatz", type: ActionManager.TALENT, cost: ActionManager.CONTINUING, source: ActionManager.DEFAULT, - eval: () => true + eval: (options) => true }, { name: "Zaubern", type: ActionManager.SPELL, cost: ActionManager.CONTINUING, source: ActionManager.SF, - eval: () => this.#hatSonderfertigkeitBeginnendMit("Repräsentation:") + eval: (options) => this.#hatSonderfertigkeitBeginnendMit("Repräsentation:", options) }, { name: "Liturgie wirken", type: ActionManager.SPELL, cost: ActionManager.CONTINUING, source: ActionManager.SF, - eval: () => this.#hatSonderfertigkeitBeginnendMit("Liturgiekenntnis") + eval: (options) => this.#hatSonderfertigkeitBeginnendMit("Liturgiekenntnis", options) } ] @@ -236,25 +262,30 @@ export class ActionManager { return item != null } - #hatSonderfertigkeitBeginnendMit(name) { + #hatSonderfertigkeitBeginnendMit(name, options) { return this.actor.itemTypes["SpecialAbility"]?.find(p => p.name.startsWith(name)) != null } - #hatSonderfertigkeit(name) { + #hatSonderfertigkeit(name, options) { return this.actor.itemTypes["SpecialAbility"]?.find(p => p.name === name) != null } - #evalSonderfertigkeitRequirements(nameOfSF) { + #evalSonderfertigkeitRequirements(nameOfSF, options) { const sf = this.actor.itemTypes["SpecialAbility"].find(p => p.name === nameOfSF) - return sf.system.isActive() + return sf.system.isActive(options).passes } - evaluate() { + /** + * + * @param {{target: String?}} options + */ + + evaluate(options) { let actionArray = [...this.#freeActions, ...this.#regularActions, ...this.#continuingActions] console.log(this.actor, actionArray.map((action) => { return { ...action, - eval: action.eval() + eval: action.eval(options) } })) const validActions = actionArray.filter(action => action.eval()) diff --git a/src/module/sheets/characterSheet.mjs b/src/module/sheets/characterSheet.mjs index 8f7483ba..8e5621ec 100644 --- a/src/module/sheets/characterSheet.mjs +++ b/src/module/sheets/characterSheet.mjs @@ -7,6 +7,7 @@ import Meta from "./character/meta.mjs" import Skills from "./character/skills.mjs" import Social from "./character/social.mjs"; import Spells from "./character/spells.mjs" +import {CombatActionDialog} from "../dialog/combatAction.mjs"; const {HandlebarsApplicationMixin} = foundry.applications.api const {ActorSheetV2} = foundry.applications.sheets @@ -36,6 +37,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) { openEmbeddedDocument: CharacterSheet.#openEmbeddedDocument, openCultureDocument: CharacterSheet.#openCultureDocument, openSpeciesDocument: CharacterSheet.#openSpeciesDocument, + openCombatAction: CharacterSheet.#openCombatAction, } } @@ -143,6 +145,10 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) { this.document.itemTypes["Species"]?.[0]?.sheet.render(true) } + static #openCombatAction() { + new CombatActionDialog(this.document).render(true) + } + /** * Handle form submission * @this {AdvantageSheet} diff --git a/src/style/organisms/_combat-action-dialog.scss b/src/style/organisms/_combat-action-dialog.scss new file mode 100644 index 00000000..0fd892df --- /dev/null +++ b/src/style/organisms/_combat-action-dialog.scss @@ -0,0 +1,91 @@ +@keyframes pulse { + 0% { + text-shadow: 0 0 0 red; + } + 75% { + text-shadow: 0 0 4px red; + } + 100% { + text-shadow: 0 0 0 red; + } +} + +.dsa41.dialog.combat { + + .window-content > section { + + display: flex; + flex-direction: column; + height: 100%; + + fieldset { + + flex: 1; + + border-bottom: 0; + border-left: 0; + border-right: 0; + padding: 0; + margin: 0; + + legend { + text-align: center; + padding: 0 16px; + } + + } + + ul { + + list-style: none; + padding: 0; + margin: 0; + text-indent: 0; + + li { + + height: 32px; + display: grid; + line-height: 32px; + vertical-align: middle; + + grid-template-columns: 32px 1fr 83px; + grid-template-rows: 1fr; + border: 1px transparent; + margin: 8px; + gap: 0 8px; + + &.selected { + background-color: rgba(255, 140, 0, 0.2); + border: 1px solid orange; + border-radius: 16px; + } + + &.name-only { + display: block; + } + } + } + + .malus-and-mod { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + gap: 0 8px; + } + + .actions { + flex: 0; + display: flex; + justify-content: center; + + button.ready { + animation: pulse 2s infinite; + } + + } + + } + + +} \ No newline at end of file diff --git a/src/style/styles.scss b/src/style/styles.scss index f88ab0fc..80d733a7 100644 --- a/src/style/styles.scss +++ b/src/style/styles.scss @@ -24,4 +24,5 @@ @use "organisms/species-sheet"; @use "organisms/profession-sheet"; @use "organisms/xml-import-dialog"; +@use "organisms/combat-action-dialog"; diff --git a/src/templates/dialog/combat-action.hbs b/src/templates/dialog/combat-action.hbs new file mode 100644 index 00000000..20cda01b --- /dev/null +++ b/src/templates/dialog/combat-action.hbs @@ -0,0 +1,58 @@ +
+ +
+ Ziel auswählen + + + +
+ +
+ Waffe auswählen + +
+ +
+ Manöver auswählen + +
+ +
+ Erschwernisse und Ansagen +
+ + +
+
+ + +
+ +
+ +
\ No newline at end of file diff --git a/src/templates/ui/partial-action-button.hbs b/src/templates/ui/partial-action-button.hbs index af20fe5c..df191a38 100644 --- a/src/templates/ui/partial-action-button.hbs +++ b/src/templates/ui/partial-action-button.hbs @@ -1,4 +1,4 @@ -
+
{{this.name}} {{this.cost}}