From 53806e620f4d9f25b9033017eb68b2c046c314e5 Mon Sep 17 00:00:00 2001 From: macniel Date: Sun, 25 Jan 2026 18:51:40 +0100 Subject: [PATCH] adds chat messages + die roll as visual result of a skill/attribute check --- src/module/data/skill.mjs | 3 + src/module/data/talent.mjs | 52 +++------------- src/module/dialog/attributeDialog.mjs | 28 ++++++++- src/module/dialog/talentDialog.mjs | 39 ++++++++++-- src/module/documents/character.mjs | 2 + src/module/globals/DSARoll.mjs | 12 +++- src/module/globals/DSARoll.test.mjs | 24 ++++++++ src/module/globals/displayRoll.js | 40 ++++++++++++ src/style/atoms/_chat.scss | 61 +++++++++++++++++++ src/style/atoms/_typography.scss | 1 - src/style/styles.scss | 3 +- src/system.json | 4 +- src/templates/chat/attribute-chat-message.hbs | 10 +++ src/templates/chat/skill-chat-message.hbs | 20 ++++++ 14 files changed, 243 insertions(+), 56 deletions(-) create mode 100644 src/module/globals/displayRoll.js create mode 100644 src/style/atoms/_chat.scss create mode 100644 src/templates/chat/attribute-chat-message.hbs create mode 100644 src/templates/chat/skill-chat-message.hbs diff --git a/src/module/data/skill.mjs b/src/module/data/skill.mjs index 3783ede2..63c4c2a5 100644 --- a/src/module/data/skill.mjs +++ b/src/module/data/skill.mjs @@ -91,6 +91,7 @@ export class SkillDataModel extends BaseItem { targetNumber = this.pa + owner.system.pa.basis } + // TODO: Migrate to DSARoll let roll1 = new Roll(`1d20cs<${targetNumber}`, owner.getRollData()); let evaluated1 = (await roll1.evaluate()) @@ -120,6 +121,7 @@ export class SkillDataModel extends BaseItem { speaker: ChatMessage.getSpeaker({actor: owner}), flavor: message, rollMode, + }, { }) } @@ -127,6 +129,7 @@ export class SkillDataModel extends BaseItem { async #talentRoll(rollMode) { const owner = this.parent.parent + // TODO: Migrate to DSARoll let roll1 = new Roll("3d20", owner.getRollData()); let evaluated1 = (await roll1.evaluate()) diff --git a/src/module/data/talent.mjs b/src/module/data/talent.mjs index 3f477213..3f58144e 100644 --- a/src/module/data/talent.mjs +++ b/src/module/data/talent.mjs @@ -1,3 +1,5 @@ +import {evaluateRoll} from "../globals/DSARoll.mjs"; + export class Talent { @@ -18,6 +20,7 @@ export class Talent { * @property {String} name * @property {Number} taw * @property {Number} mod + * @property {Actor} owner * @property {TalentEigenschaften} eigenschaften * @property {"mu","kl","in","ch","ff","ge","ko","kk"} eigenschaft1 * @property {"mu","kl","in","ch","ff","ge","ko","kk"} eigenschaft2 @@ -50,54 +53,15 @@ export class Talent { 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, + // TODO: Migrate to DSARoll + const dsaDieRollEvaluated = await evaluateRoll("3d20", { + value: data.taw, mod: data.mod, + owner: data.owner, werte: [data.eigenschaften[data.eigenschaft1], data.eigenschaften[data.eigenschaft2], data.eigenschaften[data.eigenschaft3]], }) - return { - ...dsaDieRollEvaluated, - evaluatedRoll: evaluated1, - } - } - - _evaluateRoll(rolledDice, { - taw, - mod, - lowerThreshold = 1, - upperThreshold = 20, - countToMeisterlich = 3, - countToPatzer = 3, - werte = [] - }) { - let meisterlichCounter = 0; - let patzerCounter = 0; - let failCounter = 0; - - rolledDice.forEach((rolledDie, index) => { - if (mod < 0 && rolledDie.result > werte[index]) { - mod -= rolledDie.result - werte[index]; - if (mod < 0) { // konnte nicht vollständig ausgeglichen werden - failCounter++; - } - } else if (rolledDie.result > werte[index]) { // taw ist bereits aufgebraucht und wert kann nicht ausgeglichen werden - mod -= rolledDie.result - werte[index]; - failCounter++; - } - if (rolledDie.result <= lowerThreshold) meisterlichCounter++; - if (rolledDie.result > upperThreshold) patzerCounter++; - }) - - return { - tap: Math.min(taw, mod), - meisterlich: meisterlichCounter === countToMeisterlich, - patzer: patzerCounter === countToPatzer, - } + return dsaDieRollEvaluated } } \ No newline at end of file diff --git a/src/module/dialog/attributeDialog.mjs b/src/module/dialog/attributeDialog.mjs index 2d620e78..cc0b0b62 100644 --- a/src/module/dialog/attributeDialog.mjs +++ b/src/module/dialog/attributeDialog.mjs @@ -1,4 +1,5 @@ import {ATTRIBUTE, ATTRIBUTE_DESCRIPTIONS} from "../data/attribute.mjs"; +import {displayRoll} from "../globals/displayRoll.js"; const { ApplicationV2, @@ -96,14 +97,35 @@ export class AttributeDialog extends HandlebarsApplicationMixin(ApplicationV2) { const targetValue = this._value + modValue + // TODO: Migrate to DSARoll let roll = await new Roll(`1d20`).evaluate() let diff = targetValue - roll.terms[0].results[0].result - roll.toMessage({ - speaker: ChatMessage.getSpeaker({actor: this.actor}), - flavor: `${this._flaw ? "Schlechte " : ""}Eigenschaft: ${this._name}
Ergebnis: ${Math.abs(diff)}${diff > 0 ? " übrig" : " daneben"}`, + let creationData = roll.toMessage({ + //speaker: ChatMessage.getSpeaker({actor: this.actor}), + //flavor: `${this._flaw ? "Schlechte " : ""}Eigenschaft: ${this._name}
Ergebnis: ${Math.abs(diff)}${diff > 0 ? " übrig" : " daneben"}`, rollMode: game.settings.get('core', 'rollMode'), + }, { + create: false }); + const context = { + attribute: this._name, + dieResult: roll.terms[0].results[0].result, + target: targetValue, + remaining: diff, + + } + + if(diff>0) { + context.remaining = Math.abs(diff) + } else if (diff===0) { + context.remaining = 1 + } else { + context.missing = Math.abs(diff) + } + + await displayRoll(roll, game.user, this.actor, false, true, 'systems/DSA_4-1/templates/chat/attribute-chat-message.hbs', context) + this.close() } diff --git a/src/module/dialog/talentDialog.mjs b/src/module/dialog/talentDialog.mjs index 49723eed..1eaec417 100644 --- a/src/module/dialog/talentDialog.mjs +++ b/src/module/dialog/talentDialog.mjs @@ -1,5 +1,6 @@ import {Talent} from "../data/talent.mjs"; import {ATTRIBUTE} from "../data/attribute.mjs"; +import {displayRoll} from "../globals/displayRoll.js"; const { ApplicationV2, @@ -85,6 +86,7 @@ export class TalentDialog extends HandlebarsApplicationMixin(ApplicationV2) { name: this._talent.name, taw: taw, mod: modValue, + owner: this._actor, eigenschaft1: this._talent.system.probe[0].toLowerCase(), eigenschaft2: this._talent.system.probe[1].toLowerCase(), eigenschaft3: this._talent.system.probe[2].toLowerCase(), @@ -97,10 +99,39 @@ export class TalentDialog extends HandlebarsApplicationMixin(ApplicationV2) { const result = await new Talent(payload).evaluate("publicroll") - result.evaluatedRoll.toMessage({ - speaker: ChatMessage.getSpeaker({actor: this._actor}), - flavor: `Talent: ${this._talent.name}
TaP*: ${result.tap}
${result.meisterlich ? "Meisterlich" : ""}${result.patzer ? "Petzer" : ""}
${this._talent.system.talent}`, - }) + const context = { + talent: this._talent.name, + taw, + ergebnis: [ + { + eigenschaft: this._talent.system.probe[0], + eigenschaftWert: this._actor.system.attribute[this._talent.system.probe[0].toLowerCase()].aktuell, + wuerfelErgebnis: result.evaluated.terms[0].results[0].result + }, + { + eigenschaft: this._talent.system.probe[1], + eigenschaftWert: this._actor.system.attribute[this._talent.system.probe[1].toLowerCase()].aktuell, + wuerfelErgebnis: result.evaluated.terms[0].results[1].result + }, + { + eigenschaft: this._talent.system.probe[2], + eigenschaftWert: this._actor.system.attribute[this._talent.system.probe[2].toLowerCase()].aktuell, + wuerfelErgebnis: result.evaluated.terms[0].results[2].result + } + ], + patzer: result.patzer, + meisterlich: result.meisterlich, + } + + if(result.tap>0) { + context.remaining = Math.abs(result.tap) + } else if (result.tap===0) { + context.remaining = 1 + } else { + context.missing = Math.abs(result.tap) + } + + await displayRoll(result.evaluated, game.user, this.actor, false, true, 'systems/DSA_4-1/templates/chat/skill-chat-message.hbs', context) this.close() } diff --git a/src/module/documents/character.mjs b/src/module/documents/character.mjs index bf116fee..58628172 100644 --- a/src/module/documents/character.mjs +++ b/src/module/documents/character.mjs @@ -477,6 +477,7 @@ export class Character extends Actor { 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) + // TODO: Migrate to DSARoll const roll = new Roll("1d20cs<" + data.targetNumber) const evaluated1 = (await roll.evaluate()) @@ -501,6 +502,7 @@ export class Character extends Actor { const skill = this.itemTypes["Skill"].find(p => p._id === data.skill) const target = game.actors.get(game.scenes.current.tokens.find(p => p._id === data.target).actorId) + // TODO: Migrate to DSARoll const roll = new Roll("1d20cs<" + data.targetNumber) const evaluated1 = (await roll.evaluate()) diff --git a/src/module/globals/DSARoll.mjs b/src/module/globals/DSARoll.mjs index 7f3b135c..713e39a3 100644 --- a/src/module/globals/DSARoll.mjs +++ b/src/module/globals/DSARoll.mjs @@ -2,6 +2,7 @@ * * @param {[{result: Number}]|String} rolledDice either the result of a roll or a roll-formula * @param {Number} value the value of this dice roll + * @param {Number} mod modifier to the value * @param {[number]} werte an array of values that the dice roll is compared against * @param {{getRollData:() => {} }} owner the actor of this roll that is required when rolledDice is a roll-formula * @param {Number} lowerThreshold this is the threshold against a critical success is counted against @@ -14,6 +15,7 @@ const evaluateRoll = async (rolledDice, { value, + mod = 0, werte = [], owner, lowerThreshold = 1, @@ -21,7 +23,7 @@ const evaluateRoll = async (rolledDice, { countToMeisterlich = 3, countToPatzer = 3, }, rollObject = Roll) => { - let tap = value; + let tap = value - mod; let meisterlichCounter = 0; let patzerCounter = 0; let failCounter = 0; @@ -71,6 +73,14 @@ const evaluateRoll = async (rolledDice, { tap = value } + if (tap > value) { // can't have more tap than value as result + tap = value + } + + if (tap === 0) { + tap = 1 + } + return { tap, meisterlich: meisterlichCounter === countToMeisterlich, diff --git a/src/module/globals/DSARoll.test.mjs b/src/module/globals/DSARoll.test.mjs index 6506df1f..7297ab77 100644 --- a/src/module/globals/DSARoll.test.mjs +++ b/src/module/globals/DSARoll.test.mjs @@ -194,4 +194,28 @@ describe('Skill Checks', () => { expect(actual.tap).toBe(2) }) + it('should yield a failure when the die roll is above the reduced attributes', async () => { + + let actual = await evaluateRoll("3d20",{ + value: 0, + mod: -3, + werte: [12, 12, 12], + owner + }, RollWithMockResults(13, 13, 13)) + + expect(actual.tap).toBe(-3) + }) + + it('should yield a success when the die roll is below the reduced attributes', async () => { + + let actual = await evaluateRoll("3d20",{ + value: 0, + mod: -1, + werte: [12, 12, 12], + owner + }, RollWithMockResults(11, 11, 11)) + + expect(actual.tap).toBe(1) + }) + }) diff --git a/src/module/globals/displayRoll.js b/src/module/globals/displayRoll.js new file mode 100644 index 00000000..a1117f0d --- /dev/null +++ b/src/module/globals/displayRoll.js @@ -0,0 +1,40 @@ +/** + * + * @param {Roll} roll instance of a foundry Die Roll + * @param {User} user the user rolled the Die + * @param {Actor} actor + * @param {Boolean} blind determines if the faces of the die should be obfuscated to user + * @param {Boolean} secret determines if others can see the die result + * @param {String} template path to the template that should be displayed in chat + * @param {[String: *]} templateContext values to be used for rendering the template + * @returns {Promise} + */ +const displayRoll = async(roll, user=game.user, actor, blind=true, secret=true, template, templateContext) => { + + if (game.dice3d) { + game.dice3d.showForRoll(roll, user, true, null, blind, null, + ChatMessage.getSpeaker({actor}), { + ghost: false, + secret + }) + } + + if (template) { + try { + await loadTemplates([template]) + const htmlContent = await renderTemplate(template, templateContext) + ChatMessage.create({ + user: user._id, + speaker: {actor}, + content: htmlContent, + }) + } catch (err) { + console.log(err) + } + + } +} + +export { + displayRoll +} \ No newline at end of file diff --git a/src/style/atoms/_chat.scss b/src/style/atoms/_chat.scss new file mode 100644 index 00000000..2c1fb644 --- /dev/null +++ b/src/style/atoms/_chat.scss @@ -0,0 +1,61 @@ +.system-DSA_4-1 { + + .message-content { + + section.die { + + display: flex; + height: 48px; + + div { + + flex-grow: 1; + + position: relative; + + span.provided { + position: absolute; + left: 0; + right: 0; + bottom: 0; + text-align: center; + line-height: 16px; + height: 16px; + vertical-align: middle; + } + + span.value { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 16px; + vertical-align: middle; + text-align: center; + line-height: 32px; + font-weight: bold; + font-size: 16px; + } + + } + + } + + hr { + margin: 2px; + background-image: none; + border-top: 1px solid var(--color-dark-2); + } + } + + .theme-dark { + + .message-content { + + hr { + border-top: 1px solid var(--color-light-2); + } + } + } + +} \ No newline at end of file diff --git a/src/style/atoms/_typography.scss b/src/style/atoms/_typography.scss index cdbf65cc..dfe9345e 100644 --- a/src/style/atoms/_typography.scss +++ b/src/style/atoms/_typography.scss @@ -41,4 +41,3 @@ } } - diff --git a/src/style/styles.scss b/src/style/styles.scss index 070b26d5..4e7a99fa 100644 --- a/src/style/styles.scss +++ b/src/style/styles.scss @@ -36,4 +36,5 @@ @use "organisms/liturgy-sheet"; @use "organisms/dialog"; @use "organisms/deity-sheet"; -@use "organisms/item-browser-dialog"; \ No newline at end of file +@use "organisms/item-browser-dialog"; +@use "atoms/chat"; \ No newline at end of file diff --git a/src/system.json b/src/system.json index 329c91f8..ce48c34a 100644 --- a/src/system.json +++ b/src/system.json @@ -2,7 +2,7 @@ "id": "DSA_4-1", "title": "Das Schwarze Auge 4.1", "description": "Noch ein Spielsystem für Das Schwarze Auge 4.1", - "version": "0.7.0-rc", + "version": "0.0.1", "compatibility": { "minimum": 12, "verified": 13 @@ -364,5 +364,5 @@ "primaryTokenAttribute": "lep.aktuell", "url": "https://git.macniel.online/macniel/foundry-dsa41-game", "manifest": "https://git.macniel.online/macniel/foundry-dsa41-game/raw/branch/main/src/system.json", - "download": "https://git.macniel.online/macniel/foundry-dsa41-game/releases/download/0.7.0-rc/release.zip" + "download": "https://git.macniel.online/macniel/foundry-dsa41-game/releases/download/0.0.1/release.zip" } diff --git a/src/templates/chat/attribute-chat-message.hbs b/src/templates/chat/attribute-chat-message.hbs new file mode 100644 index 00000000..73cd8f9e --- /dev/null +++ b/src/templates/chat/attribute-chat-message.hbs @@ -0,0 +1,10 @@ +
+
{{attribute}}({{target}})
+
Gewürfelt: {{dieResult}}
+
+ {{#if missing}} +
Gefehlt: {{missing}}
+ {{else}} +
Übrig: {{remaining}}
+ {{/if}} +
\ No newline at end of file diff --git a/src/templates/chat/skill-chat-message.hbs b/src/templates/chat/skill-chat-message.hbs new file mode 100644 index 00000000..c232d1e3 --- /dev/null +++ b/src/templates/chat/skill-chat-message.hbs @@ -0,0 +1,20 @@ + + +
+
{{talent}} (TaW: {{taw}})
+
Gewürfelt:
+
+ {{#each ergebnis}} +
+ {{eigenschaft}} ({{eigenschaftWert}}) + {{wuerfelErgebnis}} +
+ {{/each}} +
+
+ {{#if missing}} +
Gefehlt: {{missing}}
+ {{else}} +
Übrig: {{remaining}}
+ {{/if}} +
\ No newline at end of file