Merge pull request 'feature/battle-dialog' (#62) from feature/battle-dialog into main

Reviewed-on: #62
pull/63/head
macniel 2025-10-29 15:16:46 +01:00
commit 1833027cef
9 changed files with 912 additions and 18 deletions

View File

@ -31,6 +31,7 @@ import {XmlImportDialog} from "./module/dialog/xmlImportDialog.mjs";
import {MerchantDataModel} from "./module/data/merchant.mjs";
import {MerchantSheet} from "./module/sheets/merchantSheet.mjs";
import {RestingDialog} from "./module/dialog/restingDialog.mjs";
import {BattleDialog} from "./module/dialog/battleDialog.mjs";
async function preloadHandlebarsTemplates() {
return foundry.applications.handlebars.loadTemplates([
@ -58,7 +59,8 @@ Hooks.once("init", () => {
Zonenwunde,
Trefferzone,
Wunde,
RestingDialog
RestingDialog,
BattleDialog
}
// Configure custom Document implementations.

View File

@ -0,0 +1,101 @@
export class Talent {
/**
* @typedef TalentEigenschaften
* @property Number mu
* @property Number kl
* @property Number in
* @property Number ch
* @property Number ff
* @property Number ge
* @property Number ko
* @property Number kk
*/
/**
* @typedef TalentData
* @property {String} name
* @property {Number} taw
* @property {TalentEigenschaften} eigenschaften
* @property {"mu","kl","in","ch","ff","ge","ko","kk"} eigenschaft1
* @property {"mu","kl","in","ch","ff","ge","ko","kk"} eigenschaft2
* @property {"mu","kl","in","ch","ff","ge","ko","kk"} eigenschaft3
*/
/**
*
* @param {TalentData} data
**/
constructor(data) {
this.data = data
}
/**
* @param {"publicroll", "gmroll", "privateroll"} rollMode
* @returns {Promise<{tap: any, meisterlich: boolean, patzer: boolean, evaluatedRoll: Roll<EmptyObject> & {_evaluated: true, _total: number, readonly total: number}}>}
*/
evaluate(rollMode) {
return this.#talentRoll(this.data, rollMode)
}
/**
*
* @param {TalentData} data
* @param {"publicroll", "gmroll", "privateroll"} rollMode
* @returns {Promise<{tap: any, meisterlich: boolean, patzer: boolean, evaluatedRoll: Roll<EmptyObject> & {_evaluated: true, _total: number, readonly total: number}}>}
*/
async #talentRoll(data, rollMode) {
let roll1 = new Roll("3d20");
let evaluated1 = (await roll1.evaluate())
const dsaDieRollEvaluated = this._evaluateRoll(evaluated1.terms[0].results, {
taw: data.taw,
werte: [data.eigenschaften[data.eigenschaft1], data.eigenschaften[data.eigenschaft2], data.eigenschaften[data.eigenschaft3]],
})
return {
...dsaDieRollEvaluated,
evaluatedRoll: evaluated1,
}
}
_evaluateRoll(rolledDice, {
taw,
lowerThreshold = 1,
upperThreshold = 20,
countToMeisterlich = 3,
countToPatzer = 3,
werte = []
}) {
let tap = taw;
let meisterlichCounter = 0;
let patzerCounter = 0;
let failCounter = 0;
rolledDice.forEach((rolledDie, index) => {
if (tap < 0 && rolledDie.result > werte[index]) {
tap -= rolledDie.result - werte[index];
if (tap < 0) { // konnte nicht vollständig ausgeglichen werden
failCounter++;
}
} else if (rolledDie.result > werte[index]) { // taw ist bereits aufgebraucht und wert kann nicht ausgeglichen werden
tap -= rolledDie.result - werte[index];
failCounter++;
}
if (rolledDie.result <= lowerThreshold) meisterlichCounter++;
if (rolledDie.result > upperThreshold) patzerCounter++;
})
return {
tap,
meisterlich: meisterlichCounter === countToMeisterlich,
patzer: patzerCounter === countToPatzer,
}
}
}

View File

@ -0,0 +1,247 @@
import {ActionManager} from "../sheets/actions/action-manager.mjs";
import {Talent} from "../data/talent.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api
/**
* @typedef TokenDistance
* @property {Number} x
* @property {Number} y
* @property {Number} d
* @property {Token} token
*/
export class BattleDialog extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
classes: ['dsa41', 'dialog', 'battle'],
tag: "form",
position: {
width: 640,
height: 518
},
window: {
resizable: false,
},
form: {
submitOnChange: true,
closeOnSubmit: false,
handler: BattleDialog.#onSubmitForm
},
actions: {
selectOffenseActor: BattleDialog.#setOffenseActor,
selectDefenseActor: BattleDialog.#setDefenseActor,
doBattle: BattleDialog.#doBattle,
}
}
static PARTS = {
form: {
template: 'systems/DSA_4-1/templates/dialog/battle-dialog.hbs',
}
}
/**
* @type {Actor}
* @private
*/
_offenseActor = null
_defenseActor = null
constructor() {
super()
}
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
this._offenseTalent = formData.object['offense.talent']
this._defenseTalent = formData.object['defense.talent']
}
static #setOffenseActor(event, target) {
const {id} = target.dataset
this._offenseActor = game.actors.get(id)
this.render({parts: ["form"]})
}
static #setDefenseActor(event, target) {
const {id} = target.dataset
this._defenseActor = game.actors.get(id)
this.render({parts: ["form"]})
}
static async #doBattle(event, target) {
// TODO perform Dice Rolls but in secret mode so its up to the GM if they want to display the result or not
let offenseTalent = {}
if (this._offenseActor && this._offenseActor.items.get(this._offenseTalent)) {
const skill = this._offenseActor.items.get(this._offenseTalent)
offenseTalent.name = skill.name
offenseTalent.taw = skill.system.taw
offenseTalent.probe = skill.system.probe
} else {
offenseTalent.name = this.element.querySelector('input[name="offense.talent.name"]').value
offenseTalent.taw = this.element.querySelector('input[name="offense.talent.taw"]').value
offenseTalent.probe = [
this.element.querySelector('input[name="offense.talent.probe.0.name"]').value,
this.element.querySelector('input[name="offense.talent.probe.1.name"]').value,
this.element.querySelector('input[name="offense.talent.probe.2.name"]').value
]
}
offenseTalent.eigenschaften = {}
if (this._offenseActor && this._offenseActor.system.attribute) {
offenseTalent.eigenschaften = this._offenseActor.system.attribute
} else {
offenseTalent.eigenschaften = {
mu: this.element.querySelector('input[name="offenseAttributes.mu"]').value,
kl: this.element.querySelector('input[name="offenseAttributes.in"]').value,
in: this.element.querySelector('input[name="offenseAttributes.kl"]').value,
ch: this.element.querySelector('input[name="offenseAttributes.ch"]').value,
ff: this.element.querySelector('input[name="offenseAttributes.ff"]').value,
ge: this.element.querySelector('input[name="offenseAttributes.ge"]').value,
ko: this.element.querySelector('input[name="offenseAttributes.ko"]').value,
kk: this.element.querySelector('input[name="offenseAttributes.kk"]').value,
}
}
let defenseTalent = {}
if (this._defenseActor && this._defenseActor.items.get(this._defenseTalent)) {
const skill = this._defenseActor.items.get(this._defenseTalent)
defenseTalent.name = skill.name
defenseTalent.taw = skill.system.taw
defenseTalent.probe = skill.system.probe
} else {
defenseTalent.name = this.element.querySelector('input[name="defense.talent.name"]').value
defenseTalent.taw = this.element.querySelector('input[name="defense.talent.taw"]').value
defenseTalent.probe = [
this.element.querySelector('input[name="defense.talent.probe.0.name"]').value,
this.element.querySelector('input[name="defense.talent.probe.1.name"]').value,
this.element.querySelector('input[name="defense.talent.probe.2.name"]').value
]
}
defenseTalent.eigenschaften = {}
if (this._defenseActor && this._defenseActor.system.attribute) {
defenseTalent.eigenschaften = this._defenseActor.system.attribute
} else {
defenseTalent.eigenschaften = {
mu: this.element.querySelector('input[name="defenseAttributes.mu"]').value,
kl: this.element.querySelector('input[name="defenseAttributes.in"]').value,
in: this.element.querySelector('input[name="defenseAttributes.kl"]').value,
ch: this.element.querySelector('input[name="defenseAttributes.ch"]').value,
ff: this.element.querySelector('input[name="defenseAttributes.ff"]').value,
ge: this.element.querySelector('input[name="defenseAttributes.ge"]').value,
ko: this.element.querySelector('input[name="defenseAttributes.ko"]').value,
kk: this.element.querySelector('input[name="defenseAttributes.kk"]').value,
}
}
const offense = await (new Talent(offenseTalent)).evaluate("gmroll")
const defense = await (new Talent(defenseTalent)).evaluate("gmroll")
offense.evaluatedRoll.toMessage({
speaker: ChatMessage.getSpeaker({actor: this._offenseActor}),
flavor: `Talent: ${offenseTalent.name}<br/>TaP: ${offense.tap}<br/>${offense.meisterlich ? "Meisterlich" : ""}${offense.patzer ? "Petzer" : ""}`,
})
defense.evaluatedRoll.toMessage({
speaker: ChatMessage.getSpeaker({actor: this._defenseActor}),
flavor: `Talent: ${defenseTalent.name}<br/>TaP: ${defense.tap}<br/>${defense.meisterlich ? "Meisterlich" : ""}${defense.patzer ? "Petzer" : ""}`,
})
this.close()
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
if (options.window) {
options.window.title = "Vergleichende Proben"
}
return options
}
async _prepareContext(options) {
const context = await super._prepareContext(options)
context.actors = game.actors.filter(actor => actor.type === "character" || actor.type === "creature")
context.offenseTalent = this._offenseTalent ?? ''
context.offenseTalents = {}
if (this._offenseActor) {
context.offenseActor = this._offenseActor._id
if (this._offenseActor.system.attribute) {
context.offenseAttributes = {}
Object.entries(this._offenseActor.system.attribute)?.forEach(([key, eigenschaft]) => {
context.offenseAttributes[key] = eigenschaft?.aktuell ?? 0
})
} else {
context.offenseAttributes = false
}
if (this._offenseActor.itemTypes["Skill"]?.length > 0) {
this._offenseActor.itemTypes["Skill"]?.forEach((skill) => {
if (skill.system.probe.length === 3) {
context.offenseTalents[`${skill.name}: ${skill.system.taw} (${skill.system.probe[0]}/${skill.system.probe[1]}/${skill.system.probe[2]})`] = skill.id
}
})
} else {
context.offenseTalents = false
}
}
context.defenseTalent = this._defenseTalent ?? ''
context.defenseTalents = {}
if (this._defenseActor) {
context.defenseActor = this._defenseActor._id
if (this._defenseActor.system.attribute) {
context.defenseAttributes = {}
Object.entries(this._defenseActor.system.attribute)?.forEach(([key, eigenschaft]) => {
context.defenseAttributes[key] = eigenschaft?.aktuell ?? 0
})
} else {
context.defenseAttributes = false
}
if (this._defenseActor.itemTypes["Skill"]?.length > 0) {
this._defenseActor.itemTypes["Skill"]?.forEach((skill) => {
if (skill.system.probe.length === 3) {
context.defenseTalents[`${skill.name}: ${skill.system.taw} (${skill.system.probe[0]}/${skill.system.probe[1]}/${skill.system.probe[2]})`] = skill.id
}
})
} else {
context.defenseTalents = false
}
}
return context
}
_onRender(context, options) {
}
}

