Compare commits

...

91 Commits
0.3.3 ... main

Author SHA1 Message Date
root 0e0fd22fad auto commit 2025-12-03 20:21:56 +01:00
macniel 80d1b29486 Merge pull request 'feature/zauber-rework' (#67) from feature/zauber-rework into main
Reviewed-on: #67
2025-12-03 20:20:40 +01:00
macniel 24bb15f502 Merge remote-tracking branch 'origin/main' into feature/zauber-rework
Pull Request Check / testing (pull_request) Successful in 21s Details
# Conflicts:
#	src/system.json
2025-12-03 20:20:24 +01:00
macniel 75280e5590 finalises spell system on basic implementation level 2025-12-03 20:07:35 +01:00
macniel 98864464b1 adds value statistics to message 2025-11-30 12:27:00 +01:00
macniel 8be096f464 changes on the form persist now, spell is now correctly queued. 2025-11-30 12:20:53 +01:00
macniel c51e281530 adjust dialog flow to match spell casting in 4.1 2025-11-30 11:21:26 +01:00
macniel 50c28cb380 moves roll mechanic into global file 2025-11-30 11:21:07 +01:00
macniel 2ee6ed775b equalise field probenMod 2025-11-30 11:20:39 +01:00
macniel 235dd0928d adds clausel that Kosten Einsparen reduces the costs by at least 1 AsP. 2025-11-28 15:05:45 +01:00
macniel 07ba1c889b fixes error found with ATTRIBUTO 2025-11-28 14:59:42 +01:00
macniel cc60f9e57f finalises calculation for SpoMods 2025-11-28 14:50:47 +01:00
macniel 94a80eb321 implements spell restrictions 2025-11-27 15:25:40 +01:00
macniel 959f47f348 implements partial (display) support for SpoMods 2025-11-26 20:54:32 +01:00
macniel 5f85631679 displays ZfW based on selected Mods and its initial Spell Value 2025-11-26 19:03:52 +01:00
macniel 957584206d first attempts at normalising dialog flow 2025-11-23 20:51:14 +01:00
macniel 9727d7bc3a src/system.json aktualisiert 2025-11-19 16:14:35 +01:00
macniel 4a9bfe2865 adds english language file 2025-11-19 16:13:53 +01:00
macniel ce6207be95 reworked BRW spells as a PoC 2025-11-17 17:35:39 +01:00
root 468984a83b auto commit 2025-11-16 23:35:11 +01:00
macniel 3f8794e181 the return of the paperdoll 2025-11-16 23:32:49 +01:00
macniel f5b4633f76 README.md aktualisiert 2025-11-16 17:03:41 +01:00
macniel c6829ff697 README.md aktualisiert 2025-11-16 17:03:21 +01:00
macniel 3207020d57 README.md aktualisiert 2025-11-16 17:03:07 +01:00
root df61621565 auto commit 2025-11-16 17:02:10 +01:00
macniel 61e1bd2836 Merge pull request 'feature/after_review' (#65) from feature/after_review into main
Reviewed-on: #65
2025-11-16 16:58:15 +01:00
macniel f7d772b6ca Merge branch 'main' into feature/after_review
Pull Request Check / testing (pull_request) Successful in 19s Details
2025-11-16 16:58:01 +01:00
macniel 02f0ecc9dd implements last standalone tab 2025-11-16 16:56:54 +01:00
macniel 182aeb2dc6 implements optional fatigue system. 2025-11-16 16:07:42 +01:00
macniel c00a6b11b7 fixes NPE as ranged items could be equipped when the actor doesnt even have the skill to do so. 2025-11-16 13:35:39 +01:00
macniel eca965e434 increases discoverability of die interactions 2025-11-16 13:26:02 +01:00
macniel 6f935644c1 spell and liturgy dice are now easier to recognise 2025-11-16 13:19:54 +01:00
macniel 4f4446d327 introduces socketlib as hard requirement enabling players to loot and buy even when they lack the permission to update items they don't own (yet). 2025-11-16 12:36:19 +01:00
macniel e0e70d126f fix faulty tab config on creatureSheet 2025-11-16 11:52:56 +01:00
macniel 692867f2ac finalises missing sidebutton tabs 2025-11-16 11:41:35 +01:00
macniel 46b6ed8f2a implements colorful sidebar buttons 2025-11-16 11:03:50 +01:00
macniel 189db593aa fixes styling in merchant sheet 2025-11-16 10:18:02 +01:00
macniel d355cb2d5c fixes orientiation of sidebuttons 2025-11-14 21:29:00 +01:00
macniel accd2d1f16 continues development on mini character sheet 2025-11-14 15:57:07 +01:00
macniel 0fffebdab9 implements mini character sheet, now it also needs tabs on the side to open dedicated sections from the big boy character sheet as dialogs. 2025-11-14 00:07:31 +01:00
macniel 6d366188ea begins implementing sidebar elements for pinned Items 2025-11-13 22:40:48 +01:00
macniel 47280f7216 beginning operation:health. 2025-11-13 15:04:34 +01:00
macniel bbf181eb8c implements selecting an item and buying that if the coin is sufficent. 2025-11-13 15:04:11 +01:00
macniel 1bc6d9673a inverts cooldown display, removes unintuitive progress/activate button and also fixes a bug in the tooltip 2025-11-12 20:42:29 +01:00
macniel 811806a68b makes talents more subtle also adds tooltips to rollable on the skill sheet. 2025-11-12 20:13:21 +01:00
macniel bdcb09c82e fixes rounding issue 2025-11-12 19:09:44 +01:00
macniel cfc5bc15b1 fixes rounding issue 2025-11-12 19:09:07 +01:00
macniel 29d25f8afe Implements Filtering 2025-11-12 17:05:45 +01:00
macniel c2b8a7d895 implements creating Equipment from scratch and also an Equipment/Item Browser from where to drag and drop (perhaps even buy from) new Equipment onto an Actor. 2025-11-12 00:21:55 +01:00
macniel ed893f6b9d fixes some user experience issues 2025-11-11 22:20:01 +01:00
macniel 316ab90c67 fixes some user experience issues 2025-11-11 22:19:54 +01:00
macniel 7e34251397 fixes some user experience issues 2025-11-09 19:14:35 +01:00
root 28f19772f2 auto commit 2025-11-09 10:54:04 +01:00
macniel d51aa18a19 finalise darkmode 2025-11-09 10:53:11 +01:00
macniel 0f9032c3f5 beginnings of darkmode 2025-11-09 00:41:54 +01:00
root 34d7175c39 auto commit 2025-11-08 20:52:11 +01:00
macniel 97fe0fa9a6 implements deity model and sheet.
the model includes stats important for clerics as it contains miracle Plus and miracle Minus.
2025-11-08 14:21:28 +01:00
root 7b0f407239 auto commit 2025-11-07 22:53:23 +01:00
macniel f402661488 fixes jsons 2025-11-07 22:51:47 +01:00
macniel 03de483e9a fixes paths 2025-11-07 22:49:09 +01:00
root 1adff7568d auto commit 2025-11-04 18:20:18 +01:00
macniel 97466ed45d Merge pull request 'starts cleaning code and also localisation' (#64) from feature/cleanup-and-localisation into main
Reviewed-on: #64
2025-11-04 18:19:30 +01:00
macniel 12c9b0766a starts cleaning code and also localisation 2025-11-04 00:20:41 +01:00
macniel 5104f43e2f inventory is now contained in its own scrollView. 2025-11-03 18:14:53 +01:00
macniel 34c95891e6 cleans up code 2025-11-03 18:14:39 +01:00
macniel 223ea9e26b cleans up init Hook 2025-11-03 16:26:05 +01:00
macniel 4be53924d8 best stock prices guaranteed. 2025-11-02 21:00:50 +01:00
macniel 8258f53a3a enables buying of goods and reducing the wealth of the actor who bought the good and proclaiming it loudly in chat. 2025-11-02 20:53:09 +01:00
root 6d4f8694df auto commit 2025-11-01 12:30:31 +01:00
macniel 72248f2635 fixes styling of merchant items 2025-11-01 12:29:05 +01:00
macniel 1d714a3773 enables selling of goods 2025-11-01 12:26:14 +01:00
root 95712704b5 auto commit 2025-11-01 11:54:43 +01:00
macniel 7788035c8c restores lep calculation 2025-11-01 11:53:13 +01:00
root b66e4e77ac auto commit 2025-11-01 11:45:08 +01:00
macniel e862185803 fixes casing 2025-11-01 11:44:06 +01:00
root dc97fffba2 auto commit 2025-11-01 11:29:28 +01:00
macniel db81e2def1 fixes asynchronous error with gitkeep, but do not process them 2025-11-01 11:28:30 +01:00
macniel 373781b5b0 fixes asynchronous error with gitkeep as the directory was not pushed 2025-11-01 11:26:40 +01:00
macniel 394992d447 fixes asynchronous error with a pinky promise 2025-11-01 11:24:15 +01:00
macniel ec4c9768dd fixes asynchronous error with a pinky promise 2025-11-01 11:21:10 +01:00
macniel df4fd6061d fixes asynchronous error with a pinky promise 2025-11-01 11:16:08 +01:00
macniel bb35d0d6e7 fixes asynchronous error with a pinky promise 2025-11-01 11:08:29 +01:00
macniel 7476c55e63 fixes asynchronous error 2025-11-01 10:58:44 +01:00
macniel 5b46f36c58 fixes asynchronous error 2025-11-01 10:55:13 +01:00
macniel 026f222718 fixes asynchronous error 2025-11-01 10:45:08 +01:00
root b69511f93a auto commit 2025-11-01 10:35:41 +01:00
macniel 9a57bcd77a establishes fixed randomIDs 2025-11-01 10:26:24 +01:00
macniel 62c5702992 removes stray generated abvenenum 2025-11-01 10:25:24 +01:00
macniel 9948521512 liturgies can now be pushed into queue. 2025-10-31 19:15:15 +01:00
macniel a861245d44 adds special ability group support. 2025-10-31 16:12:43 +01:00
root e69374f6f4 auto commit 2025-10-31 16:03:21 +01:00
384 changed files with 9749 additions and 1527 deletions

View File

@ -44,6 +44,14 @@ Es ist möglich via Kontextmenü Gegenstands-Stapel in zwei Stapel aufzuteilen,
Es ist möglich den Rasten und Regenerations Dialog von dem Charakterbogen eines Charakters aufzurufen worin man die Einstellungen vornehmen kann die die Regeneration während einer Rest von Lebensenergie und Astralenergie sowie der Heilung von Wunden beeinflussen.
### Mini Charakterbogen
Wenn der Charakterbogen schmall genug gezogen ist, wird dieser aktiviert und enthält lediglich die Seitenleiste sowie die Attribute auf denen gewürfelt werden können. Daneben allerdings befinden sich die Reiter des großen Charakterbogens welche die jeweiligen Seiten in kleiner Form als separates Fenster öffnen lassen.
### Item Browser
Es ist nun möglich auf dem Charakterbogen unter dem Reiter "Inventar" den Item Browser zu öffnen wo alle Gegenstände der aktuellen Welt enthalten sind und mit rudimentären Filtern durchsucht werden können. Ein Spieler kann hierrüber neue Gegenstände kaufen, der Spielleiter hingegen kann per Drag and Drop die gesuchten Gegenstände auf alle Actor Sheets hinzufügen.
## GM Tools
### Gruppenmanagement
@ -74,3 +82,5 @@ Um heimlich vergleichend Talentproben von zwei Charakteren und oder Kreaturen du
Icon Theme of Equipment Items (e.g. Weapons, Armory, Adventuring Gear) is made by https://soda-1.itch.io/
Tanja für den UI UX Support.

View File

@ -1,8 +1,9 @@
import {dest, series, src} from 'gulp';
import gulp from 'gulp';
import process from 'node:process';
import replace from 'gulp-replace';
import jsonModify from 'gulp-json-modify';
import {getRandomValues} from 'node:crypto';
import {subtle} from 'node:crypto';
import * as dartSass from 'sass';
import gulpSass from 'gulp-sass';
import {deleteAsync} from 'del';
@ -11,28 +12,42 @@ import {join} from 'node:path';
import {compilePack} from '@foundryvtt/foundryvtt-cli';
const sass = gulpSass(dartSass);
const sass = gulpSass(dartSass)
/**
* Generate a random alphanumeric string ID of a given requested length using `crypto.getRandomValues()`.
* @param {string} reference The reference which should be used to generate a semi random ID
* @param {number} length The length of the random string to generate, which must be at most 16384.
* @returns {string} A string containing random letters (A-Z, a-z) and numbers (0-9).
*/
function randomID(length = 16) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const cutoff = 0x100000000 - (0x100000000 % chars.length);
const random = new Uint32Array(length);
do {
getRandomValues(random);
} while (random.some(x => x >= cutoff));
let id = "";
for (let i = 0; i < length; i++) id += chars[random[i] % chars.length];
return id;
function randomID(reference = "", length = 16) {
const encoder = new TextEncoder()
const data = encoder.encode(reference)
return subtle.digest('SHA-256', data).then(hashBuffer => {
// Step 2: Convert the hash to a Base62 string
const base62Chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const hashArray = new Uint8Array(hashBuffer)
let num = BigInt(0)
// Convert hash buffer to a BigInt
for (let byte of hashArray) {
num = (num << BigInt(8)) | BigInt(byte)
}
let base62Id = '';
while (num > 0) {
const remainder = num % BigInt(62)
base62Id = base62Chars[Number(remainder)] + base62Id
num = num / BigInt(62)
}
// Step 3: Return the first 16 characters
return base62Id.slice(-length)
});
}
const convert = function (from, to, ofType, overwrite = true) {
const SOURCE = from;
const DEST = to;
const TYPE = ofType;
@ -45,38 +60,50 @@ const convert = function (from, to, ofType, overwrite = true) {
mkdirSync(DEST)
}
const filewalker = (source) => {
console.debug("entering directory", source);
readdirSync(source).forEach(file => {
if (statSync(join(source, file)).isDirectory()) {
filewalker(join(source, file));
} else {
console.debug("processing file", join(source, file))
let originalSource = JSON.parse(readFileSync(join(source, file), {encoding: "utf8"}));
let id = randomID();
let promises = []
let targetSource = {
_id: id,
_key: "!items!" + id,
type: TYPE,
img: originalSource.image,
name: originalSource.name.trim(),
system: {...originalSource},
const filewalker = async (source) => {
console.debug("entering directory", source)
for (let file of readdirSync(source)) {
if (statSync(join(source, file)).isDirectory()) {
await filewalker(join(source, file))
} else {
if (file.endsWith(".json")) {
console.debug("processing file", join(source, file))
let originalSource = JSON.parse(readFileSync(join(source, file), {encoding: "utf8"}))
promises.push(new Promise((resolve2) => {
randomID("DSA_4-1" + TYPE + originalSource.name.trim()).then(id => {
let targetSource = {
_id: id,
_key: "!items!" + id,
type: TYPE,
img: originalSource.image,
name: originalSource.name.trim(),
system: {...originalSource},
}
delete targetSource.system.image;
let target = JSON.stringify(targetSource, null, 2)
let newFileName = "./" + join(DEST, id + ".json")
if (!existsSync(join("./", DEST))) {
mkdirSync(join("./", DEST))
}
writeFileSync(newFileName, target, {encoding: "utf8"})
resolve2()
})
}))
}
delete targetSource.system.image;
let target = JSON.stringify(targetSource, null, 2);
let newFileName = "./" + join(DEST, id + ".json");
writeFileSync(newFileName, target, {encoding: "utf8"});
}
});
}
}
filewalker(SOURCE)
return Promise.allSettled(promises)
}
function cleanDist() {
return deleteAsync(['dist/**']);
return deleteAsync(['dist/**'])
}
function buildStyles() {
@ -112,44 +139,44 @@ function updateManifestFile() {
.pipe(dest('src/'))
}
async function prepareDB() {
gulp.task('prepareDB', async function (done) {
try {
if (!existsSync("./src/packs/__source")) {
mkdirSync("./src/packs/__source");
}
convert("./src/packs/_source/talente", "./src/packs/__source/talente", "Skill");
convert("./src/packs/_source/zauber", "./src/packs/__source/zauber", "Spell");
convert("./src/packs/_source/vorteile", "./src/packs/__source/vorteile", "Advantage");
convert("./src/packs/_source/nachteile", "./src/packs/__source/vorteile", "Advantage", false);
convert("./src/packs/_source/sonderfertigkeiten", "./src/packs/__source/sonderfertigkeiten", "SpecialAbility");
convert("./src/packs/_source/waehrungen", "./src/packs/__source/waehrungen", "Equipment");
convert("./src/packs/_source/Gegenstaende/Waffen", "./src/packs/__source/Waffen", "Equipment");
convert("./src/packs/_source/Gegenstaende/Munition", "./src/packs/__source/Munition", "Equipment");
convert("./src/packs/_source/Gegenstaende/Ruestzeug", "./src/packs/__source/Ruestzeug", "Equipment");
convert("./src/packs/_source/Gegenstaende/Behaelter", "./src/packs/__source/Gegenstaende", "Equipment", false);
convert("./src/packs/_source/Gegenstaende/Bekleidung", "./src/packs/__source/Gegenstaende", "Equipment", false);
convert("./src/packs/_source/Gegenstaende/Beleuchtung", "./src/packs/__source/Gegenstaende", "Equipment", false);
convert("./src/packs/_source/Gegenstaende/Buecher", "./src/packs/__source/Gegenstaende", "Equipment", false);
convert("./src/packs/_source/Gegenstaende/Essutensilien", "./src/packs/__source/Gegenstaende", "Equipment", false);
convert("./src/packs/_source/Gegenstaende/Sonstiges", "./src/packs/__source/Gegenstaende", "Equipment", false);
convert("./src/packs/_source/Gegenstaende/Werkzeug", "./src/packs/__source/Gegenstaende", "Equipment", false);
convert("./src/packs/_source/Gegenstaende/Seile", "./src/packs/__source/Gegenstaende", "Equipment", false);
convert("./src/packs/_source/liturgien-und-segnungen", "./src/packs/__source/liturgien", "Liturgy");
convert("./src/packs/_source/wunden", "./src/packs/__source/wunden", "ActiveEffect");
convert("./src/packs/_source/kulturen", "./src/packs/__source/kulturen", "Culture");
convert("./src/packs/_source/spezien", "./src/packs/__source/spezien", "Species");
convert("./src/packs/_source/professionen", "./src/packs/__source/professionen", "Profession");
await convert("./src/packs/_source/talente", "./src/packs/__source/talente", "Skill")
await convert("./src/packs/_source/zauber-brw", "./src/packs/__source/zauber", "Spell")
await convert("./src/packs/_source/vorteile", "./src/packs/__source/vorteile", "Advantage")
await convert("./src/packs/_source/nachteile", "./src/packs/__source/vorteile", "Advantage", false)
await convert("./src/packs/_source/sonderfertigkeiten", "./src/packs/__source/sonderfertigkeiten", "SpecialAbility")
await convert("./src/packs/_source/waehrungen", "./src/packs/__source/waehrungen", "Equipment")
await convert("./src/packs/_source/Gegenstaende/Waffen", "./src/packs/__source/waffen", "Equipment")
await convert("./src/packs/_source/Gegenstaende/Munition", "./src/packs/__source/munition", "Equipment")
await convert("./src/packs/_source/Gegenstaende/Ruestzeug", "./src/packs/__source/ruestzeug", "Equipment")
await convert("./src/packs/_source/Gegenstaende/Behaelter", "./src/packs/__source/gegenstaende", "Equipment", false)
await convert("./src/packs/_source/Gegenstaende/Bekleidung", "./src/packs/__source/gegenstaende", "Equipment", false)
await convert("./src/packs/_source/Gegenstaende/Beleuchtung", "./src/packs/__source/gegenstaende", "Equipment", false)
await convert("./src/packs/_source/Gegenstaende/Buecher", "./src/packs/__source/gegenstaende", "Equipment", false)
await convert("./src/packs/_source/Gegenstaende/Essutensilien", "./src/packs/__source/gegenstaende", "Equipment", false)
await convert("./src/packs/_source/Gegenstaende/Sonstiges", "./src/packs/__source/gegenstaende", "Equipment", false)
await convert("./src/packs/_source/Gegenstaende/Werkzeug", "./src/packs/__source/gegenstaende", "Equipment", false)
await convert("./src/packs/_source/Gegenstaende/Seile", "./src/packs/__source/gegenstaende", "Equipment", false)
await convert("./src/packs/_source/liturgien-und-segnungen", "./src/packs/__source/liturgien", "Liturgy")
await convert("./src/packs/_source/wunden", "./src/packs/__source/wunden", "ActiveEffect")
await convert("./src/packs/_source/kulturen", "./src/packs/__source/kulturen", "Culture")
await convert("./src/packs/_source/spezien", "./src/packs/__source/spezien", "Species")
await convert("./src/packs/_source/professionen", "./src/packs/__source/professionen", "Profession")
done()
} catch (err) {
console.error(err);
console.error(err)
}
})
}
function buildDB() {
gulp.task('buildDB', function (done) {
// Determine which source folders to process
const PACK_SRC = "src/packs/__source"
@ -161,15 +188,16 @@ function buildDB() {
);
for (const folder of folders) {
const src = join(PACK_SRC, folder.name);
const dest = join(PACK_DEST, folder.name);
console.info(`Compiling pack ${folder.name}`);
await compilePack(src, dest, {recursive: true, nedb: false});
const src = join(PACK_SRC, folder.name)
const dest = join(PACK_DEST, folder.name)
console.info(`Compiling pack ${folder.name}`)
await compilePack(src, dest, {recursive: true, nedb: false})
}
resolve()
done()
})
}
})
export default series(
@ -178,6 +206,6 @@ export default series(
copySource,
copyAssets,
buildStyles,
prepareDB,
buildDB
gulp.task('prepareDB'),
gulp.task('buildDB')
)

8
package-lock.json generated
View File

@ -12,7 +12,7 @@
"gulp-json-modify": "^1.0.2"
},
"devDependencies": {
"@foundryvtt/foundryvtt-cli": "^3.0.0",
"@foundryvtt/foundryvtt-cli": "^3.0.2",
"cb": "^0.1.1",
"del": "^8.0.1",
"fvtt-types": "npm:@league-of-foundry-developers/foundry-vtt-types@^13.346.0-beta.20250812191140",
@ -209,9 +209,9 @@
}
},
"node_modules/@foundryvtt/foundryvtt-cli": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@foundryvtt/foundryvtt-cli/-/foundryvtt-cli-3.0.0.tgz",
"integrity": "sha512-OiF4HtnYg5An1ivVxB68mOj5LO5gMHd4uHmC5nWdD8IYxpK0pSYw3t+cHrUYDp+Tic78uwFuHxLyc+ZNeZXulA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@foundryvtt/foundryvtt-cli/-/foundryvtt-cli-3.0.2.tgz",
"integrity": "sha512-coh4Cf4FD/GHxk2QMsd+3wLMivNeih4rfkbZy8CaYjdlpo6iciFQwxLqznZWtn+5p06zekvS2xLUF55NnbXQDw==",
"dev": true,
"dependencies": {
"chalk": "^5.4.1",

View File

@ -10,10 +10,11 @@
"scripts": {
"test": "true",
"build": "gulp",
"localBuild": "VERSION=0.0.1 gulp",
"installToFoundry": "node installToFoundry.mjs"
},
"devDependencies": {
"@foundryvtt/foundryvtt-cli": "^3.0.0",
"@foundryvtt/foundryvtt-cli": "^3.0.2",
"cb": "^0.1.1",
"del": "^8.0.1",
"fvtt-types": "npm:@league-of-foundry-developers/foundry-vtt-types@^13.346.0-beta.20250812191140",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 280 B

View File

@ -1 +1,59 @@
{}
{
"TYPES": {
"Actor": {
"Character": "Held",
"Creature": "Kreatur",
"Group": "Heldengruppe",
"Merchant": "Händler"
},
"Item": {
"ActiveEffect": "Aktiver Effekt",
"Equipment": "Ausrüstungsgegenstand",
"Skill": "Talent",
"Advantage": "Vor-/Nachteil",
"SpecialAbility": "Sonderfertigkeit",
"Spell": "Zauber",
"Liturgy": "Liturgie",
"Species": "Spezies",
"Culture": "Kultur",
"Profession": "Profession"
}
},
"COOLDOWN": {
"cancel": "{t} abbrechen"
},
"WEAPON": {
"attack": "Mit {weapon} angreifen",
"parry": "Mit {weapon} parrieren",
"damage": "Mit {weapon} schaden machen",
"initiative": "Initiative würfeln"
},
"COMBAT_DIALOG": {
"notReadyReason": {
"title": "Angriff kann aus folgenden Gründen nicht ausgeführt werden:",
"noTarget": "Kein Ziel ausgewählt",
"noWeapon": "Keine Waffe ausgewählt",
"noSkill": "Kein Waffentalent ausgewählt",
"noManeuver": "Kein Manöver ausgewählt",
"impossible": "Erschwernis zu hoch für Talentwert"
}
},
"COMBAT_DIALOG_TP": {
"windowTitle": "Schaden Würfeln",
"regularFormula": "Schadensformel:",
"bonusDamage": "Zusätzlicher Schaden:",
"buttonText": "Würfeln"
},
"SPELL_DIALOG": {
"notReadyReason": {
"title": "Zauber kann aus folgenden Gründen nicht gewirkt werden:",
"noRepresentation": "Keine Repräsentation gewählt",
"tooManySpoMods": "Zu viele Spontane Modifikationen ausgewählt",
"noZFPDataAvailable": "Noch keine Zauberprobe gewürfelt",
"overspentZFP": "Zu viele ZfP ausgegeben"
}
},
"ITEM_BROWSER": {
"progress": "{current}/{max}: Importiere von {compendium}"
}
}

50
src/lang/en.json 100644
View File

@ -0,0 +1,50 @@
{
"TYPES": {
"Actor": {
"Character": "Hero",
"Creature": "Creature",
"Group": "Adventure Group",
"Merchant": "Merchant"
},
"Item": {
"ActiveEffect": "Active Effect",
"Equipment": "Equipment",
"Skill": "Skill",
"Advantage": "Dis-/advantage",
"SpecialAbility": "Special Ability",
"Spell": "Spell",
"Liturgy": "Liturgy",
"Species": "Species",
"Culture": "Culture",
"Profession": "Profession"
}
},
"COOLDOWN": {
"cancel": "cancel {t}"
},
"WEAPON": {
"attack": "Attack with {weapon}",
"parry": "Parry with {weapon}",
"damage": "Deal damage with {weapon}",
"initiative": "Roll initiative"
},
"COMBAT_DIALOG": {
"notReadyReason": {
"title": "Attack can't be executed due to:",
"noTarget": "No Target selected",
"noWeapon": "No Weapon selected",
"noSkill": "No Skill selected",
"noManeuver": "No Maneuver selected",
"impossible": "Difficulty exceeds Skill Value"
}
},
"COMBAT_DIALOG_TP": {
"windowTitle": "Roll Damage",
"regularFormula": "Damage formula:",
"bonusDamage": "Additional damage formula:",
"buttonText": "Roll Dice"
},
"ITEM_BROWSER": {
"progress": "{current}/{max}: imported from {compendium}"
}
}

View File

@ -1,401 +1,43 @@
import {PlayerCharacterDataModel} from "./module/data/character.mjs";
import {SkillSheet} from "./module/sheets/skillSheet.mjs";
import {SpellSheet} from "./module/sheets/spellSheet.mjs";
import {SkillDataModel} from "./module/data/skill.mjs";
import {SpellDataModel} from "./module/data/spell.mjs";
import {VornachteileDataModel} from "./module/data/vornachteile.mjs";
import {Character} from "./module/documents/character.mjs";
import CharacterSheet from "./module/sheets/characterSheet.mjs";
import {AdvantageSheet} from "./module/sheets/advantageSheet.mjs";
import {GroupDataModel} from "./module/data/group.mjs";
import {GroupSheet} from "./module/sheets/groupSheet.mjs";
import {EquipmentDataModel} from "./module/data/equipment.mjs";
import {EquipmentSheet} from "./module/sheets/equipmentSheet.mjs";
import {CreatureDataModel} from "./module/data/creature.mjs";
import {CreatureSheet} from "./module/sheets/creatureSheet.mjs";
import {LiturgySheet} from "./module/sheets/liturgySheet.mjs";
import {LiturgyDataModel} from "./module/data/liturgy.mjs";
import {BlessingDataModel} from "./module/data/blessing.mjs";
import {SpecialAbilityDataModel} from "./module/data/specialAbility.mjs";
import {SpecialAbilitySheet} from "./module/sheets/specialAbilitySheet.mjs";
import {ActiveEffectSheet} from "./module/sheets/activeEffectSheet.mjs";
import {ActiveEffectDataModel} from "./module/data/activeeffect.mjs";
import {Trefferzone, Wunde, Zonenruestung, Zonenwunde} from "./module/data/Trefferzone.js";
import {ProfessionDataModel} from "./module/data/profession.mjs";
import {SpeciesDataModel} from "./module/data/species.mjs";
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 {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([
// ui partials.
'systems/DSA_4-1/templates/ui/partial-rollable-button.hbs',
'systems/DSA_4-1/templates/ui/partial-rollable-weaponskill-button.hbs',
'systems/DSA_4-1/templates/ui/partial-rollable-language-button.hbs',
'systems/DSA_4-1/templates/ui/partial-attribute-button.hbs',
'systems/DSA_4-1/templates/ui/partial-talent-editable.hbs',
'systems/DSA_4-1/templates/ui/partial-die.hbs',
'systems/DSA_4-1/templates/ui/partial-advantage-button.hbs',
'systems/DSA_4-1/templates/ui/partial-sf-button.hbs',
'systems/DSA_4-1/templates/ui/partial-action-button.hbs',
'systems/DSA_4-1/templates/ui/partial-equipment-button.hbs',
'systems/DSA_4-1/templates/ui/partial-array-editor.hbs',
'systems/DSA_4-1/templates/dialog/liturgy-dialog.hbs'
]);
}
import {initGlobalSettings, initUserSettings} from "./module/settings/global-settings.mjs";
import {setUpActorSheets, setUpItemSheets} from "./module/setup/sheets.mjs";
import {loadPartials} from "./module/setup/partials.mjs";
import {
initSocketLib,
initCombat,
initDataModels,
initDocumentClasses,
initGlobalAccess
} from "./module/setup/config.mjs";
import {initHandlebarHelpers} from "./module/handlebar-helpers/index.mjs";
Hooks.once("init", () => {
game.DSA41 = {
rollItemMacro,
Zonenruestung,
Zonenwunde,
Trefferzone,
Wunde,
RestingDialog,
BattleDialog
}
// Configure custom Document implementations.
CONFIG.Actor.documentClass = Character;
// Configure System Data Models.
CONFIG.Actor.dataModels = {
character: PlayerCharacterDataModel,
group: GroupDataModel,
creature: CreatureDataModel,
Merchant: MerchantDataModel,
};
CONFIG.Item.dataModels = {
Skill: SkillDataModel,
Spell: SpellDataModel,
Advantage: VornachteileDataModel,
Equipment: EquipmentDataModel,
Liturgy: LiturgyDataModel,
Blessing: BlessingDataModel,
SpecialAbility: SpecialAbilityDataModel,
ActiveEffect: ActiveEffectDataModel,
Profession: ProfessionDataModel,
Spezies: SpeciesDataModel,
Kultur: CultureDataModel,
}
CONFIG.Combat.initiative = {
formula: `(@ini.wuerfel)d6 + @ini.aktuell`,
decimals: 0
}
const setMovementSpeeds = () => {
const movementActions = CONFIG.Token.movement.actions
for (const key of ["swim", "climb", "crawl", "walk", "drive", "ride", "fly"]) {
delete movementActions[key]?.getCostFunction
}
movementActions.climb.canSelect = (token) => {
const actor = token.actor | null;
return (actor.type === "Character" && actor.system.itemTypes["Skill"].find(p => p.name === "Klettern")?.system.taw > 0) || actor.type === "Creature"
}
movementActions.crawl.canSelect = (token) => {
const actor = token.actor | null;
return actor.type === "Character" || actor.type === "Creature"
}
movementActions.walk.canSelect = (token) => {
const actor = token.actor | null;
return actor.type === "Character" || actor.type === "Creature"
}
movementActions.swim = {
label: "TOKEN.MOVEMENT.ACTIONS.swim.label",
icon: "fa-solid fa-swim",
order: 0,
canSelect: (token) => {
const actor = token.actor | null;
return actor.type === "Character" || actor.type === "Creature"
},
deriveTerrainDifficulty: () => 1,
}
movementActions.drive = {
label: "TOKEN.MOVEMENT.ACTIONS.drive.label",
icon: "fa-solid fa-car-side",
order: 0,
canSelect: (token) => {
const actor = token.actor | null;
return (actor.type === "Character" && actor.system.itemTypes["Skill"].find(p => p.name === "Fahrzeuge lenken")?.system.taw > 0) || actor.type === "Creature"
},
deriveTerrainDifficulty: () => 1,
}
movementActions.ride = {
label: "TOKEN.MOVEMENT.ACTIONS.ride.label",
icon: "fa-solid fa-horse",
order: 0,
canSelect: (token) => {
const actor = token.actor | null;
return (actor.type === "Character" && actor.system.itemTypes["Skill"].find(p => p.name === "Reiten")?.system.taw > 0) || actor.type === "Creature"
},
deriveTerrainDifficulty: () => 1,
}
movementActions.fly = {
label: "TOKEN.MOVEMENT.ACTIONS.fly.label",
icon: "fa-solid fa-wings",
order: 0,
canSelect: (token) => {
const actor = token.actor | null;
return (actor.type === "Character" && actor.system.itemTypes["Skill"].find(p => p.name === "Fliegen")?.system.taw > 0) || actor.type === "Creature"
},
deriveTerrainDifficulty: () => 1,
}
}
setMovementSpeeds()
console.log("DSA 4.1 is ready for development!")
foundry.documents.collections.Actors.registerSheet('dsa41.character', CharacterSheet, {
types: ["character"],
makeDefault: true,
})
foundry.documents.collections.Actors.registerSheet('dsa41.creature', CreatureSheet, {
types: ["creature"],
makeDefault: true,
})
foundry.documents.collections.Actors.registerSheet('dsa41.group', GroupSheet, {
types: ["group"],
makeDefault: true,
})
foundry.documents.collections.Items.registerSheet('dsa41.skill', SkillSheet, {
types: ["Skill"],
makeDefault: true,
});
foundry.documents.collections.Items.registerSheet('dsa41.spell', SpellSheet, {
types: ["Spell"],
makeDefault: true,
});
foundry.documents.collections.Items.registerSheet('dsa41.advantage', AdvantageSheet, {
types: ["Advantage"],
makeDefault: true,
})
foundry.documents.collections.Items.registerSheet('dsa41.equipment', EquipmentSheet, {
types: ["Equipment"],
makeDefault: false,
})
foundry.documents.collections.Items.registerSheet('dsa41.liturgy', LiturgySheet, {
types: ["Liturgy"],
makeDefault: true,
})
foundry.documents.collections.Items.registerSheet('dsa41.specialAbility', SpecialAbilitySheet, {
types: ["SpecialAbility"],
makeDefault: true,
})
foundry.documents.collections.Items.registerSheet('dsa41.activeEffect', ActiveEffectSheet, {
types: ['ActiveEffect'],
makeDefault: true,
})
foundry.documents.collections.Items.registerSheet('dsa41.culture', CultureSheet, {
types: ['Culture'],
makeDefault: true,
label: 'DSA41.CultureLabels.Culture'
})
foundry.documents.collections.Items.registerSheet('dsa41.spezien', SpeciesSheet, {
types: ['Species'],
makeDefault: true,
})
foundry.documents.collections.Items.registerSheet('dsa41.profession', ProfessionSheet, {
types: ['Profession'],
makeDefault: true,
})
foundry.documents.collections.Actors.registerSheet('dsa41.merchant', MerchantSheet, {
types: ['Merchant'],
makeDefault: true,
game.DSA41 = {
...game.DSA41,
...initGlobalAccess()
}
initDocumentClasses(CONFIG)
initUserSettings(game.settings)
initGlobalSettings(game.settings)
initDataModels(CONFIG)
initCombat(CONFIG)
setUpActorSheets(foundry.documents.collections.Actors)
setUpItemSheets(foundry.documents.collections.Items)
loadPartials(foundry.applications.handlebars).then(() => {
})
game.settings.register('DSA_4-1', 'optional_colorfuldice', {
name: "Optional: Farbige Würfel nach Paramanthus",
hint: "Färbt die Würfel je nach Attribut ein",
scope: "client",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: false
})
game.settings.register('DSA_4-1', 'optional_trefferzonen', {
name: "Optional: Trefferzonen",
hint: "Ersetzt das Wundensystem aus dem BRW durch das Trefferzonensystem aus WdH",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: true
})
game.settings.register('DSA_4-1', 'optional_ruestungzonen', {
name: "Optional: Zonenrüstung",
hint: "Ersetzt das Rüstungssystem aus dem BRW durch das Zonenrüstungssystem aus WdH",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: true
})
game.settings.register('DSA_4-1', 'optional_ausdauer', {
name: "Optional: Ausdauerregeln",
hint: "Aktiviert Regeln für das Spiel mit Ausdauer",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: true
})
game.settings.register('DSA_4-1', 'optional_distanzklassen', {
name: "Optional: Distanzklassen",
hint: "Aktiviert Regeln für das Spiel mit Distanzklassen",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: true
})
game.settings.register('DSA_4-1', 'optional_aufstufen_von_liturgien', {
name: "Optional: Aufstufen von Liturgien",
hint: "Aktiviert die Regeln zum Aufstufen von Liturgien",
scope: "world",
config: true,
type: Boolean,
default: false,
disabled: true,
requiresReload: true
})
Handlebars.registerHelper("weight", (data) => {
const baseValue = data * 1000 // to get to gramms (1/1000 Stone)
const stone = Math.floor(baseValue / 1000)
const remainder = baseValue - (stone * 1000)
const ounces = remainder / 25
let stoneRepresentation = ''
let ouncesRepresentation = ''
if (stone > 0) {
stoneRepresentation = `<span class="stone">${stone}</span>`
}
if (ounces > 0) {
ouncesRepresentation = `<span class="ounces">${ounces}</span>`
}
return new Handlebars.SafeString(`<span class="weight">${stoneRepresentation}${ouncesRepresentation}</span>`)
})
Handlebars.registerHelper("fieldTooltip", (...args) => {
const [fieldName, actorId] = args
const actor = game.actors.find(p => p._id === actorId)
let tooltip = ""
if (actor) {
Object.entries(actor.getModificationsOn(fieldName)).forEach(([key, value]) => {
tooltip += `${key}: ${value}<br/>`
})
} else {
tooltip = `${fieldName} not found`
}
return new Handlebars.SafeString(tooltip)
})
Handlebars.registerHelper("currency", (data) => {
// schema for Mittelreich: 1 Ducat = 10 Silver = 100 Kreutzer = 1000 Heller
// internally the price is always given in Silver
// so we need to inflate the value of price by 100 to be able to divide beginning from Heller
const baseValue = data * 100
// then we can regex over it
const currencyRegexp = /(.*)(.)(.)(.)/g
const withDucats = currencyRegexp.exec(baseValue)
let _ = undefined
let ducats = 0
let silver = 0
let kreutzer = 0
let heller = 0
if (withDucats) {
[_, ducats, silver, kreutzer, heller] = withDucats
} else {
const currencyRegexp = /(.)(.)(.)/g
const withSilver = currencyRegexp.exec(baseValue)
if (withSilver) {
[_, silver, kreutzer, heller] = withSilver
} else {
const currencyRegexp = /(.)(.)/g
const withKreutzer = currencyRegexp.exec(baseValue)
if (withKreutzer) {
[_, kreutzer, heller] = withKreutzer
} else {
heller = baseValue
}
}
}
let str = `<span class='coins' data-tooltip="${ducats > 0 ? ducats + ' Dukaten ' : ''}${silver > 0 ? silver + ' Silbertaler ' : ''}${kreutzer > 0 ? kreutzer + ' Kreuzer ' : ''}${heller > 0 ? heller + ' Heller' : ''}">`
if (ducats > 0) {
str += ducats + "<i class='symbol ducat'></i>"
}
if (silver > 0) {
str += silver + "<i class='symbol silver'></i>"
}
if (kreutzer > 0) {
str += kreutzer + "<i class='symbol kreutzer'></i>"
}
if (heller > 0) {
str += heller + "<i class='symbol heller'></i>"
}
str = str + "</span>"
return new Handlebars.SafeString(str)
})
return preloadHandlebarsTemplates();
initHandlebarHelpers(Handlebars)
})
Hooks.once("ready", async function () {
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
Hooks.on("hotbarDrop", (bar, data, slot) => {
return createTalentMacro(data, slot)
});
});
game.DSA41 = {}
initSocketLib(game.DSA41)
Hooks.on("getActorContextOptions", (application, menuItems) => {
menuItems.push({
@ -404,44 +46,7 @@ Hooks.on("getActorContextOptions", (application, menuItems) => {
callback: (li) => {
const actorId = li.getAttribute("data-entry-id")
const actor = game.actors.get(actorId)
//actor.import()
new XmlImportDialog(actor).render(true)
}
})
})
async function createTalentMacro(data, slot) {
if (data.type !== "Item") return;
const uuid = foundry.utils.parseUuid(data.uuid)
const itemId = uuid.id;
const actorId = uuid.primaryId;
const item = await game.actors.get(actorId).items.get(itemId);
// Create the macro command
const command = `game.DSA41.rollItemMacro("${data.uuid}");`;
const macro = await Macro.create({
name: item.name,
type: "script",
img: item.img,
command: command,
flags: {"dsa41.skillMacro": true}
});
game.user.assignHotbarMacro(macro, slot);
return false;
}
function rollItemMacro(_uuid) {
const speaker = ChatMessage.getSpeaker();
const uuid = foundry.utils.parseUuid(_uuid)
const itemId = uuid.id;
const actorId = uuid.primaryId;
let actor = game.actors.get(actorId);
const item = actor ? actor.items.get(itemId) : null;
if (!item) return ui.notifications.warn(`Your controlled Actor does not have an item with id ${itemId}`);
return item.system.roll();
}
})

View File

@ -1,4 +1,4 @@
import BaseItem from "./base-item.mjs";
import BaseItem from "./baseItem.mjs";
const {ArrayField, BooleanField, NumberField, AnyField, StringField, HTMLField} = foundry.data.fields;

View File

@ -1,8 +1,15 @@
import BaseItem from "./base-item.mjs";
import BaseItem from "./baseItem.mjs";
const {ArrayField, SchemaField, BooleanField, NumberField, StringField, HTMLField} = foundry.data.fields;
const {
ArrayField,
SchemaField,
BooleanField,
NumberField,
StringField,
HTMLField
} = foundry.data.fields;
export class VornachteileDataModel extends BaseItem {
export class AdvantageDataModel extends BaseItem {
static defineSchema() {
return {
@ -21,7 +28,7 @@ export class VornachteileDataModel extends BaseItem {
auswahl: new ArrayField(
new SchemaField({
name: new StringField(),
requirement: new ArrayField(
requirements: new ArrayField(
new SchemaField({
attribute: new StringField(),
minValue: new NumberField(),

View File

@ -6,7 +6,8 @@ export const ATTRIBUTE = {
"ff": "Fingerfertigkeit",
"ge": "Gewandtheit",
"ko": "Konstitution",
"kk": "Körperkraft"
"kk": "Körperkraft",
"UNKNOWN": "Fehlende Variante"
}
export const ATTRIBUTE_DESCRIPTIONS = {

View File

@ -138,6 +138,10 @@ export class PlayerCharacterDataModel extends foundry.abstract.TypeDataModel {
key: new StringField(),
notiz: new StringField(),
})),
erschoepfung: new SchemaField({ // only with DSA_4-1.optional_erschoepfung
max: new NumberField({required: true, integer: true}),
aktuell: new NumberField({required: true, integer: true}),
}),
wunden: new SchemaField({
aktuell: new NumberField({required: true, integer: true}), // only with DSA_4-1.optional_trefferzonen = false
max: new NumberField({required: true, integer: true}), // only with DSA_4-1.optional_trefferzonen = false

View File

@ -1,6 +1,9 @@
import BaseItem from "./base-item.mjs";
import BaseItem from "./baseItem.mjs";
const {BooleanField, StringField, HTMLField} = foundry.data.fields;
const {
StringField,
HTMLField
} = foundry.data.fields;
export class CultureDataModel extends BaseItem {

View File

@ -0,0 +1,35 @@
import BaseItem from "./baseItem.mjs";
import {Liturgy} from "../documents/liturgy.mjs";
const {
ArrayField,
NumberField,
StringField,
HTMLField,
SchemaField,
DocumentIdField,
} = foundry.data.fields;
export class DeityDataModel extends BaseItem {
static defineSchema() {
return {
alias: new ArrayField(new StringField()),
description: new HTMLField(),
miracleMinus: new ArrayField(new StringField()),
miraclePlus: new ArrayField(new StringField()),
karmicEnergy: new NumberField({integer: true}),
liturgies: new SchemaField({
rank0: new ArrayField(new StringField()),
rank1: new ArrayField(new StringField()),
rank2: new ArrayField(new StringField()),
rank3: new ArrayField(new StringField()),
rank4: new ArrayField(new StringField()),
rank5: new ArrayField(new StringField()),
rank6: new ArrayField(new StringField()),
rank7: new ArrayField(new StringField()),
rank8: new ArrayField(new StringField()),
}),
}
}
}

View File

@ -1,8 +1,11 @@
import BaseItem from "./base-item.mjs";
import {Equipment} from "../documents/equipment.mjs";
import BaseItem from "./baseItem.mjs";
const {
ArrayField, EmbeddedCollectionField, SchemaField, NumberField, StringField, HTMLField
ArrayField,
SchemaField,
NumberField,
StringField,
HTMLField
} = foundry.data.fields;
export class EquipmentDataModel extends BaseItem {

View File

@ -3,10 +3,8 @@ const {
ObjectField,
NumberField,
StringField,
EmbeddedDocumentField,
DocumentIdField,
ArrayField,
ForeignDocumentField
} = foundry.data.fields;
export class GroupDataModel extends foundry.abstract.TypeDataModel {

View File

@ -1,6 +1,12 @@
import BaseItem from "./base-item.mjs";
import BaseItem from "./baseItem.mjs";
const {BooleanField, NumberField, SchemaField, ArrayField, StringField, HTMLField} = foundry.data.fields;
const {
NumberField,
SchemaField,
ArrayField,
StringField,
HTMLField
} = foundry.data.fields;
export class LiturgyDataModel extends BaseItem {
@ -17,14 +23,14 @@ export class LiturgyDataModel extends BaseItem {
wirkungsdauer: new StringField(),
zauberdauer: new StringField(),
auswirkung: new SchemaField({
I: new StringField(),
II: new StringField(),
III: new StringField(),
IV: new StringField(),
V: new StringField(),
VI: new StringField(),
VII: new StringField(),
VIII: new StringField(),
I: new HTMLField(),
II: new HTMLField(),
III: new HTMLField(),
IV: new HTMLField(),
V: new HTMLField(),
VI: new HTMLField(),
VII: new HTMLField(),
VIII: new HTMLField(),
})
}

View File

@ -1,11 +1,9 @@
const {
SchemaField,
NumberField,
ObjectField,
StringField,
HTMLField,
FilePathField,
DocumentIdField,
ArrayField,
} = foundry.data.fields;

View File

@ -165,7 +165,6 @@ export class LiturgyData {
if (found) {
durationText = this.#ranks[currentDuration].duration
console.log({currentDuration, durationText, adjustedDurationText})
return {currentDuration, durationText, adjustedDurationText}
}

View File

@ -1,6 +1,10 @@
import BaseItem from "./base-item.mjs";
import BaseItem from "./baseItem.mjs";
const {BooleanField, StringField, HTMLField} = foundry.data.fields;
const {
BooleanField,
StringField,
HTMLField
} = foundry.data.fields;
export class ProfessionDataModel extends BaseItem {

View File

@ -1,7 +1,7 @@
import BaseItem from "./base-item.mjs";
import BaseItem from "./baseItem.mjs";
import {evaluateRoll} from "../globals/DSARoll.mjs";
const {
BooleanField,
DocumentIdField,
ArrayField,
NumberField,
@ -131,7 +131,7 @@ export class SkillDataModel extends BaseItem {
let evaluated1 = (await roll1.evaluate())
const dsaDieRollEvaluated = this._evaluateRoll(evaluated1.terms[0].results, {
const dsaDieRollEvaluated = evaluateRoll(evaluated1.terms[0].results, {
taw: this.taw,
werte: [this.probe[0], this.probe[1], this.probe[2]],
})
@ -151,37 +151,4 @@ export class SkillDataModel extends BaseItem {
}
}
_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

@ -1,14 +1,12 @@
import BaseItem from "./base-item.mjs";
import BaseItem from "./baseItem.mjs";
const {
AnyField,
BooleanField,
NumberField,
SchemaField,
ArrayField,
StringField,
HTMLField,
ObjectField
} = foundry.data.fields;
export class SpecialAbilityDataModel extends BaseItem {
@ -35,6 +33,7 @@ export class SpecialAbilityDataModel extends BaseItem {
}))
}),
),
gruppe: new StringField(),
seite: new NumberField(),
aktionsText: new HTMLField(),
text: new HTMLField(),

View File

@ -1,6 +1,12 @@
import BaseItem from "./base-item.mjs";
import BaseItem from "./baseItem.mjs";
const {BooleanField, ArrayField, SchemaField, NumberField, StringField, HTMLField} = foundry.data.fields;
const {
ArrayField,
SchemaField,
NumberField,
StringField,
HTMLField
} = foundry.data.fields;
export class SpeciesDataModel extends BaseItem {

View File

@ -1,6 +1,14 @@
import BaseItem from "./base-item.mjs";
import BaseItem from "./baseItem.mjs";
const {BooleanField, NumberField, SchemaField, ArrayField, StringField, HTMLField} = foundry.data.fields;
const {
SchemaField,
BooleanField,
NumberField,
ArrayField,
StringField,
HTMLField,
ObjectField,
} = foundry.data.fields;
export class SpellDataModel extends BaseItem {
@ -13,9 +21,23 @@ export class SpellDataModel extends BaseItem {
probeMod: new StringField(),
hauszauber: new BooleanField(),
technik: new StringField(),
zauberdauer: new StringField(),
wirkung: new StringField(),
kosten: new StringField(),
zauberdauer: new SchemaField({
min: new StringField(),
normal: new StringField(),
additionalFormula: new StringField(),
variables: new ArrayField(new StringField()),
additionalFormulaTimeUnit: new StringField(),
}),
wirkung: new HTMLField(),
kosten: new ArrayField(
new SchemaField({
min: new NumberField(),
cost: new NumberField(),
additionalFormula: new StringField(),
variables: new ArrayField(new StringField()),
repräsentation: new StringField()
})
),
zielobjekt: new StringField(),
reichweite: new StringField({required: true}),
wirkungsdauer: new StringField({required: true}),
@ -24,8 +46,14 @@ export class SpellDataModel extends BaseItem {
antimagie: new StringField(),
merkmal: new StringField(),
komplexität: new StringField(),
repräsentation: new StringField(),
info: new StringField()
repräsentation: new ObjectField(),
info: new StringField(),
varianten: new ArrayField(new SchemaField({
name: new StringField(),
description: new HTMLField(),
mod: new StringField(),
limit: new NumberField(),
}))
}
}

View File

@ -0,0 +1,91 @@
export const leadingAttribute = {
"Alchimist": "KL",
"Borbaradianer": "KL",
"Druide": "KL",
"Geode (Herren der Erde)": "KL",
"Magier": "KL",
"Scharlatane": "KL",
"Zibilijas": "KL",
"Achaz": "IN",
"Derwisch": "IN",
"Durro-Dûn": "IN",
"Elfe": "IN",
"Ferkina": "IN",
"Geode (Diener Sumus)": "IN",
"Hexe": "IN",
"Schamane": "IN",
"Schelm": "IN",
"Zaubertänzer": "IN"
}
/*
die ohne ZfP Kosten mussen vorher ausgewählt werden
*/
/**
*
* @type {[String: {name: string, description: string, modFn: string, castTimeModFn: string, costModFn: string}]}
*/
export const spoModData = {
"Veränderte Technik": {
name: "Veränderte Technik",
description: "Verändert die Technik",
modFn: "mod -7",
castTimeModFn: "castTime +3",
costModFn: "cost",
},
"Veränderte Technik, zentral": {
name: "Veränderte Technik, zentral",
description: "Verändert die Technik",
modFn: "mod -12",
castTimeModFn: "castTime +3",
costModFn: "cost",
},
"Halbierte Zauberdauer": {
name: "Halbierte Zauberdauer",
description: "Halbiert die Zauberdauer für eine Erschwernis von 5",
modFn: "mod -5",
castTimeModFn: "castTime / 2",
costModFn: "cost",
},
"Verdoppelte Zauberdauer": {
name: "Verdoppelte Zauberdauer",
description: "Verdoppelt die Zauberdauer für eine Erleichterung von 3",
modFn: "mod +3",
castTimeModFn: "castTime *2",
costModFn: "cost",
},
"Erzwingen": {
name: "Erzwingen",
description: "Verringert Erschwernis um 1 je quadrierten AsP Punkt",
modFn: "mod +1",
castTimeModFn: "castTime +1",
costModFn: "cost ** cost",
},
"Kosten einsparen": {
name: "Kosten einsparen",
description: "Reduziert die Kosten des Zaubers um 10% für jede zusätzlich aufgewendete Aktion",
modFn: "mod -3",
castTimeModFn: "castTime +1",
costModFn: "cost - Math.max(cost * 0.1, 1)", // at least a reduction of 1 AsP
},
// more to come
"Vergrößerung von Reichweite oder Wirkungsradius": {
name: "Vergrößerung von Reichweite oder Wirkungsradius",
description: "Vergrößert die Reichweite oder wenn möglich den Wirkungsradius auf kosten von Aktionen",
modFn: "mod -5",
castTimeModFn: "castTime +1",
costModFn: "cost",
},
"Verkleinerung von Reichweite oder Wirkungsradius": {
name: "Verkleinerung von Reichweite oder Wirkungsradius",
description: "Verkleinert die Reichweite oder wenn möglich den Wirkungsradius auf kosten von Aktionen",
modFn: "mod -3",
castTimeModFn: "castTime +1",
costModFn: "cost",
},
}

View File

@ -1,8 +1,9 @@
import {LiturgyData} from "../data/miracle/liturgydata.mjs";
import {Talent} from "../data/talent.mjs";
import {ATTRIBUTE, ATTRIBUTE_DESCRIPTIONS} from "../data/attribute.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
export class AttributeDialog extends HandlebarsApplicationMixin(ApplicationV2) {

View File

@ -1,7 +1,9 @@
import {ActionManager} from "../sheets/actions/action-manager.mjs";
import {Talent} from "../data/talent.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
/**
@ -179,7 +181,7 @@ export class BattleDialog extends HandlebarsApplicationMixin(ApplicationV2) {
async _prepareContext(options) {
const context = await super._prepareContext(options)
context.actors = game.actors.filter(actor => actor.type === "character" || actor.type === "creature")
context.actors = game.actors.filter(actor => actor.type === "Character" || actor.type === "Creature")
context.offenseTalent = this._offenseTalent ?? ''
context.offenseTalents = {}

View File

@ -1,6 +1,9 @@
import {ActionManager} from "../sheets/actions/action-manager.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
/**
@ -47,40 +50,60 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
*/
_actor = null
constructor(actor) {
constructor(actor, data) {
super();
this._actor = actor
this._targetId = null
this._skillId = null
this._weaponId = null
this._skillId = data.skill ? data.skill : null
this._weaponId = data.weapon ? data.weapon : null
this._defenseManeuverId = null
this._actionManager = new ActionManager(this._actor)
CombatActionDialog._instance = this
}
static _instance = null
static async #onSelectTarget(event, target) {
async #processOnSelectTarget(event, target) {
const {targetId} = target.dataset
this._targetId = this._targetId === targetId ? null : targetId
this.render({parts: ["form"]})
}
static async #onSelectManeuver(event, target) {
static async #onSelectTarget(event, target) {
event.preventDefault()
CombatActionDialog._instance.#processOnSelectTarget(event, target)
}
async #processOnSelectManeuver(event, target) {
const {maneuverId} = target.dataset
this._defenseManeuverId = this._defenseManeuverId === maneuverId ? null : maneuverId
this.render({parts: ["form"]})
}
static async #onSelectWeaponAndSkill(event, target) {
static async #onSelectManeuver(event, target) {
event.preventDefault()
CombatActionDialog._instance.#processOnSelectManeuver(event, target)
}
async #processOnSelectWeaponAndSkill(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) {
static async #onSelectWeaponAndSkill(event, target) {
event.preventDefault()
const maneuver = this.#evaluateManeuvers().find(p => p.id === this._defenseManeuverId)
CombatActionDialog._instance.#processOnSelectWeaponAndSkill(event, target)
}
async #processOnSubmitForm(event, form, formData) {
const maneuver = CombatActionDialog._instance.#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)
@ -89,7 +112,8 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
weapon: this._weaponId,
skill: this._skillId,
target: this._targetId,
maneuver,
title: maneuver.name,
maneuver: maneuver.activate.toString(),
mod: this._mod,
circumstance: this._circumstance,
penalty: this._penalty,
@ -102,12 +126,14 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
/** @type Cooldown */
const newCooldown = {
start: maneuver.cooldown({weapon, skill, target, mod: this._mod}),
current: maneuver.cooldown({weapon, skill, target, mod: this._mod}),
current: 0,
data: {
cssClass: "Kampf",
weapon: this._weaponId,
skill: this._skillId,
target: this._targetId,
maneuver,
title: maneuver.name,
maneuver: maneuver.activate.toString(),
mod: this._mod,
circumstance: this._circumstance,
penalty: this._penalty,
@ -126,6 +152,12 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
}
}
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
CombatActionDialog._instance.#processOnSubmitForm(event, form, formData)
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
if (options.window) {
@ -256,9 +288,9 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
const context = await super._prepareContext(options)
context.actor = this._actor
context.distanceUnit = game.scenes.current.grid.units
context.distanceUnit = game.scenes.current?.grid.units
if (this._actor.getActiveTokens()[0]?.id) {
if (context.distanceUnit && this._actor.getActiveTokens()[0]?.id) {
context.tokenDistances = this.#evaluateDistances()
context.weapons = this.#evaluateWeapons()
@ -275,7 +307,28 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
// TODO get W/M of weapon NOW
context.ready = this._targetId && this._weaponId && this._skillId && this._defenseManeuverId
if (this._targetNumber >= 0 && this._targetId && this._weaponId && this._skillId && maneuver) {
context.ready = true
} else {
context.notReadyReason = `<em>${game.i18n.format("COMBAT_DIALOG.notReadyReason.title")}</em><ul>`
if (!this._targetId) {
context.notReadyReason += `<li>${game.i18n.format("COMBAT_DIALOG.notReadyReason.noTarget")}</li>`
}
if (!this._weaponId) {
context.notReadyReason += `<li>${game.i18n.format("COMBAT_DIALOG.notReadyReason.noWeapon")}</li>`
}
if (!this._skillId) {
context.notReadyReason += `<li>${game.i18n.format("COMBAT_DIALOG.notReadyReason.noSkill")}</li>`
}
if (!maneuver) {
context.notReadyReason += `<li>${game.i18n.format("COMBAT_DIALOG.notReadyReason.noManeuver")}</li>`
}
if (!this._targetNumber < 0) {
context.notReadyReason += `<li>${game.i18n.format("COMBAT_DIALOG.notReadyReason.impossible")}</li>`
}
context.notReadyReason += "</ul>"
context.ready = false
}
return context
} else {
ui.notifications.error(`Feature funktioniert nur wenn der Akteur ein Token auf der aktuellen Szene hat`);
@ -303,12 +356,10 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
target.textContent = `(${result})`
targetDescription.textContent = this._modDescription
if (result <= 0) {
context.ready = false
if (result <= 0 || !context.ready) {
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")
}

View File

@ -1,6 +1,9 @@
import {ActionManager} from "../sheets/actions/action-manager.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
/**
@ -46,16 +49,16 @@ export class DefenseActionDialog extends HandlebarsApplicationMixin(ApplicationV
*/
_actor = null
constructor(actor, attackData) {
constructor(actor, data, attackData) {
super();
this._attackData = attackData ?? {
/*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._skillId = data.skill ? data.skill : null
this._weaponId = data.weapon ? data.weapon : null
this._defenseManeuverId = null
this._actionManager = new ActionManager(this._actor)
//if (this._actor) {
@ -251,7 +254,26 @@ export class DefenseActionDialog extends HandlebarsApplicationMixin(ApplicationV
// TODO get W/M of weapon NOW
context.ready = this._targetId && this._weaponId && this._skillId && this._defenseManeuverId
if (this._weaponId && this._skillId && this._defenseManeuverId) {
context.ready = true
} else {
context.notReadyReason = `<em>${game.i18n.format("COMBAT_DIALOG.notReadyReason.title")}</em><ul>`
if (!this._weaponId) {
context.notReadyReason += `<li>${game.i18n.format("COMBAT_DIALOG.notReadyReason.noWeapon")}</li>`
}
if (!this._skillId) {
context.notReadyReason += `<li>${game.i18n.format("COMBAT_DIALOG.notReadyReason.noSkill")}</li>`
}
if (!maneuver) {
context.notReadyReason += `<li>${game.i18n.format("COMBAT_DIALOG.notReadyReason.noManeuver")}</li>`
}
if (!this._targetNumber < 0) {
context.notReadyReason += `<li>${game.i18n.format("COMBAT_DIALOG.notReadyReason.impossible")}</li>`
}
context.notReadyReason += "</ul>"
context.ready = false
}
return context
} else {
ui.notifications.error(`Feature funktioniert nur wenn der Akteur ein Token auf der aktuellen Szene hat`);
@ -279,12 +301,10 @@ export class DefenseActionDialog extends HandlebarsApplicationMixin(ApplicationV
target.textContent = `(${result})`
targetDescription.textContent = this._modDescription
if (result <= 0) {
context.ready = false
if (result <= 0 || !context.ready) {
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")
}

View File

@ -0,0 +1,243 @@
import {Equipment} from "../documents/equipment.mjs";
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
export class ItemBrowserDialog extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
classes: ['dsa41', 'dialog', 'item-browser'],
tag: "form",
form: {
submitOnChange: true,
closeOnSubmit: false,
handler: ItemBrowserDialog.#onSubmitForm
},
position: {
width: 640,
height: 480
},
window: {
resizable: true,
title: "Gegenstände Browser"
},
actions: {
select: ItemBrowserDialog.#selectItem,
buy: ItemBrowserDialog.#buyItem
}
}
static PARTS = {
form: {
template: 'systems/DSA_4-1/templates/dialog/item-browser-dialog.hbs',
}
}
/**
* @type {Actor}
* @private
*/
_actor = null
/**
*
* @type {[Equipment]}
* @private
*/
_items = []
_selectedItem = null
filter_price_lower = 0
filter_price_upper = 0
filter_weight_lower = 0
filter_weight_upper = 0
filter_name = ""
filter_category = ""
constructor(actor) {
super();
this._actor = actor
this._items = []
this._selectedItem = null
}
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
this.filter_price_lower = formData.object.filter_price_lower
this.filter_price_upper = formData.object.filter_price_upper
this.filter_weight_lower = formData.object.filter_weight_lower
this.filter_weight_upper = formData.object.filter_weight_upper
this.filter_name = formData.object.filter_name
this.filter_category = formData.object.filter_category
this.render({parts: ["form"]})
}
static async #selectItem(event, target) {
const {itemId} = target.dataset
const selectedItem = this._items.find(item => item.uuid === itemId)
this._items?.forEach((item) => {
item.selected = item.uuid === itemId
})
if (selectedItem) {
this._selectedItem = selectedItem
}
this.render({parts: ["form"]})
}
static async #buyItem(event, target) {
if (this._actor && this._selectedItem) {
const canBuy = await this._actor.reduceWealth(this._selectedItem.price)
if (canBuy) {
const document = await foundry.utils.fromUuid(this._selectedItem.uuid)
if (document) {
await this._actor.createEmbeddedDocuments("Item", [document])
ui.notifications.info(this._selectedItem.name + " wurde von " + this._actor.name + " gekauft")
}
} else {
ui.notifications.error(this._selectedItem.name + " ist zu teuer für " + this._actor.name)
}
}
}
_canDragDrop(event, options) {
return game.user.isGM
}
_canDrag(event, options) {
return true
}
/**
* An event that occurs when a drag workflow begins for a draggable item on the sheet.
* @param {DragEvent} event The initiating drag start event
* @returns {Promise<void>}
* @protected
*/
async _onDragStart(event) {
const target = event.currentTarget;
let dragData;
if (target.dataset.itemId) {
dragData = {
type: "Item",
uuid: target.dataset.itemId
}
}
// Set data transfer
if (!dragData) return;
event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
}
async _prepareContext(options) {
const context = await super._prepareContext(options)
context.categories = {
"": "",
"Gegenstand": "Gegenstand",
"Nahkampfwaffe": "Nahkampfwaffe",
"Fernkampfwaffe": "Fernkampfwaffe",
"Munition": "Munition",
"Währung": "Währung"
}
context.filterName = this.filter_name
context.filterCategory = this.filter_category
context.filter_price_lower = this.filter_price_lower ?? this._minPrice
context.filter_price_upper = this.filter_price_upper ?? this._maxPrice
context.filter_weight_lower = this.filter_weight_lower ?? this._minWeight
context.filter_weight_upper = this.filter_weight_upper ?? this._maxWeight
context.price_lower = this._minPrice
context.price_upper = this._maxPrice
context.weight_lower = this._minWeight
context.weight_upper = this._maxWeight
context.hasSelectedItem = this._selectedItem != null
context.items = this._items
?.filter(p => p.name.toLowerCase().indexOf(context.filterName.toLowerCase()) !== -1 || context.filterName === "")
?.filter(p => p.category.indexOf(context.filterCategory) !== -1 || context.filterCategory === "")
?.filter(p => Number(context.filter_price_lower) <= p.price && p.price <= Number(context.filter_price_upper))
?.filter(p => Number(context.filter_weight_lower) <= p.weight && p.weight <= Number(context.filter_weight_upper))
return context
}
#updateProgress(compendiumName, current, max) {
if (compendiumName && current && max) {
this.element.querySelector('.progress').style.display = 'block';
this.element.querySelector('.progress .fill').style.width = (current / max * 100) + "%";
this.element.querySelector('.progress .text').textContent = game.i18n.format("ITEM_BROWSER.progress", {
compendium: compendiumName,
current: current,
max: max
})
} else {
this.element.querySelector('.progress').style.display = 'none';
}
}
async _onRender(context, options) {
if (this._items.length === 0) {
const compendia = [
game.packs.get('DSA_4-1.Armor'),
game.packs.get('DSA_4-1.Weapons'),
game.packs.get('DSA_4-1.Ammunition'),
game.packs.get('DSA_4-1.Items')
]
let totalEntries = compendia.reduce((p, c) => p + c.index.size, 0)
let parsedEntries = 0
let currentCompendiumName = ""
for (const c of compendia) {
const it = await c.getDocuments()
currentCompendiumName = c.metadata.label
it.forEach((item) => {
const uuid = item.uuid
const e = new Equipment(item)
this._items.push({
img: item.img,
uuid,
type: item.type,
name: item.name,
price: e.system.price,
weight: e.system.weight,
category: e.system.category.join(", "),
selected: false,
})
parsedEntries += 1
this.#updateProgress(currentCompendiumName, parsedEntries, totalEntries)
})
}
this._minPrice = Math.min(...this._items.map(item => item.price))
this._maxPrice = Math.max(...this._items.map(item => item.price))
this._minWeight = Math.min(...this._items.map(item => item.weight))
this._maxWeight = Math.max(...this._items.map(item => item.weight))
this.#updateProgress()
this.render({parts: ["form"]})
}
new foundry.applications.ux.DragDrop.implementation({
dropSelector: ".window-content",
dragSelector: ".item",
permissions: {
drag: this._canDrag.bind(this)
},
callbacks: {
dragstart: this._onDragStart.bind(this),
}
}).bind(this.element)
}
}

View File

@ -1,7 +1,10 @@
import {LiturgyData} from "../data/miracle/liturgydata.mjs";
import {LiturgyData} from "../data/miracle/liturgyData.mjs";
import {Talent} from "../data/talent.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
export class LiturgyDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -135,10 +138,55 @@ export class LiturgyDialog extends HandlebarsApplicationMixin(ApplicationV2) {
const castingTime = this.#normalizeCastingTime(this._liturgy)
//TODO push it into the sun eeerh cooldown queue
//if (castingTime > 0) {
if (castingTime > 0) {
const cooldowns = this._actor.system.cooldowns
let m = (queue, data) => {
new game.DSA41.Talent({
name: data.title,
taw: data.taw,
mod: data.mod,
eigenschaften: {
mu: data.eigenschaften.mu,
in: data.eigenschaften.in,
ch: data.eigenschaften.ch,
},
eigenschaft1: "mu",
eigenschaft2: "in",
eigenschaft3: "ch"
}).evaluate("publicroll").then(result => {
// this._actor.system.cooldowns.push()
//} else {
result.evaluatedRoll.toMessage({
speaker: ChatMessage.getSpeaker({actor: game.actors.get(data.actorId)}),
flavor: `Liturgie: ${data.title}<br/>LkP*: ${result.tap}<br/>${result.meisterlich ? "Meisterlich" : ""}${result.patzer ? "Petzer" : ""}<br/>${data.variant}`,
})
})
}
cooldowns.push({
start: castingTime,
current: 0,
data: {
cssClass: "Karmal",
title: this._liturgy.name,
taw: lkp,
mod: mod,
actorId: this._actor._id,
variant: this._variation.effect,
eigenschaften: {
mu: this._actor.system.attribute.mu.aktuell,
in: this._actor.system.attribute.in.aktuell,
ch: this._actor.system.attribute.ch.aktuell,
},
eigenschaft1: "mu",
eigenschaft2: "in",
eigenschaft3: "ch",
circumstance: circumstance,
maneuver: m.toString()
}
})
await this._actor.update({"system.cooldowns": cooldowns})
ui.notifications.info(`Neue Aktion für ${this._liturgy.name} mit Abklingzeit von ${castingTime} Aktionen hinzugefügt`);
} else {
const result = await new Talent({
name: this._liturgy.name,
taw: lkp,
@ -157,7 +205,7 @@ export class LiturgyDialog extends HandlebarsApplicationMixin(ApplicationV2) {
speaker: ChatMessage.getSpeaker({actor: this._actor}),
flavor: `Liturgie: ${this._liturgy.name}<br/>Zauberdauer: ${castingTime > 0 ? castingTime + " Aktionen" : resultingLiturgy.castduration}<br/>LkP*: ${result.tap}<br/>${result.meisterlich ? "Meisterlich" : ""}${result.patzer ? "Petzer" : ""}<br/>${this._variation.effect}`,
})
//}
}
this.close()
}
@ -175,7 +223,7 @@ export class LiturgyDialog extends HandlebarsApplicationMixin(ApplicationV2) {
const [_, actions] = castingTime.match(stoßgebetRegExp)
return actions
} else if (castingTime.match(gebetRegExp)) {
const [_, actions] = castingTime.match(stoßgebetRegExp)
const [_, actions] = castingTime.match(gebetRegExp)
return actions * 20
} else if (castingTime.match(invalidForCooldownRegExp)) {
return -1

View File

@ -1,6 +1,7 @@
import {XmlImport} from "../xml-import/xml-import.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -98,6 +99,7 @@ export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
lepMod: [-1, -2, -3, -4, -5],
display: "range",
noLabel: "Gutes Wetter",
nestingLevel: 1,
labels: [
"Schlechtes Wetter I",
"Schlechtes Wetter II",
@ -113,6 +115,7 @@ export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
aspMod: -1,
lepMod: -1,
display: "boolean",
nestingLevel: 1,
group: "bad_camp",
active: false,
value: "on"
@ -219,6 +222,14 @@ export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
const elementInMod = this.element.querySelector('output[name="inMod"]')
const elementWoundMod = this.element.querySelector('output[name="woundMod"]')
if (this.restingType === this.#type.DRAUßEN) {
this.element.querySelector('input[name="bad_weather"]').removeAttribute('disabled')
this.element.querySelector('input[name="bad_camp"]').removeAttribute('disabled')
} else {
this.element.querySelector('input[name="bad_weather"]').setAttribute('disabled', 'disabled')
this.element.querySelector('input[name="bad_camp"]').setAttribute('disabled', 'disabled')
}
const context = this.#updateData()
elementLepMod.value = context.lepModDisplay
elementKoMod.value = context.koRollDisplay
@ -333,8 +344,6 @@ export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
context.inRollDisplay = `1w20+${this.regInMod}`
}
console.log(this, context)
return context
}

View File

@ -0,0 +1,584 @@
import {ATTRIBUTE} from "../data/attribute.mjs";
import {spoModData, leadingAttribute} from "../data/spellData/spellData.mjs";
import {evaluateRoll} from "../globals/DSARoll.mjs";
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
export class SpellDialog extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
classes: ['dsa41', 'dialog', 'spell'],
tag: "form",
position: {
width: 480,
height: 800
},
window: {
resizable: false,
title: "Zauber wirken"
},
form: {
submitOnChange: true,
closeOnSubmit: false,
handler: SpellDialog.#onSubmitForm
},
actions: {
cast: SpellDialog.#cast,
diceRoll: SpellDialog.#diceRoll
}
}
static PARTS = {
form: {
template: 'systems/DSA_4-1/templates/dialog/spell-dialog.hbs',
}
}
static data = {}
/**
*
* @type {Actor}
* @private
*/
_actor = null
_costMutators = {}
_castTimeMutators = {}
_variants = {}
_costModel = {}
_castTimeModel = {}
_spoMods = {}
displayModResult = 0
constructor(actor, spellId) {
super()
this._actor = actor
this._spell = this._actor.itemTypes["Spell"].find(p => p._id === spellId)
this._circumstance = 0
this._mods = []
this.mod = 0
this._costMutators = {}
this._castTimeMutators = {}
this._selectedRepresentation = this._spell.getFlag("DSA_4-1", "representation")
this._spellDie = null
this._variants = {}
this._costModel = this._spell.system.kosten.find(c => c.repräsentation === context.selectedRepresentation) ?? this._spell.system.kosten.find(c => c.repräsentation === "")
this._castTimeModel = this._spell.system.zauberdauer
this._castTimeMutators = {}
this._costMutators = {}
this._costModel.variables.forEach(v => this._costMutators[v] = 0)
this._castTimeModel.variables.forEach(v => this._castTimeMutators[v] = 0)
this.cost = this.normalizeCastingCost() ?? 0
this.castingTime = this.#normalizeCastingTime(this._spell)
this.zfp = null
this.zfpDetermined = false
if (this._selectedRepresentation) {
this._costModel = this._spell.system.kosten.find(c => c.repräsentation === context.selectedRepresentation) ?? this._spell.system.kosten.find(c => c.repräsentation === "")
this._castTimeModel = this._spell.system.zauberdauer
this._castTimeMutators = {}
this._costMutators = {}
this._costModel.variables.forEach(v => this._costMutators[v] = 0)
this._castTimeModel.variables.forEach(v => this._castTimeMutators[v] = 0)
}
}
/**
* @typedef FormulaData
* @property {String} additionalFormula mathematical expression that can be eval'd by replacing the variables with the user added input
* @property {[String]} variables contains all tokens which will be replaceable inside the formula
* @property {[String: Number]} substitutions user input with which the variables with the same key will be replaced in the formula text
* @property {"Aktionen"|"SR"} unit gives the evaluated formula its appropriate time unit
*/
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
// handle changes in variable Inputs
this._selectedRepresentation = formData.object.representation ?? this._selectedRepresentation
this._variants = foundry.utils.expandObject(formData.object)["variants"] ?? this._variants
if (this._spell.system.probe.includes("*")) { // ATTRIBUTO
if (this._variants["Mut"]) {
this._spellDie = "MU"
} else if (this._variants["Klugheit"]) {
this._spellDie = "KL"
} else if (this._variants["Intuition"]) {
this._spellDie = "IN"
} else if (this._variants["Charisma"]) {
this._spellDie = "CH"
} else if (this._variants["Fingerfertigkeit"]) {
this._spellDie = "FF"
} else if (this._variants["Gewandtheit"]) {
this._spellDie = "GE"
} else if (this._variants["Konstitution"]) {
this._spellDie = "KO"
} else if (this._variants["Konstitution"]) {
this._spellDie = "KK"
} else {
this._spellDie = null
}
}
let costMutators = foundry.utils.expandObject(formData.object)["costMutators"] ?? this._costMutators
if (costMutators) {
this._costMutators = costMutators
}
this.cost = this.normalizeCastingCost()
let castTimeMutators = foundry.utils.expandObject(formData.object)["castTimeMutators"] ?? this._castTimeMutators
this._castTimeMutators = castTimeMutators
this.mod = 0
this._activeVariants = Object.entries(this._variants)
.filter(([key, truthiness]) => truthiness)
.map(([key, truthiness]) => this._spell.system.varianten.find(v => v.name === key))
this._activeVariants.forEach(variant => {
if (variant.mod) {
this.mod += Number(variant.mod)
}
})
this.castingTime = this.#normalizeCastingTime(this._spell, this._castTimeMutators)
// eval probeMod
if (formData.object["checkMod"]) {
this.mod -= formData.object["checkMod"]
this._checkModValue = formData.object["checkMod"]
}
// eval spomods
this._spoMods = foundry.utils.expandObject(formData.object)["spoMods"] ?? {}
let totalMod = this.mod
let totalCost = this.cost
let totalCastingTime = Number(this.castingTime)
Object.entries(this._spoMods).forEach(([modName, times]) => {
const actualMod = spoModData[modName]
for (let i = 0; i < times; i++) {
const ctfn = new Function("castTime", "return " + actualMod.castTimeModFn)
totalCastingTime = ctfn(totalCastingTime)
const cfn = new Function("cost", "return " + actualMod.costModFn)
totalCost = cfn(totalCost)
const zmfn = new Function("mod", "return " + actualMod.modFn)
totalMod = zmfn(totalMod)
}
})
this.mod = totalMod
this.cost = totalCost
this.castingTime = totalCastingTime
this.render({parts: ["form"]})
}
static async #cast(event, target) {
ChatMessage.create({
user: game.user._id,
speaker: {actor: this._actor},
content: `beginnt ${this._spell.name} zu wirken`,
type: CONST.CHAT_MESSAGE_TYPES.IC
})
const cooldowns = this._actor.system.cooldowns
let m = (queue, data) => {
ChatMessage.create({
user: game.user._id,
speaker: {actor: this._actor},
content: data.message,
type: CONST.CHAT_MESSAGE_TYPES.IC
})
}
let message = this._spell.system.wirkung
if (this._activeVariants.length > 0) {
message += "<hr/>"
message += this._activeVariants.map(v => v.name).join(", ")
}
if (Object.keys(this._spoMods).length > 0) {
message += "<hr/>"
Object.entries(this._spoMods).forEach(([modName, times]) => {
if (times > 0) {
message += times + "x" + modName + "<br/>"
}
})
}
if (Object.keys({...this._castTimeMutators, ...this._costMutators}).length > 0) {
message += "<hr/>"
Object.entries({...this._castTimeMutators, ...this._costMutators}).forEach(([mutatorName, mutatorValue]) => {
message += mutatorName + ": " + mutatorValue + "<br/>"
})
}
message += "<hr/>" + this.zfp + " ZfP*<br/>" + this._spell.system.zfw + " ZfW"
cooldowns.push({
start: this.castingTime,
current: 0,
data: {
cssClass: "Magisch",
title: this._spell.name,
taw: this.zfp,
mod: 0,
actorId: this._actor._id,
spellId: this._spell._id,
message,
maneuver: m.toString()
}
})
await this._actor.update({"system.cooldowns": cooldowns})
}
static async #diceRoll(event, target) {
const result = await evaluateRoll(
"3d20",
{
value: this._spell.system.zfw + this.mod,
werte: this.#getProbenWerte(),
owner: this._actor
}
)
if (result.tap >= 0) { // erfolg
await result.evaluated.toMessage({
speaker: ChatMessage.getSpeaker({actor: this._actor}),
flavor: ` ${result.meisterlich ? 'Meisterlich geschafft' : 'Geschafft'} mit ${result.tap} Punkten übrig`,
})
} else { // misserfolg
await result.evaluated.toMessage({
speaker: ChatMessage.getSpeaker({actor: this._actor}),
flavor: ` ${result.meisterlich ? 'Gepatzt' : ''} mit ${Math.abs(result.tap)} Punkten daneben`,
})
}
this.zfp = result.tap
this.zfpDetermined = true
this.render({parts: ["form"]})
}
normalizeCastingCost() {
let costFormula = this._costModel.additionalFormula
if (costFormula) {
this._costModel.variables.forEach(v => {
costFormula = costFormula.replace(v, this._costMutators[v])
})
costFormula = Number(eval(costFormula)) + Number(this._costModel.cost)
} else {
costFormula = this._costModel.cost
}
if (costFormula <= this._costModel.min) {
costFormula = this._costModel.min
}
return costFormula
}
/**
*
* @param spell
* @param {FormulaData} additionalFormulaData
* @returns {number|*}
*/
#normalizeCastingTime(spell, additionalFormulaData) {
// min: Wenn ein Zauber eine mindest dauer hat kann diese nachdem diese abgelaufen ist jederzeit abgebrochen werden
// normal: Standard Zauberzeit eines Zaubers
// additionalFormulaData: enthält die zur Normalzeit zusätzlichen Zauberdauer
const castingTime = spell.system.zauberdauer.normal ?? 0
let baseCastTime = 0
const minCastingTime = spell.system.zauberdauer.min ?? 0
let baseMinCastTime = 0
if (castingTime) {
baseCastTime = castingTime.replace(/(.*) Aktionen/g, (_, aktionen) => {
return aktionen
})
baseCastTime = baseCastTime.replace(/(.*) SR/g, (_, aktionen) => {
return aktionen * 20
})
}
if (minCastingTime) {
baseMinCastTime = minCastingTime.replace(/(.*) Aktionen/g, (_, aktionen) => {
return aktionen
})
baseMinCastTime = baseMinCastTime.replace(/(.*) SR/g, (_, aktionen) => {
return aktionen * 20
})
}
let actualCastingTime = 0
let formula = spell.system.zauberdauer.additionalFormula
if (formula) {
Object.entries(additionalFormulaData).forEach(([variableName, variableValue]) => {
formula = formula.replaceAll(variableName, variableValue)
})
if (spell.system.zauberdauer.additionalFormulaTimeUnit == "Aktionen") {
actualCastingTime = (Number(baseCastTime) + Number(eval(formula)) ?? 0)
} else {
actualCastingTime = (Number(baseCastTime) + (Number(eval(formula)) * 20) ?? 0)
}
} else {
actualCastingTime = baseCastTime
}
if (Number(actualCastingTime) <= Number(baseMinCastTime)) {
actualCastingTime = baseMinCastTime
}
return actualCastingTime
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
if (options.window) {
if (this._spell) {
options.window.title = `${this._spell.name} [${this._spell.system.zfw}]`
}
}
return options
}
#getProbenWerte() {
let dice = []
this._spell.system.probe.map(p => {
if (p === "*") {
return this._spellDie ?? null
} else {
return p
}
}).forEach(p => {
if (p !== null) {
dice.push(
this._actor.system.attribute[p.toLowerCase()].aktuell
)
} else {
dice.push(
"??"
)
}
})
return dice
}
async _prepareContext(options) {
const context = await super._prepareContext(options)
context.actor = this._actor
context.spell = this._spell
context.representationOptions = {}
context.selectedRepresentation = this._selectedRepresentation
context.text = this._spell.system.wirkung
context.dice = []
context.colorfulDice = game.settings.get('DSA_4-1', 'optional_colorfuldice')
context.modResult = this._spell.system.zfw + this.mod
context.penalty = (this.mod > 0 ? "+" : "") + this.mod
context.displayModResult = (context.modResult > 0 ? "+" : "") + context.modResult
context.castingTime = this.castingTime
context.ready = true
context.zfpDetermined = this.zfpDetermined
// variable probe (should consider Achaz as they can replace one KL in a KL/KL/* spell with IN
this._spell.system.probe.map(p => {
if (p === "*") {
return this._spellDie ?? null
} else {
return p
}
}).forEach(p => {
if (p !== null) {
context.dice.push({
wert: this._actor.system.attribute[p.toLowerCase()].aktuell,
name: p,
tooltip: ATTRIBUTE[p.toLowerCase()],
})
} else {
context.dice.push({
wert: "??",
name: "??",
tooltip: ATTRIBUTE["UNKNOWN"],
})
}
})
context.variants = this._spell.system.varianten.map(v => {
return {
variantText: v.description,
variantName: v.name,
variantPenalty: v.mod ?? "0",
variantChecked: this._variants[v.name]
}
})
// Repräsentation
context.representationOptions[""] = ""
Object.entries(this._spell.system.repräsentation).forEach(([key, value]) => {
context.representationOptions[key] = key
})
if (!this._selectedRepresentation) {
context.ready = false
}
// Costs and Mutators
context.castingCosts = this.cost
// set probe to current held probe variables or take from _spell
context.costMutators = this._costMutators
if (this._costModel) {
context.costVariables = this._costModel.variables
} else {
context.costVariables = []
}
// probeMod
if (this._spell.system.probeMod) {
context.checkModTest = this._spell.system.probeMod
context.checkModValue = this._checkModValue
}
// SpoMods
context.spoModCount = Object.values(this._spoMods).reduce((previousValue, currentValue) => previousValue + currentValue, 0)
context.maxSpoModCount = 0
if (this._selectedRepresentation) {
const leadingAttributKey = leadingAttribute[this._selectedRepresentation]
context.maxSpoModCount = (this._actor.system.attribute[leadingAttributKey.toLowerCase()].aktuell ?? 0) - 12
if (context.maxSpoModCount < 0) {
context.maxSpoModCount = 0
}
}
if (context.spoModCount > context.maxSpoModCount) {
context.ready = false
}
const mapper = (spoModName) => {
let data = spoModData[spoModName]
let value = this._spoMods[data.name] ?? 0
let totalModValue = data.mod * value
return {
...data,
value,
totalModValue
}
}
context.spoMods = []
if (this._spell.system.modifikationen) {
this._spell.system.modifikationen.split(",").forEach(spoMod => {
switch (spoMod.trim()) {
case "Zauberdauer":
context.spoMods.push(mapper("Halbierte Zauberdauer"))
context.spoMods.push(mapper("Verdoppelte Zauberdauer"))
break;
case "Kosten":
context.spoMods.push(mapper("Kosten einsparen"))
break;
case "Reichweite":
context.spoMods.push(mapper("Verkleinerung von Reichweite oder Wirkungsradius"))
context.spoMods.push(mapper("Vergrößerung von Reichweite oder Wirkungsradius"))
break;
}
})
}
// if this.zfp is null then we are in the first step pre dice roll
if (this.zfp == null) {
context.ready = false
context.diceRoll = true
} else {
if (this.zfp === 0) {
this.zfp = 1
}
let zfpMod = 0
Object.entries(this._spoMods).forEach(([modName, times]) => {
const actualMod = spoModData[modName]
for (let i = 0; i < times; i++) {
const zmfn = new Function("mod", "return " + actualMod.modFn)
zfpMod = zmfn(zfpMod)
}
})
if (this.zfp + zfpMod > this._spell.system.zfw) { // cant be higher than the learnt level
context.zfpModified = this._spell.system.zfw
} else {
context.zfpModified = this.zfp + zfpMod
}
context.spellName = this._spell.system.name
context.variant = context.variants.filter(v => v.variantChecked).map(v => `<em>${v.variantName}</em>&mdash;${v.variantText}`).join("<br/><br/>")
if (context.zfpModified < 0) {
context.ready = false
}
}
if (!context.ready) { // rules have changed, it cant be cast when zfp - selected mutators is below 0
context.notReadyReasons = `<em>${game.i18n.format("SPELL_DIALOG.notReadyReason.title")}</em><ul>`
if (this.zfp == null) {
context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.noZFPDataAvailable")}</li>`
}
if (context.zfpModified < 0) {
context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.overspentZFP")}</li>`
}
if (context.spoModCount > context.maxSpoModCount) {
context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.tooManySpoMods")}</li>`
}
if (!this._selectedRepresentation) {
context.notReadyReasons += `<li>${game.i18n.format("SPELL_DIALOG.notReadyReason.noRepresentation")}</li>`
}
context.notReadyReasons += "</ul>"
}
return context
}
async _onRender(context, options) {
}
}

View File

@ -1,8 +1,10 @@
import {LiturgyData} from "../data/miracle/liturgydata.mjs";
import {Talent} from "../data/talent.mjs";
import {ATTRIBUTE} from "../data/attribute.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
export class TalentDialog extends HandlebarsApplicationMixin(ApplicationV2) {

View File

@ -1,6 +1,9 @@
import {XmlImport} from "../xml-import/xml-import.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api
const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
export class XmlImportDialog extends HandlebarsApplicationMixin(ApplicationV2) {

View File

@ -1,5 +1,5 @@
import {LiturgyData} from "../data/miracle/liturgydata.mjs";
import {Zonenruestung, Zonenwunde, Wunde} from "../data/Trefferzone.js";
import {LiturgyData} from "../data/miracle/liturgyData.mjs";
import {Zonenruestung, Zonenwunde, Wunde} from "../data/trefferzone.mjs";
import {PlayerCharacterDataModel} from "../data/character.mjs";
export class Character extends Actor {
@ -26,7 +26,7 @@ export class Character extends Actor {
*/
prepareDerivedData() {
if (this.type === "character") {
if (this.type === "Character") {
const actorData = this;
const systemData = actorData.system;
@ -49,9 +49,9 @@ export class Character extends Actor {
const ko = systemData.attribute.ko.aktuell
const kk = systemData.attribute.kk.aktuell
systemData.lep.max = Math.round((ko + ko + kk) / 2)
systemData.aup.max = Math.round((mu + ko + ge) / 2)
systemData.asp.max = Math.round((mu + _in + ch) / 2)
systemData.lep.max = Math.round((ko + ko + kk) / 2) + systemData.lep.mod
systemData.aup.max = Math.round((mu + ko + ge) / 2) + systemData.aup.mod
systemData.asp.max = Math.round((mu + _in + ch) / 2) + systemData.asp.mod
systemData.regeneration = systemData.regeneration ?? {
lep: "1d6",
@ -105,6 +105,19 @@ export class Character extends Actor {
systemData.ausweichen.basis = systemData.pa.basis
systemData.ausweichen.aktuell = systemData.ausweichen.basis
systemData.ueberanstrengung = 0
if (game.settings.get("DSA_4-1", "optional_erschoepfung")) {
systemData.erschoepfung = {
aktuell: systemData.erschoepfung.aktuell ?? 0,
max: ko
}
if (systemData.erschoepfung.aktuell > systemData.erschoepfung.max) {
systemData.ueberanstrengung = systemData.erschoepfung.aktuell - systemData.erschoepfung.max
}
}
if (game.settings.get("DSA_4-1", "optional_ruestungzonen")) {
systemData.rs = {
@ -119,11 +132,11 @@ export class Character extends Actor {
} else {
systemData.rs = 0; // only with DSA_4-1.optional_trefferzonen = false
}
systemData.be = 0;
systemData.be = 0 + systemData.ueberanstrengung;
// half KO is the maximum a character can sustain wounds before collapsing
systemData.wunden.max = ko / 2;
systemData.wunden.max = Math.round(ko / 2);
if (game.settings.get("DSA_4-1", "optional_trefferzonen")) {
systemData.wunden.kopf = 0
systemData.wunden.brust = 0
@ -310,6 +323,136 @@ export class Character extends Actor {
return updateObject;
}
/**
* reduce the wealth owned by this character by the given amount in Silver
* @param by amount of Silver to reduce the wealth by the character
* @returns {boolean}
*/
async reduceWealth(by) {
const ducats = this.itemTypes["Equipment"].filter(p => p.name === "Dukate")
const silver = this.itemTypes["Equipment"].filter(p => p.name === "Silbertaler")
const kreutzer = this.itemTypes["Equipment"].filter(p => p.name === "Kreuzer")
const heller = this.itemTypes["Equipment"].filter(p => p.name === "Heller")
let ducatsAmount = 0
let silverAmount = 0
let kreutzerAmount = 0
let hellerAmount = 0
ducats.forEach(d => {
ducatsAmount += d.system.quantity ?? 1
})
silver.forEach(s => {
silverAmount += s.system.quantity ?? 1
})
kreutzer.forEach(k => {
kreutzerAmount += k.system.quantity ?? 1
})
heller.forEach(h => {
hellerAmount += h.system.quantity ?? 1
})
// Convert total wealth to silver
let totalSilver = ducatsAmount * 10 + silverAmount + kreutzerAmount * 0.1 + hellerAmount * 0.01;
if (totalSilver < by) {
return false; // Indicate that the reduction can't be performed
}
// Subtract the given sum from total silver
totalSilver -= by;
// Handle if the total goes below zero
if (totalSilver < 0) totalSilver = 0;
// Convert back to coinages
let newDucats = Math.floor(totalSilver / 10);
totalSilver %= 10;
let newSilver = Math.floor(totalSilver);
totalSilver %= 1;
let newKreutzer = Math.floor(totalSilver / 0.1);
totalSilver %= 0.1;
let newHeller = Math.round(totalSilver / 0.01);
// remove all coinage items
let deleteDocuments = []
ducats.forEach(d => deleteDocuments.push(d._id))
silver.forEach(s => deleteDocuments.push(s._id))
kreutzer.forEach(k => deleteDocuments.push(k._id))
heller.forEach(h => deleteDocuments.push(h._id))
await this.deleteEmbeddedDocuments('Item', deleteDocuments)
// rebuild coinage documents
const compendiumOfCoins = game.packs.get('DSA_4-1.Currency');
if (newDucats > 0) {
let coin = compendiumOfCoins.index.find(coin => coin.name === "Dukate")
const ducatDocument = await compendiumOfCoins.getDocument(coin._id);
try {
this.createEmbeddedDocuments('Item', [ducatDocument]).then(
embeddedDocuments => {
embeddedDocuments[0].update({"system.quantity": newDucats}).then(_ => {
console.log("created new Ducats with qty of", newDucats)
})
})
} catch (err) {
}
}
if (newSilver > 0) {
let coin = compendiumOfCoins.index.find(coin => coin.name === "Silbertaler")
const silverDocument = await compendiumOfCoins.getDocument(coin._id);
try {
this.createEmbeddedDocuments('Item', [silverDocument]).then(
embeddedDocuments => {
embeddedDocuments[0].update({"system.quantity": newSilver}).then(_ => {
console.log("created new Silver with qty of", newSilver)
})
})
} catch (err) {
}
}
if (newKreutzer > 0) {
let coin = compendiumOfCoins.index.find(coin => coin.name === "Kreuzer")
const kreutzerDocument = await compendiumOfCoins.getDocument(coin._id);
try {
this.createEmbeddedDocuments('Item', [kreutzerDocument]).then(
embeddedDocuments => {
embeddedDocuments[0].update({"system.quantity": newKreutzer}).then(_ => {
console.log("created new Kreutzer with qty of", newKreutzer)
})
})
} catch (err) {
}
}
if (newHeller > 0) {
let coin = compendiumOfCoins.index.find(coin => coin.name === "Heller")
const hellerDocument = await compendiumOfCoins.getDocument(coin._id);
try {
this.createEmbeddedDocuments('Item', [hellerDocument]).then(
embeddedDocuments => {
embeddedDocuments[0].update({"system.quantity": newHeller}).then(_ => {
console.log("created new Heller with qty of", newHeller)
})
})
} catch (err) {
}
}
return true
}
isWorn(itemId) {

View File

@ -0,0 +1,9 @@
export class Deity extends Item {
/**
* Augment the basic Item data model with additional dynamic data.
*/
prepareData() {
super.prepareData();
}
}

View File

@ -0,0 +1,72 @@
/**
*
* @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]} 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
* @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} countToPatzer amount of critical fumbles are needed for the dice roll to be a critical failure
* @returns {{tap: number, meisterlich: boolean, patzer: boolean, evaluated: Roll.Evaluated}}
*/
const evaluateRoll = async (rolledDice, {
value,
werte = [],
owner,
lowerThreshold = 1,
upperThreshold = 20,
countToMeisterlich = 3,
countToPatzer = 3,
}) => {
let tap = value;
let meisterlichCounter = 0;
let patzerCounter = 0;
let failCounter = 0;
let evaluated = null
if (typeof rolledDice == "string") { // we need to roll it ourself
let roll1 = new Roll(rolledDice, owner.getRollData());
evaluated = await roll1.evaluate()
rolledDice = evaluated.terms[0].results
}
if (tap < 0) { // increases rolledDice by |tap| (as this defacto lowers the target value)
rolledDice = rolledDice.map(({result, active}) => {
return {
result: result - tap,
active
}
})
tap = 0 // and then reset tap to 0 as we applied the reduction
}
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,
evaluated
}
}
export {
evaluateRoll
}

View File

@ -0,0 +1,75 @@
function currency(st) {
// schema for Mittelreich: 1 Ducat = 10 Silver = 100 Kreutzer = 1000 Heller
// internally the price is always given in Silver
// so we need to inflate the value of price by 100 to be able to divide beginning from Heller
const baseValue = Math.round(st * 100)
// then we can regex over it
const currencyRegexp = /(.*)(.)(.)(.)/g
const withDucats = currencyRegexp.exec(baseValue)
let _ = undefined
let ducats = 0
let silver = 0
let kreutzer = 0
let heller = 0
if (withDucats) {
[_, ducats, silver, kreutzer, heller] = withDucats
} else {
const currencyRegexp = /(.)(.)(.)/g
const withSilver = currencyRegexp.exec(baseValue)
if (withSilver) {
[_, silver, kreutzer, heller] = withSilver
} else {
const currencyRegexp = /(.)(.)/g
const withKreutzer = currencyRegexp.exec(baseValue)
if (withKreutzer) {
[_, kreutzer, heller] = withKreutzer
} else {
heller = baseValue
}
}
}
return {
ducats,
silver,
kreutzer,
heller
}
}
function registerHelper(hbs) {
hbs?.registerHelper('currency', (data) => {
const {ducats, silver, kreutzer, heller} = currency(data)
let str = `<span class='coins' data-tooltip="${ducats > 0 ? ducats + ' Dukaten ' : ''}${silver > 0 ? silver + ' Silbertaler ' : ''}${kreutzer > 0 ? kreutzer + ' Kreuzer ' : ''}${heller > 0 ? heller + ' Heller' : ''}">`
if (ducats > 0) {
str += ducats + "<i class='symbol ducat'></i>"
}
if (silver > 0) {
str += silver + "<i class='symbol silver'></i>"
}
if (kreutzer > 0) {
str += kreutzer + "<i class='symbol kreutzer'></i>"
}
if (heller > 0) {
str += heller + "<i class='symbol heller'></i>"
}
str += "</span>"
return new Handlebars.SafeString(str)
})
}
export {
currency,
registerHelper
}

View File

@ -0,0 +1,36 @@
function fieldTooltip(forActor, fieldName) {
let tooltip = {}
if (forActor) {
Object.entries(forActor.getModificationsOn(fieldName)).forEach(([key, value]) => {
tooltip[key] = value
})
}
return tooltip
}
function registerHelper(hbs) {
hbs?.registerHelper('fieldTooltip', (data) => {
const [fieldName, actorId] = data
const forActor = game.actors.find(p => p._id === actorId)
const tooltip = fieldTooltip(forActor, fieldName)
let template = ``
Object.entries(tooltip).forEach(([key, value]) => {
template += `${key}: ${value}<br/>`
})
return new Handlebars.SafeString(template)
})
}
export {
fieldTooltip,
registerHelper
}

View File

@ -0,0 +1,13 @@
import * as currency from "./currency.mjs";
import * as fieldTooltip from "./field-tooltip.mjs";
import * as weight from "./weight.mjs";
function initHandlebarHelpers(hbs) {
currency.registerHelper(hbs)
fieldTooltip.registerHelper(hbs)
weight.registerHelper(hbs)
}
export {
initHandlebarHelpers
}

View File

@ -0,0 +1,38 @@
function weight(money) {
const baseValue = money * 1000 // to get to gramms (1/1000 Stone)
const stone = Math.floor(baseValue / 1000)
const remainder = baseValue - (stone * 1000)
const ounces = remainder / 25
return {
stone,
ounces,
}
}
function registerHelper(hbs) {
hbs?.registerHelper('weight', (data) => {
let template = `<span class="weight">`
const {stone, ounces} = weight(data)
if (stone > 0) {
template += `<span class="stone">${stone}</span>`
}
if (ounces > 0) {
template += `<span class="ounces">${ounces}</span>`
}
template += `</span>`
return new Handlebars.SafeString(template)
})
}
export {
weight,
registerHelper
}

View File

@ -0,0 +1,95 @@
function initGlobalSettings(settings) {
settings.register('DSA_4-1', 'optional_trefferzonen', {
name: "Optional: Trefferzonen",
hint: "Ersetzt das Wundensystem aus dem BRW durch das Trefferzonensystem aus WdH",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: true
})
settings.register('DSA_4-1', 'optional_ruestungzonen', {
name: "Optional: Zonenrüstung",
hint: "Ersetzt das Rüstungssystem aus dem BRW durch das Zonenrüstungssystem aus WdH",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: true
})
settings.register('DSA_4-1', 'optional_ausdauer', {
name: "Optional: Ausdauerregeln",
hint: "Aktiviert Regeln für das Spiel mit Ausdauer",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: true
})
settings.register('DSA_4-1', 'optional_erschoepfung', {
name: "Optional: Erschöpfung",
hint: "Aktiviert Regeln für das Spiel mit Erschöpfung und Überanstregung",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: true
})
settings.register('DSA_4-1', 'optional_distanzklassen', {
name: "Optional: Distanzklassen",
hint: "Aktiviert Regeln für das Spiel mit Distanzklassen",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: true
})
settings.register('DSA_4-1', 'optional_aufstufen_von_liturgien', {
name: "Optional: Aufstufen von Liturgien",
hint: "Aktiviert die Regeln zum Aufstufen von Liturgien",
scope: "world",
config: true,
type: Boolean,
default: false,
disabled: true,
requiresReload: true
})
}
function initUserSettings(settings) {
settings.register('DSA_4-1', 'optional_colorfuldice', {
name: "Optional: Farbige Würfel nach Paramanthus",
hint: "Färbt die Würfel je nach Attribut ein",
scope: "client",
config: true,
type: Boolean,
default: false,
onChange: value => {
},
requiresReload: false
})
}
export {
initGlobalSettings,
initUserSettings
}

View File

@ -0,0 +1,121 @@
import {PlayerCharacterDataModel} from "../data/character.mjs";
import {GroupDataModel} from "../data/group.mjs";
import {CreatureDataModel} from "../data/creature.mjs";
import {MerchantDataModel} from "../data/merchant.mjs";
import {SkillDataModel} from "../data/skill.mjs";
import {SpellDataModel} from "../data/spell.mjs";
import {AdvantageDataModel} from "../data/advantage.mjs";
import {EquipmentDataModel} from "../data/equipment.mjs";
import {LiturgyDataModel} from "../data/liturgy.mjs";
import {BlessingDataModel} from "../data/blessing.mjs";
import {SpecialAbilityDataModel} from "../data/specialAbility.mjs";
import {ActiveEffectDataModel} from "../data/activeEffect.mjs";
import {ProfessionDataModel} from "../data/profession.mjs";
import {SpeciesDataModel} from "../data/species.mjs";
import {CultureDataModel} from "../data/culture.mjs";
import {Trefferzone, Wunde, Zonenruestung, Zonenwunde} from "../data/trefferzone.mjs";
import {RestingDialog} from "../dialog/restingDialog.mjs";
import {BattleDialog} from "../dialog/battleDialog.mjs";
import {Talent} from "../data/talent.mjs";
import {Character} from "../documents/character.mjs";
import {currency} from "../handlebar-helpers/currency.mjs";
import {DeityDataModel} from "../data/deity.mjs";
import {ItemBrowserDialog} from "../dialog/itemBrowserDialog.mjs";
function initGlobalAccess() {
return {
Zonenruestung,
Zonenwunde,
Trefferzone,
Wunde,
RestingDialog,
BattleDialog,
ItemBrowserDialog,
Talent,
displayCurrency: currency
}
}
function initDocumentClasses(config) {
config.Actor.documentClass = Character
}
function initDataModels(config) {
config.Actor.dataModels = {
Character: PlayerCharacterDataModel,
Group: GroupDataModel,
Creature: CreatureDataModel,
Merchant: MerchantDataModel,
}
config.Item.dataModels = {
Skill: SkillDataModel,
Spell: SpellDataModel,
Advantage: AdvantageDataModel,
Equipment: EquipmentDataModel,
Liturgy: LiturgyDataModel,
Blessing: BlessingDataModel,
SpecialAbility: SpecialAbilityDataModel,
ActiveEffect: ActiveEffectDataModel,
Profession: ProfessionDataModel,
Spezies: SpeciesDataModel,
Kultur: CultureDataModel,
Deity: DeityDataModel,
}
}
function initCombat(config) {
config.Combat.initiative = {
formula: `(@ini.wuerfel)d6 + @ini.aktuell`,
decimals: 0
}
}
function initSocketLib() {
Hooks.on("socketlib.ready", () => {
let socket = socketlib.registerSystem("DSA_4-1")
socket.register("removeFromLootTable", removeFromLootTable)
socket.register("buyFromLootTable", buyFromLootTable)
if (!game.DSA41) {
game.DSA41 = {}
}
game.DSA41.socket = socket
})
async function removeFromLootTable(actorId, itemId) {
if (actorId && game.actors.get(actorId)) {
const actor = game.actors.get(actorId)
return await actor.deleteEmbeddedDocuments('Item', [itemId])
}
}
async function buyFromLootTable(actorId, itemId) {
if (actorId && game.actors.get(actorId)) {
const actor = game.actors.get(actorId)
const item = actor.items.find(p => p.id === itemId)
if (item.system.quantity != -1) { // -1 means infinite
if (item.system.quantity > 1) {
item.update({'system.quantity': item.system.quantity - 1})
} else {
actor.deleteEmbeddedDocuments('Item', [item._id]) // delete when the quantity is equal to 0
}
}
return true
}
}
}
export {
initSocketLib,
initGlobalAccess,
initDocumentClasses,
initDataModels,
initCombat,
}

View File

@ -0,0 +1,32 @@
function loadPartials(hbs) {
return new Promise(resolve => {
hbs.loadTemplates([
// ui partials.
'systems/DSA_4-1/templates/ui/partial-rollable-button.hbs',
'systems/DSA_4-1/templates/ui/partial-cooldown.hbs',
'systems/DSA_4-1/templates/ui/partial-rollable-weaponskill-button.hbs',
'systems/DSA_4-1/templates/ui/partial-rollable-language-button.hbs',
'systems/DSA_4-1/templates/ui/partial-attribute-button.hbs',
'systems/DSA_4-1/templates/ui/partial-talent-editable.hbs',
'systems/DSA_4-1/templates/ui/partial-die.hbs',
'systems/DSA_4-1/templates/ui/partial-advantage-button.hbs',
'systems/DSA_4-1/templates/ui/partial-sf-button.hbs',
'systems/DSA_4-1/templates/ui/partial-action-button.hbs',
'systems/DSA_4-1/templates/ui/partial-equipment-button.hbs',
'systems/DSA_4-1/templates/ui/partial-array-editor.hbs',
'systems/DSA_4-1/templates/actor/character/tab-set.hbs',
'systems/DSA_4-1/templates/dialog/liturgy-dialog.hbs',
'systems/DSA_4-1/templates/ui/partial-mini-rollable-button.hbs',
'systems/DSA_4-1/templates/ui/partial-mini-rollable-liturgy-button.hbs',
'systems/DSA_4-1/templates/ui/partial-mini-rollable-weaponskill-button.hbs',
'systems/DSA_4-1/templates/ui/partial-mini-rollable-language-button.hbs',
'systems/DSA_4-1/templates/ui/partial-mini-rollable-spell-button.hbs',
]).then(resolve);
})
}
export {
loadPartials,
}

View File

@ -0,0 +1,102 @@
import CharacterSheet from "../sheets/characterSheet.mjs";
import {CreatureSheet} from "../sheets/creatureSheet.mjs";
import {GroupSheet} from "../sheets/groupSheet.mjs";
import {SkillSheet} from "../sheets/skillSheet.mjs";
import {SpellSheet} from "../sheets/spellSheet.mjs";
import {AdvantageSheet} from "../sheets/advantageSheet.mjs";
import EquipmentSheet from "../sheets/equipmentSheet.mjs";
import {LiturgySheet} from "../sheets/liturgySheet.mjs";
import {SpecialAbilitySheet} from "../sheets/specialAbilitySheet.mjs";
import {ActiveEffectSheet} from "../sheets/activeEffectSheet.mjs";
import {CultureSheet} from "../sheets/cultureSheet.mjs";
import {SpeciesSheet} from "../sheets/SpeciesSheet.mjs";
import {ProfessionSheet} from "../sheets/professionSheet.mjs";
import {MerchantSheet} from "../sheets/merchantSheet.mjs";
import {DeitySheet} from "../sheets/deitySheet.mjs";
function setUpActorSheets(registry) {
registry.registerSheet('dsa41.character', CharacterSheet, {
types: ["Character"],
makeDefault: true,
})
registry.registerSheet('dsa41.creature', CreatureSheet, {
types: ["Creature"],
makeDefault: true,
})
registry.registerSheet('dsa41.group', GroupSheet, {
types: ["Group"],
makeDefault: true,
})
registry.registerSheet('dsa41.merchant', MerchantSheet, {
types: ['Merchant'],
makeDefault: true,
})
}
function setUpItemSheets(registry) {
registry.registerSheet('dsa41.skill', SkillSheet, {
types: ["Skill"],
makeDefault: true,
});
registry.registerSheet('dsa41.spell', SpellSheet, {
types: ["Spell"],
makeDefault: true,
});
registry.registerSheet('dsa41.advantage', AdvantageSheet, {
types: ["Advantage"],
makeDefault: true,
})
registry.registerSheet('dsa41.equipment', EquipmentSheet, {
types: ["Equipment"],
makeDefault: false,
})
registry.registerSheet('dsa41.liturgy', LiturgySheet, {
types: ["Liturgy"],
makeDefault: true,
})
registry.registerSheet('dsa41.specialAbility', SpecialAbilitySheet, {
types: ["SpecialAbility"],
makeDefault: true,
})
registry.registerSheet('dsa41.activeEffect', ActiveEffectSheet, {
types: ['ActiveEffect'],
makeDefault: true,
})
registry.registerSheet('dsa41.culture', CultureSheet, {
types: ['Culture'],
makeDefault: true,
label: 'DSA41.CultureLabels.Culture'
})
registry.registerSheet('dsa41.spezien', SpeciesSheet, {
types: ['Species'],
makeDefault: true,
})
registry.registerSheet('dsa41.profession', ProfessionSheet, {
types: ['Profession'],
makeDefault: true,
})
registry.registerSheet('dsa41.deity', DeitySheet, {
types: ['Deity'],
makeDefault: true,
})
}
export {
setUpActorSheets,
setUpItemSheets,
}

View File

@ -80,7 +80,11 @@ export class ActionManager {
type: ActionManager.ATTACK,
cost: ActionManager.REGULAR,
source: ActionManager.DEFAULT,
eval: (options) => this.#hatWaffeinHand(options)
activate: (queue, data) => {
data.actor.rollAttack(data)
return true
},
eval: (options) => this.#hatWaffeinHand(options),
},
{
name: "Fernkampfangriff",
@ -89,6 +93,8 @@ export class ActionManager {
source: ActionManager.DEFAULT,
cooldown: (options) => 1,
activate: (queue, data) => {
data.actor.rollAttack(data)
return true
},
eval: (options) => this.#hatFernkampfWaffeinHand(options),
},
@ -98,6 +104,10 @@ export class ActionManager {
cost: ActionManager.CONTINUING,
source: ActionManager.DEFAULT,
cooldown: (options) => options.mod,
activate: (queue, data) => {
data.actor.rollAttack(data)
return true
},
eval: (options) => {
const step1 = this.#hatFernkampfWaffeinHand(options)
const step2 = !this.#hatSonderfertigkeit("Scharfschütze", options)
@ -211,6 +221,10 @@ export class ActionManager {
source: ActionManager.SF,
modDescription: "verringert PA des Ziels um {}",
mod: (value) => value,
activate: (queue, data) => {
data.actor.rollAttack(data)
return true
},
eval: (options) => {
const step1 = this.#hatWaffeinHand(options) && this.#hatSonderfertigkeit("Finte", options)
const step2WithBenefits = this.#evalSonderfertigkeitRequirements("Finte", options)
@ -227,6 +241,10 @@ export class ActionManager {
source: ActionManager.DEFAULT,
modDescription: "erhöht TP vom Angriff um {}",
mod: (value) => -(value * 2),
activate: (queue, data) => {
data.actor.rollAttack(data)
return true
},
eval: (options) => {
const step1 = !this.#hatFernkampfWaffeinHand(options)
const step2 = !this.#hatSonderfertigkeit("Wuchtschlag", options)
@ -244,6 +262,10 @@ export class ActionManager {
source: ActionManager.SF,
modDescription: "erhöht TP vom Angriff um {}",
mod: (value) => -(value),
activate: (queue, data) => {
data.actor.rollAttack(data)
return true
},
eval: (options) => {
const step1 = !this.#hatFernkampfWaffeinHand(options) && this.#hatSonderfertigkeit("Wuchtschlag", options)
const step2WithBenefits = this.#evalSonderfertigkeitRequirements("Wuchtschlag", options)
@ -258,6 +280,10 @@ export class ActionManager {
type: ActionManager.ATTACK,
cost: ActionManager.REGULAR,
source: ActionManager.SF,
activate: (queue, data) => {
data.actor.rollAttack(data)
return true
},
eval: (options) => {
const step1 = !this.#hatFernkampfWaffeinHand(options) && this.#hatSonderfertigkeit("Betäubungsschlag", options)
const step2WithBenefits = this.#evalSonderfertigkeitRequirements("Betäubungsschlag", options)

View File

@ -7,10 +7,20 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
position: {width: 520, height: 480},
classes: ['dsa41', 'sheet', 'item', 'advantage'],
tag: 'form',
window: {
resizable: true
},
form: {
submitOnChange: true,
closeOnSubmit: false,
handler: AdvantageSheet.#onSubmitForm
},
actions: {
addRequirement: AdvantageSheet.#addRequirement,
removeRequirement: AdvantageSheet.#removeRequirement,
addMod: AdvantageSheet.#addMod,
removeMod: AdvantageSheet.#removeMod,
saveVariant: AdvantageSheet.#saveVariant
}
}
@ -23,6 +33,7 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
}
}
/** @inheritDoc */
static PARTS = {
form: {
@ -30,9 +41,14 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
},
advantage: {
template: `systems/DSA_4-1/templates/item/advantage/tab-advantage.hbs`
},
variants: {
template: `systems/DSA_4-1/templates/ui/tab-variants.hbs`
}
}
static _instance = null
_configureRenderOptions(options) {
super._configureRenderOptions(options)
@ -43,6 +59,173 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
return options
}
constructor(...args) {
super(...args);
AdvantageSheet._instance = this
}
static async #addRequirement(event, target) {
const selections =
"<option value='talentMin'>Mindest Talentwert</option>" +
"<option value='talentMax'>Maximal Talentwert</option>" +
"<option value='attributeMin'>Mindest Attributswert</option>" +
"<option value='attributeMax'>Maximal Attributswert</option>" +
"<option value='compare'>Vergleich</option>"
const type = await foundry.applications.api.DialogV2.prompt({
window: {title: "Neue Voraussetzung"},
content: `<select name="type">${selections}</select>`,
ok: {
label: `Hinzufügen`,
callback: (event, button, dialog) => button.form.elements.type.value
}
});
if (type) {
let newReq = {}
switch (type) {
case 'talentMin':
newReq = {
talent: 'Klettern',
minValue: '0'
}
break;
case 'talentMax':
newReq = {
talent: 'Zechen',
maxValue: '0'
}
break;
case 'attributeMin':
newReq = {
attribute: 'ge',
minValue: '0'
}
break;
case 'attributeMax':
newReq = {
attribute: 'mu',
maxValue: '0'
}
break;
case 'compare':
newReq = {
compare: {
ownAttribute: 'ini.aktuell',
operation: 'eq',
targetAttribute: 'ini.aktuell'
}
}
break;
}
const lastIndex = Object.keys(AdvantageSheet._instance._currentSelectedVariant.requirements).length
AdvantageSheet._instance._currentSelectedVariant.requirements[lastIndex] = newReq
AdvantageSheet._instance.render({parts: ["form", "advantage", "variants"]})
}
}
static async #removeRequirement(event, target) {
const {index} = target.dataset
if (index) {
delete AdvantageSheet._instance._currentSelectedVariant.requirements[index]
}
}
static async #addMod(event, target) {
const selections =
"<option value='talent'>Talent</option>" +
"<option value='talentGroup'>Talentgruppe</option>" +
"<option value='attribute'>Attribute</option>" +
"<option value='trait'>Merkmal</option>"
const type = await foundry.applications.api.DialogV2.prompt({
window: {title: "Neuer Modifikator"},
content: `<select name="type">${selections}</select>`,
ok: {
label: `Hinzufügen`,
callback: (event, button, dialog) => button.form.elements.type.value
}
});
if (type) {
let newReq = {}
switch (type) {
case 'talent':
newReq = {
talent: 'Klettern',
value: '0'
}
break;
case 'talentGroup':
newReq = {
talentGruppe: 'Gesellschaft',
value: '0'
}
break;
case 'trait':
newReq = {
merkmal: 'Elementar',
value: '0'
}
break;
case 'attribute':
newReq = {
name: 'ge',
value: '0'
}
break;
}
const lastIndex = Object.keys(AdvantageSheet._instance._currentSelectedVariant.mod).length
AdvantageSheet._instance._currentSelectedVariant.mod[lastIndex] = newReq
AdvantageSheet._instance.render({parts: ["form", "advantage", "variants"]})
}
}
static async #removeMod(event, target) {
const {index} = target.dataset
if (index) {
delete AdvantageSheet._instance._currentSelectedVariant.mod[index]
}
}
static async #saveVariant(event, target) {
/**
* @type {HTMLFormElement}
*/
const form = AdvantageSheet._instance.form
let flattenObject = {}
Object.values(form).forEach(input => {
if (input.name.startsWith('mod') || input.name.startsWith('requirements')) {
flattenObject[`${input.name}`] = input.value
}
if (input.name === "vName") {
flattenObject[`name`] = input.value
}
})
let auswahl = AdvantageSheet._instance.document.system.auswahl
const fo = foundry.utils.expandObject(flattenObject)
auswahl[AdvantageSheet._instance._currentSelectedVariantIndex] = {
name: fo.name,
mod: Object.values(fo.mod),
requirements: Object.values(fo.requirements)
}
AdvantageSheet._instance.document.update({system: {auswahl}})
}
/**
* Handle form submission
* @this {AdvantageSheet}
@ -52,10 +235,32 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
*/
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
if (!form.querySelector('.tab.advantage.active')) {
const obj = foundry.utils.expandObject(formData.object)
await this.document.update(formData.object) // Note: formData.object
if (obj.mod) this._currentSelectedVariant.mod = obj.mod
if (obj.vName) this._currentSelectedVariant.name = obj.vName
if (obj.requirements) this._currentSelectedVariant.requirements = obj.requirements
console.log(formData.object, this.document)
} else {
delete formData.object.mod
delete formData.object.vName
delete formData.object.requirements
delete formData.object.variant
await this.document.update(formData.object) // Note: formData.object
}
this.render({parts: ["form", "advantage", "variants"]})
}
_getTabsConfig(group) {
const tabs = foundry.utils.deepClone(super._getTabsConfig(group))
if (this.document.system.auswahl) {
tabs.tabs.push({id: 'variants', group: 'sheet', label: 'Varianten'})
}
return tabs
}
/** @override */
@ -71,7 +276,34 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
context.choices[a.name] = a.name
})
context.hasModality = context.system.value != null
context.name = advantageData.name
context.variantChoices = {}
context.variants = []
advantageData.system.auswahl?.forEach(variant => {
context.variantChoices[variant.name] = variant.name
context.variants.push(variant)
})
context.currentSelectedVariantName = this._currentSelectedVariant?.name
context.currentSelectedVariant = this._currentSelectedVariant
context.currentSelectedVariantIndex = this._currentSelectedVariantIndex
return context;
}
_onRender(context, options) {
if (this._selectedVariant == null) {
this._selectedVariant = this.document.system.auswahl[0].name
this._currentSelectedVariant = this.document.system.auswahl?.find(p => p.name === this._selectedVariant)
this._currentSelectedVariantIndex = this.document.system.auswahl?.findIndex(p => p.name === this._selectedVariant)
}
this.element.querySelector('select[name="variant"]').addEventListener('change', (event, target) => {
if (event.target.value != this._selectedVariant) {
this._selectedVariant = event.target.value
this._currentSelectedVariant = this.document.system.auswahl?.find(p => p.name === this._selectedVariant)
this._currentSelectedVariantIndex = this.document.system.auswahl?.findIndex(p => p.name === this._selectedVariant)
}
})
}
}

View File

@ -0,0 +1,118 @@
const {HandlebarsApplicationMixin, DocumentSheetV2} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets
export class StandaloneADVSF extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
position: {width: 520, height: 480},
classes: ['dsa41', 'sheet', 'actor', 'character', 'standalone', 'advsf'],
tag: 'form',
actions: {
rollFlaw: StandaloneADVSF.#rollFlaw,
openEmbeddedDocument: StandaloneADVSF.#openEmbeddedDocument,
}
}
/** @inheritDoc */
static PARTS = {
form: {
template: `systems/DSA_4-1/templates/actor/character/standalone/advsf.hbs`
}
}
_actor = null
constructor(actor) {
super(actor)
this._actor = actor
this.render(true)
this.options.window.title = `${this.document.name} Vor und Nachteile`
}
static async #rollFlaw(event, target) {
this._actor?.sheet.options.actions.rollFlaw.bind(this)(event, target)
}
static async #openEmbeddedDocument(event, target) {
this._actor?.sheet.options.actions.openEmbeddedDocument.bind(this)(event, target)
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
options.window.title = `${this.document.name}: Vor und Nachteile`
return options
}
async _prepareContext(context, options, object) {
if (this._actor) {
const actorData = this.document
context.system = actorData.system
context.flags = actorData.flags
context.derived = actorData.system
context.originalName = actorData.name
context.name = context.derived.name ?? actorData.name
context.effects = actorData.effects ?? []
context.advantages = []
context.flaws = []
actorData.itemTypes.Advantage.forEach((item) => {
if (!item.system.schlechteEigenschaft) {
context.advantages.push({
id: item._id,
name: item.name,
value: item.system.value,
options: item.system.auswahl,
description: item.system.description,
isAdvantage: !item.system.nachteil,
isDisadvantage: item.system.nachteil,
isBadAttribute: item.system.schlechteEigenschaft,
fav: item.getFlag("DSA_4-1", "favourite")
})
} else {
context.flaws.push({
id: item._id,
name: item.name,
value: item.system.value,
options: item.system.auswahl,
description: item.system.description,
isAdvantage: !item.system.nachteil,
isDisadvantage: item.system.nachteil,
isBadAttribute: item.system.schlechteEigenschaft,
fav: item.getFlag("DSA_4-1", "favourite")
})
}
}
)
context.specialAbilities = []
actorData.itemTypes.SpecialAbility.forEach((item) => {
context.specialAbilities.push({
id: item._id,
name: item.system.value ? item.system.value : item.name,
fav: item.getFlag("DSA_4-1", "favourite")
});
}
);
return context
}
}
_onRender(context, options) {
if (this._actor) {
new foundry.applications.ux.DragDrop.implementation({
dropSelector: ".advantages, .special-abilities",
permissions: {
drop: this._actor.sheet._canDragDrop.bind(this._actor.sheet)
},
callbacks: {
drop: this._actor.sheet._onDrop.bind(this._actor.sheet),
}
}).bind(this.element);
}
}
}

View File

@ -0,0 +1,98 @@
const {HandlebarsApplicationMixin, DocumentSheetV2} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets
export class Bagpack extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
position: {width: 520, height: 480},
classes: ['dsa41', 'sheet', 'actor', 'character', 'standalone', 'bagpack'],
tag: 'form',
actions: {
openItemBrowser: Bagpack.#openItemBrowser,
newItem: Bagpack.#newItem,
openEmbeddedDocument: Bagpack.#openEmbeddedDocument,
}
}
/** @inheritDoc */
static PARTS = {
form: {
template: `systems/DSA_4-1/templates/actor/character/standalone/bagpack.hbs`
}
}
_actor = null
constructor(actor) {
super(actor)
this._actor = actor
this.render(true)
}
static async #openItemBrowser(event, target) {
this._actor?.sheet.options.actions.openItemBrowser().bind(this)(event, target)
}
static async #newItem(event, target) {
this._actor?.sheet.options.actions.newItem.bind(this)(event, target)
}
static async #openEmbeddedDocument(event, target) {
this._actor?.sheet.options.actions.openEmbeddedDocument.bind(this)(event, target)
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
options.window.title = `${this.document.name}: Inventar`
return options
}
async _prepareContext(context, options, object) {
const actorData = this.document
context.system = actorData.system
context.flags = actorData.flags
context.equipments = []
context.carryingweight = 0
actorData.itemTypes["Equipment"].sort((a, b) => a.sort - b.sort).forEach((item, index) => {
// worn items are halved weight
let effectiveWeight = item.system.weight ?? 0
if (this.document.isWorn(item._id)) {
effectiveWeight = item.system.weight ? item.system.weight / 2 : 0
}
context.equipments.push({
index: index,
id: item._id,
quantity: item.system.quantity,
name: item.name,
icon: item.img ?? "",
weight: item.system.weight,
worn: this.document.isWorn(item._id)
})
context.carryingweight += item.system.quantity * effectiveWeight;
})
context.maxcarryingcapacity = actorData.system.attribute.kk.aktuell
context.carryingpercentage = Math.min((context.carryingweight / context.maxcarryingcapacity) * 100, 100);
context.wealth = 0
actorData.itemTypes["Equipment"].forEach(coin => {
if (coin.system.category.indexOf("Währung") !== -1) {
context.wealth += (coin.system.quantity * coin.system.currencyDenominator)
}
})
return context
}
_onRender(context, options) {
}
}

View File

@ -0,0 +1,111 @@
const {HandlebarsApplicationMixin, DocumentSheetV2} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets
export class StandaloneHealth extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
position: {width: 520, height: 716},
classes: ['dsa41', 'sheet', 'actor', 'character', 'standalone', 'health'],
tag: 'form',
actions: {
openEmbeddedDocument: StandaloneHealth.#openEmbeddedDocument,
setWounds: StandaloneHealth.#setWounds,
}
}
/** @inheritDoc */
static PARTS = {
form: {
template: `systems/DSA_4-1/templates/actor/character/standalone/health.hbs`
}
}
_actor = null
constructor(actor) {
super(actor)
this._actor = actor
this.render(true)
}
static async #openEmbeddedDocument(event, target) {
this._actor?.sheet.options.actions.openEmbeddedDocument.bind(this)(event, target)
}
static async #setWounds(event, target) {
this._actor?.sheet.options.actions.setWounds.bind(this)(event, target)
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
options.window.title = `${this.document.name}: Gesundheit`
return options
}
async _prepareContext(context, options, object) {
const actorData = this.document
context.system = actorData.system
context.flags = actorData.flags
context.derived = this.document.system
context.originalName = actorData.name
context.name = context.derived.name ?? actorData.name
context.effects = actorData.effects ?? []
const findEquipmentOnSlot = (slot, setNumber, object) => {
return object.items.get(object.system.heldenausruestung[setNumber]?.[slot])
}
context.inidice = actorData.system.ini.wuerfel
context.inivalue = actorData.system.ini.aktuell
context.inimod = actorData.system.ini.mod
context.zonenruestung = game.settings.get("DSA_4-1", "optional_ruestungzonen")
context.trefferzonen = game.settings.get("DSA_4-1", "optional_trefferzonen")
context.ausdauer = game.settings.get("DSA_4-1", "optional_ausdauer")
context.colorfulDice = game.settings.get('DSA_4-1', 'optional_colorfuldice')
context.aupper = Math.min((actorData.system.aup.aktuell / actorData.system.aup.max) * 100, 100)
context.lepper = Math.min((actorData.system.lep.aktuell / actorData.system.lep.max) * 100, 100)
context.keper = Math.min((actorData.system.kap.aktuell / actorData.system.kap.max) * 100, 100)
context.aspper = Math.min((actorData.system.asp.aktuell / actorData.system.asp.max) * 100, 100)
context.lepcurrent = actorData.system.lep.aktuell ?? 0
context.aupcurrent = actorData.system.aup.aktuell ?? 0
context.aspcurrent = actorData.system.asp.aktuell ?? 0
context.kapcurrent = actorData.system.kap.aktuell ?? 0
context.maxWounds = actorData.system.wunden.max ?? 3
context.wounds = actorData.system.wunden.aktuell ?? 0
context.woundsFilled = []
for (let i = 1; i <= context.maxWounds; i++) {
context.woundsFilled[i] = i <= context.wounds
}
context.withErschoepfung = game.settings.get("DSA_4-1", "optional_erschoepfung")
context.ueberanstrengung = actorData.system.ueberanstrengung
context.erschoepfung = actorData.system.erschoepfung.aktuell
context.maxErschoepfung = actorData.system.erschoepfung.max
context.erschoepfungFilled = []
for (let i = 1; i <= context.maxErschoepfung; i++) {
context.erschoepfungFilled[i] = i <= context.erschoepfung
}
context.effects = []
for (let i = 0; i < actorData.appliedEffects.length; i++) {
const item = actorData.appliedEffects[i]
context.effects.push(item.name)
}
return context
}
_onRender(context, options) {
}
}

View File

@ -0,0 +1,151 @@
import {LiturgyData} from "../../data/miracle/liturgyData.mjs";
const {HandlebarsApplicationMixin, DocumentSheetV2} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets
export class StandaloneLiturgies extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
position: {width: 520, height: 480},
classes: ['dsa41', 'sheet', 'actor', 'character', 'standalone', 'liturgies'],
tag: 'form',
actions: {
openEmbeddedDocument: StandaloneLiturgies.#openEmbeddedDocument,
openLiturgyDialog: StandaloneLiturgies.#openLiturgyDialog,
}
}
/** @inheritDoc */
static PARTS = {
form: {
template: `systems/DSA_4-1/templates/actor/character/standalone/liturgies.hbs`
}
}
_actor = null
constructor(actor) {
super(actor)
this._actor = actor
this.render(true)
}
static async #openEmbeddedDocument(event, target) {
this._actor?.sheet.options.actions.openEmbeddedDocument.bind(this)(event, target)
}
static async #openLiturgyDialog(event, target) {
this._actor?.sheet.options.actions.openLiturgyDialog.bind(this)(event, target)
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
options.window.title = `${this.document.name}: Segnungen und Liturgien`
return options
}
async _prepareContext(context, options, object) {
const actorData = this.document
context.system = actorData.system
context.flags = actorData.flags
context.derived = this.document.system
context.originalName = actorData.name
context.name = context.derived.name ?? actorData.name
context.effects = actorData.effects ?? []
context.liturgies = [];
context.blessings = [];
actorData.itemTypes.Blessing.forEach((item, index) => {
context.blessings.push({
deity: item.system.gottheit,
value: item.system.wert
})
})
actorData.itemTypes.Liturgy.forEach((item, index) => {
context.blessings.forEach(({deity, value}) => {
let insertObject = context.liturgies.find(p => p.deity === deity);
if (!insertObject) {
insertObject = {
deity: deity,
lkp: value,
O: [],
I: [],
II: [],
III: [],
IV: [],
V: [],
VI: [],
VII: [],
VIII: [],
"NA": [],
countO: 1,
countI: 1,
countII: 1,
countIII: 1,
countIV: 1,
countV: 1,
countVI: 1,
countVII: 1,
countVIII: 1,
countNA: 0,
total: 3,
}
context.liturgies.push(insertObject);
}
// sort by rank
const rankData = LiturgyData.getRankOfLiturgy(item.system, deity)
if (rankData) {
let {index, name, lkp, mod, costKaP} = rankData;
insertObject["count" + name] = insertObject["count" + name] + 1;
insertObject[name]?.push({
id: item._id,
name: item.name,
lkpReq: lkp,
lkpMod: mod,
costKaP,
fav: item.getFlag("DSA_4-1", "favourite"),
rank: index, // get effective liturgy rank based on deity
liturgiekenntnis: deity,
})
insertObject.total = insertObject.total + 2;
}
})
})
// clean up counter
Object.values(context.liturgies).forEach((litObject) => {
if (litObject.I.length === 0) litObject.countI = false;
if (litObject.II.length === 0) litObject.countII = false;
if (litObject.III.length === 0) litObject.countIII = false;
if (litObject.IV.length === 0) litObject.countIV = false;
if (litObject.V.length === 0) litObject.countV = false;
if (litObject.VI.length === 0) litObject.countVI = false;
if (litObject.VII.length === 0) litObject.countVII = false;
if (litObject.VIII.length === 0) litObject.countVIII = false;
if (litObject.NA.length === 0) litObject.countNA = false;
})
context.hasLiturgies = context.blessings.length > 0;
return context
}
_onRender(context, options) {
}
}

View File

@ -0,0 +1,127 @@
const {HandlebarsApplicationMixin, DocumentSheetV2} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets
export class StandaloneSkills extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
position: {width: 520, height: 480},
classes: ['dsa41', 'sheet', 'actor', 'character', 'standalone', 'skills'],
tag: 'form',
actions: {
rollCombatSkill: StandaloneSkills.#rollCombatSkill,
rollSkill: StandaloneSkills.#rollSkill,
openEmbeddedDocument: StandaloneSkills.#openEmbeddedDocument,
}
}
/** @inheritDoc */
static PARTS = {
form: {
template: `systems/DSA_4-1/templates/actor/character/standalone/skills.hbs`
}
}
_actor = null
constructor(actor) {
super(actor)
this._actor = actor
this.render(true)
}
static async #rollCombatSkill(event, target) {
this._actor?.sheet.options.actions.rollCombatSkill.bind(this)(event, target)
}
static async #rollSkill(event, target) {
this._actor?.sheet.options.actions.rollSkill.bind(this)(event, target)
}
static async #openEmbeddedDocument(event, target) {
this._actor?.sheet.options.actions.openEmbeddedDocument.bind(this)(event, target)
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
options.window.title = `${this.document.name}: Talente`
return options
}
async _prepareContext(context, options, object) {
const actorData = this.document
context.system = actorData.system
context.flags = actorData.flags
context.derived = this.document.system
context.originalName = actorData.name
context.name = context.derived.name ?? actorData.name
context.effects = actorData.effects ?? []
const prepareEigenschaftRoll = (actorData, name) => {
if (name && name !== "*") {
return actorData.system.attribute[name.toLowerCase()].aktuell
} else {
return 0
}
}
context.skills = {};
context.flatSkills = [];
actorData.itemTypes.Skill.forEach((item, index) => {
const talentGruppe = item.system.gruppe;
const eigenschaften = Object.values(item.system.probe);
const werte = [
{name: eigenschaften[0], value: prepareEigenschaftRoll(actorData, eigenschaften[0])},
{name: eigenschaften[1], value: prepareEigenschaftRoll(actorData, eigenschaften[1])},
{name: eigenschaften[2], value: prepareEigenschaftRoll(actorData, eigenschaften[2])}
]
if (context.skills[talentGruppe] == null) {
context.skills[talentGruppe] = [];
}
const obj = {
type: "talent",
gruppe: talentGruppe,
name: item.name.replace(/Sprachen kennen/g, "Sprache:").replace(/Lesen\/Schreiben/g, "Schrift: "),
taw: "" + item.system.taw,
tawPath: `system.items.${index}.taw`,
werte,
rollEigenschaft1: werte[0].value,
rollEigenschaft2: werte[1].value,
rollEigenschaft3: werte[2].value,
eigenschaft1: werte[0].name,
eigenschaft2: werte[1].name,
eigenschaft3: werte[2].name,
probe: `(${eigenschaften.join("/")})`,
id: item._id,
at: item.system.at,
pa: item.system.pa,
komplexität: item.system.komplexität,
fav: item.getFlag("DSA_4-1", "favourite")
};
if (talentGruppe === "Kampf") {
if (item.system.pa != null) { // has no parry value so it must be ranged talent (TODO: but it isnt as there can be combatstatistics which has no pa value assigned to)
obj.at = item.system.at + context.derived.at.aktuell
obj.pa = item.system.pa + context.derived.pa.aktuell
} else {
obj.at = item.system.at + context.derived.fk.aktuell
}
}
context.skills[talentGruppe].push(obj);
context.flatSkills.push(obj);
}
)
return context
}
_onRender(context, options) {
}
}

View File

@ -0,0 +1,102 @@
const {HandlebarsApplicationMixin, DocumentSheetV2} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets
export class StandaloneSpells extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
position: {width: 520, height: 480},
classes: ['dsa41', 'sheet', 'actor', 'character', 'standalone', 'spells'],
tag: 'form',
actions: {
openEmbeddedDocument: StandaloneSpells.#openEmbeddedDocument,
castSpell: StandaloneSpells.castSpell,
}
}
/** @inheritDoc */
static PARTS = {
form: {
template: `systems/DSA_4-1/templates/actor/character/standalone/spells.hbs`
}
}
_actor = null
constructor(actor) {
super(actor)
this._actor = actor
this.render(true)
}
static async #openEmbeddedDocument(event, target) {
this._actor?.sheet.options.actions.openEmbeddedDocument.bind(this)(event, target)
}
static async castSpell(event, target) {
this._actor?.sheet.options.actions.castSpell.bind(this)(event, target)
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
options.window.title = `${this.document.name}: Zauber und Rituale`
return options
}
async _prepareContext(context, options, object) {
const actorData = this.document
context.spells = []
context.system = actorData.system
context.flags = actorData.flags
context.derived = this.document.system
context.originalName = actorData.name
context.name = context.derived.name ?? actorData.name
context.effects = actorData.effects ?? []
const cleanUpMerkmal = (merkmale) => {
return merkmale.split(",").map((merkmal) => merkmal.trim())
}
const prepareEigenschaftRoll = (actorData, name) => {
if (name && name !== "*") {
return actorData.system.attribute[name.toLowerCase()].aktuell
} else {
return 0
}
}
actorData.itemTypes["Spell"].forEach((item, index) => {
const eigenschaften = item.system.probe;
const werte = [
{name: eigenschaften[0], value: prepareEigenschaftRoll(actorData, eigenschaften[0])},
{name: eigenschaften[1], value: prepareEigenschaftRoll(actorData, eigenschaften[1])},
{name: eigenschaften[2], value: prepareEigenschaftRoll(actorData, eigenschaften[2])}
]
context.spells.push({
id: item._id,
name: item.name,
zfw: item.system.zfw,
hauszauber: item.system.hauszauber,
merkmal: cleanUpMerkmal(item.system.merkmal),
rollEigenschaft1: werte[0].value,
rollEigenschaft2: werte[1].value,
rollEigenschaft3: werte[2].value,
eigenschaft1: werte[0].name,
eigenschaft2: werte[1].name,
eigenschaft3: werte[2].name,
fav: item.getFlag("DSA_4-1", "favourite")
})
})
context.hasSpells = context.spells.length > 0
return context
}
_onRender(context, options) {
}
}

View File

@ -21,7 +21,8 @@ export default {
description: item.system.description,
isAdvantage: !item.system.nachteil,
isDisadvantage: item.system.nachteil,
isBadAttribute: item.system.schlechteEigenschaft
isBadAttribute: item.system.schlechteEigenschaft,
fav: item.getFlag("DSA_4-1", "favourite")
})
} else {
context.flaws.push({
@ -32,7 +33,8 @@ export default {
description: item.system.description,
isAdvantage: !item.system.nachteil,
isDisadvantage: item.system.nachteil,
isBadAttribute: item.system.schlechteEigenschaft
isBadAttribute: item.system.schlechteEigenschaft,
fav: item.getFlag("DSA_4-1", "favourite")
})
}
}
@ -43,6 +45,7 @@ export default {
context.specialAbilities.push({
id: item._id,
name: item.system.value ? item.system.value : item.name,
fav: item.getFlag("DSA_4-1", "favourite")
});
}
);

View File

@ -1,7 +1,5 @@
import {PlayerCharacterDataModel} from "../../data/character.mjs";
export default {
_prepareContext: (context) => {
_prepareContext: (context, actor, thisObject) => {
const actorData = context.document
context.spells = []
@ -52,9 +50,11 @@ export default {
for (let setIndex = 0; setIndex < maxSets; setIndex++) {
context.sets.push({
tab: "set" + (setIndex + 1),
name: romanNumerals[setIndex],
tab: "pane" + (setIndex + 1),
label: romanNumerals[setIndex],
index: setIndex,
actorId: actorData.id,
setEquipped: actorData.system.setEquipped === setIndex,
slots: [
{
target: "links",
@ -131,6 +131,8 @@ export default {
]
})
}
context.selectedTab = thisObject.selectedTab ?? context.sets[0].tab
context.setEquipped = actorData.system.setEquipped
return context
},
@ -150,9 +152,50 @@ export default {
}
}).bind(thisObject.element);
const tabs = new foundry.applications.ux.Tabs({
navSelector: ".set .tabs.sets",
contentSelector: ".set .tab",
initial: thisObject.actor.system.setEquipped ? "pane" + (thisObject.actor.system.setEquipped + 1) : "pane1",
group: "set-tabs",
callback: (event, tab, tabName) => {
thisObject.selectedTab = tabName
thisObject.element.querySelectorAll(tab._contentSelector).forEach(
(tab) => {
if (tab.dataset["tab"] === tabName) {
tab.classList.add("active")
} else {
tab.classList.remove("active")
}
}
)
}
})
tabs.bind(thisObject.element)
new ContextMenu(
thisObject.element,
".equipment",
".paperdoll .equipped",
[
{
name: "Abrüsten",
icon: '<i class="fa-solid fa-suitcase"></i>',
callback: (targetElement) => {
const {setId, target} = targetElement.dataset
const updateObject = thisObject.document.getEquipmentSetUpdateObject()
delete updateObject[`system.heldenausruestung.${setId}.${target}`]
thisObject.document.update(updateObject)
},
condition: (target) => {
const {itemId} = target.dataset
return thisObject.document.isWorn(itemId)
}
},
], {jQuery: false})
new ContextMenu(
thisObject.element,
".inventory-table .equipment",
[
{
name: "Abrüsten",
@ -280,7 +323,7 @@ export default {
name: "Aus dem Inventar entfernen",
icon: '<i class="fa-solid fa-trash"></i>',
callback: (target) => {
thisObject.document.deleteEmbeddedDocuments('Item', [target.dataset.itemId])
game.DSA41.socket.executeAsGM("removeFromLootTable", thisObject.document.id, target.dataset.itemId)
},
condition: (target) => {
const {itemId} = target.dataset
@ -290,7 +333,7 @@ export default {
], {jQuery: false});
},
_getTabConfig: (group) => {
group.tabs.push({id: "equipment", group: "sheet", label: "Ausrüstung"})
group?.tabs.push({id: "equipment", group: "sheet", label: "Ausrüstung"})
},
template: `systems/DSA_4-1/templates/actor/character/tab-equipment.hbs`
}

View File

@ -1,5 +1,3 @@
import {ActionManager} from "../actions/action-manager.mjs";
export default {
_prepareContext: async (context, object) => {
@ -15,9 +13,6 @@ export default {
return object.items.get(object.system.heldenausruestung[setNumber]?.[slot])
}
const am = new ActionManager(actorData)
context.actions = am.evaluate().filter(action => action.type !== ActionManager.ATTACK)
context.inidice = actorData.system.ini.wuerfel
context.inivalue = actorData.system.ini.aktuell
context.inimod = actorData.system.ini.mod
@ -26,9 +21,35 @@ export default {
context.lepper = Math.min((actorData.system.lep.aktuell / actorData.system.lep.max) * 100, 100)
context.keper = Math.min((actorData.system.kap.aktuell / actorData.system.kap.max) * 100, 100)
context.aspper = Math.min((actorData.system.asp.aktuell / actorData.system.asp.max) * 100, 100)
context.lepcurrent = actorData.system.lep.aktuell ?? 0
context.lepcurrent = actorData.system.lep.aktuell ?? 0
context.aupcurrent = actorData.system.aup.aktuell ?? 0
context.aspcurrent = actorData.system.asp.aktuell ?? 0
context.kapcurrent = actorData.system.kap.aktuell ?? 0
context.maxWounds = actorData.system.wunden.max ?? 3
context.wounds = actorData.system.wunden.aktuell ?? 0
context.woundsFilled = []
for (let i = 1; i <= context.maxWounds; i++) {
context.woundsFilled[i] = i <= context.wounds
}
context.withErschoepfung = game.settings.get("DSA_4-1", "optional_erschoepfung")
context.ueberanstrengung = actorData.system.ueberanstrengung
context.erschoepfung = actorData.system.erschoepfung.aktuell
context.maxErschoepfung = actorData.system.erschoepfung.max
context.erschoepfungFilled = []
for (let i = 1; i <= context.maxErschoepfung; i++) {
context.erschoepfungFilled[i] = i <= context.erschoepfung
}
context.effects = []
for (let i = 0; i < actorData.appliedEffects.length; i++) {
const item = actorData.appliedEffects[i]
context.effects.push(item.name)
}
return context
@ -37,7 +58,7 @@ export default {
},
_getTabConfig: (group) => {
group.tabs.push({id: "combat", group: "sheet", label: "Kampf"})
group.tabs.push({id: "health", group: "sheet", label: "Gesundheit"})
},
template: `systems/DSA_4-1/templates/actor/character/tab-combat.hbs`
template: `systems/DSA_4-1/templates/actor/character/tab-health.hbs`
}

View File

@ -1,4 +1,4 @@
import {LiturgyData} from "../../data/miracle/liturgydata.mjs";
import {LiturgyData} from "../../data/miracle/liturgyData.mjs";
export default {
_prepareContext: (context) => {
@ -57,17 +57,17 @@ export default {
// sort by rank
const rankData = LiturgyData.getRankOfLiturgy(item.system, deity)
if (rankData) {
console.log(rankData)
let {index, name, lkp, mod, costKaP} = rankData;
insertObject["count" + name] = insertObject["count" + name] + 1;
insertObject[name].push({
insertObject[name]?.push({
id: item._id,
name: item.name,
lkpReq: lkp,
lkpMod: mod,
costKaP,
fav: item.getFlag("DSA_4-1", "favourite"),
rank: index, // get effective liturgy rank based on deity
liturgiekenntnis: deity,
})
@ -104,7 +104,7 @@ export default {
_getTabConfig: (group, thisObject) => {
const hasLiturgies = thisObject.document.items.filter(p => p.type === "Liturgy").length > 0 ?? false
if (hasLiturgies) {
group.tabs.push({id: "liturgies", group: "sheet", label: "Liturgien"})
group?.tabs.push({id: "liturgies", group: "sheet", label: "Liturgien"})
}
},
template: `systems/DSA_4-1/templates/actor/character/tab-liturgies.hbs`

View File

@ -14,7 +14,7 @@ export default {
},
_getTabConfig: (group) => {
group.tabs.push({id: "meta", group: "sheet", label: "Meta"})
group?.tabs.push({id: "meta", group: "sheet", label: "Meta"})
},
template: `systems/DSA_4-1/templates/actor/character/tab-meta.hbs`
}

View File

@ -2,7 +2,6 @@ export default {
_prepareContext: (context) => {
const actorData = context.document
context.spells = []
context.system = actorData.system
context.flags = actorData.flags
context.derived = context.document.system
@ -51,7 +50,8 @@ export default {
id: item._id,
at: item.system.at,
pa: item.system.pa,
komplexität: item.system.komplexität
komplexität: item.system.komplexität,
fav: item.getFlag("DSA_4-1", "favourite")
};
if (talentGruppe === "Kampf") {

View File

@ -14,14 +14,21 @@ export default {
return merkmale.split(",").map((merkmal) => merkmal.trim())
}
const prepareEigenschaftRoll = (actorData, name) => {
if (name && name !== "*") {
return actorData.system.attribute[name.toLowerCase()].aktuell
} else {
return 0
}
}
actorData.itemTypes["Spell"].forEach((item, index) => {
Object.values(actorData.items).forEach((item, index) => {
if (item.type === "Spell") {
const eigenschaften = item.system.probe;
const werte = [
{name: eigenschaften[0], value: this.prepareEigenschaftRoll(actorData, eigenschaften[0])},
{name: eigenschaften[1], value: this.prepareEigenschaftRoll(actorData, eigenschaften[1])},
{name: eigenschaften[2], value: this.prepareEigenschaftRoll(actorData, eigenschaften[2])}
{name: eigenschaften[0], value: prepareEigenschaftRoll(actorData, eigenschaften[0])},
{name: eigenschaften[1], value: prepareEigenschaftRoll(actorData, eigenschaften[1])},
{name: eigenschaften[2], value: prepareEigenschaftRoll(actorData, eigenschaften[2])}
]
context.spells.push({
id: item._id,
@ -35,8 +42,9 @@ export default {
eigenschaft1: werte[0].name,
eigenschaft2: werte[1].name,
eigenschaft3: werte[2].name,
fav: item.getFlag("DSA_4-1", "favourite")
})
}
})
context.hasSpells = context.spells.length > 0

View File

@ -1,5 +1,5 @@
import Advsf from "./character/advsf.mjs"
import Combat from "./character/combat.mjs"
import Health from "./character/health.mjs"
import Effects from "./character/effects.mjs"
import Equipment from "./character/equipment.mjs"
import Liturgies from "./character/liturgies.mjs"
@ -11,14 +11,21 @@ import {CombatActionDialog} from "../dialog/combatAction.mjs";
import {ActionManager} from "./actions/action-manager.mjs";
import {DefenseActionDialog} from "../dialog/defenseAction.mjs";
import {RestingDialog} from "../dialog/restingDialog.mjs";
import {Character} from "../documents/character.mjs";
import {LiturgyDialog} from "../dialog/liturgyDialog.mjs";
import {TalentDialog} from "../dialog/talentDialog.mjs";
import {AttributeDialog} from "../dialog/attributeDialog.mjs";
import {ItemBrowserDialog} from "../dialog/itemBrowserDialog.mjs";
import * as EquipmentDocument from "../documents/equipment.mjs";
import {StandaloneADVSF} from "./character-standalone/advsf.mjs";
import {StandaloneSkills} from "./character-standalone/skills.mjs";
import {Bagpack} from "./character-standalone/bagpack.mjs";
import {StandaloneSpells} from "./character-standalone/spells.mjs";
import {StandaloneLiturgies} from "./character-standalone/liturgies.mjs";
import {StandaloneHealth} from "./character-standalone/health.mjs";
import {SpellDialog} from "../dialog/spellDialog.mjs";
const {HandlebarsApplicationMixin, DocumentSheetV2} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets
const {ContextMenu} = foundry.applications.ux
class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
@ -39,19 +46,33 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
actions: {
rollCombatSkill: CharacterSheet.#rollCombatSkill,
rollSkill: CharacterSheet.#rollSkill,
rollFlaw: CharacterSheet.#rollFlaw,
rollFlaw: CharacterSheet.rollFlaw,
rollAttribute: CharacterSheet.#rollAttribute,
editImage: DocumentSheetV2.DEFAULT_OPTIONS.actions.editImage,
openEmbeddedDocument: CharacterSheet.#openEmbeddedDocument,
openEmbeddedDocument: CharacterSheet.openEmbeddedDocument,
openCultureDocument: CharacterSheet.#openCultureDocument,
openSpeciesDocument: CharacterSheet.#openSpeciesDocument,
openCombatAction: CharacterSheet.#openCombatAction,
openLiturgyDialog: CharacterSheet.#openLiturgyDialog,
openLiturgyDialog: CharacterSheet.openLiturgyDialog,
openSpellDialog: CharacterSheet.openSpellDialog,
castSpell: CharacterSheet.castSpell,
progressCooldown: CharacterSheet.#progressCooldown,
cancelCooldown: CharacterSheet.#cancelCooldown,
activateCooldown: CharacterSheet.#activateCooldown,
rest: CharacterSheet.#startResting,
removeEffect: CharacterSheet.#removeEffect,
rollDamage: CharacterSheet.#rollDamage,
openItemBrowser: CharacterSheet.openItemBrowser,
newItem: CharacterSheet.addNewItem,
toggleFav: CharacterSheet.toggleFav,
openStandaloneADVSF: CharacterSheet.#openStandaloneADVSF,
openStandaloneSkills: CharacterSheet.#openStandaloneSkills,
openBagpack: CharacterSheet.#openBagpack,
openStandaloneSpells: CharacterSheet.#openStandaloneSpells,
openStandaloneLiturgies: CharacterSheet.#openStandaloneLiturgies,
openStandaloneHealth: CharacterSheet.#openStandaloneHealth,
setWounds: CharacterSheet.#setWounds,
switchSet: CharacterSheet.#switchSet
}
}
@ -60,7 +81,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
sheet: {
tabs: [],
initial: 'meta'
}
},
}
/** @inheritDoc */
@ -77,15 +98,16 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
advsf: {
template: Advsf.template
},
combat: {
template: Combat.template
health: {
template: Health.template
},
equipment: {
template: Equipment.template,
scrollable: ['']
scrollable: ['.inventory']
},
skills: {
template: Skills.template
template: Skills.template,
scrollable: ['.tab.skills']
},
spells: {
template: Spells.template
@ -95,7 +117,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
},
effects: {
template: Effects.template
}
},
}
@ -130,7 +152,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
}).render(true)
}
static async #rollFlaw(event, target) {
static async rollFlaw(event, target) {
new AttributeDialog(this.document, target.dataset.itemId).render(true)
}
@ -141,12 +163,12 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
cooldowns.splice(cooldownId, 1)
if (cooldown) {
cooldown.current = cooldown.current - 1
cooldown.current = cooldown.current + 1
}
cooldowns.push(cooldown)
this.document.update({"system.cooldowns": cooldowns.sort((a, b) => a.current - b.current)})
ui.notifications.info(`Abklingzeit von ${cooldown.data.maneuver.name} um 1 Aktion reduziert`)
ui.notifications.info(`Abklingzeit von ${cooldown.data.title} um 1 Aktion reduziert`)
}
static async #cancelCooldown(event, target) {
@ -165,26 +187,26 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
const cooldowns = this.document.system.cooldowns
const cooldown = this.document.system.cooldowns[cooldownId]
if (cooldown && cooldown.current <= 0) {
if (cooldown && cooldown.current >= cooldown.start) {
const am = new ActionManager(this.document)
const action = am.evaluate().find(action => action.name === cooldown.data.maneuver.id)
const action = new Function(`return ${cooldown.data.maneuver}`)
if (action) {
action.activate(cooldowns, {...cooldown.data, actor: this.document})
action()(this.document.system.cooldowns, {...cooldown.data, actor: this.document})
}
}
cooldowns.splice(cooldownId, 1)
this.document.update({"system.cooldowns": cooldowns.sort((a, b) => a.current - b.current)})
ui.notifications.info(`${cooldown.data.maneuver.name} ausgeführt`)
ui.notifications.info(`${cooldown.data.title} ausgeführt`)
}
/**
*
* @param {MouseEvent} event
*/
static #openEmbeddedDocument(event) {
static openEmbeddedDocument(event) {
let dataset = event.target.dataset
if (!dataset.itemId && !dataset.id) {
dataset = event.target.parentElement.dataset
@ -203,21 +225,33 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
}
static #openCombatAction(event, target) {
let {weapon, skill} = target.dataset
switch (target.dataset.mode) {
case "attack":
new CombatActionDialog(this.document).render(true)
new CombatActionDialog(this.document, {weapon, skill}).render(true)
break
case "defense":
new DefenseActionDialog(this.document).render(true)
new DefenseActionDialog(this.document, {weapon, skill}).render(true)
break
}
}
static #openLiturgyDialog(event, target) {
static openLiturgyDialog(event, target) {
const {id, lkp, deity} = target.dataset
new LiturgyDialog(this.document, lkp, id, deity).render(true)
}
static openSpellDialog(event, target) {
const {itemId} = target.dataset
this.document.itemTypes["Spell"]?.find(p => p.id === itemId)?.sheet.render(true)
}
static castSpell(event, target) {
const {itemId} = target.dataset
new SpellDialog(this.document, itemId).render(true)
}
static #startResting(event, target) {
const dialog = new RestingDialog(this.document)
@ -239,6 +273,65 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
}
static async openItemBrowser(event, target) {
new ItemBrowserDialog(this.document).render(true)
}
static async addNewItem(event, target) {
let item = new EquipmentDocument.Equipment({
name: "Neuer Gegenstand",
type: "Equipment",
})
const items = await this.document.createEmbeddedDocuments("Item", [item])
items[0].sheet.render(true)
}
static async toggleFav(event, target) {
const {itemId} = target.dataset
const doc = this.document.items.find(p => p.id === itemId)
if (doc) {
const previous = doc.getFlag("DSA_4-1", "favourite") ?? false
doc.setFlag("DSA_4-1", "favourite", !previous)
}
}
static async #openStandaloneADVSF(event, target) {
new StandaloneADVSF(this.document)
}
static async #openStandaloneHealth(event, target) {
new StandaloneHealth(this.document)
}
static async #openStandaloneSkills(event, target) {
new StandaloneSkills(this.document)
}
static async #openBagpack(event, target) {
new Bagpack(this.document)
}
static async #openStandaloneSpells(event, target) {
new StandaloneSpells(this.document)
}
static async #openStandaloneLiturgies(event, target) {
new StandaloneLiturgies(this.document)
}
static async #setWounds(event, target) {
const {value} = target.dataset
this.document.update({"system.wunden.aktuell": value})
this.render(true)
}
static async #switchSet(event, target) {
const {id} = target.dataset
this.document.update({"system.setEquipped": id})
this.render(true)
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
@ -249,6 +342,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
return options
}
/**
* Handle form submission
* @this {AdvantageSheet}
@ -259,16 +353,49 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
await this.document.update(formData.object) // Note: formData.object
await this.document.update(formData.object)
}
static async #rollDamage(event, target) {
let {weapon, isRanged} = target.dataset
isRanged = isRanged == "true"
weapon = this.document.items.get(weapon)
if (weapon) {
const damageFormula = isRanged ? weapon.system.rangedAttackDamage : weapon.system.meleeAttackDamage
const calculation = await foundry.applications.api.DialogV2.prompt({
window: {title: game.i18n.format("COMBAT_DIALOG_TP.windowTitle")},
content: `<div><label><span>${game.i18n.format("COMBAT_DIALOG_TP.regularFormula")}</span><input type="text" name="formula" value="${damageFormula}"></label></div><div><label><span>${game.i18n.format("COMBAT_DIALOG_TP.bonusDamage")}</span><input type="text" name="bonusDamage" value="0"></label></div>`,
ok: {
label: game.i18n.format("COMBAT_DIALOG_TP.buttonText"),
callback: (event, button, dialog) => {
return {
formula: button.form.elements.formula.value,
bonusDamage: button.form.elements.bonusDamage.value
}
}
}
});
const sanitisedFormula = calculation.formula.replace(/wW/g, "d")
const suffix = calculation.bonusDamage >= 0 ? "+" + calculation.bonusDamage : calculation.bonusDamage
let r = new Roll(sanitisedFormula + suffix, this.document.getRollData());
const label = `Schadenswurf`
await r.toMessage({
speaker: ChatMessage.getSpeaker({actor: this.document}),
flavor: label,
rollMode: game.settings.get('core', 'rollMode'),
})
}
}
_getTabsConfig(group) {
const tabs = foundry.utils.deepClone(super._getTabsConfig(group))
Meta._getTabConfig(tabs, this);
Social._getTabConfig(tabs, this);
Meta._getTabConfig(tabs, this)
Social._getTabConfig(tabs, this)
Advsf._getTabConfig(tabs, this)
Combat._getTabConfig(tabs, this)
Health._getTabConfig(tabs, this)
Equipment._getTabConfig(tabs, this)
Skills._getTabConfig(tabs, this)
Spells._getTabConfig(tabs, this)
@ -330,13 +457,6 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
context.img = actorData.img
context.effects = actorData.effects ?? []
context.maxWounds = actorData.system.wunden.max ?? 3
context.wounds = actorData.system.wunden.gesamt ?? 0
context.woundsFilled = []
for (let i = 1; i <= context.maxWounds; i++) {
context.woundsFilled[i] = i <= context.wounds
}
context.zonenruestung = game.settings.get("DSA_4-1", "optional_ruestungzonen")
context.trefferzonen = game.settings.get("DSA_4-1", "optional_trefferzonen")
context.ausdauer = game.settings.get("DSA_4-1", "optional_ausdauer")
@ -351,8 +471,11 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
context.lepper = Math.min((actorData.system.lep.aktuell / actorData.system.lep.max) * 100, 100)
context.keper = Math.min((actorData.system.kap.aktuell / actorData.system.kap.max) * 100, 100)
context.aspper = Math.min((actorData.system.asp.aktuell / actorData.system.asp.max) * 100, 100)
context.lepcurrent = actorData.system.lep.aktuell ?? 0
context.aupcurrent = actorData.system.aup.aktuell ?? 0
context.aspcurrent = actorData.system.asp.aktuell ?? 0
context.kapcurrent = actorData.system.kap.aktuell ?? 0
const fernkampf = actorData.findEquipmentOnSlot("fernkampf", actorData.system.setEquipped, actorData)
const links = actorData.findEquipmentOnSlot("links", actorData.system.setEquipped, actorData)
@ -361,18 +484,19 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
if (fernkampf) {
const fkitems = fernkampf.system.rangedSkills.map((skillInQuestion) => actorData.items.find(p => p.name === skillInQuestion))
fkitems.forEach(async skill => {
const obj = await skill
context.attacks.push({
name: obj.name,
using: fernkampf.name,
atroll: `1d20cs<${this.document.system.fk.aktuell + obj.system.at}`,
at: `${this.document.system.fk.aktuell + obj.system.at}`,
tproll: `${fernkampf.system.rangedAttackDamage}`, // TODO consider adding TP/KK mod and Range mod
tp: `${fernkampf.system.rangedAttackDamage}`,
iniroll: `(${context.inidice})d6 + ${context.inivalue + fernkampf.system.iniModifier ?? 0}`,
ini: `${context.inidice}w6 + ${context.inivalue + fernkampf.system.iniModifier ?? 0}`,
})
fkitems?.forEach(skill => {
if (skill) {
context.attacks.push({
name: skill.name,
id: fernkampf._id,
skillId: skill._id,
using: fernkampf.name,
isRanged: true,
at: `${this.document.system.fk.aktuell + skill.system.at}`,
tp: `${fernkampf.system.rangedAttackDamage}`,
ini: `${context.inidice}w6 + ${context.inivalue + fernkampf.system.iniModifier ?? 0}`,
})
}
})
}
if (links) {
@ -383,18 +507,17 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
meitems.push(item)
}
})
meitems.forEach(skill => {
meitems?.forEach(skill => {
const obj = skill
context.attacks.push({
name: obj.name,
id: links._id,
skillId: skill._id,
using: links.name,
atroll: `1d20cs<${this.document.system.at.links.aktuell + obj.system.at + links.system.attackModifier}`, // TODO consider adding W/M
isRanged: false,
at: `${this.document.system.at.links.aktuell + obj.system.at + links.system.attackModifier}`,
paroll: `1d20cs<${this.document.system.pa.links.aktuell + obj.system.pa + links.system.parryModifier}`, // TODO consider adding W/M
pa: `${this.document.system.pa.links.aktuell + obj.system.pa + links.system.parryModifier}`,
tproll: `${links.system.meleeAttackDamage}`, // TODO consider adding TP/KK mod
tp: `${links.system.meleeAttackDamage}`,
iniroll: `(${context.inidice})d6 + ${context.inivalue + links.system.iniModifier ?? 0}`,
ini: `${context.inidice}w6 + ${context.inivalue + links.system.iniModifier ?? 0}`,
})
})
@ -407,30 +530,82 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
meitems.push(item)
}
})
meitems.forEach(skill => {
meitems?.forEach(skill => {
const obj = skill
context.attacks.push({
name: obj.name,
id: rechts._id,
skillId: skill._id,
using: rechts.name,
atroll: `1d20cs<${this.document.system.at.rechts.aktuell + obj.system.at + rechts.system.attackModifier}`, // TODO consider adding W/M
isRanged: false,
at: `${this.document.system.at.rechts.aktuell + obj.system.at + rechts.system.attackModifier}`,
paroll: `1d20cs<${this.document.system.pa.rechts.aktuell + obj.system.pa + rechts.system.parryModifier}`, // TODO consider adding W/M
pa: `${this.document.system.pa.rechts.aktuell + obj.system.pa + rechts.system.parryModifier}`,
tproll: `${rechts.system.meleeAttackDamage}`, // TODO consider adding TP/KK mod
tp: `${rechts.system.meleeAttackDamage}`,
iniroll: `(${context.inidice})d6 + ${context.inivalue + rechts.system.iniModifier ?? 0}`,
ini: `${context.inidice}w6 + ${context.inivalue + rechts.system.iniModifier ?? 0}`,
})
})
}
context.favourites = actorData.items.filter(item => item.getFlag("DSA_4-1", "favourite") === true).map(item => {
let id = item.id
let t = null
switch (item.type) {
case "Spell":
t = "systems/DSA_4-1/templates/ui/partial-mini-rollable-spell-button.hbs"
break;
case "Skill":
switch (item.system.gruppe) {
case "Kampf":
t = "systems/DSA_4-1/templates/ui/partial-mini-rollable-weaponskill-button.hbs"
break;
case "Sprachen":
t = "systems/DSA_4-1/templates/ui/partial-mini-rollable-language-button.hbs"
break;
default:
t = "systems/DSA_4-1/templates/ui/partial-mini-rollable-button.hbs"
}
break;
case "Liturgy":
t = "systems/DSA_4-1/templates/ui/partial-mini-rollable-liturgy-button.hbs"
break;
default:
t = null
}
let obj = Object.assign({}, item)
obj.fav = item.getFlag("DSA_4-1", "favourite")
obj.id = id
obj.group = item.system.gruppe ?? null
if (t) {
obj.template = t
}
return obj
})
context.cooldowns = actorData.system.cooldowns ?? []
context.cooldowns.forEach(cooldown => {
const weapon = this.document.itemTypes["Equipment"].find(p => p._id === cooldown.data.weapon)
const skill = this.document.itemTypes["Skill"].find(p => p._id === cooldown.data.skillId)
const target = game.actors.get(game.scenes.current.tokens.find(p => p._id === cooldown.data.target).actorId)
let weapon = null
let target = null
let tooltip = cooldown.data.title
if (cooldown.data.weapon) {
weapon = this.document.itemTypes["Equipment"].find(p => p._id === cooldown.data.weapon)
tooltip += `<br/>Waffe: ${weapon.name}`
}
if (cooldown.data.target) {
target = game.actors.get(game.scenes.current.tokens.find(p => p._id === cooldown.data.target).actorId)
tooltip += `<br/>Ziel: ${target.name}`
}
cooldown.title = cooldown.data.title
cooldown.progress = ((cooldown.current / cooldown.start) * 100) + "%"
cooldown.tooltip = `${cooldown.data.maneuver.name}<br/>Waffe:${weapon.name}<br/>Ziel: ${target.name}<br/>Wurfziel: ${cooldown.data.targetNumber}<br/>Aktionen verbleibend: ${cooldown.current}`
if (cooldown.start - cooldown.current > 0) {
cooldown.tooltip = tooltip + `<br/>Aktionen verbleibend: ${cooldown.start - cooldown.current}<hr/><i class="fa-solid fa-computer-mouse"></i>: 1 Aktion aufwenden`
} else {
cooldown.tooltip = tooltip + `<br/>Aktionen verbleibend: ${cooldown.start - cooldown.current}<hr/><i class="fa-solid fa-computer-mouse"></i>: Aktion durchführen`
}
})
context.hasSpells = actorData.itemTypes["Spell"].length > 0
@ -497,11 +672,11 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
case "advsf":
await Advsf._prepareContext(context, this.document)
break
case "combat":
await Combat._prepareContext(context, this.document)
case "health":
await Health._prepareContext(context, this.document)
break
case "equipment":
await Equipment._prepareContext(context, this.document)
await Equipment._prepareContext(context, this.document, this)
break
case "skills":
await Skills._prepareContext(context, this.document)
@ -519,11 +694,23 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
return context
}
_onPosition(position) {
if (position.width < 300) {
this.element.classList.add("tiny")
this.element.querySelector(".sidebuttons").style.left = (position.width + position.left) + "px"
this.element.querySelector(".sidebuttons").style.top = (position.top) + "px"
} else {
this.element.classList.remove("tiny")
}
}
_onRender(context, options) {
Meta._onRender(context, options, this.element)
Social._onRender(context, options, this.element)
Advsf._onRender(context, options, this)
Combat._onRender(context, options, this.element)
Health._onRender(context, options, this.element)
Effects._onRender(context, options, this.element)
Equipment._onRender(context, options, this)
Liturgies._onRender(context, options, this.element)
@ -535,18 +722,28 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
return true
}
async _onDrop(event) {
const data = TextEditor.implementation.getDragEventData(event);
const actor = this.actor;
const data = TextEditor.implementation.getDragEventData(event)
const targetDocument = this.actor.itemTypes["Equipment"].find(p => p._id === event.target.dataset['itemId'])
//const allowed = Hooks.call("dropActorSheetData", actor, this, data);
//if (allowed === false) return;
if (event.target.dataset["target"] && event.target.dataset["setId"]) {
const documentClass = foundry.utils.getDocumentClass(data.type)
if (documentClass) {
const document = await documentClass.fromDropData(data)
const {setId, target} = event.target.dataset
const updateObject = this.actor.getEquipmentSetUpdateObject()
updateObject[`system.heldenausruestung.${setId}.${target}`] = document.id
await this.actor.update(updateObject)
}
return
}
// Dropped Documents
const documentClass = foundry.utils.getDocumentClass(data.type);
const documentClass = foundry.utils.getDocumentClass(data.type)
if (documentClass) {
const document = await documentClass.fromDropData(data);
const document = await documentClass.fromDropData(data)
if (document.type === "Equipment" || document.type === "Advantage" || document.type === "Spell" || document.type === "Liturgy" || document.type === "ActiveEffect" || document.type === "SpecialAbility") {
// No duplication by moving items from one actor to another
@ -566,7 +763,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
} else {
if (document.parent && document.parent !== this.actor) {
document.parent.items.get(document._id).delete()
game.DSA41.socket.executeAsGM("removeFromLootTable", document.parent.id, document._id)
}
await this._onDropDocument(event, document)

View File

@ -25,7 +25,6 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
static TABS = {
sheet: {
tabs: [],
initial: 'meta'
}
}
@ -66,6 +65,19 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
tabs.tabs.push(tab)
}
}
if (!game.user.isGM) {
if (this.document.system.visibility.meta) {
tabs.initial = 'meta'
} else if (this.document.system.visibility.attacks) {
tabs.initial = 'attacks'
} else if (this.document.system.visibility.description) {
tabs.initial = 'description'
} else if (this.document.system.visibility.loot) {
tabs.initial = 'loot'
}
}
return tabs
}
@ -89,7 +101,7 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
await this.document.update(formData.object) // Note: formData.object
await this.document.update(formData.object)
}
static #openEmbeddedDocument(event, target) {
@ -100,8 +112,8 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
static async #removeAttack(evt) {
const {index} = evt.srcElement.dataset;
let sans = Array.from(this.document.system.attacks);
sans.splice(index, 1);
let sans = Array.from(this.document.system.attacks)
sans.splice(index, 1)
await this.document.update({'system.attacks': sans})
}
@ -113,7 +125,7 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
speaker: ChatMessage.getSpeaker({actor: this.document}),
flavor: label,
rollMode: game.settings.get('core', 'rollMode'),
});
})
}
static async #addAttack() {
@ -140,11 +152,11 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const actorData = context.document;
const context = await super._prepareContext(options)
const actorData = context.document
context.attacks = [];
context.actor = actorData;
context.attacks = []
context.actor = actorData
actorData.system.attacks.forEach((attack, index) => {
context.attacks.push({
@ -180,7 +192,7 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
})
return context;
return context
}
@ -199,7 +211,7 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
//const allowed = Hooks.call("dropActorSheetData", actor, this, data);
// if (allowed === false) return;
// Dropped Documents
const documentClass = foundry.utils.getDocumentClass(data.type);
const documentClass = foundry.utils.getDocumentClass(data.type)
if (documentClass) {
const document = await documentClass.fromDropData(data);
@ -209,7 +221,7 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
document.parent.items.get(document._id).delete()
}
await this._onDropDocument(event, document);
await this._onDropDocument(event, document)
}
}
}
@ -226,7 +238,7 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
}
}).bind(this.element);
}).bind(this.element)
}

View File

@ -44,7 +44,7 @@ export class CultureSheet extends HandlebarsApplicationMixin(DocumentSheetV2) {
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
await this.document.update(formData.object) // Note: formData.object
await this.document.update(formData.object)
}
/** @override */

View File

@ -0,0 +1,228 @@
const {DocumentSheetV2, HandlebarsApplicationMixin} = foundry.applications.api
export class DeitySheet extends HandlebarsApplicationMixin(DocumentSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
position: {width: 520, height: 848},
classes: ['dsa41', 'sheet', 'item', 'deity'],
tag: 'form',
form: {
submitOnChange: true,
closeOnSubmit: false,
handler: DeitySheet.#onSubmitForm
},
window: {
resizable: true,
},
actions: {
editImage: DocumentSheetV2.DEFAULT_OPTIONS.actions.editImage,
openLiturgy: DeitySheet.#openLiturgySheet,
removeLiturgy: DeitySheet.#removeLiturgy,
}
}
/** @inheritDoc */
static PARTS = {
form: {
template: `systems/DSA_4-1/templates/item/deity-sheet.hbs`
},
}
/**
* Handle form submission
* @this {SpeciesSheet}
* @param {SubmitEvent} event
* @param {HTMLFormElement} form
* @param {FormDataExtended} formData
*/
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
formData.object["system.miraclePlus"] = formData.object.miraclePlus.split(",")
formData.object["system.miracleMinus"] = formData.object.miracleMinus.split(",")
delete formData.object.miraclePlus
delete formData.object.miracleMinus
await this.document.update(formData.object) // Note: formData.object
}
static async #openLiturgySheet(event, target) {
const {rank, liturgyId} = target.dataset
if (liturgyId && rank) {
this.document.system.liturgies["rank" + rank].find(p => p._id === liturgyId)?.sheet.render(true, {editable: false})
}
}
static async #removeLiturgy(event, target) {
const {rank, liturgyId} = target.dataset
if (liturgyId && rank) {
const idx = this.document.system.liturgies["rank" + rank].findIndex(p => p._id === liturgyId)
this.document.system.liturgies["rank" + rank].splice(idx, 1)
const thisUpdateObject = {}
thisUpdateObject["system.liturgies.rank" + rank] = this.document.system.liturgies
this.document.update(thisUpdateObject)
this.render({parts: ["form"]})
}
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
if (options.window) {
options.window.title = this.document.name
}
return options
}
/** @override */
async _prepareContext(options) {
// const context = await super._prepareContext(options)
context.system = this.document.system
context.name = this.document.name
context.img = this.document.img
context.description = this.document.description
context.liturgies = {
rank0: [],
rank1: [],
rank2: [],
rank3: [],
rank4: [],
rank5: [],
rank6: [],
rank7: [],
rank8: [],
}
context.miraclePlus = this.document.system.miraclePlus?.join(",")
context.miracleMinus = this.document.system.miracleMinus?.join(",")
this.document.system.liturgies.rank0.forEach(liturgyUuid => {
fromUuid(liturgyUuid).then(liturgy => {
context.liturgies.rank0.push({
id: liturgy._id,
name: liturgy.name,
})
})
})
this.document.system.liturgies.rank1.forEach(liturgyUuid => {
fromUuid(liturgyUuid).then(liturgy => {
context.liturgies.rank1.push({
id: liturgy._id,
name: liturgy.name,
})
})
})
this.document.system.liturgies.rank2.forEach(liturgyUuid => {
fromUuid(liturgyUuid).then(liturgy => {
context.liturgies.rank2.push({
id: liturgy._id,
name: liturgy.name,
})
})
})
this.document.system.liturgies.rank3.forEach(liturgyUuid => {
fromUuid(liturgyUuid).then(liturgy => {
context.liturgies.rank3.push({
id: liturgy._id,
name: liturgy.name,
})
})
})
this.document.system.liturgies.rank4.forEach(liturgyUuid => {
fromUuid(liturgyUuid).then(liturgy => {
context.liturgies.rank4.push({
id: liturgy._id,
name: liturgy.name,
})
})
})
this.document.system.liturgies.rank5.forEach(liturgyUuid => {
fromUuid(liturgyUuid).then(liturgy => {
context.liturgies.rank5.push({
id: liturgy._id,
name: liturgy.name,
})
})
})
this.document.system.liturgies.rank6.forEach(liturgyUuid => {
fromUuid(liturgyUuid).then(liturgy => {
context.liturgies.rank6.push({
id: liturgy._id,
name: liturgy.name,
})
})
})
this.document.system.liturgies.rank7.forEach(liturgyUuid => {
fromUuid(liturgyUuid).then(liturgy => {
context.liturgies.rank7.push({
id: liturgy._id,
name: liturgy.name,
})
})
})
this.document.system.liturgies.rank8.forEach(liturgyUuid => {
fromUuid(liturgyUuid).then(liturgy => {
context.liturgies.rank8.push({
id: liturgy._id,
name: liturgy.name,
})
})
})
return context
}
async _canDragDrop() {
return true
}
_onRender(context, options) {
new foundry.applications.ux.DragDrop.implementation({
dropSelector: ".liturgy-drops",
permissions: {
drop: this._canDragDrop.bind(this)
},
callbacks: {
drop: this._onDrop.bind(this)
}
}).bind(this.element);
}
async _onDrop(event, target) {
const data = TextEditor.implementation.getDragEventData(event);
const documentClass = foundry.utils.getDocumentClass(data.type)
if (documentClass) {
const document = await documentClass.fromDropData(data);
if (document.type === "Liturgy") {
// process and drop it rightly
const {rank} = event.target.dataset
if (rank) {
let rankData = this.document.system.liturgies["rank" + rank]
rankData.push(document.uuid)
const updateData = {}
updateData["system.liturgies.rank" + rank] = rankData
this.document.update(updateData)
this.render({parts: ["form"]})
}
}
}
}
}

View File

@ -1,17 +1,6 @@
const {DocumentSheetV2, HandlebarsApplicationMixin} = foundry.applications.api
/**
* @typedef ApplicationTab
* @property {string} id
* @property {string} group
* @property {boolean} active
* @property {string} cssClass
* @property {string} [label]
* @property {string} [icon]
* @property {string} [tooltip]
*/
export class EquipmentSheet extends HandlebarsApplicationMixin(DocumentSheetV2) {
class EquipmentSheet extends HandlebarsApplicationMixin(DocumentSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
@ -322,3 +311,5 @@ export class EquipmentSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
}
}
export default EquipmentSheet

View File

@ -106,7 +106,7 @@ export class GroupSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
}
async #onUpdateCharacterSettings(data) {
if (data.type === "character") {
if (data.type === "Character") {
// update group
let settings = {...this.document.system.settings}
@ -251,7 +251,7 @@ export class GroupSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
// Drag-drop
new foundry.applications.ux.DragDrop.implementation({
dragSelector: ".inventory-table .equipment",
dropSelector: ".inventory-table",
dropSelector: ".inventory",
permissions: {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this)
@ -265,10 +265,10 @@ export class GroupSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
// Update Group Members when either an Actor was moved into the linked Folder or removed from the linked Folder
Hooks.on('updateActor', (data) => {
if (data._id !== this.document._id) { // dont update yourself when you update yourself... baka!
if (data.type === "character" && data.folder?._id === this.document.system.groupId) {
if (data.type === "Character" && data.folder?._id === this.document.system.groupId) {
this.#onUpdateCharacterSettings(data)
this.render()
} else if (data.type === "character") {
} else if (data.type === "Character") {
this.render()
}
}

View File

@ -20,6 +20,8 @@ export class MerchantSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
resizable: true,
},
actions: {
buy: MerchantSheet.#buyWare,
editItem: MerchantSheet.#openEmbeddedDocument,
editImage: DocumentSheetV2.DEFAULT_OPTIONS.actions.editImage,
editServiceImage: MerchantSheet.#editServiceImage,
editNewServiceImage: MerchantSheet.#editNewServiceImage,
@ -81,6 +83,63 @@ export class MerchantSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
await this.document.update(formData.object) // Note: formData.object
}
static async #buyWare(event, target) {
const {itemId} = target.dataset
const item = this.document.items.get(itemId)
let selections = ''
game.actors.filter(p => p.isOwner && p.type === "Character").forEach(actor => {
selections += `<option value=${actor.id}>${actor.name}</option>`
})
const actorId = await foundry.applications.api.DialogV2.prompt({
window: {title: `${item.name} kaufen mit wem?`},
content: `<select name="actor">${selections}</select>`,
ok: {
label: `Kaufen`,
callback: (event, button, dialog) => button.form.elements.actor.value
}
});
if (actorId) { // ignore the following when dialog was cancelled
const actor = game.actors.get(actorId)
const canBuy = await actor.reduceWealth(item.system.price)
if (canBuy) { // returns false when the wealth cant be reduced sufficiently
actor.createEmbeddedDocuments('Item', [item]).then(documents => {
documents[0].update({'system.quantity': 1})
})
game.DSA41.socket.executeAsGM("buyFromLootTable", this.document.id, item.id)
ChatMessage.create({
user: game.user._id,
speaker: {actor},
content: `hat ${item.name} für ${game.DSA41.displayCurrency(item.system.price)} gekauft`,
type: CONST.CHAT_MESSAGE_TYPES.IC
})
} else {
ui.notifications.error(item.name + " ist zu teuer für " + actor.name)
}
}
}
/**
*
* @param {MouseEvent} event
*/
static #openEmbeddedDocument(event) {
let dataset = event.target.dataset
if (!dataset.itemId && !dataset.id) {
dataset = event.target.parentElement.dataset
}
const id = dataset.itemId ?? dataset.id
if (this.document.isOwner) { // only shop owner can change stock and price
this.document.items.get(id).sheet.render(true)
}
}
static async #removeService(event, target) {
const {rowId} = target.dataset;
@ -204,6 +263,7 @@ export class MerchantSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
context.description = this.document.system.description
context.goods = this.document.itemTypes["Equipment"] ?? []
context.services = this.document.system.services
context.isOwner = this.document.isOwner
return context
}

View File

@ -12,9 +12,15 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
closeOnSubmit: false,
handler: SpecialAbilitySheet.#onSubmitForm
},
actions: {
addRequirement: SpecialAbilitySheet.#addRequirement,
removeRequirement: SpecialAbilitySheet.#removeRequirement,
addMod: SpecialAbilitySheet.#addMod,
removeMod: SpecialAbilitySheet.#removeMod,
saveVariant: SpecialAbilitySheet.#saveVariant
}
}
static TABS = {
sheet: {
tabs: [
@ -32,8 +38,13 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
specialability: {
template: `systems/DSA_4-1/templates/item/specialability/tab-specialability.hbs`
},
variants: {
template: `systems/DSA_4-1/templates/ui/tab-variants.hbs`
}
}
_instance = null
_configureRenderOptions(options) {
super._configureRenderOptions(options)
@ -44,6 +55,173 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
return options
}
constructor(...args) {
super(...args);
SpecialAbilitySheet._instance = this
}
static async #addRequirement(event, target) {
const selections =
"<option value='talentMin'>Mindest Talentwert</option>" +
"<option value='talentMax'>Maximal Talentwert</option>" +
"<option value='attributeMin'>Mindest Attributswert</option>" +
"<option value='attributeMax'>Maximal Attributswert</option>" +
"<option value='compare'>Vergleich</option>"
const type = await foundry.applications.api.DialogV2.prompt({
window: {title: "Neue Voraussetzung"},
content: `<select name="type">${selections}</select>`,
ok: {
label: `Hinzufügen`,
callback: (event, button, dialog) => button.form.elements.type.value
}
});
if (type) {
let newReq = {}
switch (type) {
case 'talentMin':
newReq = {
talent: 'Klettern',
minValue: '0'
}
break;
case 'talentMax':
newReq = {
talent: 'Zechen',
maxValue: '0'
}
break;
case 'attributeMin':
newReq = {
attribute: 'ge',
minValue: '0'
}
break;
case 'attributeMax':
newReq = {
attribute: 'mu',
maxValue: '0'
}
break;
case 'compare':
newReq = {
compare: {
ownAttribute: 'ini.aktuell',
operation: 'eq',
targetAttribute: 'ini.aktuell'
}
}
break;
}
const lastIndex = Object.keys(SpecialAbilitySheet._instance._currentSelectedVariant.requirements).length
SpecialAbilitySheet._instance._currentSelectedVariant.requirements[lastIndex] = newReq
SpecialAbilitySheet._instance.render({parts: ["form", "advantage", "variants"]})
}
}
static async #removeRequirement(event, target) {
const {index} = target.dataset
if (index) {
delete SpecialAbilitySheet._instance._currentSelectedVariant.requirements[index]
}
}
static async #addMod(event, target) {
const selections =
"<option value='talent'>Talent</option>" +
"<option value='talentGroup'>Talentgruppe</option>" +
"<option value='attribute'>Attribute</option>" +
"<option value='trait'>Merkmal</option>"
const type = await foundry.applications.api.DialogV2.prompt({
window: {title: "Neuer Modifikator"},
content: `<select name="type">${selections}</select>`,
ok: {
label: `Hinzufügen`,
callback: (event, button, dialog) => button.form.elements.type.value
}
});
if (type) {
let newReq = {}
switch (type) {
case 'talent':
newReq = {
talent: 'Klettern',
value: '0'
}
break;
case 'talentGroup':
newReq = {
talentGruppe: 'Gesellschaft',
value: '0'
}
break;
case 'trait':
newReq = {
merkmal: 'Elementar',
value: '0'
}
break;
case 'attribute':
newReq = {
name: 'ge',
value: '0'
}
break;
}
const lastIndex = Object.keys(SpecialAbilitySheet._instance._currentSelectedVariant.mod).length
SpecialAbilitySheet._instance._currentSelectedVariant.mod[lastIndex] = newReq
SpecialAbilitySheet._instance.render({parts: ["form", "advantage", "variants"]})
}
}
static async #removeMod(event, target) {
const {index} = target.dataset
if (index) {
delete SpecialAbilitySheet._instance._currentSelectedVariant.mod[index]
}
}
static async #saveVariant(event, target) {
/**
* @type {HTMLFormElement}
*/
const form = AdvantageSheet._instance.form
let flattenObject = {}
Object.values(form).forEach(input => {
if (input.name.startsWith('mod') || input.name.startsWith('requirements')) {
flattenObject[`${input.name}`] = input.value
}
if (input.name === "vName") {
flattenObject[`name`] = input.value
}
})
let auswahl = SpecialAbilitySheet._instance.document.system.auswahl
const fo = foundry.utils.expandObject(flattenObject)
auswahl[AdvantageSheet._instance._currentSelectedVariantIndex] = {
name: fo.name,
mod: Object.values(fo.mod),
requirements: Object.values(fo.requirements)
}
SpecialAbilitySheet._instance.document.update({system: {auswahl}})
}
/**
* Handle form submission
* @this {EquipmentSheet}
@ -53,8 +231,32 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
*/
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
if (!form.querySelector('.tab.specialability.active')) {
const obj = foundry.utils.expandObject(formData.object)
await this.document.update(formData.object) // Note: formData.object
if (obj.mod) this._currentSelectedVariant.mod = obj.mod
if (obj.vName) this._currentSelectedVariant.name = obj.vName
if (obj.requirements) this._currentSelectedVariant.requirements = obj.requirements
} else {
delete formData.object.mod
delete formData.object.vName
delete formData.object.requirements
delete formData.object.variant
await this.document.update(formData.object) // Note: formData.object
}
this.render({parts: ["form", "specialability", "variants"]})
}
_getTabsConfig(group) {
const tabs = foundry.utils.deepClone(super._getTabsConfig(group))
if (this.document.system.auswahl) {
tabs.tabs.push({id: 'variants', group: 'sheet', label: 'Varianten'})
}
return tabs
}
/** @override */
@ -82,7 +284,35 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
})
context.hasModality = context.system.value != null
context.name = specialabilityData.name
context.variantChoices = {}
context.variants = []
specialabilityData.system.auswahl?.forEach(variant => {
context.variantChoices[variant.name] = variant.name
context.variants.push(variant)
})
context.currentSelectedVariantName = this._currentSelectedVariant?.name
context.currentSelectedVariant = this._currentSelectedVariant
context.currentSelectedVariantIndex = this._currentSelectedVariantIndex
return context;
}
_onRender(context, options) {
if (this._selectedVariant == null) {
this._selectedVariant = this.document.system.auswahl[0].name
this._currentSelectedVariant = this.document.system.auswahl?.find(p => p.name === this._selectedVariant)
this._currentSelectedVariantIndex = this.document.system.auswahl?.findIndex(p => p.name === this._selectedVariant)
}
this.element.querySelector('select[name="variant"]').addEventListener('change', (event, target) => {
if (event.target.value != this._selectedVariant) {
this._selectedVariant = event.target.value
this._currentSelectedVariant = this.document.system.auswahl?.find(p => p.name === this._selectedVariant)
this._currentSelectedVariantIndex = this.document.system.auswahl?.findIndex(p => p.name === this._selectedVariant)
}
})
}
}

