Compare commits

...

69 Commits

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
366 changed files with 9481 additions and 1415 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. 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 ## GM Tools
### Gruppenmanagement ### 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/ 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

@ -147,7 +147,7 @@ gulp.task('prepareDB', async function (done) {
} }
await convert("./src/packs/_source/talente", "./src/packs/__source/talente", "Skill") await convert("./src/packs/_source/talente", "./src/packs/__source/talente", "Skill")
await convert("./src/packs/_source/zauber", "./src/packs/__source/zauber", "Spell") 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/vorteile", "./src/packs/__source/vorteile", "Advantage")
await convert("./src/packs/_source/nachteile", "./src/packs/__source/vorteile", "Advantage", false) 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/sonderfertigkeiten", "./src/packs/__source/sonderfertigkeiten", "SpecialAbility")

8
package-lock.json generated
View File

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

View File

@ -14,7 +14,7 @@
"installToFoundry": "node installToFoundry.mjs" "installToFoundry": "node installToFoundry.mjs"
}, },
"devDependencies": { "devDependencies": {
"@foundryvtt/foundryvtt-cli": "^3.0.0", "@foundryvtt/foundryvtt-cli": "^3.0.2",
"cb": "^0.1.1", "cb": "^0.1.1",
"del": "^8.0.1", "del": "^8.0.1",
"fvtt-types": "npm:@league-of-foundry-developers/foundry-vtt-types@^13.346.0-beta.20250812191140", "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,406 +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 {XmlImportDialog} from "./module/dialog/xmlImportDialog.mjs";
import {MerchantDataModel} from "./module/data/merchant.mjs"; import {initGlobalSettings, initUserSettings} from "./module/settings/global-settings.mjs";
import {MerchantSheet} from "./module/sheets/merchantSheet.mjs"; import {setUpActorSheets, setUpItemSheets} from "./module/setup/sheets.mjs";
import {RestingDialog} from "./module/dialog/restingDialog.mjs"; import {loadPartials} from "./module/setup/partials.mjs";
import {BattleDialog} from "./module/dialog/battleDialog.mjs"; import {
import {Talent} from "./module/data/talent.mjs"; initSocketLib,
initCombat,
async function preloadHandlebarsTemplates() { initDataModels,
return foundry.applications.handlebars.loadTemplates([ initDocumentClasses,
// ui partials. initGlobalAccess
'systems/DSA_4-1/templates/ui/partial-rollable-button.hbs', } from "./module/setup/config.mjs";
'systems/DSA_4-1/templates/ui/partial-rollable-weaponskill-button.hbs', import {initHandlebarHelpers} from "./module/handlebar-helpers/index.mjs";
'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'
]);
}
Hooks.once("init", () => { Hooks.once("init", () => {
const displayCurrency = (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)
}
game.DSA41 = {
rollItemMacro,
Zonenruestung,
Zonenwunde,
Trefferzone,
Wunde,
RestingDialog,
BattleDialog,
Talent,
displayCurrency
}
// 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!") console.log("DSA 4.1 is ready for development!")
foundry.documents.collections.Actors.registerSheet('dsa41.character', CharacterSheet, { game.DSA41 = {
types: ["character"], ...game.DSA41,
makeDefault: true, ...initGlobalAccess()
}) }
foundry.documents.collections.Actors.registerSheet('dsa41.creature', CreatureSheet, { initDocumentClasses(CONFIG)
types: ["creature"],
makeDefault: true, initUserSettings(game.settings)
}) initGlobalSettings(game.settings)
foundry.documents.collections.Actors.registerSheet('dsa41.group', GroupSheet, {
types: ["group"], initDataModels(CONFIG)
makeDefault: true, initCombat(CONFIG)
})
foundry.documents.collections.Items.registerSheet('dsa41.skill', SkillSheet, { setUpActorSheets(foundry.documents.collections.Actors)
types: ["Skill"], setUpItemSheets(foundry.documents.collections.Items)
makeDefault: true,
}); loadPartials(foundry.applications.handlebars).then(() => {
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.settings.register('DSA_4-1', 'optional_colorfuldice', { initHandlebarHelpers(Handlebars)
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", game.DSA41.displayCurrency)
return preloadHandlebarsTemplates();
}) })
Hooks.once("ready", async function () { game.DSA41 = {}
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to initSocketLib(game.DSA41)
Hooks.on("hotbarDrop", (bar, data, slot) => {
return createTalentMacro(data, slot)
});
});
Hooks.on("getActorContextOptions", (application, menuItems) => { Hooks.on("getActorContextOptions", (application, menuItems) => {
menuItems.push({ menuItems.push({
@ -409,44 +46,7 @@ Hooks.on("getActorContextOptions", (application, menuItems) => {
callback: (li) => { callback: (li) => {
const actorId = li.getAttribute("data-entry-id") const actorId = li.getAttribute("data-entry-id")
const actor = game.actors.get(actorId) const actor = game.actors.get(actorId)
//actor.import()
new XmlImportDialog(actor).render(true) 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; 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() { static defineSchema() {
return { return {
@ -21,7 +28,7 @@ export class VornachteileDataModel extends BaseItem {
auswahl: new ArrayField( auswahl: new ArrayField(
new SchemaField({ new SchemaField({
name: new StringField(), name: new StringField(),
requirement: new ArrayField( requirements: new ArrayField(
new SchemaField({ new SchemaField({
attribute: new StringField(), attribute: new StringField(),
minValue: new NumberField(), minValue: new NumberField(),

View File

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

View File

@ -138,6 +138,10 @@ export class PlayerCharacterDataModel extends foundry.abstract.TypeDataModel {
key: new StringField(), key: new StringField(),
notiz: 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({ wunden: new SchemaField({
aktuell: new NumberField({required: true, integer: true}), // only with DSA_4-1.optional_trefferzonen = false 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 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 { 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 BaseItem from "./baseItem.mjs";
import {Equipment} from "../documents/equipment.mjs";
const { const {
ArrayField, EmbeddedCollectionField, SchemaField, NumberField, StringField, HTMLField ArrayField,
SchemaField,
NumberField,
StringField,
HTMLField
} = foundry.data.fields; } = foundry.data.fields;
export class EquipmentDataModel extends BaseItem { export class EquipmentDataModel extends BaseItem {

View File

@ -3,10 +3,8 @@ const {
ObjectField, ObjectField,
NumberField, NumberField,
StringField, StringField,
EmbeddedDocumentField,
DocumentIdField, DocumentIdField,
ArrayField, ArrayField,
ForeignDocumentField
} = foundry.data.fields; } = foundry.data.fields;
export class GroupDataModel extends foundry.abstract.TypeDataModel { 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 { export class LiturgyDataModel extends BaseItem {
@ -17,14 +23,14 @@ export class LiturgyDataModel extends BaseItem {
wirkungsdauer: new StringField(), wirkungsdauer: new StringField(),
zauberdauer: new StringField(), zauberdauer: new StringField(),
auswirkung: new SchemaField({ auswirkung: new SchemaField({
I: new StringField(), I: new HTMLField(),
II: new StringField(), II: new HTMLField(),
III: new StringField(), III: new HTMLField(),
IV: new StringField(), IV: new HTMLField(),
V: new StringField(), V: new HTMLField(),
VI: new StringField(), VI: new HTMLField(),
VII: new StringField(), VII: new HTMLField(),
VIII: new StringField(), VIII: new HTMLField(),
}) })
} }

View File

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

View File

@ -165,7 +165,6 @@ export class LiturgyData {
if (found) { if (found) {
durationText = this.#ranks[currentDuration].duration durationText = this.#ranks[currentDuration].duration
console.log({currentDuration, durationText, adjustedDurationText})
return {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 { 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 { const {
BooleanField,
DocumentIdField, DocumentIdField,
ArrayField, ArrayField,
NumberField, NumberField,
@ -131,7 +131,7 @@ export class SkillDataModel extends BaseItem {
let evaluated1 = (await roll1.evaluate()) let evaluated1 = (await roll1.evaluate())
const dsaDieRollEvaluated = this._evaluateRoll(evaluated1.terms[0].results, { const dsaDieRollEvaluated = evaluateRoll(evaluated1.terms[0].results, {
taw: this.taw, taw: this.taw,
werte: [this.probe[0], this.probe[1], this.probe[2]], 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 { const {
AnyField,
BooleanField, BooleanField,
NumberField, NumberField,
SchemaField, SchemaField,
ArrayField, ArrayField,
StringField, StringField,
HTMLField, HTMLField,
ObjectField
} = foundry.data.fields; } = foundry.data.fields;
export class SpecialAbilityDataModel extends BaseItem { export class SpecialAbilityDataModel extends BaseItem {

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 { 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 { export class SpellDataModel extends BaseItem {
@ -13,9 +21,23 @@ export class SpellDataModel extends BaseItem {
probeMod: new StringField(), probeMod: new StringField(),
hauszauber: new BooleanField(), hauszauber: new BooleanField(),
technik: new StringField(), technik: new StringField(),
zauberdauer: new StringField(), zauberdauer: new SchemaField({
wirkung: new StringField(), min: new StringField(),
kosten: 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(), zielobjekt: new StringField(),
reichweite: new StringField({required: true}), reichweite: new StringField({required: true}),
wirkungsdauer: new StringField({required: true}), wirkungsdauer: new StringField({required: true}),
@ -24,8 +46,14 @@ export class SpellDataModel extends BaseItem {
antimagie: new StringField(), antimagie: new StringField(),
merkmal: new StringField(), merkmal: new StringField(),
komplexität: new StringField(), komplexität: new StringField(),
repräsentation: new StringField(), repräsentation: new ObjectField(),
info: new StringField() 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"; 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) { 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"; 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) { async _prepareContext(options) {
const context = await super._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.offenseTalent = this._offenseTalent ?? ''
context.offenseTalents = {} context.offenseTalents = {}

View File

@ -1,6 +1,9 @@
import {ActionManager} from "../sheets/actions/action-manager.mjs"; 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 _actor = null
constructor(actor) { constructor(actor, data) {
super(); super();
this._actor = actor this._actor = actor
this._targetId = null this._targetId = null
this._skillId = null this._skillId = data.skill ? data.skill : null
this._weaponId = null this._weaponId = data.weapon ? data.weapon : null
this._defenseManeuverId = null this._defenseManeuverId = null
this._actionManager = new ActionManager(this._actor) 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 const {targetId} = target.dataset
this._targetId = this._targetId === targetId ? null : targetId this._targetId = this._targetId === targetId ? null : targetId
this.render({parts: ["form"]}) 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 const {maneuverId} = target.dataset
this._defenseManeuverId = this._defenseManeuverId === maneuverId ? null : maneuverId this._defenseManeuverId = this._defenseManeuverId === maneuverId ? null : maneuverId
this.render({parts: ["form"]}) 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 const {weaponId, skillId} = target.dataset
this._weaponId = this._weaponId === weaponId ? null : weaponId this._weaponId = this._weaponId === weaponId ? null : weaponId
this._skillId = this._skillId === skillId ? null : skillId this._skillId = this._skillId === skillId ? null : skillId
this.render({parts: ["form"]}) this.render({parts: ["form"]})
} }
static async #onSubmitForm(event, form, formData) {
static async #onSelectWeaponAndSkill(event, target) {
event.preventDefault() 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 weapon = this._actor.itemTypes["Equipment"].find(p => p._id === this._weaponId)
const skill = this._actor.itemTypes["Skill"].find(p => p._id === this._skillId) 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) const target = game.actors.get(game.scenes.current.tokens.find(p => p._id === this._targetId).actorId)
@ -103,7 +126,7 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
/** @type Cooldown */ /** @type Cooldown */
const newCooldown = { const newCooldown = {
start: maneuver.cooldown({weapon, skill, target, mod: this._mod}), start: maneuver.cooldown({weapon, skill, target, mod: this._mod}),
current: maneuver.cooldown({weapon, skill, target, mod: this._mod}), current: 0,
data: { data: {
cssClass: "Kampf", cssClass: "Kampf",
weapon: this._weaponId, weapon: this._weaponId,
@ -129,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) { _configureRenderOptions(options) {
super._configureRenderOptions(options) super._configureRenderOptions(options)
if (options.window) { if (options.window) {
@ -259,9 +288,9 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
const context = await super._prepareContext(options) const context = await super._prepareContext(options)
context.actor = this._actor 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.tokenDistances = this.#evaluateDistances()
context.weapons = this.#evaluateWeapons() context.weapons = this.#evaluateWeapons()
@ -278,7 +307,28 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
// TODO get W/M of weapon NOW // 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 return context
} else { } else {
ui.notifications.error(`Feature funktioniert nur wenn der Akteur ein Token auf der aktuellen Szene hat`); ui.notifications.error(`Feature funktioniert nur wenn der Akteur ein Token auf der aktuellen Szene hat`);
@ -306,12 +356,10 @@ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2
target.textContent = `(${result})` target.textContent = `(${result})`
targetDescription.textContent = this._modDescription targetDescription.textContent = this._modDescription
if (result <= 0) { if (result <= 0 || !context.ready) {
context.ready = false
this.element.querySelector(".actions button").classList.remove("ready") this.element.querySelector(".actions button").classList.remove("ready")
this.element.querySelector(".actions button").setAttribute("disabled", true) this.element.querySelector(".actions button").setAttribute("disabled", true)
} else { } else {
context.ready = true
this.element.querySelector(".actions button").classList.add("ready") this.element.querySelector(".actions button").classList.add("ready")
this.element.querySelector(".actions button").removeAttribute("disabled") this.element.querySelector(".actions button").removeAttribute("disabled")
} }

View File

@ -1,6 +1,9 @@
import {ActionManager} from "../sheets/actions/action-manager.mjs"; 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 _actor = null
constructor(actor, attackData) { constructor(actor, data, attackData) {
super(); super();
this._attackData = attackData ?? { /*this._attackData = attackData ?? {
modToDefense: 0, modToDefense: 0,
attacker: null, attacker: null,
weapon: null, // is important to note as weapons like Chain Weapons or Flails can ignore Shields weapon: null, // is important to note as weapons like Chain Weapons or Flails can ignore Shields
} }*/
this._actor = actor this._actor = actor
this._skillId = null this._skillId = data.skill ? data.skill : null
this._weaponId = null this._weaponId = data.weapon ? data.weapon : null
this._defenseManeuverId = null this._defenseManeuverId = null
this._actionManager = new ActionManager(this._actor) this._actionManager = new ActionManager(this._actor)
//if (this._actor) { //if (this._actor) {
@ -251,7 +254,26 @@ export class DefenseActionDialog extends HandlebarsApplicationMixin(ApplicationV
// TODO get W/M of weapon NOW // 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 return context
} else { } else {
ui.notifications.error(`Feature funktioniert nur wenn der Akteur ein Token auf der aktuellen Szene hat`); 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})` target.textContent = `(${result})`
targetDescription.textContent = this._modDescription targetDescription.textContent = this._modDescription
if (result <= 0) { if (result <= 0 || !context.ready) {
context.ready = false
this.element.querySelector(".actions button").classList.remove("ready") this.element.querySelector(".actions button").classList.remove("ready")
this.element.querySelector(".actions button").setAttribute("disabled", true) this.element.querySelector(".actions button").setAttribute("disabled", true)
} else { } else {
context.ready = true
this.element.querySelector(".actions button").classList.add("ready") this.element.querySelector(".actions button").classList.add("ready")
this.element.querySelector(".actions button").removeAttribute("disabled") 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"; import {Talent} from "../data/talent.mjs";
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api const {
ApplicationV2,
HandlebarsApplicationMixin
} = foundry.applications.api
export class LiturgyDialog extends HandlebarsApplicationMixin(ApplicationV2) { export class LiturgyDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -160,7 +163,7 @@ export class LiturgyDialog extends HandlebarsApplicationMixin(ApplicationV2) {
} }
cooldowns.push({ cooldowns.push({
start: castingTime, start: castingTime,
current: castingTime, current: 0,
data: { data: {
cssClass: "Karmal", cssClass: "Karmal",
title: this._liturgy.name, title: this._liturgy.name,

View File

@ -1,6 +1,7 @@
import {XmlImport} from "../xml-import/xml-import.mjs"; const {
ApplicationV2,
const {ApplicationV2, HandlebarsApplicationMixin} = foundry.applications.api HandlebarsApplicationMixin
} = foundry.applications.api
export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) { export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -98,6 +99,7 @@ export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
lepMod: [-1, -2, -3, -4, -5], lepMod: [-1, -2, -3, -4, -5],
display: "range", display: "range",
noLabel: "Gutes Wetter", noLabel: "Gutes Wetter",
nestingLevel: 1,
labels: [ labels: [
"Schlechtes Wetter I", "Schlechtes Wetter I",
"Schlechtes Wetter II", "Schlechtes Wetter II",
@ -113,6 +115,7 @@ export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
aspMod: -1, aspMod: -1,
lepMod: -1, lepMod: -1,
display: "boolean", display: "boolean",
nestingLevel: 1,
group: "bad_camp", group: "bad_camp",
active: false, active: false,
value: "on" value: "on"
@ -219,6 +222,14 @@ export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
const elementInMod = this.element.querySelector('output[name="inMod"]') const elementInMod = this.element.querySelector('output[name="inMod"]')
const elementWoundMod = this.element.querySelector('output[name="woundMod"]') 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() const context = this.#updateData()
elementLepMod.value = context.lepModDisplay elementLepMod.value = context.lepModDisplay
elementKoMod.value = context.koRollDisplay elementKoMod.value = context.koRollDisplay
@ -333,8 +344,6 @@ export class RestingDialog extends HandlebarsApplicationMixin(ApplicationV2) {
context.inRollDisplay = `1w20+${this.regInMod}` context.inRollDisplay = `1w20+${this.regInMod}`
} }
console.log(this, context)
return 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 {Talent} from "../data/talent.mjs";
import {ATTRIBUTE} from "../data/attribute.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) { export class TalentDialog extends HandlebarsApplicationMixin(ApplicationV2) {

View File

@ -1,6 +1,9 @@
import {XmlImport} from "../xml-import/xml-import.mjs"; 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) { export class XmlImportDialog extends HandlebarsApplicationMixin(ApplicationV2) {

View File

@ -1,5 +1,5 @@
import {LiturgyData} from "../data/miracle/liturgydata.mjs"; import {LiturgyData} from "../data/miracle/liturgyData.mjs";
import {Zonenruestung, Zonenwunde, Wunde} from "../data/Trefferzone.js"; import {Zonenruestung, Zonenwunde, Wunde} from "../data/trefferzone.mjs";
import {PlayerCharacterDataModel} from "../data/character.mjs"; import {PlayerCharacterDataModel} from "../data/character.mjs";
export class Character extends Actor { export class Character extends Actor {
@ -26,7 +26,7 @@ export class Character extends Actor {
*/ */
prepareDerivedData() { prepareDerivedData() {
if (this.type === "character") { if (this.type === "Character") {
const actorData = this; const actorData = this;
const systemData = actorData.system; const systemData = actorData.system;
@ -105,6 +105,19 @@ export class Character extends Actor {
systemData.ausweichen.basis = systemData.pa.basis systemData.ausweichen.basis = systemData.pa.basis
systemData.ausweichen.aktuell = systemData.ausweichen.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")) { if (game.settings.get("DSA_4-1", "optional_ruestungzonen")) {
systemData.rs = { systemData.rs = {
@ -119,11 +132,11 @@ export class Character extends Actor {
} else { } else {
systemData.rs = 0; // only with DSA_4-1.optional_trefferzonen = false 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 // 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")) { if (game.settings.get("DSA_4-1", "optional_trefferzonen")) {
systemData.wunden.kopf = 0 systemData.wunden.kopf = 0
systemData.wunden.brust = 0 systemData.wunden.brust = 0
@ -310,8 +323,133 @@ export class Character extends Actor {
return updateObject; return updateObject;
} }
reduceWealth(by) { /**
console.log('Reichtum reduziert um ', by) * 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 return true
} }

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

@ -127,7 +127,6 @@ export class ActionManager {
source: ActionManager.SF, source: ActionManager.SF,
cooldown: (options) => options.mod - 2, cooldown: (options) => options.mod - 2,
activate: (queue, data) => { activate: (queue, data) => {
console.log(queue, data)
data.actor.rollAttack(data) data.actor.rollAttack(data)
return true return true
}, },

View File

@ -7,10 +7,20 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
position: {width: 520, height: 480}, position: {width: 520, height: 480},
classes: ['dsa41', 'sheet', 'item', 'advantage'], classes: ['dsa41', 'sheet', 'item', 'advantage'],
tag: 'form', tag: 'form',
window: {
resizable: true
},
form: { form: {
submitOnChange: true, submitOnChange: true,
closeOnSubmit: false, closeOnSubmit: false,
handler: AdvantageSheet.#onSubmitForm 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 */ /** @inheritDoc */
static PARTS = { static PARTS = {
form: { form: {
@ -30,9 +41,14 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
}, },
advantage: { advantage: {
template: `systems/DSA_4-1/templates/item/advantage/tab-advantage.hbs` 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) { _configureRenderOptions(options) {
super._configureRenderOptions(options) super._configureRenderOptions(options)
@ -43,6 +59,173 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
return options 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 * Handle form submission
* @this {AdvantageSheet} * @this {AdvantageSheet}
@ -52,10 +235,32 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
*/ */
static async #onSubmitForm(event, form, formData) { static async #onSubmitForm(event, form, formData) {
event.preventDefault() 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 */ /** @override */
@ -71,7 +276,34 @@ export class AdvantageSheet extends HandlebarsApplicationMixin(DocumentSheetV2)
context.choices[a.name] = a.name context.choices[a.name] = a.name
}) })
context.hasModality = context.system.value != null 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; 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, description: item.system.description,
isAdvantage: !item.system.nachteil, isAdvantage: !item.system.nachteil,
isDisadvantage: item.system.nachteil, isDisadvantage: item.system.nachteil,
isBadAttribute: item.system.schlechteEigenschaft isBadAttribute: item.system.schlechteEigenschaft,
fav: item.getFlag("DSA_4-1", "favourite")
}) })
} else { } else {
context.flaws.push({ context.flaws.push({
@ -32,7 +33,8 @@ export default {
description: item.system.description, description: item.system.description,
isAdvantage: !item.system.nachteil, isAdvantage: !item.system.nachteil,
isDisadvantage: 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({ context.specialAbilities.push({
id: item._id, id: item._id,
name: item.system.value ? item.system.value : item.name, 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 { export default {
_prepareContext: (context) => { _prepareContext: (context, actor, thisObject) => {
const actorData = context.document const actorData = context.document
context.spells = [] context.spells = []
@ -52,9 +50,11 @@ export default {
for (let setIndex = 0; setIndex < maxSets; setIndex++) { for (let setIndex = 0; setIndex < maxSets; setIndex++) {
context.sets.push({ context.sets.push({
tab: "set" + (setIndex + 1), tab: "pane" + (setIndex + 1),
name: romanNumerals[setIndex], label: romanNumerals[setIndex],
index: setIndex, index: setIndex,
actorId: actorData.id,
setEquipped: actorData.system.setEquipped === setIndex,
slots: [ slots: [
{ {
target: "links", target: "links",
@ -131,6 +131,8 @@ export default {
] ]
}) })
} }
context.selectedTab = thisObject.selectedTab ?? context.sets[0].tab
context.setEquipped = actorData.system.setEquipped
return context return context
}, },
@ -150,9 +152,50 @@ export default {
} }
}).bind(thisObject.element); }).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( new ContextMenu(
thisObject.element, 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", name: "Abrüsten",
@ -280,7 +323,7 @@ export default {
name: "Aus dem Inventar entfernen", name: "Aus dem Inventar entfernen",
icon: '<i class="fa-solid fa-trash"></i>', icon: '<i class="fa-solid fa-trash"></i>',
callback: (target) => { callback: (target) => {
thisObject.document.deleteEmbeddedDocuments('Item', [target.dataset.itemId]) game.DSA41.socket.executeAsGM("removeFromLootTable", thisObject.document.id, target.dataset.itemId)
}, },
condition: (target) => { condition: (target) => {
const {itemId} = target.dataset const {itemId} = target.dataset
@ -290,7 +333,7 @@ export default {
], {jQuery: false}); ], {jQuery: false});
}, },
_getTabConfig: (group) => { _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` 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 { export default {
_prepareContext: async (context, object) => { _prepareContext: async (context, object) => {
@ -15,9 +13,6 @@ export default {
return object.items.get(object.system.heldenausruestung[setNumber]?.[slot]) 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.inidice = actorData.system.ini.wuerfel
context.inivalue = actorData.system.ini.aktuell context.inivalue = actorData.system.ini.aktuell
context.inimod = actorData.system.ini.mod 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.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.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.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.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 return context
@ -37,7 +58,7 @@ export default {
}, },
_getTabConfig: (group) => { _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 { export default {
_prepareContext: (context) => { _prepareContext: (context) => {
@ -57,17 +57,17 @@ export default {
// sort by rank // sort by rank
const rankData = LiturgyData.getRankOfLiturgy(item.system, deity) const rankData = LiturgyData.getRankOfLiturgy(item.system, deity)
if (rankData) { if (rankData) {
console.log(rankData)
let {index, name, lkp, mod, costKaP} = rankData; let {index, name, lkp, mod, costKaP} = rankData;
insertObject["count" + name] = insertObject["count" + name] + 1; insertObject["count" + name] = insertObject["count" + name] + 1;
insertObject[name].push({ insertObject[name]?.push({
id: item._id, id: item._id,
name: item.name, name: item.name,
lkpReq: lkp, lkpReq: lkp,
lkpMod: mod, lkpMod: mod,
costKaP, costKaP,
fav: item.getFlag("DSA_4-1", "favourite"),
rank: index, // get effective liturgy rank based on deity rank: index, // get effective liturgy rank based on deity
liturgiekenntnis: deity, liturgiekenntnis: deity,
}) })
@ -104,7 +104,7 @@ export default {
_getTabConfig: (group, thisObject) => { _getTabConfig: (group, thisObject) => {
const hasLiturgies = thisObject.document.items.filter(p => p.type === "Liturgy").length > 0 ?? false const hasLiturgies = thisObject.document.items.filter(p => p.type === "Liturgy").length > 0 ?? false
if (hasLiturgies) { 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` template: `systems/DSA_4-1/templates/actor/character/tab-liturgies.hbs`

View File

@ -14,7 +14,7 @@ export default {
}, },
_getTabConfig: (group) => { _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` template: `systems/DSA_4-1/templates/actor/character/tab-meta.hbs`
} }

View File

@ -2,7 +2,6 @@ export default {
_prepareContext: (context) => { _prepareContext: (context) => {
const actorData = context.document const actorData = context.document
context.spells = []
context.system = actorData.system context.system = actorData.system
context.flags = actorData.flags context.flags = actorData.flags
context.derived = context.document.system context.derived = context.document.system
@ -51,7 +50,8 @@ export default {
id: item._id, id: item._id,
at: item.system.at, at: item.system.at,
pa: item.system.pa, 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") { if (talentGruppe === "Kampf") {

View File

@ -14,14 +14,21 @@ export default {
return merkmale.split(",").map((merkmal) => merkmal.trim()) 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 eigenschaften = item.system.probe;
const werte = [ const werte = [
{name: eigenschaften[0], value: this.prepareEigenschaftRoll(actorData, eigenschaften[0])}, {name: eigenschaften[0], value: prepareEigenschaftRoll(actorData, eigenschaften[0])},
{name: eigenschaften[1], value: this.prepareEigenschaftRoll(actorData, eigenschaften[1])}, {name: eigenschaften[1], value: prepareEigenschaftRoll(actorData, eigenschaften[1])},
{name: eigenschaften[2], value: this.prepareEigenschaftRoll(actorData, eigenschaften[2])} {name: eigenschaften[2], value: prepareEigenschaftRoll(actorData, eigenschaften[2])}
] ]
context.spells.push({ context.spells.push({
id: item._id, id: item._id,
@ -35,8 +42,9 @@ export default {
eigenschaft1: werte[0].name, eigenschaft1: werte[0].name,
eigenschaft2: werte[1].name, eigenschaft2: werte[1].name,
eigenschaft3: werte[2].name, eigenschaft3: werte[2].name,
fav: item.getFlag("DSA_4-1", "favourite")
}) })
}
}) })
context.hasSpells = context.spells.length > 0 context.hasSpells = context.spells.length > 0

View File

@ -1,5 +1,5 @@
import Advsf from "./character/advsf.mjs" 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 Effects from "./character/effects.mjs"
import Equipment from "./character/equipment.mjs" import Equipment from "./character/equipment.mjs"
import Liturgies from "./character/liturgies.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 {ActionManager} from "./actions/action-manager.mjs";
import {DefenseActionDialog} from "../dialog/defenseAction.mjs"; import {DefenseActionDialog} from "../dialog/defenseAction.mjs";
import {RestingDialog} from "../dialog/restingDialog.mjs"; import {RestingDialog} from "../dialog/restingDialog.mjs";
import {Character} from "../documents/character.mjs";
import {LiturgyDialog} from "../dialog/liturgyDialog.mjs"; import {LiturgyDialog} from "../dialog/liturgyDialog.mjs";
import {TalentDialog} from "../dialog/talentDialog.mjs"; import {TalentDialog} from "../dialog/talentDialog.mjs";
import {AttributeDialog} from "../dialog/attributeDialog.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 {HandlebarsApplicationMixin, DocumentSheetV2} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets const {ActorSheetV2} = foundry.applications.sheets
const {ContextMenu} = foundry.applications.ux
class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) { class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
@ -39,19 +46,33 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
actions: { actions: {
rollCombatSkill: CharacterSheet.#rollCombatSkill, rollCombatSkill: CharacterSheet.#rollCombatSkill,
rollSkill: CharacterSheet.#rollSkill, rollSkill: CharacterSheet.#rollSkill,
rollFlaw: CharacterSheet.#rollFlaw, rollFlaw: CharacterSheet.rollFlaw,
rollAttribute: CharacterSheet.#rollAttribute, rollAttribute: CharacterSheet.#rollAttribute,
editImage: DocumentSheetV2.DEFAULT_OPTIONS.actions.editImage, editImage: DocumentSheetV2.DEFAULT_OPTIONS.actions.editImage,
openEmbeddedDocument: CharacterSheet.#openEmbeddedDocument, openEmbeddedDocument: CharacterSheet.openEmbeddedDocument,
openCultureDocument: CharacterSheet.#openCultureDocument, openCultureDocument: CharacterSheet.#openCultureDocument,
openSpeciesDocument: CharacterSheet.#openSpeciesDocument, openSpeciesDocument: CharacterSheet.#openSpeciesDocument,
openCombatAction: CharacterSheet.#openCombatAction, openCombatAction: CharacterSheet.#openCombatAction,
openLiturgyDialog: CharacterSheet.#openLiturgyDialog, openLiturgyDialog: CharacterSheet.openLiturgyDialog,
openSpellDialog: CharacterSheet.openSpellDialog,
castSpell: CharacterSheet.castSpell,
progressCooldown: CharacterSheet.#progressCooldown, progressCooldown: CharacterSheet.#progressCooldown,
cancelCooldown: CharacterSheet.#cancelCooldown, cancelCooldown: CharacterSheet.#cancelCooldown,
activateCooldown: CharacterSheet.#activateCooldown, activateCooldown: CharacterSheet.#activateCooldown,
rest: CharacterSheet.#startResting, rest: CharacterSheet.#startResting,
removeEffect: CharacterSheet.#removeEffect, 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: { sheet: {
tabs: [], tabs: [],
initial: 'meta' initial: 'meta'
} },
} }
/** @inheritDoc */ /** @inheritDoc */
@ -77,15 +98,16 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
advsf: { advsf: {
template: Advsf.template template: Advsf.template
}, },
combat: { health: {
template: Combat.template template: Health.template
}, },
equipment: { equipment: {
template: Equipment.template, template: Equipment.template,
scrollable: [''] scrollable: ['.inventory']
}, },
skills: { skills: {
template: Skills.template template: Skills.template,
scrollable: ['.tab.skills']
}, },
spells: { spells: {
template: Spells.template template: Spells.template
@ -95,7 +117,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
}, },
effects: { effects: {
template: Effects.template template: Effects.template
} },
} }
@ -130,7 +152,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
}).render(true) }).render(true)
} }
static async #rollFlaw(event, target) { static async rollFlaw(event, target) {
new AttributeDialog(this.document, target.dataset.itemId).render(true) new AttributeDialog(this.document, target.dataset.itemId).render(true)
} }
@ -141,7 +163,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
cooldowns.splice(cooldownId, 1) cooldowns.splice(cooldownId, 1)
if (cooldown) { if (cooldown) {
cooldown.current = cooldown.current - 1 cooldown.current = cooldown.current + 1
} }
cooldowns.push(cooldown) cooldowns.push(cooldown)
@ -165,9 +187,8 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
const cooldowns = this.document.system.cooldowns const cooldowns = this.document.system.cooldowns
const cooldown = this.document.system.cooldowns[cooldownId] 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 am = new ActionManager(this.document)
console.log(cooldown.data.maneuver)
const action = new Function(`return ${cooldown.data.maneuver}`) const action = new Function(`return ${cooldown.data.maneuver}`)
if (action) { if (action) {
@ -185,7 +206,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
* *
* @param {MouseEvent} event * @param {MouseEvent} event
*/ */
static #openEmbeddedDocument(event) { static openEmbeddedDocument(event) {
let dataset = event.target.dataset let dataset = event.target.dataset
if (!dataset.itemId && !dataset.id) { if (!dataset.itemId && !dataset.id) {
dataset = event.target.parentElement.dataset dataset = event.target.parentElement.dataset
@ -204,21 +225,33 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
} }
static #openCombatAction(event, target) { static #openCombatAction(event, target) {
let {weapon, skill} = target.dataset
switch (target.dataset.mode) { switch (target.dataset.mode) {
case "attack": case "attack":
new CombatActionDialog(this.document).render(true) new CombatActionDialog(this.document, {weapon, skill}).render(true)
break break
case "defense": case "defense":
new DefenseActionDialog(this.document).render(true) new DefenseActionDialog(this.document, {weapon, skill}).render(true)
break break
} }
} }
static #openLiturgyDialog(event, target) { static openLiturgyDialog(event, target) {
const {id, lkp, deity} = target.dataset const {id, lkp, deity} = target.dataset
new LiturgyDialog(this.document, lkp, id, deity).render(true) 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) { static #startResting(event, target) {
const dialog = new RestingDialog(this.document) const dialog = new RestingDialog(this.document)
@ -240,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) { _configureRenderOptions(options) {
super._configureRenderOptions(options) super._configureRenderOptions(options)
@ -250,6 +342,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
return options return options
} }
/** /**
* Handle form submission * Handle form submission
* @this {AdvantageSheet} * @this {AdvantageSheet}
@ -260,16 +353,49 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
static async #onSubmitForm(event, form, formData) { static async #onSubmitForm(event, form, formData) {
event.preventDefault() 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) { _getTabsConfig(group) {
const tabs = foundry.utils.deepClone(super._getTabsConfig(group)) const tabs = foundry.utils.deepClone(super._getTabsConfig(group))
Meta._getTabConfig(tabs, this); Meta._getTabConfig(tabs, this)
Social._getTabConfig(tabs, this); Social._getTabConfig(tabs, this)
Advsf._getTabConfig(tabs, this) Advsf._getTabConfig(tabs, this)
Combat._getTabConfig(tabs, this) Health._getTabConfig(tabs, this)
Equipment._getTabConfig(tabs, this) Equipment._getTabConfig(tabs, this)
Skills._getTabConfig(tabs, this) Skills._getTabConfig(tabs, this)
Spells._getTabConfig(tabs, this) Spells._getTabConfig(tabs, this)
@ -331,13 +457,6 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
context.img = actorData.img context.img = actorData.img
context.effects = actorData.effects ?? [] 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.zonenruestung = game.settings.get("DSA_4-1", "optional_ruestungzonen")
context.trefferzonen = game.settings.get("DSA_4-1", "optional_trefferzonen") context.trefferzonen = game.settings.get("DSA_4-1", "optional_trefferzonen")
context.ausdauer = game.settings.get("DSA_4-1", "optional_ausdauer") context.ausdauer = game.settings.get("DSA_4-1", "optional_ausdauer")
@ -352,8 +471,11 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
context.lepper = Math.min((actorData.system.lep.aktuell / actorData.system.lep.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.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.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.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 fernkampf = actorData.findEquipmentOnSlot("fernkampf", actorData.system.setEquipped, actorData)
const links = actorData.findEquipmentOnSlot("links", actorData.system.setEquipped, actorData) const links = actorData.findEquipmentOnSlot("links", actorData.system.setEquipped, actorData)
@ -362,18 +484,19 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
if (fernkampf) { if (fernkampf) {
const fkitems = fernkampf.system.rangedSkills.map((skillInQuestion) => actorData.items.find(p => p.name === skillInQuestion)) const fkitems = fernkampf.system.rangedSkills.map((skillInQuestion) => actorData.items.find(p => p.name === skillInQuestion))
fkitems.forEach(async skill => { fkitems?.forEach(skill => {
const obj = await skill if (skill) {
context.attacks.push({ context.attacks.push({
name: obj.name, name: skill.name,
using: fernkampf.name, id: fernkampf._id,
atroll: `1d20cs<${this.document.system.fk.aktuell + obj.system.at}`, skillId: skill._id,
at: `${this.document.system.fk.aktuell + obj.system.at}`, using: fernkampf.name,
tproll: `${fernkampf.system.rangedAttackDamage}`, // TODO consider adding TP/KK mod and Range mod isRanged: true,
tp: `${fernkampf.system.rangedAttackDamage}`, at: `${this.document.system.fk.aktuell + skill.system.at}`,
iniroll: `(${context.inidice})d6 + ${context.inivalue + fernkampf.system.iniModifier ?? 0}`, tp: `${fernkampf.system.rangedAttackDamage}`,
ini: `${context.inidice}w6 + ${context.inivalue + fernkampf.system.iniModifier ?? 0}`, ini: `${context.inidice}w6 + ${context.inivalue + fernkampf.system.iniModifier ?? 0}`,
}) })
}
}) })
} }
if (links) { if (links) {
@ -384,18 +507,17 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
meitems.push(item) meitems.push(item)
} }
}) })
meitems.forEach(skill => { meitems?.forEach(skill => {
const obj = skill const obj = skill
context.attacks.push({ context.attacks.push({
name: obj.name, name: obj.name,
id: links._id,
skillId: skill._id,
using: links.name, 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}`, 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}`, 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}`, tp: `${links.system.meleeAttackDamage}`,
iniroll: `(${context.inidice})d6 + ${context.inivalue + links.system.iniModifier ?? 0}`,
ini: `${context.inidice}w6 + ${context.inivalue + links.system.iniModifier ?? 0}`, ini: `${context.inidice}w6 + ${context.inivalue + links.system.iniModifier ?? 0}`,
}) })
}) })
@ -408,23 +530,61 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
meitems.push(item) meitems.push(item)
} }
}) })
meitems.forEach(skill => { meitems?.forEach(skill => {
const obj = skill const obj = skill
context.attacks.push({ context.attacks.push({
name: obj.name, name: obj.name,
id: rechts._id,
skillId: skill._id,
using: rechts.name, 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}`, 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}`, 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}`, tp: `${rechts.system.meleeAttackDamage}`,
iniroll: `(${context.inidice})d6 + ${context.inivalue + rechts.system.iniModifier ?? 0}`,
ini: `${context.inidice}w6 + ${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 = actorData.system.cooldowns ?? []
context.cooldowns.forEach(cooldown => { context.cooldowns.forEach(cooldown => {
let weapon = null let weapon = null
@ -432,15 +592,20 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
let tooltip = cooldown.data.title let tooltip = cooldown.data.title
if (cooldown.data.weapon) { if (cooldown.data.weapon) {
weapon = this.document.itemTypes["Equipment"].find(p => p._id === cooldown.data.weapon) weapon = this.document.itemTypes["Equipment"].find(p => p._id === cooldown.data.weapon)
tooltip += `<br/>Waffe:${weapon.name}` tooltip += `<br/>Waffe: ${weapon.name}`
} }
if (cooldown.data.target) { if (cooldown.data.target) {
target = game.actors.get(game.scenes.current.tokens.find(p => p._id === cooldown.data.target).actorId) target = game.actors.get(game.scenes.current.tokens.find(p => p._id === cooldown.data.target).actorId)
tooltip += `<br/>Waffe:${target.name}` tooltip += `<br/>Ziel: ${target.name}`
} }
cooldown.title = cooldown.data.title cooldown.title = cooldown.data.title
cooldown.progress = ((cooldown.current / cooldown.start) * 100) + "%" cooldown.progress = ((cooldown.current / cooldown.start) * 100) + "%"
cooldown.tooltip = tooltip + `<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 context.hasSpells = actorData.itemTypes["Spell"].length > 0
@ -507,11 +672,11 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
case "advsf": case "advsf":
await Advsf._prepareContext(context, this.document) await Advsf._prepareContext(context, this.document)
break break
case "combat": case "health":
await Combat._prepareContext(context, this.document) await Health._prepareContext(context, this.document)
break break
case "equipment": case "equipment":
await Equipment._prepareContext(context, this.document) await Equipment._prepareContext(context, this.document, this)
break break
case "skills": case "skills":
await Skills._prepareContext(context, this.document) await Skills._prepareContext(context, this.document)
@ -529,11 +694,23 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
return context 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) { _onRender(context, options) {
Meta._onRender(context, options, this.element) Meta._onRender(context, options, this.element)
Social._onRender(context, options, this.element) Social._onRender(context, options, this.element)
Advsf._onRender(context, options, this) Advsf._onRender(context, options, this)
Combat._onRender(context, options, this.element) Health._onRender(context, options, this.element)
Effects._onRender(context, options, this.element) Effects._onRender(context, options, this.element)
Equipment._onRender(context, options, this) Equipment._onRender(context, options, this)
Liturgies._onRender(context, options, this.element) Liturgies._onRender(context, options, this.element)
@ -545,18 +722,28 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
return true return true
} }
async _onDrop(event) { async _onDrop(event) {
const data = TextEditor.implementation.getDragEventData(event); const data = TextEditor.implementation.getDragEventData(event)
const actor = this.actor;
const targetDocument = this.actor.itemTypes["Equipment"].find(p => p._id === event.target.dataset['itemId']) 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 // Dropped Documents
const documentClass = foundry.utils.getDocumentClass(data.type); const documentClass = foundry.utils.getDocumentClass(data.type)
if (documentClass) { 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") { 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 // No duplication by moving items from one actor to another
@ -576,7 +763,7 @@ class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
} else { } else {
if (document.parent && document.parent !== this.actor) { 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) await this._onDropDocument(event, document)

View File

@ -25,7 +25,6 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
static TABS = { static TABS = {
sheet: { sheet: {
tabs: [], tabs: [],
initial: 'meta'
} }
} }
@ -66,6 +65,19 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
tabs.tabs.push(tab) 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 return tabs
} }
@ -89,7 +101,7 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
static async #onSubmitForm(event, form, formData) { static async #onSubmitForm(event, form, formData) {
event.preventDefault() event.preventDefault()
await this.document.update(formData.object) // Note: formData.object await this.document.update(formData.object)
} }
static #openEmbeddedDocument(event, target) { static #openEmbeddedDocument(event, target) {
@ -100,8 +112,8 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
static async #removeAttack(evt) { static async #removeAttack(evt) {
const {index} = evt.srcElement.dataset; const {index} = evt.srcElement.dataset;
let sans = Array.from(this.document.system.attacks); let sans = Array.from(this.document.system.attacks)
sans.splice(index, 1); sans.splice(index, 1)
await this.document.update({'system.attacks': sans}) await this.document.update({'system.attacks': sans})
} }
@ -113,7 +125,7 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
speaker: ChatMessage.getSpeaker({actor: this.document}), speaker: ChatMessage.getSpeaker({actor: this.document}),
flavor: label, flavor: label,
rollMode: game.settings.get('core', 'rollMode'), rollMode: game.settings.get('core', 'rollMode'),
}); })
} }
static async #addAttack() { static async #addAttack() {
@ -140,11 +152,11 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @override */ /** @override */
async _prepareContext(options) { async _prepareContext(options) {
const context = await super._prepareContext(options); const context = await super._prepareContext(options)
const actorData = context.document; const actorData = context.document
context.attacks = []; context.attacks = []
context.actor = actorData; context.actor = actorData
actorData.system.attacks.forEach((attack, index) => { actorData.system.attacks.forEach((attack, index) => {
context.attacks.push({ 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); //const allowed = Hooks.call("dropActorSheetData", actor, this, data);
// if (allowed === false) return; // if (allowed === false) return;
// Dropped Documents // Dropped Documents
const documentClass = foundry.utils.getDocumentClass(data.type); const documentClass = foundry.utils.getDocumentClass(data.type)
if (documentClass) { if (documentClass) {
const document = await documentClass.fromDropData(data); const document = await documentClass.fromDropData(data);
@ -209,7 +221,7 @@ export class CreatureSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
document.parent.items.get(document._id).delete() 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), dragstart: this._onDragStart.bind(this),
drop: this._onDrop.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) { static async #onSubmitForm(event, form, formData) {
event.preventDefault() event.preventDefault()
await this.document.update(formData.object) // Note: formData.object await this.document.update(formData.object)
} }
/** @override */ /** @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 const {DocumentSheetV2, HandlebarsApplicationMixin} = foundry.applications.api
/** class EquipmentSheet extends HandlebarsApplicationMixin(DocumentSheetV2) {
* @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) {
/** @inheritDoc */ /** @inheritDoc */
static DEFAULT_OPTIONS = { 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) { async #onUpdateCharacterSettings(data) {
if (data.type === "character") { if (data.type === "Character") {
// update group // update group
let settings = {...this.document.system.settings} let settings = {...this.document.system.settings}
@ -251,7 +251,7 @@ export class GroupSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
// Drag-drop // Drag-drop
new foundry.applications.ux.DragDrop.implementation({ new foundry.applications.ux.DragDrop.implementation({
dragSelector: ".inventory-table .equipment", dragSelector: ".inventory-table .equipment",
dropSelector: ".inventory-table", dropSelector: ".inventory",
permissions: { permissions: {
dragstart: this._canDragStart.bind(this), dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.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 // Update Group Members when either an Actor was moved into the linked Folder or removed from the linked Folder
Hooks.on('updateActor', (data) => { Hooks.on('updateActor', (data) => {
if (data._id !== this.document._id) { // dont update yourself when you update yourself... baka! 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.#onUpdateCharacterSettings(data)
this.render() this.render()
} else if (data.type === "character") { } else if (data.type === "Character") {
this.render() this.render()
} }
} }

View File

@ -21,6 +21,7 @@ export class MerchantSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
}, },
actions: { actions: {
buy: MerchantSheet.#buyWare, buy: MerchantSheet.#buyWare,
editItem: MerchantSheet.#openEmbeddedDocument,
editImage: DocumentSheetV2.DEFAULT_OPTIONS.actions.editImage, editImage: DocumentSheetV2.DEFAULT_OPTIONS.actions.editImage,
editServiceImage: MerchantSheet.#editServiceImage, editServiceImage: MerchantSheet.#editServiceImage,
editNewServiceImage: MerchantSheet.#editNewServiceImage, editNewServiceImage: MerchantSheet.#editNewServiceImage,
@ -88,7 +89,7 @@ export class MerchantSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
const item = this.document.items.get(itemId) const item = this.document.items.get(itemId)
let selections = '' let selections = ''
game.actors.filter(p => p.isOwner).forEach(actor => { game.actors.filter(p => p.isOwner && p.type === "Character").forEach(actor => {
selections += `<option value=${actor.id}>${actor.name}</option>` selections += `<option value=${actor.id}>${actor.name}</option>`
}) })
@ -101,29 +102,42 @@ export class MerchantSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
} }
}); });
const actor = game.actors.get(actorId) 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)
let wealth = 0 ChatMessage.create({
user: game.user._id,
actor.itemTypes["Equipment"].forEach(coin => { speaker: {actor},
if (coin.system.category.indexOf("Währung") !== -1) { content: `hat ${item.name} für ${game.DSA41.displayCurrency(item.system.price)} gekauft`,
wealth += (coin.system.quantity * coin.system.currencyDenominator) type: CONST.CHAT_MESSAGE_TYPES.IC
})
} else {
ui.notifications.error(item.name + " ist zu teuer für " + actor.name)
} }
})
if (wealth >= item.system.price) {
actor.reduceWealth(item.system.price)
actor.createEmbeddedDocuments('Item', [item])
this.document.deleteEmbeddedDocuments('Item', [item._id])
} else {
ui.notifications.error(item.name + " ist zu teuer für " + actor.name)
} }
console.log(actorId) }
/**
*
* @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) { static async #removeService(event, target) {
@ -249,6 +263,7 @@ export class MerchantSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
context.description = this.document.system.description context.description = this.document.system.description
context.goods = this.document.itemTypes["Equipment"] ?? [] context.goods = this.document.itemTypes["Equipment"] ?? []
context.services = this.document.system.services context.services = this.document.system.services
context.isOwner = this.document.isOwner
return context return context
} }

View File

@ -12,9 +12,15 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
closeOnSubmit: false, closeOnSubmit: false,
handler: SpecialAbilitySheet.#onSubmitForm handler: SpecialAbilitySheet.#onSubmitForm
}, },
actions: {
addRequirement: SpecialAbilitySheet.#addRequirement,
removeRequirement: SpecialAbilitySheet.#removeRequirement,
addMod: SpecialAbilitySheet.#addMod,
removeMod: SpecialAbilitySheet.#removeMod,
saveVariant: SpecialAbilitySheet.#saveVariant
}
} }
static TABS = { static TABS = {
sheet: { sheet: {
tabs: [ tabs: [
@ -32,8 +38,13 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
specialability: { specialability: {
template: `systems/DSA_4-1/templates/item/specialability/tab-specialability.hbs` 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) { _configureRenderOptions(options) {
super._configureRenderOptions(options) super._configureRenderOptions(options)
@ -44,6 +55,173 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
return options 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 * Handle form submission
* @this {EquipmentSheet} * @this {EquipmentSheet}
@ -53,8 +231,32 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
*/ */
static async #onSubmitForm(event, form, formData) { static async #onSubmitForm(event, form, formData) {
event.preventDefault() 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 */ /** @override */
@ -82,7 +284,35 @@ export class SpecialAbilitySheet extends HandlebarsApplicationMixin(DocumentShee
}) })
context.hasModality = context.system.value != null 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; 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 {Blessing} from "../documents/blessing.mjs";
import {Profession} from "../documents/profession.mjs"; import {Profession} from "../documents/profession.mjs";
import {Culture} from "../documents/culture.mjs"; import {Culture} from "../documents/culture.mjs";
@ -383,7 +383,7 @@ export class XmlImport {
} }
async #addSpellsFromCompendiumByNameToActor(spellName, zfw, representation, hauszauber, actor) { 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 SCREAMING_NAME = spellName.toUpperCase()
const spellId = compendiumOfSpells.index.find(spell => spell.name === SCREAMING_NAME) const spellId = compendiumOfSpells.index.find(spell => spell.name === SCREAMING_NAME)
if (spellId) { if (spellId) {
@ -392,7 +392,8 @@ export class XmlImport {
try { try {
const embeddedDocument = (await actor.createEmbeddedDocuments('Item', [spell]))[0] 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) { } catch (error) {
console.error(`${spell} not found in items`, error) console.error(`${spell} not found in items`, error)
} }

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."
}

View File

@ -0,0 +1,54 @@
{
"seite": "49",
"name": "BLITZ DICH FIND",
"probe": [
"KL",
"IN",
"GE"
],
"probeMod": "+MR",
"technik": "Die Elfe deutet mit Zeige- und Mittelfinger der linken Hand auf das Opfer und spricht bhaiza dha feyra.",
"zauberdauer": {
"normal": "1 Aktionen"
},
"wirkung": "Der Zauber erzeugt im Geist des Opfers einen grellen Lichtblitz, der es für ZfW/2 Aktionen blendet und desorientiert. Danach kann es wieder normal sehen. Ein geblendetes Opfer hat starke Erschwernisse bei Talentproben, Zauberproben und Fernkampf (+ZfP* Punkte) und erleidet im Kampf Abzüge auf AT-, PA- und INI-Werte. Wissen um SF Blindkampf oder SF Konzentrationsstärke reduziert die Abzüge. Tiere werden meist so stark erschreckt, dass sie fliehen.",
"kosten": [
{
"cost": 4,
"repräsentation": ""
},
{
"cost": 3,
"repräsentation": "Schelm"
}
],
"zielobjekt": "Einzelwesen",
"reichweite": "ZfW Schritt",
"wirkungsdauer": "ZfW/2 Aktionen",
"modifikationen": "Zielobjekt (mehrere Wesen), Reichweite",
"varianten": [
{
"name": "Mehrere Personen",
"description": "Wirkt auf mehrere Ziele gleichzeitig.",
"mod": 0,
"limit": "3 AsP pro Person"
}
],
"reversalis": "hebt die Blitz-Wirkung auf",
"antimagie": "EIN EINFLUSS BANNEN beendet den Zauber vorzeitig; in einer Zone ebenfalls erschwert wirksam.",
"merkmal": [
"Einfluss"
],
"komplexität": "B",
"repräsentation": {
"Elf": 7,
"Magier": 6,
"Druide": 5,
"Hexe": 5,
"Achaz": 4,
"Geomant": 4,
"Schelm": 4,
"Scharlatan": 4
},
"info": "Ursprünglich ein Verteidigungszauber der Waldelfen, mittlerweile fast überall bekannt. Häufig als Vorbereitung für Angriffe genutzt."
}

View File

@ -0,0 +1,72 @@
{
"seite": "58",
"name": "CLAUDIBUS CLAVISTIBOR",
"probe": [
"KL",
"FF",
"KK"
],
"technik": "Der Zaubernde legt eine Hand auf den zu verschließenden Gegenstand (Tür, Truhe …) und spricht die Formel.",
"zauberdauer": {
"normal": "3 Aktionen"
},
"wirkung": "Die Formel verriegelt eine Tür, Truhe oder sonstigen Schließmechanismus auf magische Weise und verstärkt die Festigkeit des Materials. Die Tür kann nur noch mit passendem Schlüssel geöffnet werden. Zwergische Spezialschlösser können nahezu unüberwindbar gemacht werden, ebenso halbverrottete Türen temporär verstärkt.",
"kosten": [
{
"cost": 3,
"max": "ZfW",
"repräsentation": ""
}
],
"zielobjekt": "Einzelobjekt (es muss ein Verschluss vorhanden sein)",
"reichweite": "Berührung",
"wirkungsdauer": "bis die Tür geöffnet wird, maximal jedoch ZfP* Spielrunden",
"modifikationen": "Zauberdauer, Kosten, Reichweite",
"varianten": [
{
"name": "Wirkungsdauer",
"description": "Der Zauber bleibt deutlich länger wirksam. Für jede Erhöhung der maximalen Wirkungsdauer um eine Zeitkategorie ist die Probe um 2 Punkte erschwert, die Zauberdauer verdoppelt, die Kosten verdoppelt.",
"mod": "+2 pro Kategorie",
"limit": ""
},
{
"name": "Aktive Versiegelung",
"description": "Innerhalb der maximalen Wirkungsdauer bleibt der Verschlusszauber auch nach dem Öffnen bestehen und verschließt die Tür erneut.",
"mod": -3,
"limit": "11"
},
{
"name": "Schlüsselmeister (3 Personen",
"description": "Der Zaubernde kann bis zu drei/sieben Personen oder Gegenstände (Schlüssel) bestimmen, bei denen der Zauber nicht wirkt.",
"mod": -3,
"limit": "11"
},
{
"name": "Schlüsselmeister (7 Personen)",
"description": "Der Zaubernde kann bis zu drei/sieben Personen oder Gegenstände (Schlüssel) bestimmen, bei denen der Zauber nicht wirkt.",
"mod": -5,
"limit": "11"
},
{
"name": "Die Priesterkasten können",
"description": "Der Zaubernde kann innerhalb einer Minute beliebig viele Gegenstände durch Berührung versiegeln.",
"mod": -7,
"limit": "14"
}
],
"reversalis": "hebt einen wirkenden CLAUDIBUS auf",
"antimagie": "OBJEKT ENTZAUBERN wirkt gegen den Verschluss; in einer entsprechenden Zone kann der CLAUDIBUS nur erschwert gewirkt werden.",
"merkmal": [
"Objekt"
],
"komplexität": "C",
"repräsentation": {
"Magier": 6,
"Hexe": 5,
"Scharlatan": 5,
"Druide": 2,
"Elf": 2,
"Schelm": 2
},
"info": "Weit verbreitet, um Türen, Truhen und Kästchen zu sichern. Vor allem Gildenmagier und Scharlatane nutzen den Zauber, andere Repräsentationen kennen ihn seltener."
}

View File

@ -0,0 +1,54 @@
{
"seite": "70",
"name": "DUPLICATUS DOPPELBILD",
"probe": [
"KL",
"CH",
"GE"
],
"probeMod": "+2 pro zusätzlichem Doppelgänger",
"technik": "Der Magier hebt die Hände Handflächen nach außen vor das Gesicht, spreizt die Finger und verschiebt die Hände mehrfach gegeneinander; dabei spricht er die Formel.",
"zauberdauer": {
"normal": "5 Aktionen"
},
"wirkung": "Der Magier erschafft durch diese Formel für sich oder einen Gefährten einen oder mehrere optische Doppelgänger, die sich synchron mit ihm bewegen, dabei ständig mit ihm verschmelzen und wieder von ihm trennen, so dass Gegner kaum feststellen können, welches der echte Magier ist, und ihn nur noch mit geringerer Wahrscheinlichkeit treffen. Ab ZfW 7 können insgesamt zwei Doppelgänger erschaffen werden, ab ZfW 11 drei, ab ZfW 15 vier, ab ZfW 18 fünf. Nach einem unparierten Angriff wird der Verzauberte je nach Anzahl der Doppelgänger mit nur 50 % (13 auf 1W6) bei einem Doppelgänger), 33 % (12 auf 1W6 bei zwei Doppelgängern), 25 % (01 auf 1W20; drei Doppelgänger), 20 % (14 auf 1W20; vier Doppelgänger) bzw. 17 % (1 auf 1W6; fünf Doppelgänger) Wahrscheinlichkeit getroffen. Diese Regelung gilt auch für gegen den Magier gerichtete Fernwaffen und Zauber. Für einen Gegner ist es um (Zahl der Doppelgänger) erschwert, einen Angriff des Zauberers zu parieren. Kann sich der Verzauberte nur auf engstem Raum bewegen (in eine Raumecke gedrängt), verdoppelt sich die Trefferwahrscheinlichkeit. Soll der Verzauberte z.B. mitsamt seinem Reittier doppelt erscheinen, muss dieses einen eigenen Doppelgänger erhalten. Die Realitätsdichte beträgt ZfP*/2 + 7 Punkte.",
"kosten": [
{
"cost": 6,
"additionalFormula": "3 * Doppelgänger",
"variables": [
"Doppelgänger"
],
"repräsentation": ""
}
],
"zielobjekt": "Einzelwesen, freiwillig",
"reichweite": "selbst, Berührung",
"wirkungsdauer": "maximal ZfP* mal 2 KR (A)",
"modifikationen": "Zauberdauer, Kosten, Reichweite, Wirkungsdauer",
"varianten": [
{
"name": "Objektverdopplung",
"description": "Diese dynamische Illusion ist auch auf einen Gegenstand anwendbar, hier jedoch meist wenig sinnvoll, da der Zauber für den Kampf entwickelt wurde.",
"mod": -3,
"limit": ""
},
{
"name": "Spiegelkabinett",
"description": "Der Zaubernde erschafft in einem Radius von 7 Schritt um sich herum insgesamt ZfW/2 Doppelgänger, die stumm dastehen oder scheinbar typische Tätigkeiten des Zaubernden nachgehen und sich stets in seiner Nähe aufhalten, wenn sich dieser fortbewegt. Sie verschmelzen nicht mit dem Original, können Gegner aber durch ihre schiere Existenz verwirren. Diese Variante kostet 15 AsP, ist nur auf den Zaubernden selbst anwendbar und hat eine Wirkungsdauer von ZfP*/2 Spielrunden (A).",
"mod": -7,
"limit": ""
}
],
"reversalis": "hebt einen wirkenden DUPLICATUS auf.",
"antimagie": "kann in einer Zone des ILLUSION AUFLÖSEN nur erschwert gewirkt werden und wird von diesem Zauber beendet.",
"merkmal": [
"Illusion"
],
"komplexität": "C",
"repräsentation": {
"Magier": 6,
"Scharlatan": 6
},
"info": "Der DUPLICATUS zählt neben dem AURIS NASUS zu den klassischen Illusionen der Gildenmagie und hat seine Wirksamkeit als Verteidigungszauber oft genug bewiesen. Kein Wunder also, dass dieser Spruch an jeder Magierakademie erlernt werden kann und dass es mehr als genug Privatgelehrte gibt, die bereit sind, ihn gegen geringes Entgelt weiterzugeben."
}

View File

@ -0,0 +1,63 @@
{
"seite": "75",
"name": "EINFLUSS BANNEN",
"probe": [
"IN",
"CH",
"CH"
],
"probeMod": "+Mod",
"technik": "Die Hexe berührt die zu entzaubernde Person und konzentriert sich auf die hervorgerufenen Gefühle.",
"zauberdauer": {
"normal": "40 Aktionen"
},
"wirkung": "Der Spruch hebt Zauber mit dem Merkmal Einfluss auf. Die Erinnerung an die Gefühle (etwa für einen anderen Menschen) bleiben jedoch bestehen, so dass das Opfer auch nach Aufhebung des Beeinflussungszaubers noch in dessen Sinne reagieren kann (Meisterentscheid). Die Hexe kann sich mit dieser Formel nicht selbst aus einer Beeinflussung befreien. Die Zauberprobe ist um die vom gegnerischen Zauberer erzielten ZfP* erschwert, des Weiteren um 1/5 der AsP des zu brechenden Spruchs. Weitere Erschwernisse können aus einem PROTECTIONIS herrühren. Sorgfältige magische Analyse des wirkenden Zaubers im Vorfeld erleichtert die Probe: Pro 3 Punkte ZfP* bei einem ANALYS ARCANSSTRUKTUR ist die Probe auf EINFLUSS BANNEN um einen Punkt erleichtert.",
"kosten": [
{
"cost": 5,
"additionalFormula": "1/5 * Kosten des zu brechenden Spruchs",
"variables": [
"Kosten des zu brechenden Spruchs"
],
"repräsentation": ""
},
{
"cost": 3,
"additionalFormula": "2/15 * Kosten des zu brechenden Spruchs",
"variables": [
"Kosten des zu brechenden Spruchs"
],
"repräsentation": "Schelm"
}
],
"besondereKosten": "Um einen permanent wirkenden und mittels permanenter AsP fixierten Beeinflussungszauber aufzuheben, muss der Druide permanente AsP investieren: 1/10 der Kosten des wirkenden zu brechenden Zaubers, mindestens aber 1 AsP. Misslingt die Probe, entstehen keine permanenten Kosten.",
"zielobjekt": "Einzelwesen",
"reichweite": "Berührung",
"wirkungsdauer": "augenblicklich",
"modifikationen": "Zauberdauer, Erzwingen, Kosten, Reichweite",
"varianten": {
"Zone": {
"description": "Für den Einsatz von 15 AsP kann der Druide eine ortsfeste Zone von ZfW Schritt Radius erzeugen, in der das Wirken von Sprüchen mit dem Merkmal Einfluss um ZfP* Punkte erschwert ist und die ZfP* SR lang anhält.",
"mod": "+3",
"limit": "7"
}
},
"reversalis": "keine Wirkung",
"antimagie": "Der Beeinflussungszauber kann von einem PROTECTIONIS geschützt sein (S. 213).",
"merkmal": [
"Antimagie",
"Einfluss"
],
"komplexität": "B",
"repräsentation": {
"Druide": 6,
"Geomant": 6,
"Hexe": 6,
"Achaz": 5,
"Elf": 5,
"Magier": 4,
"Schelm": 4,
"Scharlatan": 4
},
"info": "Dieser Zauber ist bei fast allen Traditionen bekannt, die sich auf die Manipulation des Geistes verstehen. Die Hexen besonders gut mit Gefühlen umzugehen verstehen, bringen sie diesen Spruch eher zu Meisterschaft als andere."
}

View File

@ -0,0 +1,40 @@
{
"seite": "81",
"name": "ELFENSTIMME FLÖNTON",
"probe": [
"IN",
"CH",
"KO"
],
"probeMod": "",
"technik": "Der Elf stimmt das Instrument, auf dem er spielen will, spricht afeyra feya, iamafey dschis, konzentriert sich auf die Personen, die den Klang hören sollen, und beginnt zu spielen.",
"zauberdauer": {
"normal": "40 Aktionen"
},
"wirkung": "Die Klänge, die der Elf seinem Instrument entlockt, sind für all jene hörbar, für die der Zauber gedacht ist, unabhängig von ihrer Entfernung zu dem Musikanten. Auch Mauern oder andere Hindernisse halten die Klänge nicht auf. Der Empfänger hört die Musik nur in seinem Geist und weiß, dass es sich um einen Zauber handelt. Es wird jedoch nur die Musik übertragen, kein eventueller zusätzlicher Gesang oder dergleichen.",
"kosten": [
{
"cost": 6,
"additionalFormula": "1 * Spielzeit in SR",
"variables": [
"Spielzeit in SR"
],
"repräsentation": "Elfisch"
}
],
"zielobjekt": "Einzelperson, freiwillig",
"reichweite": "selbst; der Gruß reicht zumindest kontinenteweit, jedoch nicht sphärenübergreifend; als Empfänger können nur solche Personen ausgewählt werden, die ein enges Band zum Elfen haben (Freundschaftsfeld-Partner, Salasandra-Sippenangehörige, langjährige Freunde).",
"wirkungsdauer": "je nach AsP-Aufwand, höchstens ZfP*/2 SR",
"modifikationen": "keine bekannt",
"varianten": [],
"reversalis": "nicht bekannt",
"antimagie": "kann in einer Zone des VERSTÄNDIGUNG STÖREN nur erschwert gewirkt und von diesem Zauber unterbunden werden.",
"merkmal": [
"Verständigung"
],
"komplexität": "D",
"repräsentation": {
"Elf": 2
},
"info": "Vermutlich dient dieser Zauber in erster Linie dazu, Sippenmitgliedern einen Gruß zukommen zu lassen, die aus irgendeinem Grund in der Ferne weilen, oder umgekehrt einen Gruß an die zurückgebliebene Sippe zu schicken. Heute ist dieser Zauber selbst unter Elfen kaum noch verbreitet. Bei den Menschen gibt es nur noch Gerüchte über Musikinstrumente, deren Klang über tausend Meilen hinweg zu hören sei: Geschichten, die sich eventuell auf uralte firnelfische Flöten und Hörner beziehen. Alte tulamidische Märchen lassen den Schluss zu, dass wenigstens in Mhanadistan ebenfalls einmal ein ähnlicher Zauber bekannt gewesen sein muss, aber heute ist er vollständig in Vergessenheit geraten."
}

View File

@ -0,0 +1,59 @@
{
"seite": "83",
"name": "EXPOSAMI LEBENSKRAFT",
"probe": [
"KL",
"IN",
"IN"
],
"probeMod": "",
"technik": "Der Elf hält die Hände muschelförmig hinter die Ohren und konzentriert sich auf den Zauber, benennt den Ort und spricht dhao visyam y amarea.",
"zauberdauer": {
"normal": "15 Aktionen"
},
"wirkung": "Mit diesem Zauber kann ein Elf die Anwesenheit jedes wenigstens rattengroßen lebendigen Wesens erkennen (keine Pflanzen), selbst wenn das Wesen verborgen oder getarnt ist. Diese Auren nimmt der Elf als grün leuchtende Flecken wahr, deren Intensität und Durchmesser von der Größe des Wesens abhängen. Die Gestalt der georteten Wesen ist jedoch nicht erkennbar. Dickes Unterholz, Wasser und sogar Erdboden bis zu etwa einem halben Schritt Tiefe wird von dem Zauber durchdrungen, massive Barrieren aus Stein oder Eis jedoch nicht. Alles, was vorwiegend aus den Elementen Erz oder Eis besteht, widersteht dem Zauber. Die Dicke dieser Materialien, die der Zauber durchdringen kann, entspricht ZfP* Fingern um eine zehn Finger dicke Steinwand zu durchschauen, sind also wenigstens 10 ZfP* nötig. Außerdem schwächen solche Hindernisse die Intensität der entdeckten Auren, so dass sich der Elf in der Größe eines Wesens, das hinter einer massiven Steinwand lauert, durchaus irren kann. Die genaue Wirkungsweise des Zaubers ist unter Magietheoretikern umstritten, aber es deutet einiges darauf hin, dass es eine Variante des ODEM ARCANUM ist, die Sikaryan ortet jene Lebenskraft, von der alle Lebewesen erfüllt sind. Dafür spricht jedenfalls, dass Dämonen, Untote und ähnliche Wesenheiten durch diesen Zauber nicht wahrgenommen werden können, mit der bemerkenswerten Ausnahme von Vampiren, die vor kurzem getrunken haben und damit fremdes Sikaryan in sich aufgenommen haben. Die Aura von sterbenden oder schwer kranken Wesen verblasst im Vergleich zum gesunden Zustand, während Insektenstaaten als Einzelwesen wahrgenommen werden.",
"kosten": [
{
"cost": 4,
"repräsentation": ""
}
],
"zielobjekt": "Einzelperson, freiwillig",
"reichweite": "selbst; die Wirkungs-EXPOSAMI reicht ZfP* x 3 Schritt weit",
"wirkungsdauer": "entspricht der Zauberdauer",
"modifikationen": "Zauberdauer, Reichweite (Berührung oder Reichweite der Zauberwirkung)",
"varianten": [
{
"name": "Tierart",
"description": "Mit dieser Variante kann der Elf nur Wesen einer bestimmten Tierart wahrnehmen. Funktioniert nur bei Wesen, deren Aura der Elf schon einmal mittels EXPOSAMI oder ODEM ARCANUM gesehen hat.",
"mod": "+3"
},
{
"name": "Suche",
"description": "Der Hellsichtzauber reagiert nur auf ein bestimmtes Wesen, dessen Aura der Elf schon einmal wahrgenommen haben muss (BLICK AUFS WESEN, EXPOSAMI, ODEM ARCANUM ...).",
"mod": "+7",
"limit": "11"
},
{
"name": "Reinheit der Aura",
"description": "Der Elf kann erkennen, ob ein Wesen krank oder gesund ist, erregt oder ruhig, dämonisch verseucht oder natürlich drittspährig.",
"mod": "+10",
"limit": "14"
}
],
"reversalis": "nicht bekannt",
"antimagie": "Kann in einer Zone des HELLSICHT TRÜBEN nur erschwert gewirkt werden; die Wirkung eines außerhalb gesprochenen EXPOSAMI wird in der Zone um die ZfP* der antimagischen Formel reduziert, so dass dort die Reichweite des Hellsichtzaubers sinkt. Gezielter Einsatz der Antimagie gegen den Elfen beendet den EXPOSAMI.",
"merkmal": [
"Hellsicht"
],
"komplexität": "B",
"repräsentation": {
"Elf": 6,
"Achaz": 4,
"Druide": 4,
"Geomant": 4,
"Hexe": 3,
"Magier": 3
},
"info": "Eigentlich ein Zauber, der den Elfen im dichten Wald bei der Jagd helfen sollte, sind seine Anwendungsgebiete mittlerweile deutlich verbreiteter und können auch dazu dienen, eventuelle Hinterhalte rechtzeitig zu entdecken."
}

View File

@ -0,0 +1,43 @@
{
"seite": "84",
"name": "FALKENAUGE MEISTERSCHUSS",
"probe": [
"IN",
"FF",
"GE"
],
"probeMod": "",
"technik": "Der Elf streicht dem Schützen mit der Innenseite der flachen Hand über die Augen und flüstert dabei yara saladha. Der Schütze konzentriert sich dabei auf sein Ziel.",
"zauberdauer": {
"normal": "4 Aktionen"
},
"wirkung": "Der Zauber stellt ein geistiges Band zwischen dem Schützen und dem Ziel her. Dadurch wird die Fernkampf-Probe, mit der der Schütze sein Ziel zu treffen versucht, um die ZfP* erleichtert. In der Zeit zwischen Zauber und Schuss bzw. Wurf muss sich der Schütze ständig auf sein Ziel konzentrieren, sonst verfällt die Wirkung (in Zweifelsfällen kann der Meister von dem Schützen Selbstbeherrschungs-Proben verlangen).",
"kosten": [
{
"cost": 5,
"repräsentation": ""
}
],
"zielobjekt": "Einzelperson, freiwillig",
"reichweite": "selbst, Berührung",
"wirkungsdauer": "Der Schuss oder Wurf muss innerhalb von ZfW Kampfrunden erfolgen.",
"modifikationen": "Zauberdauer, Reichweite, Wirkungsdauer",
"varianten": [
{
"name": "Das bleibende Band",
"description": "Die Erleichterung gilt für alle Schüsse bzw. Würfe, die der Schütze innerhalb der Wirkungsdauer auf ein und dasselbe Ziel abgibt.",
"mod": "+5"
}
],
"reversalis": "Für den nächsten Schuss oder Wurf innerhalb der Zauberdauer erleidet der Schütze eine Erschwernis um ZfP* Punkte.",
"antimagie": "Kann in einer Zone des EIGENSCHAFT WIEDERHERSTELLEN nur erschwert gewirkt und von diesem Zauber beendet werden.",
"merkmal": [
"Eigenschaften"
],
"komplexität": "B",
"repräsentation": {
"Elf": 6,
"Magier": 2
},
"info": "Ein guter Schütze errichtet immer eine Art geistige Verbindung von sich bis zu seinem Ziel, dem die Waffe dann folgt, um möglichst genau zu treffen. Da ist es kein Wunder, dass die Elfen gelernt haben, dieses Band mittels ihrer Magie noch zu verstärken und man kann fast davon ausgehen, dass vielen Elfen noch nicht einmal bewusst ist, dass sie hierbei Magie anwenden. Sicherlich handelt es sich bei diesem Zauber nicht um einen Kampfzauber, sondern eine Unterstützung ihrer Jagdfertigkeiten."
}

View File

@ -0,0 +1,105 @@
{
"seite": "87",
"name": "FLIM FLAM FUNKEL",
"probe": [
"KL",
"KL",
"FF"
],
"probeMod": "",
"technik": "Der Elf besinnt sich auf das Licht, spricht feya feamia tungra und schnippt dabei mit den Fingern.",
"zauberdauer": {
"normal": "2 Aktionen"
},
"wirkung": "Durch diesen Zauber entsteht eine bläulich-weiß strahlende, stationäre Lichtkugel, die zur Beleuchtung dienen kann. Die maximale Leuchtkraft bemisst sich nach den ZfP*: Glühwürmchen (1), Kerze (2 bis 3), Fackel (4 bis 6), Lagerfeuer (7 bis 9), bis hin zu sonnenhell (ab 15 ZfP*). Der Leuchtradius erreicht dadurch etwa ZfP* Schritt, wobei die Helligkeit nach außen hin natürlich immer weiter abnimmt. Das erschaffene Licht ist kalt und erzeugt keinerlei Verbrennungen. Es kann mit den Händen oder abschirmenden Gegenständen verhüllt werden, und es leuchtet auch bei Wind oder einer Berührung weiter.",
"kosten": [
{
"cost": 1,
"additionalFormula": "1 * Helligkeitsstufe",
"variables": [
"Helligkeitsstufe"
],
"repräsentation": ""
},
{
"cost": 1,
"additionalFormula": "1 * Helligkeitsstufe",
"variables": [
"Helligkeitsstufe"
],
"repräsentation": "Schelm"
}
],
"zielobjekt": "Stelle im Raum oder auf einer Oberfläche (Zone)",
"reichweite": "Die Lichtkugel entsteht in maximal ZfW Schritt Entfernung.",
"wirkungsdauer": "nach AsP-Aufwand; Wirkungsdauer muss vorher festgelegt werden.",
"modifikationen": "Zauberdauer, Kosten, Reichweite (Entstehungsort)",
"varianten": [
{
"name": "Andere Farbe",
"description": "Beliebt sind rot, violett, gelb, seltener grün, orange und blau; nur sichtbare Farben möglich.",
"mod": -2
},
{
"name": "Pulsierendes Licht",
"description": "Lichtkugel wird in regelmäßigem Rhythmus heller und dunkler.",
"mod": -2
},
{
"name": "Lichtkegel",
"description": "Das Licht kann gleichsam wie ein Strahler in eine bestimmte Richtung (Kegel von etwa 45 Grad) gelenkt werden und diesen Bereich deutlich heller beleuchten.",
"mod": -3
},
{
"name": "Variable Helligkeit",
"description": "Die durch die ZfP* bestimmte Helligkeit ist die maximale Helligkeit der Lichtkugel; sie kann jedoch nach Belieben des Elfen abgedunkelt werden um sie allerdings wieder heller (bis zur Maximalhelligkeit) erstrahlen zu lassen, ist der Einsatz eines AsP nötig.",
"mod": -3
},
{
"name": "Bewegliche Lichtkugel",
"description": "Das Licht schwebt in unmittelbarer Nähe des Zaubernden (nach Belieben über Kopf, Gesicht, Schulter, Hand oder Füßen) und kann so mitgeführt werden.",
"mod": -3
},
{
"name": "Irrwisch",
"description": "Die Kugel kann bis zu ZfW mal 2 Schritt vom Zaubernden fortgeschickt und ferngelenkt werden.",
"mod": -5,
"limit": "nicht in geodischer und hexischer Repräsentation"
},
{
"name": "Lichtblitz",
"description": "Der Zaubernde kann die Kugel für einmalig 4 AsP in einem hellen Blitz explodieren lassen, der weithin sichtbar ist und Beobachter in einer Distanz von weniger als 7 Schritt für ZfP* Aktionen zu blenden vermag (AT/PA jeweils ZfP*/2, Fernkampf-, Talent- und Zauberproben jeweils um ZfP* erschwert, INI ZfP*). Auslösen des Lichtblitzes erfordert eine Aktion.",
"mod": -7,
"limit": "nicht in eschissischer und druidischer Repräsentation"
},
{
"name": "LeuchtTurm",
"description": "Im Endeffekt eine Kombination mehrerer Modifikationen. Eine pulsierende Lichtkugel von mindestens Helligkeit 10 (ZfP*≥9) steigt in eine Höhe von ZfW mal 10 Schritt und verbleibt dort bis zum Ende der Wirkungsdauer von 1 SR.",
"mod": -7,
"limit": "nur in gildenmagischer Repräsentation"
},
{
"name": "Immerlicht",
"description": "Die stationäre Lichtkugel erhält für einen permanenten AsP eine stabile Matrix und leuchtet dauerhaft. Jeweils nach einem Jahr wird mit 1W20 gewürfelt: Bei einer 20 wird das Licht um eine Stufe (1 ZfP*) dunkler. Nicht kombinierbar mit anderen Modifikationen (außer Andere Farbe).",
"mod": -9,
"limit": "nur in elfischer und kristallomantischer Repräsentation"
}
],
"reversalis": "Der Zauber schafft eine schwarze Kugel, die die Umgebung dunkler werden lässt, je näher man ihr kommt (praktisch eine 'leuchtende Finsternis', anstatt einer vollständigen Dunkelheit, wie sie der gleichnamige Zauber erschafft).",
"antimagie": "Kann nur erschwert in einer Zone des VERÄNDERUNG AUFHEBEN gewirkt und von diesem Zauber beendet werden.",
"merkmal": [
"Umwelt"
],
"komplexität": "A",
"repräsentation": {
"Achaz": 7,
"Elf": 7,
"Magier": 7,
"Druide": 6,
"Hexe": 6,
"Geomant": 5,
"Schelm": 4,
"Scharlatan": 4
},
"info": "Obwohl der Spruch bis zur Entdeckung der Firnelfenmagie menschlichen Zauberkundigen verschlossen war, gilt die Erschaffung einer magischen Lichtquelle heutzutage als die Zauberei überhaupt. (Es soll jedoch Zauberkundige geben, die nicht einmal diese einfachste Übung beherrschen.)"
}

View File

@ -0,0 +1,58 @@
{
"seite": "89",
"name": "FORAMEN FORAMINOR",
"probe": [
"KL",
"KL",
"FF"
],
"probeMod": true,
"technik": "Der Magier berührt das Schloss, den Mechanismus oder die Tür dreimal mit der flachen Hand und spricht leise die Formel.",
"zauberdauer": {
"normal": "5 Aktionen"
},
"wirkung": "Der Zauber öffnet mechanische Schlösser, Riegel etc. an Türen, Truhen und ähnlichem, ohne dass der Zauberer den genauen Schließmechanismus kennen muss. Die Zuschläge auf die Zauberprobe sind von der Komplexität des Schlosses abhängig. Gewöhnliche, wenn auch komplizierte Schlösser stellen für diesen Zauber kein Hindernis dar. Sollte der Schließmechanismus jedoch mit einer Falle gekoppelt sein, so können Meister einen Zuschlag für die FORAMEN-Probe verlangen, damit die Falle gesichert bleibt, während das Schloss sich öffnet. Weitere Methoden, die FORAMEN zu überlisten, ist die Anbringung mehrfacher, deutlich getrennter Schließmechanismen, die jeweils separate FORAMEN erfordern.",
"kosten": [
{
"cost": 2,
"max": 12,
"repräsentation": ""
},
{
"cost": 1,
"max": 8,
"repräsentation": "Schelm"
}
],
"zielobjekt": "Einzelobjekt",
"reichweite": "Berührung",
"wirkungsdauer": "augenblicklich; das Schloss bleibt so lange offen, bis es wieder zugesperrt wird.",
"modifikationen": "Zauberdauer, Kosten, Reichweite",
"varianten": [
{
"name": "Mehrfachschlüssel",
"description": "Für einen Zuschlag von sieben Punkten pro Schloss kann ein automatisch wieder verschließendes Schloss offen gehalten werden, während man mit einem neuen FORAMEN das nächste Schloss (einer Mehrfachkombination) öffnet. Kann auch verwendet werden, um eine Falle zu blockieren, während man ein Schloss öffnet.",
"mod": -7,
"limit": ""
},
{
"name": "Contra-Claudiubus",
"description": "Mit dieser Variante kann ein auf einem Verschluss wirkender CLAUDIUBUS neutralisiert und diese daraufhin gewöhnlich geöffnet werden (mittels Schlösser Knacken, Kraftanrest, Waffengewalt oder einem weiteren FORAMEN). Hierfür muss der Magier mindestens so viele ZfP* übrig behalten wie der Wirker des CLAUDIUBUS; die AsP-Kosten betragen so viele Punkte, wie für den CLAUDIUBUS aufgewendet wurden.",
"mod": -3,
"limit": "7"
}
],
"reversalis": "Der umgedrehte Zauber verschließt Schlösser und andere Mechanismen; diese können aber ganz normal mit einem Schlüssel wieder geöffnet werden.",
"antimagie": "Sowohl Zonen des BEWEGUNG STÖREN wie auch des HELLSICHT TRÜBEN erschweren das Wirken des Zaubers.",
"merkmal": [
"Hellsicht",
"Telekinese"
],
"komplexität": "C",
"repräsentation": {
"Magier": 6,
"Scharlatan": 5,
"Schelm": 4
},
"info": "Diese Formel gilt ebenfalls als klassische Magierformel, die fast überall gelehrt wird, aber Wesen jedoch abgelehnt anderen Zauberkundigen verschlossen bleibt. Man sieht mit welcher Leichtigkeit ein Schelm diesen Zauber erlernen kann. Man kann diese Formel am ehesten verstehen, wenn man sie sich als eine Verbindung aus einem intuitiven PENETRIZZEL und einem MOTORICUS vorstellt, wobei der Hellsichtzauber dafür sorgt, dass der Magier den Mechanismus des Schlosses durchschaut, während die Telekinese das Öffnen besorgt."
}

View File

@ -0,0 +1,56 @@
{
"seite": "91",
"name": "FULMINICTUS DONNERKEIL",
"probe": [
"IN",
"GE",
"KO"
],
"technik": "Der Elf deutet mit der linken Faust auf das Ziel, während er fal minizia daoka ausruft.",
"zauberdauer": {
"normal": "2 Aktionen"
},
"wirkung": "Der Zaubernde erzeugt eine gezielte, unsichtbare Welle magischen Schadens, die jede gewöhnliche Rüstung glatt durchdringt. Der Elf würfelt 2W6 und addiert die ZfP*. Das Gesamtergebnis ist gleich der Anzahl der Schadenspunkte, die direkt von der Lebensenergie des Getroffenen abgezogen werden. Der Schaden äußert sich bei Lebewesen meist als Summe kleinerer innerer Verletzungen und erzeugt keine (regeltechnischen) Wunden. Experten können den Schaden bei verschiedenen Arten von Verletzungen gesondert nachhalten. Der Zauber eignet sich hervorragend als letzter Ausweg, da er kaum Vorbereitung und Konzentration benötigt.",
"kosten": [
{
"cost": 0,
"additionalFormula": "1 * SP",
"variables": [
"SP"
],
"repräsentation": ""
}
],
"zielobjekt": "Einzelwesen",
"reichweite": "7 Schritt",
"wirkungsdauer": "augenblicklich",
"modifikationen": "Zauberdauer, Reichweite",
"varianten": [
{
"name": "Welle des Schmerzes",
"description": "Eine Schadenswelle breitet sich vom Elfen ungezielt in alle Richtungen aus und zieht alles bis in 3 Schritt Entfernung in Mitleidenschaft. Für je 3 AsP richtet der Zauber 1W6 SP(A) bei allen umstehenden Lebewesen an.",
"mod": -3,
"limit": "7"
},
{
"name": "Welle der Reinigung",
"description": "Vom Magier geht eine Welle der Kraft aus, die für alle Lebewesen mit nur 1 LeP oder weniger tödlich ist sprich, sie fällt alles Ungeziefer in einem Herbergszimmer tot von der Wand. Der Radius beträgt 3 Schritt.",
"mod": -3,
"limit": "7",
"kosten": "1W6 + 2 AsP"
}
],
"reversalis": "Ein teurer Fernheilzauber, der der Zielperson sofort 2W6+ZfP* LeP zurückgibt und vor allem als letzte Rettung vor dem Tod verwendet wird.",
"antimagie": "Gegen den magischen Schaden helfen weder Rüstungen noch der ARMATRUTZ. Dämonen können ihre MR als magischen Schild verwenden, wohl aber der GARDIANUM und der INVERSCAN. In einer Zone unter dem Einfluss des SCHADENSZAUBER BANNEN kann die Formel nur erschwert gewirkt werden.",
"merkmal": [
"Schaden",
"Kraft"
],
"komplexität": "C",
"repräsentation": {
"Elf": 7,
"Magier": 5,
"Hexe": 2
},
"info": "Diese Kraftentladung gilt als astrale Ausformung des Spruchs ZORN DER ELEMENTE. Obwohl es genügend andere Kampfzauber gibt, die sich besser dosieren lassen, hat der allgemein bekannte, wenn auch fast ausschließlich von Elfen und Magiern verwendete FULMINICTUS nichts von seiner Beliebtheit verloren."
}

View File

@ -0,0 +1,63 @@
{
"seite": "92",
"name": "GARDIANUM ZAUBERSCHILD",
"probe": [
"KL",
"IN",
"KO"
],
"probeMod": "",
"technik": "Der Magier hebt den Zauberstab über den Kopf und lässt ihn einmal waagerecht rotieren; zauberkundige Helden ohne Zauberstab zeichnen mit beiden Händen einen großen Kreis über sich in die Luft.",
"zauberdauer": {
"normal": "2 Aktionen"
},
"wirkung": "Um den Magier entsteht eine unsichtbare Schutzkuppel in Form einer Halbkugel von 3 Schritt Radius, die auch von Gefährten genutzt werden kann. Der Schild ist orientiert: Schadenszauber aus der Kuppel heraus sind möglich, von außen eindringende direkte, magische Trefferpunkte werden jedoch absorbiert (insbesondere Zauber des Merkmals Schaden oder von Dämonen/Elementaren/Geistern erzeugte Trefferpunkte). Nichtmagische Treffer, Flammenwoge, Attacken eines Flammenschwertes u.Ä. werden nicht abgefangen. Die Anzahl der absorbierbaren Trefferpunkte entspricht den vom Magier investierten AsP bis zum doppelten ZfP*. Sobald der Schild diese Trefferpunktsumme aufgenommen hat spätestens nach 1 Spielrunde endet er. Richtet ein Schadenszauber keine direkten TP an (sondern z.B. eine Wunde), so verliert der Schild AsP in Höhe der Kosten des Schadenszaubers. Der Schild bewegt sich stets mit dem Magier.",
"kosten": [
{
"cost": 3,
"additionalFormula": "1 * Schildstärke",
"variables": [
"Schildstärke"
],
"repräsentation": ""
}
],
"zielobjekt": "Zone",
"reichweite": "3 Schritt Radius um den Magier herum",
"wirkungsdauer": "bis die aufgewendeten AsP aufgebraucht sind, maximal 1 Spielrunde",
"modifikationen": "Zauberdauer, Reichweite (Größe der Kuppel), Wirkungsdauer",
"varianten": [
{
"name": "Schild gegen Dämonen",
"description": "Der Schild verhindert, dass Dämonen die Schutzkuppel durchdringen oder sich in ihr aufhalten können. Ebenfalls werden Angriffe mittels dämonischer Materie (z.B. Pandämonium) abgewehrt; gezielte dämonische Angriffe gegen den Schild verbrauchen einen Punkt Schildstärke pro angerichteter TP.",
"mod": -3,
"limit": "7"
},
{
"name": "Schild gegen Zauber",
"description": "Verwandlungs-, Beherrschungs-, Einfluss- und Eigenschaftenzauber, die den Geschützten direkt betreffen würden, werden am Schild abgeschwächt oder abgewehrt. Für jeden AsP, der auf diese Art neutralisiert wird, reduziert sich die Schildstärke entsprechend.",
"mod": -3,
"limit": "7"
},
{
"name": "Persönlicher Schild",
"description": "Statt einer Kuppel legt sich der Schutz als eng anliegende zweite Haut um Magier und seine getragene Ausrüstung. Diese Variante gewährt einen Schutzvorrat in Höhe von 3×ZfP* plus eingesetzte AsP und bewegt sich immer mit dem Träger.",
"mod": -5,
"limit": "11"
}
],
"reversalis": "keine Wirkung",
"antimagie": "Ein mit PROTECTIONS geschützter Zauber durchdringt den GARDIANUM, wenn PROTECTIONS-AsP + ZfP* größer sind als die ZfP* des Schildes; andernfalls wird der Effekt wie ein verzaubertes Geschoß abgewehrt. Die AsP des PROTECTIONS reduzieren die Schildstärke nicht.",
"merkmal": [
"Antimagie",
"Kraft",
"Metamagie"
],
"komplexität": "D",
"repräsentation": {
"Magier": 6,
"Achaz": 2,
"Geomant": 2
},
"info": "Diese kompakte Fassung bündelt die an Akademien gelehrten Varianten; sie ist nahezu überall bekannt und gilt als Standard-Schutzformel für magisch Reisende."
}

View File

@ -0,0 +1,76 @@
{
"seite": "94",
"name": "GEDANKENBILDER ELFENRUF",
"probe": [
"KL",
"IN",
"CH"
],
"probeMod": "",
"technik": "Der Elf legt eine Hand an seine Stirn und konzentriert sich mit geschlossenem Augen auf den Zauber feya, ama visyaray und sendet eine Botschaft.",
"zauberdauer": {
"normal": "5 Aktionen"
},
"wirkung": "Der Elf sendet eine Gedankenbotschaft aus, die jeder innerhalb der Reichweite des Zaubers empfangen kann, der diesen Zauber ebenfalls beherrscht. Die Botschaft selbst ist dabei nicht von einer Sprache abhängig: Ein Empfänger versteht den Inhalt auch dann, wenn er die Sprache des Elfen nicht sprechen kann. Es ist jedoch möglich, Botschaften in Rätsel oder Gleichnisse zu verpacken, so dass der Empfänger zwar das Rätsel versteht, die Lösung jedoch nicht unbedingt kennt. Wer die Botschaft aus irgendeinem Grund nicht empfangen will, kann sich dagegen wehren diese Abwehr gelingt immer.",
"kosten": [
{
"cost": 0,
"additionalFormula": "4 * 5 KR",
"variables": ["5 KR"],
"repräsentation": ""
}
],
"zielobjekt": "Einzelperson, freiwillig",
"reichweite": "selbst; die Botschaft wird ZfW × 100 Schritt weit gesendet",
"wirkungsdauer": "höchstens ZfP* × 5 Kampfrunden (A)",
"modifikationen": "Zauberdauer, Wirkungsdauer, Reichweite (gestaffelt in den Stufen ×3 / ×7 / ×12 / ×21 / ×49; jeweils verrechnet mit dem ZfW × 100 Schritt bzw. den ZfW × 10 Meilen beim Kreis der Eingeweihten)",
"varianten": [
{
"name": "Bestimmter Empfänger",
"description": "Ein bestimmter Empfänger im Blickfeld und in Reichweite des Zaubers (+5) oder ein Empfänger außerhalb des Blickfeldes, aber innerhalb der Reichweite (+10). Der Zaubernde fixiert ausschließlich den gewählten Empfänger.",
"mod": -5,
"limit": ""
},
{
"name": "Kreis der Eingeweihten",
"description": "Hier sendet der Elf die Botschaft nur an seine engsten Vertrauten, zumeist Sippenmitglieder oder Verwandte. Reichweite ZfW × 10 Meilen. Zusätzlich kostet auch jedem Empfänger 1 AsP.",
"mod": -5,
"limit": "nur in der elfischen Repräsentation"
},
{
"name": "Sinneseindrücke",
"description": "Der Elf sendet keine Botschaft, sondern er gibt die Sinneseindrücke wieder, die er gerade hat: was er hört, sieht, riecht, schmeckt und/oder fühlt.",
"mod": -3,
"limit": "7"
},
{
"name": "Kontakt",
"description": "Der Elf kann einen Wesen, das er berührt und das den Zauber nicht unbedingt kennen muss, Gedanken übermitteln.",
"mod": -3,
"limit": "nur elfische Repräsentation"
},
{
"name": "Illusionen",
"description": "Der Magier sendet nicht seine aktuellen Sinneseindrücke, sondern gedachte erdachte Bilder. Er muss eine Selbstbeherrschungs-Probe bestehen, sonst vermischen sich gedachte mit realen Eindrücken.",
"mod": -7,
"limit": "11; nur gildenmagische Repräsentationen"
},
{
"name": "Erzwungene Botschaft",
"description": "Eine Botschaft kann auch gegen den Willen des Empfängers aufgezwungen werden. Dieser muss eine Selbstbeherrschungs-Probe ablegen. Solche Botschaften halten maximal 5 Kampfrunden lang.",
"mod": -3,
"limit": "11; nur gildenmagische Repräsentationen"
}
],
"reversalis": "nicht bekannt",
"antimagie": "kann in einer Zone des VERSTÄNDIGUNG STÖREN nur erschwert gewirkt werden; gezielter Einsatz dieser Antimagie kann den GEDANKENBILDER beenden.",
"merkmal": [
"Verständigung"
],
"komplexität": "B",
"repräsentation": {
"Elf": 7,
"Magier": 3
},
"info": "Es wundert nicht, dass fast jeder Elf diesen Spruch kennt. An Menschen wird er jedoch nur sehr selten weitergegeben."
}

View File

@ -0,0 +1,51 @@
{
"seite": "121",
"name": "HORRIPHOBUS SCHRECKGESTALT",
"probe": [
"MU",
"IN",
"CH"
],
"probeMod": "+MR",
"technik": "Der Magier droht mit der Faust in Richtung seines Opfers und brüllt die Formel.",
"zauberdauer": {
"normal": "3 Aktionen"
},
"wirkung": "Der Magier erscheint seinem Opfer als eine bedrohliche Gestalt und schüchtert es abhängig von den ZfP* ein: \n\n1 ZfP*: Das Opfer hat gehörigen Respekt vor dem Zaubernden und hält ihn für einen überlegenen Gegner, dem es sich wenn überhaupt nur vorsichtig nähert. Attacken gegen den Zaubernden sind nur nach einer vorherigen MU-Probe möglich, die um die ZfP* erschwert ist. \n4 ZfP*: Das Opfer fürchtet den Zaubernden und wird wenn möglich zurückweichen, d.h. im Kampf ein Rückzugsgefecht führen (MU, AT, INI-Basis = 1W6, wenn eine MU-Probe, erschwert um die ZfP*, misslingt; Opfer setzt keine Aktionsmanöver mit Zuschlägen von +4 oder höher ein). \n7 ZfP*: Das Opfer sieht in dem Zaubernden einen Furcht erregenden, zu allem entschlossenen und übermächtigen Feind. Stark demoralisierend, wird es sich (nach Natur und örtlichen Gegebenheiten) entweder ängstlich in einer Ecke zusammenkauern oder zumindest so weit fliehen, bis der Zaubernde außer Sichtweite ist. Es muss eine MU-Probe ablegen, die um die ZfP* erschwert ist. Misslingt diese, erleidet das Opfer Abzüge von je 1W6 Punkten auf alle Werte MU, KL, CH, FF, AT, PA, FK und INI-Basis. Dieser Malus baut sich fern der Gegenwart des Zaubernden mit einem Punkt pro Spielrunde wieder ab. \n10+ ZfP*: Dem Opfer erscheint der Zaubernde wie aus einem Alptraum entsprungen; es verfällt in heillose Panik und sucht schreiend das Weite. MU-Probe und Abzüge auf Eigenschaften wie oben. \n\nDieser Zauber wirkt gegen alle Arten von denkenden und fühlenden Lebewesen sowie gegen einige Geisterwesen (Letzteres ist Meisterentscheid). Schleimgetier oder Insekten lassen sich von der Formel jedoch nicht beeindrucken, genauso wenig wie Elementare, Untote und Dämonen. Beim HORRIPHOBUS ist die MR des Opfers um die Hälfte der stärksten Angst (also derjenigen Schlechten Eigenschaft, in denen der Begriff 'Angst' oder 'Phobie' vorkommt, einschließlich Aberglaube) reduziert. Tiere, bei denen kein MU angegeben ist, verwenden stattdessen ihren GW für die Gegen-Proben.",
"kosten": [
{
"cost": 7,
"repräsentation": ""
},
{
"text": "2W6 AsP bzw. die Hälfte dieser Kosten als LeP",
"repräsentation": "Borbardianisch"
}
],
"zielobjekt": "Einzelwesen",
"reichweite": "7 Schritt",
"wirkungsdauer": "ZfP* /2 SR",
"modifikationen": "Zauberdauer, Erzwingen, Kosten, Zielobjekt (mehrere), Reichweite, Wirkungsdauer",
"varianten": [
{
"name": "gegen mehrere Personen",
"description": "kostet der Zauber nur 6 AsP pro Opfer.",
"mod": 0,
"limit": ""
}
],
"reversalis": "bringt das Opfer dazu, sich für die Wirkungsdauer in den Zauber zu verlieben.",
"antimagie": "kann in einer Zone des EINFLUSS BANNEN nur erschwert gewirkt und mit dessen Antimagie-Spruch beendet werden. ANGSTE LINDERN kann die Wirkung des HORRIPHOBUS aufheben oder zumindest abschwächen.",
"merkmal": [
"Einfluss (Bor: Einfluss, Dämonisch (allgemein))"
],
"komplexität": "C",
"repräsentation": {
"Magier": 6,
"Borbardianisch": 4,
"Druide": 3,
"Hexe": 2,
"Scharlatan": 1
},
"info": "Hierbei handelt es sich um einen der 'klassischsten' Magierzauber überhaupt, der vor allem zur Kampfvermeidung als höchst effizient angesehen wird. Die Ähnlichkeit zum BÖSEN BLICK hat es schon vor geraumer Zeit ermöglicht, dass auch Druiden eine eigenständige Repräsentation entwickeln haben, während die borbardianische Variante erst aus jüngster Zeit stammt, allerdings schnell Verbreitung gefunden hat. In der druidischen Variante ist er auch Hexen bekannt, und Gerüchten zufolge haben auch einige Scharlatane diesen Spruch gemeistert."
}

View File

@ -0,0 +1,73 @@
{
"seite": "122",
"name": "IGNIFAXIUS FLAMMENSTRAHL",
"probe": [
"KL",
"FF",
"KO"
],
"probeMod": "",
"technik": "Der Magier hebt die rechte Hand zur linken Schulter, konzentriert sich auf die Formel und deutet dann ruckartig mit Zeige- und Mittelfinger auf sein Opfer.",
"zauberdauer": {
"normal": "4 Aktionen"
},
"wirkung": "Aus den Fingern des Zaubernden schießt ein Strahl elementarer Gewalten: eine Lanze aus Feuer und Licht. Der Spieler muss vor der Zauberprobe ansagen, wie viele Würfel er für den IGNIFAXIUS einsetzen will: Dem Magier stehen maximal ZfW Würfel zur Verfügung. Dann würfelt er und addiert alle Punkte zu einer Treffpunktsumme. (Dadurch kann es unter Umständen passieren, dass mehr TP erwürfelt werden, als dem Magier AsP zur Verfügung stehen in diesem Fall misslingt der Zauber mit den üblichen Folgen.) Ein Opfer, das den Zauberverlauf beobachtet und erkennt, kann versuchen, ihm auszuweichen; dazu ist ein um 5 Punkte erschwertes Ausweichen-Manöver (Wege des Schwerts 66) erforderlich: Für jeden Punkt, den das Opfer besser ausweicht, kann es einen Würfel von der Schadenswirkung abziehen. Der Zauber verursacht Trefferpunkte, die noch um Rüstungsschutz des Opfers vermindert werden, jedoch wird auch die Rüstung angegriffen: Für je 10 Trefferpunkte verliert das Opfer einen Punkt seines Rüstungsschutzes (nicht bei natürlicher oder magischer Rüstung). Durch die direkten Schadenspunkte können dem Opfer eine oder mehrere Wunden entstehen.",
"kosten": [
{
"cost": 0,
"additionalFormula": "1/2 * TP",
"variables": ["TP"],
"repräsentation": ""
}
],
"zielobjekt": "Einzelwesen (kann auch auf ein einzelnes Objekt gezielt werden, um Struktursschaden anzurichten).",
"reichweite": "21 Schritt",
"wirkungsdauer": "augenblicklich",
"modifikationen": "Zauberdauer, Reichweite (Bei Reichweite: Horizont gelten alle Modifikationen aus WdZ 21 zusätzlich zu denen der folgenden Varianten, die Zielerfassungs-Probe ist gleichzeitig die Zauberprobe)",
"varianten": [
{
"name": "Kegel",
"description": "Es entsteht ein Flammenkegel mit 2 Schritt Basisbreite, der große Oberflächenwirkung hat: Er richtet zwar nur 1W61 TP pro eingesetztem Würfel an, dafür sinkt der RS des Opfers für je 5 Punkte um 1. Pro Punkt zusätzlichen Erschwernis kann die Basisbreite des Kegels um 1 Schritt vergrößert werden. Wird der Kegel auf mehrere Personen gleichzeitig gezielt, so erhält jedes Opfer nur einen entsprechenden Anteil der ausgewürfelten TP (hierbei immer abrunden). Es ist um weitere 5 Punkte (insgesamt also 10 Punkte) erschwert, einem kegelförmigen IGNIFAXIUS auszuweichen.",
"mod": -2,
"limit": "11"
},
{
"name": "Enger Strahl",
"description": "Es entsteht nur ein fingerdicker Flammenstrahl, der keinen Rüstungsschutzandurchdringt, dafür aber pro KO/22 SP eine Wunde anrichtet. Die Trefferzone hierfür wird per Zufall bestimmt: 1920 Kopf, 1518 Brust, 914 Arme (ungerade für links, gerade für rechts), 78 Bauch, 16 Beine (ungerade für links, gerade für rechts). Um eine bestimmte Körperzone anzuvisieren, ist die Probe nochmals erschwert: +5 für Kopf oder Arm, +4 für Bein, +3 für Brust/Rücken oder Bauch.",
"mod": -3,
"limit": "11"
},
{
"name": "Mehrere Ziele",
"description": "Strahlen schießen aus beliebig vielen Fingern des Magiers: Die erwürfelten TP können auf bis zu 5 Opfer gleichmäßig verteilt werden.",
"mod": -5,
"limit": "7"
},
{
"name": "Doppelschuss",
"description": "Der Magier kann mit einer Probe und unter Aufwendung einer Zauberdauer gleichzeitig zwei Zauber auf ein und dasselbe Ziel wirken; für jeden der Zauber stehen ihm maximal ZfW5 Würfel zur Verfügung (Zauberfertigkeit mindert die Schwierigkeit der Modifikation); dazu verwendet er beide Hände. Ab einer Schwelle von ZfW 11 misslingt der Zauber nicht automatisch, wenn die AsP nicht ausreichen, weil ein zu hohes Resultat erwürfelt wurde (≤30). Stattdessen verbraucht der Spruch dann nur genau die noch vorhandenen AsP und wandelt sie in würkende TP um.",
"mod": -5,
"limit": "11"
},
{
"name": "Elementare Varianten",
"description": "FRIGIFAXIUS, ARCHOFAXIUS, AQUAFAXIUS, AQUAFAXIUS, HUMOFAXIUS erfordern separate Zauber, deren Repräsentationen sehr selten (Geo, Mag, Ach, Dru (Mag) je 2) zu finden sind. Auch diese Zauber verfügen jeweils über zerstörerische Aspekte eines reinen Elements herbei und haben entsprechend ähnliche Auswirkungen orientieren sich da an den Elementaren Sekundäreffekten unten auf Seite 123.",
"mod": 0,
"limit": ""
}
],
"reversalis": "Die elementare Wirkung wird zu einem AQUAFAXIUS (mit gleicher Schadenswirkung wie der IGNIFAXIUS) umgekehrt, der die Elementaren Sekundäreffekte (siehe unten) des Wassers hervorruft.",
"antimagie": "kann nicht in eine Zone eindringen, die mit SCHADENSZAUBER BANNEN oder FEUERBANN errichtet wurde; kann mit diesen beiden Zaubern beim Schaffen des Zaubers gestört werden; wird vom GARDIANUM aufgefangen und vom INVERCANO reflektiert; LEIB DES FEUERS reduziert den entstehenden Schaden.",
"merkmal": [
"Schaden",
"Elementar (Feuer)"
],
"komplexität": "C",
"repräsentation": {
"Magier": 5,
"Achaz": 3,
"Geomant": 2,
"Druide": 2
},
"info": "Bei sehr vielen Gildenmagiern bekannte Formel, die als exemplarischer Kampfzauber gilt; dabei Grundlage für elementare Transitionen. Speziell vom elementaren Ansatz her ist sie auch den Achaz-Kristallomanten zugänglich. Gerüchten zufolge kennen auch einige wenige Druiden diesen Spruch in gildenmagischer Repräsentation."
}

View File

@ -0,0 +1,57 @@
{
"seite": "138",
"name": "KLARUM PURUM",
"probe": [
"KL",
"KL",
"CH"
],
"probeMod": "(+Stufe des Giftes)",
"technik": "Der Magier legt dem Vergifteten die Hand aufs Herz und spricht die Formel.",
"zauberdauer": {
"normal": "7 Aktionen"
},
"wirkung": "Sämtliche giftigen Stoffe in der Blutbahn des Patienten werden aufgelöst, so dass der Vergiftungsprozess zum Stillstand kommt. Bis dahin eingetretener LE-Verlust wird nicht ausgeglichen, jedoch werden alle anderen Wirkungen (z.B. Lähmungen, Eigenschaftenabsenkungen etc.) sofort aufgehoben. Mit dem Spruch kann sich der Zaubernde auch selbst heilen. Die Wirkung setzt erst zum Abschluss der Zauberdauer ein, so dass es noch immer eine Angelegenheit von Augenblicken bleibt, ein Tulmadron- oder Purpurblick-Opfer zu retten. Der KLARUM PURUM beendet auch Rauschzustände, die durch entsprechende Mittel entstehen; Alkohol hat eine Giftstufe von 2 bis 5, je nach Konzentration und Menge.",
"kosten": [
{
"cost": 0,
"additionalFormula": "1 * Stufe des Giftes",
"variables": ["Stufe des Giftes"],
"repräsentation": ""
}
],
"zielobjekt": "Einzelwesen, freiwillig",
"reichweite": "selbst, Berührung",
"wirkungsdauer": "augenblicklich",
"modifikationen": "Zauberdauer, Erzwingen, Kosten, Reichweite",
"varianten": [
{
"name": "Schutz vor Vergiftung",
"description": "Für 1 AsP pro Stufe des Giftes ist die verzauberte Person für ZfP* Stunden resistent gegen alle Gifte bis zu dieser Stufe (heißt: die KO-Probe zur Abwehr des Giftes ist um 7 Punkte erleichtert).",
"mod": -5,
"limit": "11"
},
{
"name": "Schutz vor Vergiftung",
"description": "Für 2 AsP pro Giftstufe ist sie immun gegen Gifte bis zu dieser Stufe (KO-Probe um 15 Punkte erleichtert).",
"mod": -7,
"limit": "11"
}
],
"reversalis": "Der Zauber erzeugt beim Opfer eine 'Quasi-Vergiftung': Die Stufe des Giftes entspricht den eingesetzten AsP. Orientieren Sie sich zur genauen Wirkungsbestimmung an bekannten Giften ähnlicher Stufe oder an den allgemeinen Richtlinien in Wege des Schwerts.",
"antimagie": "Während der Zauber gewirkt wird, kann er mit HEILKRAFT BANNEN und VERWANDLUNG BEENDEN zum Scheitern gebracht werden; er wirkt nicht in entsprechenden antimagischen Zonen.",
"merkmal": [
"Form",
"Heilung"
],
"komplexität": "D",
"repräsentation": {
"Magier": 6,
"Achaz": 3,
"Druide": 3,
"Elf": 3,
"Geomant": 3,
"Hexe": 3
},
"info": "Diese alte Formel ist weit verbreitet und gehört zum Rüstzeug jedes Hofmagiers. Gerade deswegen kursieren aber auch etliche falsche Thesenmilderlegungen des KLARUM PURUM, mit denen viele Hofmagier versuchten, konkurrierende Magier zu verwirren und am Erlernen der Formel zu hindern."
}

View File

@ -0,0 +1,73 @@
{
"seite": "181",
"name": "MOTORICUS GEISTESHAND",
"probe": ["KL", "FF", "KK"],
"technik": "Der Magier fixiert den Gegenstand, deutet in dessen Richtung und spricht die Formel.",
"zauberdauer": {
"normal": "5 Aktionen"
},
"wirkung": "Der Zauber lässt einen Gegenstand durch die Luft schweben (ZfP*/2 Schritt pro Aktion). Pro Aktion kann nur eine Kraft angewendet werden: heben, schieben, ziehen, drehen oder kippen. Verbergen oder zerquetschen ist nicht möglich, auch keine komplexen Bahnen. Der Magier muss Sichtkontakt haben. Gewichtsbeschränkung: ZfP*/2 + 7 Stein. Kosten: 1 AsP pro 20 Unzen Gewicht, mindestens 3 AsP.",
"kosten": [
{
"min": 3,
"additionalFormula": "1 * 5 Stein Gewicht",
"variables": ["5 Stein Gewicht"],
"repräsentation": ""
},
{
"min": 3,
"additionalFormula": "2 * 15 Stein Gewicht",
"variables": ["15 Stein Gewicht"],
"repräsentation": "Schelm"
}
],
"zielobjekt": "Einzelobjekt",
"reichweite": "bis 7 Schritt",
"wirkungsdauer": "bis zu 1 Spielrunde (A)",
"modifikationen": "Zauberdauer, Kosten, Zielobjekt (mehrere), Reichweite, Wirkungsdauer",
"varianten": [
{
"name": "Verbiegen",
"description": "Ermöglicht Verformen (biegen, zerreißen, etc.). Kosten: 7 AsP pro Aktion.",
"mod": -3,
"limit": ""
},
{
"name": "Unsichtbarer Heb",
"description": "Erlaubt Angriffe (wie waffenlose Attacke, KK-Ersatz durch ZfP*).",
"mod": -3,
"limit": "7"
},
{
"name": "Rote und Weiße Kamele",
"description": "Erlaubt eine Anzahl Objekte (maximal 5x ZfW Stück mit gesamt 10x ZfP* Unzen Gewicht) für ZfP* Stunden zu bewegen.",
"mod": -3,
"limit": "7"
},
{
"name": "Fesselfeld",
"description": "Erzeugt Zone (Radius ZfP* Schritt), in der keine unbelebten Objekte ohne KK > ZfP*+7 bewegt werden können. Waffen/Schilde der Betroffenen sind erschwert. Kosten: 17 AsP.",
"mod": -7,
"limit": "11"
},
{
"name": "Telemanipulation",
"description": "Erlaubt feine Manipulationen wie Schloss öffnen, Schreiben, etc. Kosten: +2 AsP/Aufschlag pro Probe.",
"mod": -2,
"limit": "7"
}
],
"reversalis": "Hebt den wirkenden MOTORICUS auf.",
"antimagie": "Kann in einer Zone des BEWEGUNG STÖREN nur erschwert gewirkt werden.",
"merkmal": ["Telekinese"],
"komplexität": "C",
"repräsentation": {
"Magier": 6,
"Schelm": 5,
"Achaz": 4,
"Elf": 4,
"Hexe": 4,
"Scharlatan": 4
},
"info": "Gilt als Ursprung aller Bewegungszauber (z. B. FORMEN). Druiden sehen ihn als Grundlage für verwandte Zauber."
}

View File

@ -0,0 +1,49 @@
{
"seite": "183",
"name": "MOVIMENTO DAUERLAUF",
"probe": ["IN", "GE", "KO"],
"technik": "Der Elf atmet tief durch, läuft barfuß auf der Stelle und konzentriert sich auf die Melodie von malwan biunda daola.",
"zauberdauer": {
"normal": "20 Aktionen"
},
"wirkung": "Unter dem Einfluss des Zaubers ist der Elf in der Lage, in einem schnellen und unermüdlichen Dauerlauf zu verfallen. Er kann sich während der gesamten Wirkungsdauer mit seiner maximalen Dauerlauf-Geschwindigkeit (GS/2, 1,8 x GS Meilen pro Stunde) bewegen, ohne Erschöpfung zu erleiden. Andere Tätigkeiten (z. B. Kampf) sind aber ebenso anstrengend wie normal. Der Zauber erhöht nicht die maximal mögliche Geschwindigkeit, erlaubt aber ein anhaltendes Ausdauerniveau.",
"kosten": [
{
"cost": 5,
"additionalFormula": "1 * Stunde",
"variables": [
"Stunde"
],
"repräsentation": ""
}
],
"zielobjekt": "Einzelseele, freiwillig (auch Tiere mit LO > 12)",
"reichweite": "selbst",
"wirkungsdauer": "nach AsP-Aufwand, höchstens ZfP* Stunden (A)",
"modifikationen": "Zauberdauer, Reichweite, Wirkungsdauer",
"varianten": [
{
"name": "Wie ein Fisch",
"description": "Statt auf das Laufen bezieht sich der Zauber auf das Schwimmen.",
"mod": -7,
"limit": ""
},
{
"name": "Grenzenlose Ausdauer",
"description": "Zauber gilt für alle Aktivitäten, z. B. auch Kampf.",
"mod": -7,
"limit": "11"
}
],
"reversalis": "Der Elf geht normales Gehen ebenso an wie ein Dauerlauf. Laufen ist so ermüdend wie ein Sprint.",
"antimagie": "Kann in einer Zone des EIGENSCHAFT WIEDERHERSTELLEN nur erschwert gewirkt und von diesem Zauber beendet werden.",
"merkmal": ["Eigenschaften"],
"komplexität": "A",
"repräsentation": {
"Elf": 7,
"Druide": 3,
"Hexe": 3,
"Magier": 3
},
"info": "Besonders bei Au- und Steppenelfen verbreitet, andere Elfen kennen ihn ebenfalls. Menschen erkennen oft nicht, dass es sich um Magie handelt."
}

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