View File

@ -0,0 +1,14 @@
.dsa41 {
fieldset {
border-left: 0;
border-bottom: 0;
border-right: 0;
legend {
padding: 0 16px;
text-align: center;
}
}
}

View File

@ -0,0 +1,195 @@
.dsa41.dialog.battle {
section[data-application-part="form"] {
display: grid;
height: 100%;
grid-template-columns: 1fr 1fr;
grid-template-rows: 32px 1fr 32px;
gap: 8px;
grid-template-areas: "presets presets" "offense defense" "summary summary";
.presets {
grid-area: presets;
label {
display: flex;
flex-direction: row;
height: 32px;
justify-content: center;
gap: 0 8px;
span {
height: 32px;
line-height: 32px;
vertical-align: middle;
flex: 0;
}
select {
flex: 0;
width: 180px;
}
button {
height: 32px;
flex: 0;
}
}
}
.offense-character {
grid-area: offense;
}
.defense-character {
grid-area: defense;
}
.summary {
grid-area: summary;
}
.scroll-y {
overflow: hidden;
overflow-y: auto;
height: calc(3 * (32px + 8px))
}
.actor {
height: 32px;
margin-bottom: 8px;
display: grid;
grid-template-columns: 32px 1fr;
img {
width: 32px;
height: 32px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
border: 1px inset;
}
span {
height: 32px;
line-height: 32px;
vertical-align: middle;
}
&.selected {
background-color: rgba(255, 140, 0, 0.2);
border: 1px solid orange;
border-radius: 4px;
}
}
span.dummylabel {
height: 16px;
line-height: 16px;
display: block;
}
.attributes {
display: grid;
grid-template-columns: repeat(8, 1fr);
height: 48px;
.attribut {
label {
span {
display: block;
height: 16px;
line-height: 16px;
vertical-align: middle;
width: 100%;
text-align: center;
}
input {
height: 32px;
width: 100%;
}
output {
height: 32px;
width: 100%;
line-height: 32px;
vertical-align: middle;
display: block;
padding: 0 8px;
border: 1px inset;
border-radius: 4px;
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1);
}
}
}
}
.talent {
display: grid;
grid-template-columns: 1fr 32px 32px 32px 32px;
height: 48px;
label {
span {
display: block;
height: 16px;
line-height: 16px;
vertical-align: middle;
text-align: center;
width: 100%;
}
input {
height: 32px;
width: 100%;
&.attrib {
padding: 0;
text-align: center;
}
}
}
}
.summary {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-areas: "offense buttons defense";
button {
grid-area: buttons;
}
.offenseActorSave {
grid-area: offense;
input {
position: relative;
top: 2px;
}
}
.defenseActorSave {
grid-area: defense;
justify-self: end;
input {
position: relative;
top: 2px;
}
}
}
}
}