View File

@ -1,4 +1,4 @@
import {LiturgyData} from "../data/miracle/liturgydata.mjs";
import {LiturgyData} from "../data/miracle/liturgyData.mjs";
import {Blessing} from "../documents/blessing.mjs";
import {Profession} from "../documents/profession.mjs";
import {Culture} from "../documents/culture.mjs";
@ -383,7 +383,7 @@ export class XmlImport {
}
async #addSpellsFromCompendiumByNameToActor(spellName, zfw, representation, hauszauber, actor) {
const compendiumOfSpells = game.packs.get('DSA_4-1.Spells');
const compendiumOfSpells = game.packs.get('DSA_4-1.Spells')
const SCREAMING_NAME = spellName.toUpperCase()
const spellId = compendiumOfSpells.index.find(spell => spell.name === SCREAMING_NAME)
if (spellId) {
@ -392,7 +392,8 @@ export class XmlImport {
try {
const embeddedDocument = (await actor.createEmbeddedDocuments('Item', [spell]))[0]
embeddedDocument.update({system: {zfw: zfw, hauszauber: hauszauber, repräsentation: representation}});
embeddedDocument.update({system: {zfw: zfw, hauszauber: hauszauber}})
embeddedDocument.setFlag("DSA_4-1", "representation", representation)
} catch (error) {
console.error(`${spell} not found in items`, error)
}

View File

@ -1,6 +1,7 @@
{
"name": "Aufmerksamkeit",
"seite": "54",
"gruppe": "Kampf",
"requirement": [
{
"attribute": "attribute.in.aktuell",

View File

@ -2,6 +2,7 @@
"todo": "Wir brauchen hier ein Konztept!",
"name": "Ausfall",
"seite": "59",
"gruppe": "Kampf",
"sfPflicht": true,
"requirement": [
{

View File

@ -1,6 +1,7 @@
{
"name": "Ausweichen",
"value": ["Ausweichen I","Ausweichen II","Ausweichen III"],
"gruppe": "Kampf",
"auswahl": [
{
"name": "Ausweichen I",

View File

@ -1,6 +1,7 @@
{
"name": "Befreiungsschlag",
"seite": "60",
"gruppe": "Kampf",
"sfPflicht": true,
"requirement": [
{

View File

@ -1,5 +1,6 @@
{
"name": "Beidhändiger Kampf",
"gruppe": "Kampf",
"value": ["Beidhändiger Kampf I","Beidhändiger Kampf II"],
"auswahl": [
{

View File

@ -1,5 +1,6 @@
{
"name": "Betäubungsschlag",
"gruppe": "Kampf",
"seite": "61",
"sfPflicht": true,
"requirement": [

View File

@ -1,5 +1,6 @@
{
"name": "Binden",
"gruppe": "Kampf",
"seite": "67",
"requirement": [
{

View File

@ -1,5 +1,6 @@
{
"name": "Blindkampf",
"gruppe": "Kampf",
"seite": "67",
"requirement": [
{

View File

@ -1,5 +1,6 @@
{
"name": "Defensiver Kampfstil",
"gruppe": "Kampf",
"seite": "81",
"requirement": [
{

View File

@ -1,5 +1,6 @@
{
"name": "Doppelangriff",
"gruppe": "Kampf",
"seite": "61",
"sfPflicht": true,
"requirement": [

View File

@ -1,5 +1,6 @@
{
"name": "Entwaffnen",
"gruppe": "Kampf",
"seite": "61",
"sfPflicht": true,
"requirement": [

View File

@ -1,5 +1,6 @@
{
"name": "Festnageln",
"gruppe": "Kampf",
"seite": "62",
"sfPflicht": true,
"requirement": [

View File

@ -1,5 +1,6 @@
{
"name": "Finte",
"gruppe": "Kampf",
"seite": "62",
"sfPflicht": true,
"requirement": [

View File

@ -1,5 +1,6 @@
{
"name": "Formation",
"gruppe": "Kampf",
"seite": "62",
"sfPflicht": true,
"requirement": [

View File

@ -1,5 +1,6 @@
{
"name": "Kampfreflexe",
"gruppe": "Kampf",
"seite": "",
"requirement": [
{

View File

@ -1,5 +1,6 @@
{
"name": "Meisterliches Entwaffnen",
"gruppe": "Kampf",
"seite": "61",
"sfPflicht": true,
"requirement": [

View File

@ -0,0 +1,57 @@
{
"seite": "11",
"name": "ABVENENUM REINE SPEISE",
"probe": [
"KL",
"KL",
"FF"
],
"probeMod": "+Mod.",
"technik": "Der Elf spricht bhasama venya bhaza yalza über die zu reinigende Nahrung.",
"zauberdauer": {
"min": "15 Aktionen"
},
"wirkung": "Der Zauber reinigt Nahrungsmittel und Ge tränke von sämtlichen Giften und Krankheitskeimen; verdorbene Nahr ung wird frisch und genießbar. Die Zauberprobe ist um die doppelte Stufe des Giftes oder der beim Verzehr zu befürchtenden Krankheit erschwert. Verdorbene Nahrung kann je nach Zustand einen Zuschlag von 2 bis 12 Punkten auf die Probe bedeuten. Dieser Zauber versetzt Nahrung in einen Zustand, die dem Spruchanwender nicht gefährlich werden kann. Da es aber von dem Entwickler des Spruches abhängt, in welchem Zustand Nahrung als gefährlich angesehen werden muss und in welchem nicht, variiert die genaue Wirkung je nach Repräsentation recht erheblich. So wandelt die elfische Repräsentation selbst Wein in Traubensaft um, während die Achazform des Spruches manche für Menschen und Elfen giftige Mahlzeiten unverändert lässt. Gift, das sich nicht in Nahrung befindet, wird von dem Zauber nicht als solches erkannt, weswegen das Gift auf der Klinge eines Meuchlers oder in der Phiole eines Alchimisten nicht verändert wird. Wenn allerdings zum Beispiel in einer Pilzpfanne giftige Pilze enthalten sind, dann wirkt der Zauber sehr wohl.",
"kosten": [
{
"cost": 4,
"repräsentation": ""
},
{
"cost": 3,
"repräsentation": "Schamane"
}
],
"zielobjekt": "Nahrungsmenge (mehrere Objekte) nach AsP-Aufwand",
"reichweite": "1 Schritt",
"wirkungsdauer": "augenblicklich",
"modifikationen": "Zauberdauer, Reichweite",
"varianten": [
{
"name": "Schutz vor Übelkeit",
"description": "Nahrung, die an sich nicht giftig ist, deren Genuss aber heftige Übelkeit erzeugt, wird in einen Zustand versetzt, der als harmlos gelten kann. So ist es zum Beispiel möglich, auch Meerwasser in Trinkwasser zu verwandeln. Horasische Sahnetorten werden jedoch nur von den wenigsten Repräsentationen in Mitleidenschaft gezogen.",
"mod": "+5",
"limit": ""
},
{
"name": "Schutz vor Vergiftung",
"description": "Bewahrt die Nahrung für die nächsten ZfP* Stunden vor jeglicher in diesem Zeitraum geschehener Vergiftung und jedem Verschimmeln oder Verderben. Der Zaubernde muss sich hierbei selbst einen Aufschlag auf die Zauberprobe auferlegen, und zwar in Höhe der Stufe der Krankheit/des Giftes, gegen die der Schutz höchstens wirksam sein soll. Eine bereits vergiftete Speise wird dabei entgiftet.Vereinzelt existieren eingeschränkte Variationen dieses Spruches, die (bei geringerem Probenaufschlag und AsP-Aufwand) nur gegen gewisse Giftgruppen (Schlangengifte) oder gar nur gegen einzelne Gifte und Krankheiten wirken.",
"mod": "+5",
"limit": ""
}
],
"reversalis": "bewirkt das schlagartige Verfaulen und Verschimmeln von L ebensmitteln.",
"antimagie": "kann in einer Zone des OBJEKT ENTZAUBERN nur erschwert gesprochen wer den; lässt sich wegen der Wirkungsdauer augenblicklich nicht mittels Antimagie widerrufen.",
"merkmal": "Objekt",
"komplexität": "C",
"repräsentation": {
"Druide": 6,
"Elf": 6,
"Hexe": 6,
"Magier": 6,
"Geomant": 5,
"Achaz": 4,
"Schelm": 4
},
"info": "Dieser ursprünglich von den Waldelfen stammende Spruch ist bei fast allen Zauberkundigen weit verbreitet. An allen Orten, an denen die lokalen Potentaten in ständiger Furcht vor Meuchlern und Giftmischern leben, werden Meisterinnen und Meister des ABVENENUM mit Kusshand in den Hofstaat aufgenommen (wenn sie nach einem Demonstrationszaubern den Genuss der vorher präparierten Narung überlebt haben)."
}

View File

@ -0,0 +1,46 @@
{
"seite": "15",
"name": "ADLERAUGE LUCHSENOHR",
"probe": [
"KL",
"IN",
"FF"
],
"probeMod": "",
"technik": "Die Elfe legt die Hände an die Schläfen und konzentriert sich auf die Melodie des a'dao bunda visya'roel.",
"zauberdauer": {
"normal": "5 Aktionen"
},
"wirkung": "Das gesamte Wahrnehmungsvermögen der Zaubernden (alle fünf Sinne) wird so stark verbessert, dass alle Proben auf das Talent Sinnesschärfe um ZfP* Punkte erleichtert werden. Man kann auf große Distanz feinste Geräusche, Gerüche oder Bewegungen wahrnehmen. Der Zauber ermöglicht keine Nachtsicht, aber verstärkt tatsächlich vorhandene Sinneswahrnehmung. Geschärfte Sinne können zu Desorientierung führen, wenn plötzliche Reize auftreten (Selbstbeherrschungs-Probe nötig). Mit ADLERAUGE können auch andere Sinneszauber wie KATZENAUGEN (Nachtsicht) oder WARMES BLUT (Wärmesicht) verstärkt werden.",
"kosten": [
{
"cost": 4,
"repräsentation": ""
}
],
"zielobjekt": "Einzelperson, freiwillig",
"reichweite": "selbst",
"wirkungsdauer": "1 Spielrunde (A)",
"modifikationen": "Zauberdauer, Reichweite, Wirkungsdauer",
"varianten": [
{
"name": "Einzelsinn schärfen",
"description": "Der Zauber betrifft nur einen einzigen Sinn, der dafür umso stärker wird. Alle Sinnesschärfe-Proben für diesen Sinn sind um die doppelten ZfP* erleichtert.",
"mod": "+4",
"limit": ""
}
],
"reversalis": "Die Sinne der Verzauberten werden abgestumpft, dadurch verliert sie ZfP* Punkte Sinnesschärfe.",
"antimagie": "HELLSICHT TRÜBEN und EIGENSCHAFT WIEDERHERSTELLEN heben die Wirkung des Spruches auf; kann in entsprechenden Zonen nur erschwert gewirkt werden.",
"merkmal": "Hellsicht, Eigenschaften",
"komplexität": "B",
"repräsentation": {
"Druide": 3,
"Elf": 7,
"Hexe": 3,
"Magier": 3,
"Geomant": 3,
"Achaz": 3
},
"info": "Der ADLERAUGE ist Ursprung vieler elfischer Sagen über Sinnesleistungen und gilt als Kanonspruch. Jede Hellsicht-Akademie lehrt ihn, ebenso Norburg und Donnerbach."
}

View File

@ -0,0 +1,75 @@
{
"seite": "16",
"name": "ADLERSCHWINGE WOLFSGESTALT",
"probe": [
"MU",
"IN",
"GE"
],
"probeMod": "+Mod.",
"technik": "Der Elf kauert sich auf den Boden zusammen und spricht leise die Formel adao valva iama — es folgt der Name des Tieres, in das er sich verwandeln will.",
"zauberdauer": {
"normal": "20 Aktionen"
},
"wirkung": "Der Zaubernde nimmt die Gestalt des beim Erlernen des Zaubers gewählten Tieres an. Kleidung und Ausrüstung werden nicht mitverwandelt. Der Elf behält seinen Verstand, erhält jedoch die körperlichen Eigenschaften, Sinne und Lebensenergie des Tieres. Zaubern im Tiergestalt ist nicht möglich. Körperliche Aktivitäten entsprechen den Möglichkeiten des Tieres, eingeschränkt durch das Maß an Menschenverständnis des Elfen. Lebenspunkte werden bei Rückverwandlung umgerechnet: Hat der Elf vor der Rückwandlung die Hälfte seiner LeP verloren, so hat er auch danach die Hälfte seiner LeP. Verletzungen werden übertragen. Der Zauber ist ein Kernzauber der Elfenmagie und wird häufig zur Jagd oder als Schutz genutzt.",
"kosten": [
{
"cost": 4,
"repräsentation": ""
}
],
"zielobjekt": "Einzelperson, freiwillig",
"reichweite": "selbst",
"wirkungsdauer": "nach AsP-Aufwand (Meisterentscheid)",
"modifikationen": "Zauberdauer, Erzwingen, Kosten, Wirkungsdauer",
"varianten": [
{
"name": "Achaz-Repräsentation",
"description": "Erlaubt lediglich die Verwandlung in Reptilien oder Amphibien.",
"mod": "",
"limit": ""
},
{
"name": "Extreme Tiere",
"description": "Bei Tieren mit extrem hoher LE steigen die Kosten um 1 bis 7 AsP (mindestens 7 AsP, wenn LE mindestens doppelt so hoch ist wie die des Helden).",
"mod": "",
"limit": ""
},
{
"name": "Bewusste Gestalt",
"description": "Die Wirkungsdauer ist veränderbar, wenn der Zaubernde seine Aufmerksamkeit ändert.",
"mod": "+7",
"limit": "11"
},
{
"name": "Weitere Tierarten",
"description": "Erlaubt das Erlernen zusätzlicher Tierarten, nach den Regeln für Hexalogien (WdZ 383ff).",
"mod": "",
"limit": ""
},
{
"name": "Grenzenlose Gestalt",
"description": "Wenn mindestens eine Version mit ZfW 15+ beherrscht wird, können alle Tiere bis Pferdgröße erlernt werden.",
"mod": "+7",
"limit": ""
},
{
"name": "Haut des Seelentiers",
"description": "Der Elf verschmilzt mit seinem Seelentier und übernimmt dessen Instinkte vollständig. Dauer: 1 Tag. Alle Selbstbeherrschungsproben +10 erleichtert.",
"mod": "+7",
"limit": ""
}
],
"reversalis": "Verwandelt eine Tierform zurück in einen Elfen.",
"antimagie": "Kann in einer Zone des VERWANDLUNG BEENDEN nur erschwert gewirkt werden; endet durch Antimagie-Spruch.",
"merkmal": "Form",
"komplexität": "D",
"repräsentation": {
"Druide": 2,
"Elf": 6,
"Hexe": 2,
"Magier": 3,
"Achaz": 3
},
"info": "Die elfische Variante dient vor allem dazu, sich mit dem Seelentier zu verbinden, sodass jeder Elf beim Erlernen ein zu ihm passendes Tier wählt. Manche Waldelfen beherrschen mehrere Varianten (Seelen- und Sipptier). Bei Firn- und Auelfen ist das Verwandeln in Seelentiere verbreitet, aber das Beherrschen unterschiedlicher Tierformen unbekannt. Menschen fällt dieser Zauber schwer; er ist nur an wenigen Akademien (z. B. Lowangen, Kuslik, Punin, Tuzak, Verformung zu Lowangen) bekannt. Die Formel wird auch in Zauberstern, Silberhaar und Corpus Mutantis behandelt."
}

View File

@ -0,0 +1,37 @@
{
"seite": "22",
"name": "ANALYS ARCANSTRUKTUR",
"probe": [
"KL",
"KL",
"IN"
],
"probeMod": "+Mod",
"technik": "Die Magierin fixiert das Ziel ihres Interesses und spricht die Formel.",
"zauberdauer": {
"min": "1 SR"
},
"wirkung": "Ermöglicht es, magische Wirkungsstrukturen und Gewebe aus den Fäden und Bahnen der Kraft zu erkennen. Damit können Artefakte, magische Wesen oder Zauber identifiziert und klassifiziert werden. Je nach Höhe der ZfP* können Merkmale, Repräsentationen, Matrixfehler oder versteckte Zauber ermittelt werden. Erweiterte Effekte: Erkennen von Zaubertradition, Signatur des Schöpfers, Art von Artefakten, Wirkungen von Elixieren oder Tränken. Je länger die Konzentration aufrechterhalten wird, desto mehr Details können sichtbar werden.",
"kosten": [
{
"cost": 6,
"repräsentation": ""
}
],
"zielobjekt": "Einzelobjekt, Einzelwesen",
"reichweite": "1 Schritt",
"wirkungsdauer": "Identisch mit Zauberdauer, nach AsP-Aufwand",
"modifikationen": "Zauberdauer, Kosten, Reichweite",
"reversalis": "keine Wirkung",
"antimagie": "HELLSICHT TRÜBEN und SCHLEIER DER UNWISSENHEIT erschweren die Analyse.",
"merkmal": "Hellsicht, Metamagie",
"komplexität": "D",
"repräsentation": {
"Magier": 6,
"Achaz": 3,
"Druide": 2,
"Geomant": 2,
"Hexe": 2
},
"info": "Bietet weitgehende Freiheit, Zauberstrukturen zu beschreiben. In vielen Werken enthalten. Nahe Verwandtschaft zum OCULUS ASTRALIS. Wird fast jeder Akademie gelehrt."
}

View File

@ -0,0 +1,55 @@
{
"seite": "28",
"name": "ARMATRUTZ",
"probe": [
"IN",
"GE",
"KO"
],
"probeMod": "",
"technik": "Die Elfe streicht mit den Händen über ihre Brust, während sie ama tharza spricht.",
"zauberdauer": {
"normal": "3 Aktionen"
},
"wirkung": "Die Zaubernde erhält eine zusätzliche 'stählerne Haut', die ihren natürlichen Rüstungsschutz erhöht. Die zusätzliche Rüstung beträgt maximal ZfW/2 Punkte. Sie schützt gegen materielle Angriffe (auch Dämonen- und Elementarattacken), jedoch nicht gegen Zauber, die direkten Schaden verursachen. Bei Bissen von Tieren wie Ratten oder Fledermäusen schützt der Zauber ebenfalls.",
"kosten": [
{
"cost": 4,
"additionalFormula": "RS * RS - ZFP*/2",
"variables": [
"RS",
"ZFP*"
],
"repräsentation": ""
}
],
"zielobjekt": "Einzelperson, freiwillig",
"reichweite": "selbst",
"wirkungsdauer": "maximal eine Spielrunde (A)",
"modifikationen": "Zauberdauer, Kosten, Reichweite (Berührung), Wirkungsdauer",
"varianten": [
{
"name": "Körperschild",
"description": "Der Zauber wirkt nur auf einen Teil des Körpers. AsP-Kosten halbiert, aber mindestens 3 AsP.",
"mod": -4,
"limit": ""
},
{
"name": "Kraft des Fakirs",
"description": "Verleiht Resistenz gegen kleine Schadenswirkungen. Opfer kann z. B. schmerzlos über Scherben gehen.",
"mod": -3,
"limit": 7
}
],
"reversalis": "hebt einen wirkenden ARMATRUTZ auf",
"antimagie": "EIGENSCHAFT WIEDERHERSTELLEN und ERZBANN können die Wirkung beenden; in den entsprechenden Zonen erschwert.",
"merkmal": "Eigenschaften, Elementar (Erz)",
"komplexität": "B",
"repräsentation": {
"Elf": 6,
"Magier": 6,
"Hexe": 3,
"Druide": 2
},
"info": "Ursprünglich ein elfischer Schutzzauber, später in menschlichen Akademien verbreitet. Auch Hexen und Druiden nutzen ihn, um Leib und Leben zu schützen. Vergleichbar mit dem Flim Flam als Basiszauber."
}

View File

@ -0,0 +1,112 @@
{
"seite": "30",
"name": "ATTRIBUTO",
"probe": [
"KL",
"CH",
"*"
],
"probeMod": "[gesteigerte Eigenschaft]",
"technik": "Der Magier berührt seinen Gefährten mit beiden Händen und spricht die Formel. Je nach Eigenschaft z. B. Oberarm für Körperkraft, Schläfen für Klugheit, Augen für Intuition, Hände für Fingerfertigkeit usw.",
"zauberdauer": {
"normal": "30 Aktionen"
},
"wirkung": "Der Zauber hebt die in der Probe durch 'Eigenschaft' bezeichneten Werte des Verzauberten für die Dauer einer Stunde um ZfP* / 3 Punkte.",
"kosten": [
{
"cost": 7,
"repräsentation": ""
},
{
"cost": 5,
"repräsentation": "Schelm"
}
],
"zielobjekt": "Einzelperson, Einzelwesen (s. u.), freiwillig",
"reichweite": "Berührung",
"wirkungsdauer": "1 Stunde",
"modifikationen": "Zauberdauer, Reichweite (Selbst), Wirkungsdauer",
"varianten": [
{
"name": "Mut",
"description": "Wirkt auch auf Tiere beliebiger Art. In diesem Fall ist die Probe um 3 Punkte erschwert.",
"mod": "",
"limit": ""
},
{
"name": "Klugheit",
"description": "Kann auch auf Vertraute, Reit- und Haustiere angewandt werden, sogar auf Pflanzen. 35 Punkte für Tiere, 7 Punkte für Pflanzen. Erlaubt auch eine Art 'Erinnerungslesen'.",
"mod": "",
"limit": ""
},
{
"name": "Intuition",
"description": "Bei Lebewesen ohne höhere Verstandesfunktionen (Tiere) wird Wahrnehmung und Aufmerksamkeit um 3 Punkte gesteigert.",
"mod": "",
"limit": ""
},
{
"name": "Charisma",
"description": "Wirkt ausschließlich auf kulturschaffende Wesen.",
"mod": "",
"limit": ""
},
{
"name": "Fingerfertigkeit",
"description": "Kann nur auf Lebewesen angewandt werden, die über einen Greifhand verfügen.",
"mod": "",
"limit": ""
},
{
"name": "Gewandtheit",
"description": "Erhöht GE von Vierbeinern oder Schlangen: +3 Punkte, Vögel/Schlangen +5 Punkte, Achtbeiner/Flugwesen +7 Punkte.",
"mod": "",
"limit": ""
},
{
"name": "Konstitution",
"description": "Verändert die KO von Tieren. Kosten und Schwierigkeit steigen je nach Körpermasse des Tieres. Mindestens 40 AsP bei Pferden.",
"mod": "",
"limit": ""
},
{
"name": "Körperkraft",
"description": "Wie Konstitution.",
"mod": "",
"limit": ""
},
{
"name": "Schnellsteigerung",
"description": "Nur in gildenmagischer, elfischer, druidischer oder saurischer Rep. Kleine Mutanda: +ZfP* Punkte für ZfW KR, Kosten 1 AsP pro Punkt. Mächtigere Version bringt Dauerbonus.",
"mod": "-3",
"limit": "ab ZfW 11"
},
{
"name": "Übernatürliche Begabung",
"description": "Nur für Magiedilettanten. Steigert eine Eigenschaft um ZfP* Punkte für ZfP* KR, kostet ZfP* AsP.",
"mod": "",
"limit": ""
},
{
"name": "Scharlatanische Version",
"description": "Ermöglicht Reichweite 'selbst', Zielobjekt 'Einzelperson, freiwillig'.",
"mod": "",
"limit": ""
}
],
"reversalis": "Die entsprechende Eigenschaft des Opfers sinkt um den entsprechenden Betrag.",
"antimagie": "In einer Zone des EIGENSCHAFT WIEDERHERSTELLEN nur erschwert wirksam und kann mit diesem Antimagie-Spruch beendet werden.",
"merkmal": "Eigenschaften",
"komplexität": "B",
"repräsentation": {
"Druide": 5,
"Elf": 5,
"Geomant": 5,
"Hexe": 5,
"Magier": 5,
"Achaz": 3,
"Schelm": 3,
"Scharlatan": 3
},
"info": "Die Varianten dieses Zaubers wurden lange Zeit als eigenständige Sprüche (z. B. KLU, WIS, INTELLECT, CHARISMA AUGETE, VORAHNUNG, STARKE STEIGERN) gelehrt. Heute meist zusammengefasst. Verbreitet bei fast allen Zauberkundigen."
}

View File

@ -0,0 +1,77 @@
{
"seite": "35",
"name": "AURIS NASUS OCULUS",
"probe": [
"KL",
"CH",
"FF"
],
"technik": "Der Magier konzentriert sich auf den Ort, an dem die Illusion erscheinen soll, und murmelt mit geschlossenen Augen die Formel.",
"zauberdauer": {
"min": 0
},
"wirkung": "Mit dieser Formel kann der Magier illusorische Geräusche, Gerüche und dreidimensionale (aber unbewegte) Bilder erschaffen. Beispiele: Schrift an der Wand, eine vorgelagerte Wand, eine Stimme aus dem Nichts, Brandgeruch. Jede einzelne Bild-, Geräusch- oder Geruchskomponente zählt als eine Illusionskomponente. Die Größe der Bilder beträgt maximal ZfW × 5 RaumSchritt. Mehrere Illusionsarten können kombiniert werden, die Probe erschwert sich dabei um 2 Punkte pro zusätzliche Komponente. Einmal erschaffene Illusionen bestehen ohne weiteres Zutun fort. Die Realitätsdichte beträgt ZfP* ÷ 2 + 7.",
"kosten": [
{
"cost": 0,
"repräsentation": ""
}
],
"zielobjekt": "Zone",
"reichweite": "ZfW × 3 Schritt",
"wirkungsdauer": "maximal ZfP* ÷ 2 Spielrunden",
"modifikationen": "Zauberdauer, Kosten, Wirkungsdauer",
"varianten": {
"Verpuffung": {
"description": "Illusionen lösen sich langsam auf oder verpuffen schlagartig.",
"mod": -3,
"cost": 4,
"zauberdauer": 5,
"limit": ""
},
"Außer Sicht": {
"description": "Illusionen erscheinen auch an Orten, die der Zaubernde nicht direkt sehen kann, sofern er sie zuvor gesehen hat.",
"mod": -3,
"cost": 4,
"zauberdauer": 5,
"limit": ""
},
"Entfernte Phantasmagorie": {
"description": "Illusionen können weiter entfernt erscheinen, bis zu ZfW × 30 Schritt.",
"mod": -5,
"cost": 4,
"zauberdauer": 5,
"limit": ""
},
"Bewegte Bilder": {
"description": "Illusionen sind beweglich (z. B. ein fliegender Drache).",
"mod": -7,
"cost": 4,
"zauberdauer": 5,
"limit": "11"
},
"Selbst leuchtend": {
"description": "Illusionen leuchten und können eine Lichtquelle ersetzen.",
"mod": -7,
"cost": 4,
"zauberdauer": 5,
"limit": "11"
},
"Geschmack und Tastsinn": {
"description": "Illusionen täuschen auch den Geschmack oder Tastsinn.",
"mod": -12,
"cost": 4,
"zauberdauer": 5,
"limit": "14"
}
},
"reversalis": "Der Zauber wird aufgehoben.",
"antimagie": "In einer Zone des ILLUSION AUFLÖSEN erschwert wirksam und kann den Zauber aufheben.",
"merkmal": "Illusion",
"komplexität": "D",
"repräsentation": {
"Magier": 5,
"Scharlatan": 5
},
"info": "AURIS NASUS gilt als Mutter aller Illusionen und erlaubt Täuschung von Bild, Ton und Geruch, mit Erweiterungen auch von Tastsinn und Geschmack. Sehr mächtige Formel, die in Zorgan, Grangor, Khunchom und Punin gelehrt wird. Wichtige Werke wie Theorie der Wahrnehmung und Beobachtung und Liber Metheslesae behandeln den Spruch ausführlich."
}

View File

@ -0,0 +1,53 @@
{
"seite": "36",
"name": "AXXELERATUS BLITZGESCHWIND",
"probe": [
"KL",
"GE",
"KO"
],
"technik": "Die Elfe konzentriert sich auf alle Muskeln und Sehnen ihres Körpers und spricht asela dulo biwandan.",
"zauberdauer": {
"normal": "2 Aktionen"
},
"wirkung": "Der Zauber verleiht dem Verzauberten enorme Beschleunigung. Bewegungen wirken fließend und schnell, jedoch etwas verschwommen. Körperliche Aktionen werden stark erleichtert, geistige Tätigkeiten bleiben unbeeinflusst. Erhöhte Koordination vermindert Sturz- oder Verstauchungsrisiken. Parade-Basiswert +2, Ausweichen +2, TP von Nahkampfangriffen +2, Abwehr von bewaffneten Angriffen +2, INI-Basiswert +2. Geschwindigkeit für Sprints verdoppelt. Während der Wirkungsdauer entsprechen die Werte den Sonderfertigkeiten Schnellelfen und Schnellerladen. Laden und Ziehen einer Waffe kann um 1 Aktion verkürzt werden.",
"kosten": [
{
"cost": 7,
"repräsentation": ""
},
{
"cost": 5,
"repräsentation": "Schelm"
}
],
"zielobjekt": "Einzelperson, freiwillig",
"reichweite": "selbst, 7 Schritt",
"wirkungsdauer": "ZfP* × 3 Kampfrunden (A)",
"modifikationen": "Zauberdauer, Kosten, Zielobjekt (mehrere), Reichweite",
"varianten": [
{
"name": "Blitzgeschwind",
"description": "Der Verzauberte erhält die Geschwindigkeit eines Pfeils. Zusätzlich wird der Athletik-Wert für Sprints und Sprungweiten verdoppelt.",
"mod": -7,
"limit": "11"
},
{
"name": "Koboldisch",
"description": "Die Beschleunigung wird auf die Sprache der Elfen angewendet. Der Zaubernde spricht Koboldisch. Schabernack-Variante ohne Kampfvorteile.",
"mod": 0,
"limit": "3"
}
],
"reversalis": "Der Zauber bewirkt Verlangsamung: GS halbiert, AT/PA/Ausweichen halbiert, TP -2, Athletik stark reduziert.",
"antimagie": "In einer Zone des EIGENSCHAFT WIEDERHERSTELLEN erschwert wirksam und kann den Zauber aufheben.",
"merkmal": "Eigenschaften",
"komplexität": "C",
"repräsentation": {
"Elf": 6,
"Achaz": 5,
"Schelm": 5,
"Magier": 3
},
"info": "Die Waldelfen nutzen den Zauber traditionell für Jagd und Tanz. Acheer-Schamanen verwenden ihn für rituelle Tänze. Gildenmagische Repräsentation selten, gelehrt in Gerasim, Belhanka und der Akademie der Verformungen zu Lowangen. Auch Tamara beschrieb die Formel in ihren Werken."
}

View File

@ -0,0 +1,58 @@
{
"seite": "37",
"name": "BALSAM SALABUNDE",
"probe": [
"KL",
"IN",
"CH"
],
"probeMod": "evtl. +Mod",
"technik": "Der Elf legt dem Verletzten sanft eine Hand auf die Verletzung (bei großflächiger oder innerer Verletzung aufs Herz) und wiederholt die Melodie des bhassama sala bian dao so lange, bis die heilende Wirkung einsetzt.",
"zauberdauer": {
"min": "5 Aktionen",
"normal": "1 SR"
},
"wirkung": "Heilt sämtliche Wunden und inneren Verletzungen des Verzauberten, je nach eingesetzten AsP. Pro AsP wird 1 LeP zurückgewonnen (maximal jedoch ZfW × 2 LeP). Der Zauber kann auch auf den Zaubernden selbst gewirkt werden. Schädliche Wirkungen von Krankheiten und Giften werden nicht gestoppt.",
"kosten": [{
"cost": 5,
"additionalFormula": "LeP",
"variables": [
"LeP"
],
"repräsentation": ""
}],
"zielobjekt": "Einzelwesen, freiwillig",
"reichweite": "selbst, Berührung",
"wirkungsdauer": "augenblicklich",
"modifikationen": "Zauberdauer, Kosten, Reichweite",
"varianten": [
{
"name": "Lebenskraft stärken",
"description": "Die Lebensdauer des Nutznießers wird für kurze Zeit über das normale Maß hinaus verlängert. Für je 3 AsP erhält der Verzauberte 1 LeP über den LeP-Grundwert hinaus, bis max. ZfW LeP. Jeder ZfP* überzieht den Körper mit zusätzlichem LeP, der aber wieder verloren geht.",
"mod": -5,
"limit": "11"
},
{
"name": "Sofortige Regeneration",
"description": "Der Körper erhält sofortige Regeneration. Jede KR regeneriert der Nutznießer 1W6 LeP, Wunden schließen sich sofort wieder.",
"mod": -15,
"limit": "18"
}
],
"reversalis": "Als schleichender Schadenszauber: Das Opfer verliert pro eingesetztem AsP 1 LeP.",
"antimagie": "Während der Zauber gewirkt wird, kann er mit HEILKRAFT BANNEN und VERWANDLUNG BEENDEN zum Scheitern gebracht werden; erschwert wirksam in entsprechenden Zonen.",
"merkmal": [
"Heilung",
"Form"
],
"komplexität": "C",
"repräsentation": {
"Elf": 9,
"Magier": 7,
"Achaz": 5,
"Geomant": 5,
"Druide": 4,
"Hexe": 3
},
"info": "Eine der bekanntesten Heilzauberformeln, ursprünglich von den Elfen entwickelt und später von Gildenmagiern übernommen. Weit verbreitet, auch unter Geoden und Achaz. In klassischen Magierkreisen lange abgelehnt, da man glaubte, dass Heilung göttlicher Macht vorbehalten sei."
}

View File

@ -0,0 +1,62 @@
{
"seite": "39",
"name": "BANNBALADIN",
"probe": [
"IN",
"CH",
"CH"
],
"probeMod": "+MR",
"technik": "Der Elf blickt seinem Opfer in die Augen und spricht blah bla ladin.",
"zauberdauer": {
"normal": "5 Aktionen"
},
"wirkung": "Der Verzauberte sieht in dem Spruchanwender einen Freund. Die Intensität hängt von den ZfP* ab: 1 ZfP* = unbestimmtes Gefühl, 4 ZfP* = freundschaftlich verbunden, 7 ZfP* = enger Freund, 10 ZfP* = bedingungslos loyal, 13 ZfP* = bereit, schweren Schaden auf sich zu nehmen, 16 ZfP* = vollkommen verfallen. Der Zauberer kann Charisma-, Überreden- oder Eigenschaften-Proben durch die Bindung erleichtern. Der Bannbaladin wirkt nur, solange Sichtkontakt besteht. Mehrere Opfer gleichzeitig sind möglich.",
"kosten": [
{
"cost": 7,
"repräsentation": ""
}
],
"zielobjekt": "Einzelperson",
"reichweite": "3 Schritt",
"wirkungsdauer": {
"formula": "ZfP* Spielrunden",
"variables": [
"ZfP*"
]
},
"modifikationen": "Zauberdauer, Zielobjekt (mehrere), Reichweite, Wirkungsdauer",
"varianten": [
{
"name": "Tiefenruf",
"description": "Nur in elfischer Repräsentation. Auch Tiere können beeinflusst werden.",
"mod": -5,
"limit": ""
},
{
"name": "Gemeinsame Erinnerungen",
"description": "Der Zauberer pflanzt gemeinsame Erinnerungen in den Geist des Opfers ein. Spieler dürfen diese Erinnerungen beschreiben.",
"mod": -3,
"limit": "7"
},
{
"name": "Keine Erinnerung",
"description": "Das Opfer erinnert sich nach Ende des Zaubers nicht daran, dass es verzaubert wurde.",
"mod": -7,
"limit": "11"
}
],
"reversalis": "Hebt einen bestehenden BANNBALADIN auf.",
"antimagie": "EINFLUSS BANNEN beendet die Wirkung und löscht die Gefühle.",
"merkmal": "Einfluss",
"komplexität": "B",
"repräsentation": {
"Elf": 7,
"Magier": 6,
"Druide": 3,
"Hexe": 3,
"Scharlatan": 3
},
"info": "Ursprünglich elfischer Freundschaftszauber, später von Gildenmagiern zu einem der wichtigsten Herrschafts- und Beeinflussungszauber gewandelt. Zwischen Elfen und Magiern sorgt die Anwendung regelmäßig für Konflikte."
}

View File

@ -0,0 +1,82 @@
{
"seite": "47",
"name": "BLICK IN DIE GEDANKEN",
"probe": [
"KL",
"KL",
"CH"
],
"probeMod": "+MR",
"technik": "Die Elfe blickt ihrem Opfer ins Gesicht und konzentriert sich dann auf die Melodie des ibhanda dhara feya dendra.",
"zauberdauer": {
"normal": "10 Aktionen"
},
"wirkung": "Die Zaubernde erhält Einblick in die momentanen Gedankengänge des Opfers, die sie als verschwommene Bilder vor ihrem geistigen Auge sieht. Mit einer gelungenen Selbstbeherrschungs-Probe kann das Opfer versuchen, andere Gedanken vorzuschieben oder falsche Fährten zu legen. Das Lesen fremder Gedanken (z. B. Drachen, Einhörner, Chimären) ist erschwert.",
"kosten": [
{
"cost": 0,
"additionalFormula": "6 * 5 KR",
"variables": [
"5 KR"
],
"repräsentation": ""
}
],
"zielobjekt": "Einzelperson",
"reichweite": "3 Schritt",
"wirkungsdauer": "nach AsP-Aufwand (A)",
"modifikationen": "Zauberdauer, Erzwingen, Reichweite",
"varianten": [
{
"name": "Keine Sicht",
"description": "Das Opfer muss nicht innerhalb der Reichweite erkennbar sein.",
"mod": -5,
"limit": ""
},
{
"name": "Traumlese",
"description": "Wirkt auf Träumende, erlaubt Teilhabe und Einwirkung in die Träume.",
"mod": -3,
"limit": "7"
},
{
"name": "Drachisch",
"description": "Ermöglicht das Lesen drachischer Gedanken. Erfordert GE-DANKENBILDER und idealerweise einen Muttersprachler.",
"mod": 0,
"limit": "7"
},
{
"name": "Liebessinn",
"description": "Während der Kampfsinn auf den Gegner harmonisiert, teilt der Zaubernde seine Gedanken und Gefühle mit dem Opfer.",
"mod": -3,
"limit": "11"
},
{
"name": "Kampfsinn",
"description": "Der Zaubernde teilt die Aktionen und Wahrnehmung des Gegners im Kampf.",
"mod": -5,
"limit": "11"
},
{
"name": "Tiefentelepathie",
"description": "Intensive Lesung, bei der Gedanken, Gefühle und Erinnerungen tiefgehend untersucht werden. Hohe Kosten, aber präzise Ergebnisse.",
"mod": -7,
"limit": "11"
}
],
"reversalis": "Offenbart dem Gegner die Gedanken der Zaubernden.",
"antimagie": "In einer Zone des HELLSICHT TRÜBEN nur erschwert wirksam, gezielter Einsatz von HELLSICHT TRÜBEN hebt den Zauber auf.",
"merkmal": [
"Hellsicht"
],
"komplexität": "D",
"repräsentation": {
"Elf": 5,
"Magier": 5,
"Achaz": 4,
"Druide": 4,
"Hexe": 4,
"Geomant": 3
},
"info": "Ursprünglich ein Verständigungszauber der Elfen, wurde er von den Gildenmagiern zu einem Verhörzauber entwickelt. Unter Elfen ist der Spruch verbreitet, aber mit Vorsicht wird er an Menschen weitergegeben, da er als stark verfälschtes Werkzeug gilt."
}

Some files were not shown because too many files have changed in this diff Show More