Compare commits

...

2 Commits

17 changed files with 4076 additions and 265 deletions

View File

@ -10,5 +10,6 @@
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="@types/jest" level="application" />
</component> </component>
</module> </module>

3819
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
}, },
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "true", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"build": "gulp", "build": "gulp",
"localBuild": "VERSION=0.0.1 gulp", "localBuild": "VERSION=0.0.1 gulp",
"installToFoundry": "node installToFoundry.mjs" "installToFoundry": "node installToFoundry.mjs"
@ -21,6 +21,7 @@
"gulp": "^5.0.1", "gulp": "^5.0.1",
"gulp-replace": "^1.1.4", "gulp-replace": "^1.1.4",
"gulp-sass": "^6.0.1", "gulp-sass": "^6.0.1",
"jest": "^30.2.0",
"merge-stream": "^2.0.0", "merge-stream": "^2.0.0",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"sass": "^1.93.2", "sass": "^1.93.2",

View File

@ -91,6 +91,7 @@ export class SkillDataModel extends BaseItem {
targetNumber = this.pa + owner.system.pa.basis targetNumber = this.pa + owner.system.pa.basis
} }
// TODO: Migrate to DSARoll
let roll1 = new Roll(`1d20cs<${targetNumber}`, owner.getRollData()); let roll1 = new Roll(`1d20cs<${targetNumber}`, owner.getRollData());
let evaluated1 = (await roll1.evaluate()) let evaluated1 = (await roll1.evaluate())
@ -120,6 +121,7 @@ export class SkillDataModel extends BaseItem {
speaker: ChatMessage.getSpeaker({actor: owner}), speaker: ChatMessage.getSpeaker({actor: owner}),
flavor: message, flavor: message,
rollMode, rollMode,
}, {
}) })
} }
@ -127,6 +129,7 @@ export class SkillDataModel extends BaseItem {
async #talentRoll(rollMode) { async #talentRoll(rollMode) {
const owner = this.parent.parent const owner = this.parent.parent
// TODO: Migrate to DSARoll
let roll1 = new Roll("3d20", owner.getRollData()); let roll1 = new Roll("3d20", owner.getRollData());
let evaluated1 = (await roll1.evaluate()) let evaluated1 = (await roll1.evaluate())

View File

@ -1,3 +1,5 @@
import {evaluateRoll} from "../globals/DSARoll.mjs";
export class Talent { export class Talent {
@ -18,6 +20,7 @@ export class Talent {
* @property {String} name * @property {String} name
* @property {Number} taw * @property {Number} taw
* @property {Number} mod * @property {Number} mod
* @property {Actor} owner
* @property {TalentEigenschaften} eigenschaften * @property {TalentEigenschaften} eigenschaften
* @property {"mu","kl","in","ch","ff","ge","ko","kk"} eigenschaft1 * @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"} eigenschaft2
@ -50,54 +53,15 @@ export class Talent {
async #talentRoll(data, rollMode) { async #talentRoll(data, rollMode) {
let roll1 = new Roll("3d20"); // TODO: Migrate to DSARoll
const dsaDieRollEvaluated = await evaluateRoll("3d20", {
let evaluated1 = (await roll1.evaluate()) value: data.taw,
const dsaDieRollEvaluated = this._evaluateRoll(evaluated1.terms[0].results, {
taw: data.taw,
mod: data.mod, mod: data.mod,
owner: data.owner,
werte: [data.eigenschaften[data.eigenschaft1], data.eigenschaften[data.eigenschaft2], data.eigenschaften[data.eigenschaft3]], werte: [data.eigenschaften[data.eigenschaft1], data.eigenschaften[data.eigenschaft2], data.eigenschaften[data.eigenschaft3]],
}) })
return { return dsaDieRollEvaluated
...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,
}
} }
} }

View File

@ -1,4 +1,5 @@
import {ATTRIBUTE, ATTRIBUTE_DESCRIPTIONS} from "../data/attribute.mjs"; import {ATTRIBUTE, ATTRIBUTE_DESCRIPTIONS} from "../data/attribute.mjs";
import {displayRoll} from "../globals/displayRoll.js";
const { const {
ApplicationV2, ApplicationV2,
@ -96,14 +97,35 @@ export class AttributeDialog extends HandlebarsApplicationMixin(ApplicationV2) {
const targetValue = this._value + modValue const targetValue = this._value + modValue
// TODO: Migrate to DSARoll
let roll = await new Roll(`1d20`).evaluate() let roll = await new Roll(`1d20`).evaluate()
let diff = targetValue - roll.terms[0].results[0].result let diff = targetValue - roll.terms[0].results[0].result
roll.toMessage({ let creationData = roll.toMessage({
speaker: ChatMessage.getSpeaker({actor: this.actor}), //speaker: ChatMessage.getSpeaker({actor: this.actor}),
flavor: `${this._flaw ? "Schlechte " : ""}Eigenschaft: ${this._name}<br/>Ergebnis: ${Math.abs(diff)}${diff > 0 ? " übrig" : " daneben"}`, //flavor: `${this._flaw ? "Schlechte " : ""}Eigenschaft: ${this._name}<br/>Ergebnis: ${Math.abs(diff)}${diff > 0 ? " übrig" : " daneben"}`,
rollMode: game.settings.get('core', 'rollMode'), 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() this.close()
} }

View File

@ -1,5 +1,6 @@
import {Talent} from "../data/talent.mjs"; import {Talent} from "../data/talent.mjs";
import {ATTRIBUTE} from "../data/attribute.mjs"; import {ATTRIBUTE} from "../data/attribute.mjs";
import {displayRoll} from "../globals/displayRoll.js";
const { const {
ApplicationV2, ApplicationV2,
@ -85,6 +86,7 @@ export class TalentDialog extends HandlebarsApplicationMixin(ApplicationV2) {
name: this._talent.name, name: this._talent.name,
taw: taw, taw: taw,
mod: modValue, mod: modValue,
owner: this._actor,
eigenschaft1: this._talent.system.probe[0].toLowerCase(), eigenschaft1: this._talent.system.probe[0].toLowerCase(),
eigenschaft2: this._talent.system.probe[1].toLowerCase(), eigenschaft2: this._talent.system.probe[1].toLowerCase(),
eigenschaft3: this._talent.system.probe[2].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") const result = await new Talent(payload).evaluate("publicroll")
result.evaluatedRoll.toMessage({ const context = {
speaker: ChatMessage.getSpeaker({actor: this._actor}), talent: this._talent.name,
flavor: `Talent: ${this._talent.name}<br/>TaP*: ${result.tap}<br/>${result.meisterlich ? "Meisterlich" : ""}${result.patzer ? "Petzer" : ""}<br/>${this._talent.system.talent}`, 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() this.close()
} }

View File

@ -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 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 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 roll = new Roll("1d20cs<" + data.targetNumber)
const evaluated1 = (await roll.evaluate()) 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 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) 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 roll = new Roll("1d20cs<" + data.targetNumber)
const evaluated1 = (await roll.evaluate()) const evaluated1 = (await roll.evaluate())

View File

@ -2,33 +2,43 @@
* *
* @param {[{result: Number}]|String} rolledDice either the result of a roll or a roll-formula * @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} 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 {[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 {{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 * @param {Number} lowerThreshold this is the threshold against a critical success is counted against
* @param {Number} upperThreshold this is the threshold against a critical fumble is counted against * @param {Number} upperThreshold this is the threshold against a critical fumble is counted against
* @param {Number} countToMeisterlich amount of critical success are needed for the dice roll to be a critical success * @param {Number} countToMeisterlich amount of critical success are needed for the dice roll to be a critical success
* @param {Number} countToPatzer amount of critical fumbles are needed for the dice roll to be a critical failure * @param {Number} countToPatzer amount of critical fumbles are needed for the dice roll to be a critical failure
* @param {*} rollObject the foundry Roll Object
* @returns {{tap: number, meisterlich: boolean, patzer: boolean, evaluated: Roll.Evaluated}} * @returns {{tap: number, meisterlich: boolean, patzer: boolean, evaluated: Roll.Evaluated}}
*/ */
const evaluateRoll = async (rolledDice, { const evaluateRoll = async (rolledDice, {
value, value,
mod = 0,
werte = [], werte = [],
owner, owner,
lowerThreshold = 1, lowerThreshold = 1,
upperThreshold = 20, upperThreshold = 20,
countToMeisterlich = 3, countToMeisterlich = 3,
countToPatzer = 3, countToPatzer = 3,
}) => { }, rollObject = Roll) => {
let tap = value; let tap = value - mod;
let meisterlichCounter = 0; let meisterlichCounter = 0;
let patzerCounter = 0; let patzerCounter = 0;
let failCounter = 0; let failCounter = 0;
let evaluated = null let evaluated = null
// normalise counts
if (countToMeisterlich > werte.length) {
countToMeisterlich = werte.length
}
if (countToPatzer > werte.length) {
countToPatzer = werte.length
}
if (typeof rolledDice == "string") { // we need to roll it ourself if (typeof rolledDice == "string") { // we need to roll it ourselves
let roll1 = new Roll(rolledDice, owner.getRollData()); let roll1 = new rollObject(rolledDice, owner.getRollData());
evaluated = await roll1.evaluate() evaluated = await roll1.evaluate()
rolledDice = evaluated.terms[0].results rolledDice = evaluated.terms[0].results
} }
@ -56,9 +66,21 @@ const evaluateRoll = async (rolledDice, {
failCounter++; failCounter++;
} }
if (rolledDie.result <= lowerThreshold) meisterlichCounter++; if (rolledDie.result <= lowerThreshold) meisterlichCounter++;
if (rolledDie.result > upperThreshold) patzerCounter++; if (rolledDie.result >= upperThreshold) patzerCounter++;
}) })
if (meisterlichCounter >= countToMeisterlich) {
tap = value
}
if (tap > value) { // can't have more tap than value as result
tap = value
}
if (tap === 0) {
tap = 1
}
return { return {
tap, tap,
meisterlich: meisterlichCounter === countToMeisterlich, meisterlich: meisterlichCounter === countToMeisterlich,

View File

@ -0,0 +1,221 @@
import { evaluateRoll } from './DSARoll'
const RollWithMockResults = (a, b, c) => {
return class Roll {
constructor(formula, actorData) {
}
async evaluate() {
return new Promise(resolve => {
resolve({
terms: [{
results: [
{
result: a,
active: true
},
{
result: b,
active: true
},
{
result: c,
active: true
}
]
}]
})
})
}
}
}
const owner = {
getRollData: () => {
}
}
describe('Attribute Checks', () => {
it('should yield a success when the Roll is below the used attribute', async () => {
let actual = await evaluateRoll("1d20",{
value: 0,
werte: [12],
owner
}, RollWithMockResults(12))
expect(actual.tap).toBeGreaterThanOrEqual(0)
})
it('should yield a failure when the Roll is above the used attribute', async () => {
let actual = await evaluateRoll("1d20",{
value: 0,
werte: [12],
owner
}, RollWithMockResults(13))
expect(actual.tap).toBeLessThan(0)
})
it('should yield a Patzer when the Roll does meet the Patzer Threshold', async () => {
let actual = await evaluateRoll("1d20",{
value: 0,
werte: [12],
owner
}, RollWithMockResults(20))
expect(actual.tap).toBeLessThan(0)
expect(actual.patzer).toBe(true)
})
it('should yield a Meisterlich when the Roll does meet the Meisterlich Threshold', async () => {
let actual = await evaluateRoll("1d20",{
value: 0,
werte: [12],
owner
}, RollWithMockResults(1))
expect(actual.tap).toBeGreaterThanOrEqual(0)
expect(actual.meisterlich).toBe(true)
})
})
describe('Skill Checks', () => {
it('should yield positive TAP when the die roll matches values of used attributes', async () => {
let actual = await evaluateRoll("3d20",{
value: 1,
werte: [12, 12, 12],
owner
}, RollWithMockResults(12, 12, 12))
expect(actual.tap).toBeGreaterThan(0)
})
it('should succeed with a TaP* of 1 when the die roll are exactly the values of used attributes', async () => {
let actual = await evaluateRoll("3d20",{
value: 0,
werte: [12, 12, 12],
owner
}, RollWithMockResults(12, 12, 12))
expect(actual.tap).toBe(0)
expect(actual.patzer).toBe(false)
})
it('should fail with negative TaP* when the die roll are above the values of used attributes', async () => {
let actual = await evaluateRoll("3d20",{
value: 0,
werte: [12, 12, 12],
owner
}, RollWithMockResults(13, 13, 13))
expect(actual.tap).toBe(-3)
})
it('should yield a Patzer when the die roll does meet the Patzer Threshold', async () => {
let actual = await evaluateRoll("3d20",{
value: 1,
werte: [12, 12, 12],
owner
}, RollWithMockResults(20, 20, 20))
expect(actual.tap).toBeLessThan(0)
expect(actual.patzer).toBe(true)
})
it('should yield a Patzer when the die roll does meet a modified Patzer Threshold', async () => {
let actual = await evaluateRoll("3d20",{
value: 1,
werte: [12, 12, 12],
countToPatzer: 2,
owner
}, RollWithMockResults(20, 20, 1))
expect(actual.tap).toBeLessThan(0)
expect(actual.patzer).toBe(true)
})
it('should yield a Meisterlich when the die roll does meet the Meisterlich Threshold', async () => {
const tap = 5
let actual = await evaluateRoll("3d20",{
value: tap,
werte: [12, 12, 12],
owner
}, RollWithMockResults(1, 1, 1))
expect(actual.tap).toBe(tap)
expect(actual.meisterlich).toBe(true)
})
it('should yield a Meisterlich when the die roll does meet the modified Meisterlich Threshold', async () => {
const tap = 5
let actual = await evaluateRoll("3d20",{
value: tap,
werte: [12, 12, 12],
countToMeisterlich: 2,
owner
}, RollWithMockResults(1, 20, 1))
expect(actual.tap).toBe(tap)
expect(actual.meisterlich).toBe(true)
})
it('should yield a success when the die roll can be buffered with TaP', async () => {
let actual = await evaluateRoll("3d20",{
value: 8,
werte: [12, 12, 12],
owner
}, RollWithMockResults(15, 15, 8))
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)
})
})

View File

@ -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<void>}
*/
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
}

View File

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

View File

@ -41,4 +41,3 @@
} }
} }

View File

@ -36,4 +36,5 @@
@use "organisms/liturgy-sheet"; @use "organisms/liturgy-sheet";
@use "organisms/dialog"; @use "organisms/dialog";
@use "organisms/deity-sheet"; @use "organisms/deity-sheet";
@use "organisms/item-browser-dialog"; @use "organisms/item-browser-dialog";
@use "atoms/chat";

View File

@ -2,7 +2,7 @@
"id": "DSA_4-1", "id": "DSA_4-1",
"title": "Das Schwarze Auge 4.1", "title": "Das Schwarze Auge 4.1",
"description": "Noch ein Spielsystem für 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": { "compatibility": {
"minimum": 12, "minimum": 12,
"verified": 13 "verified": 13
@ -364,5 +364,5 @@
"primaryTokenAttribute": "lep.aktuell", "primaryTokenAttribute": "lep.aktuell",
"url": "https://git.macniel.online/macniel/foundry-dsa41-game", "url": "https://git.macniel.online/macniel/foundry-dsa41-game",
"manifest": "https://git.macniel.online/macniel/foundry-dsa41-game/raw/branch/main/src/system.json", "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"
} }

View File

@ -0,0 +1,10 @@
<div>
<div>{{attribute}}({{target}})</div>
<div>Gewürfelt: {{dieResult}}</div>
<hr>
{{#if missing}}
<div>Gefehlt: {{missing}}</div>
{{else}}
<div>Übrig: {{remaining}}</div>
{{/if}}
</div>

View File

@ -0,0 +1,20 @@
<div>
<div>{{talent}} (TaW: {{taw}})</div>
<div>Gewürfelt:</div>
<section class="die">
{{#each ergebnis}}
<div>
<span class="provided">{{eigenschaft}} ({{eigenschaftWert}})</span>
<span class="value">{{wuerfelErgebnis}}</span>
</div>
{{/each}}
</section>
<hr>
{{#if missing}}
<div>Gefehlt: {{missing}}</div>
{{else}}
<div>Übrig: {{remaining}}</div>
{{/if}}
</div>