View File

@ -23,17 +23,9 @@
flex: 1;
border-bottom: 0;
border-left: 0;
border-right: 0;
padding: 0;
margin: 0;
legend {
text-align: center;
padding: 0 16px;
}
&.modding {
flex: 0;
}

View File

@ -30,14 +30,6 @@
fieldset {
grid-area: options;
border-left: 0;
border-bottom: 0;
border-right: 0;
legend {
padding: 0 16px;
text-align: center;
}
div {

View File

@ -10,6 +10,7 @@
@use "molecules/sheet-header";
@use "molecules/coins";
@use "molecules/weights";
@use "molecules/fieldset";
@use "molecules/tabs";
@use "molecules/paperdoll";
@ -30,4 +31,5 @@
@use "organisms/xml-import-dialog";
@use "organisms/combat-action-dialog";
@use "organisms/merchant-sheet";
@use "organisms/resting-dialog";
@use "organisms/resting-dialog";
@use "organisms/battle-dialog";

View File

@ -0,0 +1,349 @@
<section>
<div class="presets">
<label><span>Voreinstellungen</span>
{{!-- TODO implement logic --}}
<select name="battle-presets">
<option value="" selected></option>
<option value="zechen-v-zechen">Vergleichend: Zechen</option>
<option value="überreden-v-menschenkenntnis">Überreden</option>
<option value="überzeugen-v-menschenkenntnis">Überzeugen</option>
<option value="schleichen-v-sinnenschärfe">Schleichen</option>
<option value="sich-verstecken-v-sinnenschärfe">Sich verstecken</option>
<option value="glückspiel-v-glückspiel">Vergleichend: Glücksspiel</option>
<option value="falschspiel-v-sinnenschärfe">Schummeln</option>
</select>
<button data-action="applyPreset"><i class="fa-solid fa-fill"></i></button>
</label>
</div>
<div class="offense-character">
<fieldset>
<legend>Charakterauswahl</legend>
<div class="scroll-y">
{{#each actors}}
<div class="actor {{#if (eq ../offenseActor this._id)}}selected{{/if}}" data-id="{{this._id}}"
data-action="selectOffenseActor">
<img src="{{this.img}}" alt="{{this.name}}">
<span>{{this.name}}</span>
</div>
{{/each}}
</div>
</fieldset>
{{#if (not offenseAttributes)}}
<fieldset>
<legend>Eigenschaften</legend>
<div class="attributes">
<div class="attribut">
<label><span>MU</span>
<input name="offenseAttributes.mu" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>KL</span>
<input name="offenseAttributes.kl" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>IN</span>
<input name="offenseAttributes.in" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>CH</span>
<input name="offenseAttributes.ch" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>FF</span>
<input name="offenseAttributes.ff" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>GE</span>
<input name="offenseAttributes.ge" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>KO</span>
<input name="offenseAttributes.ko" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>KK</span>
<input name="offenseAttributes.kk" type="number"/>
</label>
</div>
</div>
</fieldset>
{{else}}
<fieldset>
<legend>Eigenschaften</legend>
<div class="attributes">
<div class="attribut">
<label><span>MU</span>
<output name="offenseAttributes.mu">{{offenseAttributes.mu}}</output>
</label>
</div>
<div class="attribut">
<label><span>KL</span>
<output name="offenseAttributes.mu">{{offenseAttributes.kl}}</output>
</label>
</div>
<div class="attribut">
<label><span>IN</span>
<output name="offenseAttributes.mu">{{offenseAttributes.in}}</output>
</label>
</div>
<div class="attribut">
<label><span>CH</span>
<output name="offenseAttributes.mu">{{offenseAttributes.ch}}</output>
</label>
</div>
<div class="attribut">
<label><span>FF</span>
<output name="offenseAttributes.mu">{{offenseAttributes.ff}}</output>
</label>
</div>
<div class="attribut">
<label><span>GE</span>
<output name="offenseAttributes.mu">{{offenseAttributes.ge}}</output>
</label>
</div>
<div class="attribut">
<label><span>KK</span>
<output name="offenseAttributes.mu">{{offenseAttributes.kk}}</output>
</label>
</div>
<div class="attribut">
<label><span>KO</span>
<output name="offenseAttributes.mu">{{offenseAttributes.ko}}</output>
</label>
</div>
</div>
</fieldset>
{{/if}}
{{#if offenseTalents}}
<fieldset>
<legend>Talent</legend>
<span class="dummylabel">&nbsp;</span>
<select name="offense.talent">
{{selectOptions offenseTalents selected=offenseTalent inverted=true}}
</select>
</fieldset>
{{else}}
<fieldset>
<legend>Talent</legend>
<div class="talent">
<label class="name"><span>Name</span> <input type="text" name="offense.talent.name"/></label>
<label class="value"><span>TaW</span> <input type="text" name="offense.talent.taw"/></label>
<label class="p1"><span></span> <input class="attrib" type="text"
name="offense.talent.probe.0.name"/></label>
<label class="p2"><span></span> <input class="attrib" type="text"
name="offense.talent.probe.1.name"/></label>
<label class="p3"><span></span> <input class="attrib" type="text"
name="offense.talent.probe.2.name"/></label>
</div>
</fieldset>
{{/if}}
<fieldset>
<legend>Erschwernisse</legend>
<input name="offense_penalty" type="number"/>
</fieldset>
</div>
<div class="defense-character">
<fieldset>
<legend>Charakterauswahl</legend>
<div class="scroll-y">
{{#each actors}}
<div class="actor {{#if (eq ../defenseActor this._id)}}selected{{/if}}" data-id="{{this._id}}"
data-action="selectDefenseActor">
<img src="{{this.img}}" alt="{{this.name}}">
<span>{{this.name}}</span>
</div>
{{/each}}
</div>
</fieldset>
{{#if (not defenseAttributes)}}
<fieldset>
<legend>Eigenschaften</legend>
<div class="attributes">
<div class="attribut">
<label><span>MU</span>
<input name="defenseAttributes.mu" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>KL</span>
<input name="defenseAttributes.kl" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>IN</span>
<input name="defenseAttributes.in" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>CH</span>
<input name="defenseAttributes.ch" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>FF</span>
<input name="defenseAttributes.ff" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>GE</span>
<input name="defenseAttributes.ge" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>KK</span>
<input name="defenseAttributes.ko" type="number"/>
</label>
</div>
<div class="attribut">
<label><span>KO</span>
<input name="defenseAttributes.kk" type="number"/>
</label>
</div>
</div>
</fieldset>
{{else}}
<fieldset>
<legend>Eigenschaften</legend>
<div class="attributes">
<div class="attribut">
<label><span>MU</span>
<output name="defenseAttributes.mu">{{defenseAttributes.mu}}</output>
</label>
</div>
<div class="attribut">
<label><span>KL</span>
<output name="defenseAttributes.kl">{{defenseAttributes.kl}}</output>
</label>
</div>
<div class="attribut">
<label><span>IN</span>
<output name="defenseAttributes.in">{{defenseAttributes.in}}</output>
</label>
</div>
<div class="attribut">
<label><span>CH</span>
<output name="defenseAttributes.ch">{{defenseAttributes.ch}}</output>
</label>
</div>
<div class="attribut">
<label><span>FF</span>
<output name="defenseAttributes.ff">{{defenseAttributes.ff}}</output>
</label>
</div>
<div class="attribut">
<label><span>GE</span>
<output name="defenseAttributes.ge">{{defenseAttributes.ge}}</output>
</label>
</div>
<div class="attribut">
<label><span>KO</span>
<output name="defenseAttributes.ko">{{defenseAttributes.ko}}</output>
</label>
</div>
<div class="attribut">
<label><span>KK</span>
<output name="defenseAttributes.kk">{{defenseAttributes.kk}}</output>
</label>
</div>
</div>
</fieldset>
{{/if}}
{{#if defenseTalents}}
<fieldset>
<legend>Talent</legend>
<span class="dummylabel">&nbsp;</span>
<select name="defense.talent">
{{selectOptions defenseTalents selected=defenseTalent inverted=true}}
</select>
</fieldset>
{{else}}
<fieldset>
<legend>Talent</legend>
<div class="talent">
<label class="name"><span>Name</span> <input type="text" name="defense.talent.name"/></label>
<label class="value"><span>TaW</span> <input type="text" name="defense.talent.taw"/></label>
<label class="p1"><span></span> <input class="attrib" type="text"
name="defense.talent.probe.0.name"/></label>
<label class="p2"><span></span> <input class="attrib" type="text"
name="defense.talent.probe.1.name"/></label>
<label class="p3"><span></span> <input class="attrib" type="text"
name="defense.talent.probe.2.name"/></label>
</div>
</fieldset>
{{/if}}
<fieldset>
<legend>Erschwernisse</legend>
<input name="defense_penalty" type="number"/>
</fieldset>
</div>
<div class="summary">
{{#if (or (not offenseTalents) (not offenseAttributes))}}
<div class="offenseActorSave"><label><input name="saveOffenseData" type="checkbox">Daten speichern</label>
</div>
{{/if}}
<button data-action="doBattle"><i class="fa-solid fa-user-secret"></i> Würfeln</button>
{{#if (or (not defenseTalents) (not defenseAttributes))}}
<div class="defenseActorSave"><label><input name="saveDefenseData" type="checkbox">Daten speichern</label>
</div>
{{/if}}
</div>
</section>