Merge branch '55-creatures-compendium' of git.f3l.de:dungeonslayers/ds4 into 55-creatures-compendium

This commit is contained in:
Sascha Martens 2021-07-21 15:12:44 +02:00
commit 0d79e38898
96 changed files with 3236 additions and 3108 deletions

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-License-Identifier: MIT

View file

@ -2,7 +2,7 @@
"private": true,
"name": "dungeonslayers4",
"description": "An implementation of the Dungeonslayers 4 game system for Foundry Virtual Tabletop.",
"version": "0.8.0",
"version": "1.1.3",
"license": "https://git.f3l.de/dungeonslayers/ds4#licensing",
"homepage": "https://git.f3l.de/dungeonslayers/ds4",
"repository": {
@ -52,32 +52,32 @@
"postinstall": "husky install"
},
"devDependencies": {
"@league-of-foundry-developers/foundry-vtt-types": "^0.7.10-0",
"@league-of-foundry-developers/foundry-vtt-types": "^0.8.8-5",
"@rollup/plugin-node-resolve": "^13.0.0",
"@types/fs-extra": "^9.0.11",
"@types/jest": "^26.0.23",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"@types/fs-extra": "^9.0.12",
"@types/jest": "^26.0.24",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"chalk": "^4.1.1",
"eslint": "^7.29.0",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-prettier": "^3.4.0",
"fs-extra": "^10.0.0",
"gulp": "^4.0.2",
"gulp-sass": "^5.0.0",
"husky": "^6.0.0",
"jest": "^27.0.5",
"husky": "^7.0.1",
"jest": "^27.0.6",
"jest-junit": "^12.2.0",
"lint-staged": "^11.0.0",
"prettier": "^2.3.2",
"rollup": "^2.52.3",
"rollup": "^2.53.1",
"rollup-plugin-typescript2": "^0.30.0",
"sass": "1.32.8",
"sass": "1.35.2",
"semver": "^7.3.5",
"ts-jest": "^27.0.3",
"tslib": "^2.3.0",
"typescript": "^4.3.4",
"typescript": "^4.3.5",
"yargs": "^17.0.1"
},
"lint-staged": {

View file

@ -5,7 +5,23 @@
import evaluateCheck from "../../src/module/rolls/check-evaluation";
Object.defineProperty(globalThis, "game", { value: { i18n: { localize: (key: string) => key } } });
class StubGame {
constructor() {
this.i18n = {
localize: (key: string) => key,
};
}
i18n: {
localize: (key: string) => string;
};
}
const game = new StubGame();
Object.defineProperties(globalThis, {
game: { value: game },
Game: { value: StubGame },
});
describe("evaluateCheck with no dice", () => {
it("should throw an error.", () => {

View file

@ -5,7 +5,7 @@
"DS4.UserInteractionAddEffect": "Neuer Effekt",
"DS4.UserInteractionEditEffect": "Effekt bearbeiten",
"DS4.UserInteractionDeleteEffect": "Effekt löschen",
"DS4.EntityImageAltText": "Bild von {name}",
"DS4.DocumentImageAltText": "Bild von {name}",
"DS4.RollableImageRollableTitle": "Für {name} würfeln",
"DS4.DiceOverlayImageAltText": "Bild eines W20",
"DS4.NotOwned": "Nicht besessen",
@ -15,7 +15,7 @@
"DS4.HeadingEffects": "Effekte",
"DS4.HeadingInventory": "Inventar",
"DS4.HeadingProfile": "Profil",
"DS4.HeadingTalentsAbilities": "Talente & Fähigkeiten",
"DS4.HeadingAbilities": "Fähigkeiten",
"DS4.HeadingSpells": "Zaubersprüche",
"DS4.HeadingDescription": "Beschreibung",
"DS4.HeadingSpecialCreatureAbilities": "Besondere Fähigkeiten",
@ -121,6 +121,8 @@
"DS4.SpellMinimumLevelsSorcerer": "Zugangsstufe für Schwarzmagier",
"DS4.SpellMinimumLevelsSorcererAbbr": "Zugangsstufe Sch",
"DS4.SpellPrice": "Preis (Gold)",
"DS4.ActorName": "Name",
"DS4.ActorImageAltText": "Bild des Aktors",
"DS4.ActorTypeCharacter": "Charakter",
"DS4.ActorTypeCreature": "Kreatur",
"DS4.Attribute": "Attribut",
@ -144,6 +146,14 @@
"DS4.CombatValuesRangedAttack": "Schießen",
"DS4.CombatValuesSpellcasting": "Zaubern",
"DS4.CombatValuesTargetedSpellcasting": "Zielzaubern",
"DS4.CombatValuesHitPointsSheet": "Lebenskraft",
"DS4.CombatValuesDefenseSheet": "Abwehr",
"DS4.CombatValuesInitiativeSheet": "Initiative",
"DS4.CombatValuesMovementSheet": "Laufen",
"DS4.CombatValuesMeleeAttackSheet": "Schlagen",
"DS4.CombatValuesRangedAttackSheet": "Schießen",
"DS4.CombatValuesSpellcastingSheet": "Zaubern",
"DS4.CombatValuesTargetedSpellcastingSheet": "Zielzaubern",
"DS4.CharacterBaseInfoRace": "Volk",
"DS4.CharacterBaseInfoClass": "Klasse",
"DS4.CharacterBaseInfoHeroClass": "Heldenklasse",

View file

@ -5,7 +5,7 @@
"DS4.UserInteractionAddEffect": "Add Effect",
"DS4.UserInteractionEditEffect": "Edit Effect",
"DS4.UserInteractionDeleteEffect": "Delete Effect",
"DS4.EntityImageAltText": "Image of {name}",
"DS4.DocumentImageAltText": "Image of {name}",
"DS4.RollableImageRollableTitle": "Roll for {name}",
"DS4.DiceOverlayImageAltText": "Image of a d20",
"DS4.NotOwned": "No owner",
@ -15,7 +15,7 @@
"DS4.HeadingEffects": "Effects",
"DS4.HeadingInventory": "Inventory",
"DS4.HeadingProfile": "Profile",
"DS4.HeadingTalentsAbilities": "Talents & Abilities",
"DS4.HeadingAbilities": "Abilities",
"DS4.HeadingSpells": "Spells",
"DS4.HeadingDescription": "Description",
"DS4.HeadingSpecialCreatureAbilities": "Special Abilities",
@ -121,6 +121,8 @@
"DS4.SpellMinimumLevelsSorcerer": "Minimum level for Sorcerers",
"DS4.SpellMinimumLevelsSorcererAbbr": "Min lvl SRC",
"DS4.SpellPrice": "Price (Gold)",
"DS4.ActorName": "Name",
"DS4.ActorImageAltText": "Image of the Actor",
"DS4.ActorTypeCharacter": "Character",
"DS4.ActorTypeCreature": "Creature",
"DS4.Attribute": "Attribute",
@ -144,6 +146,14 @@
"DS4.CombatValuesRangedAttack": "Ranged Attack",
"DS4.CombatValuesSpellcasting": "Spellcasting",
"DS4.CombatValuesTargetedSpellcasting": "Targeted Spellcasting",
"DS4.CombatValuesHitPointsSheet": "Hit Points",
"DS4.CombatValuesDefenseSheet": "Defense",
"DS4.CombatValuesInitiativeSheet": "Initiative",
"DS4.CombatValuesMovementSheet": "Movement",
"DS4.CombatValuesMeleeAttackSheet": "Melee Attack",
"DS4.CombatValuesRangedAttackSheet": "RAT",
"DS4.CombatValuesSpellcastingSheet": "Spellcasting",
"DS4.CombatValuesTargetedSpellcastingSheet": "TSC",
"DS4.CharacterBaseInfoRace": "Race",
"DS4.CharacterBaseInfoClass": "Class",
"DS4.CharacterBaseInfoHeroClass": "Hero Class",

View file

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4Actor } from "./actor/actor";
declare global {
interface DocumentClassConfig {
ActiveEffect: typeof DS4ActiveEffect;
}
}
export class DS4ActiveEffect extends ActiveEffect {
/** @override */
apply(actor: DS4Actor, change: foundry.data.ActiveEffectData["changes"][number]): unknown {
change.value = Roll.replaceFormulaData(change.value, actor.data);
try {
change.value = Roll.safeEval(change.value).toString();
} catch (e) {
// this is a valid case, e.g., if the effect change simply is a string
}
return super.apply(actor, change);
}
}

View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { ModifiableDataBaseTotal, ResourceDataBaseTotalMax } from "../common/common-data";
import { DS4 } from "../config";
import {
DS4CharacterDataSourceDataBaseInfo,
DS4CharacterDataSourceDataCurrency,
DS4CharacterDataSourceDataProfile,
DS4CharacterDataSourceDataProgression,
DS4CharacterDataSourceDataSlayerPoints,
DS4CreatureDataSourceDataBaseInfo,
} from "./actor-data-source";
declare global {
interface DataConfig {
Actor: DS4ActorDataProperties;
}
}
export type DS4ActorDataProperties = DS4CharacterDataProperties | DS4CreatureDataProperties;
interface DS4CharacterDataProperties {
type: "character";
data: DS4CharacterDataPropertiesData;
}
interface DS4CreatureDataProperties {
type: "creature";
data: DS4CreatureDataPropertiesData;
}
// templates
interface DS4ActorDataPropertiesDataBase {
attributes: DS4ActorDataPropertiesDataAttributes;
traits: DS4ActorDataPropertiesDataTraits;
combatValues: DS4ActorDataPropertiesDataCombatValues;
rolling: DS4ActorDataPropertiesDataRolling;
checks: DS4ActorDataPropertiesDataChecks;
}
type DS4ActorDataPropertiesDataAttributes = {
[Key in keyof typeof DS4.i18n.attributes]: ModifiableDataBaseTotal<number>;
};
type DS4ActorDataPropertiesDataTraits = { [Key in keyof typeof DS4.i18n.traits]: ModifiableDataBaseTotal<number> };
type DS4ActorDataPropertiesDataCombatValues = {
[Key in keyof typeof DS4.i18n.combatValues]: Key extends "hitPoints"
? ResourceDataBaseTotalMax<number>
: ModifiableDataBaseTotal<number>;
};
interface DS4ActorDataPropertiesDataRolling {
maximumCoupResult: number;
minimumFumbleResult: number;
}
type DS4ActorDataPropertiesDataChecks = {
[key in Check]: number;
};
export type Check = keyof typeof DS4.i18n.checks;
export function isCheck(value: string): value is Check {
return Object.keys(DS4.i18n.checks).includes(value);
}
// types
interface DS4CreatureDataPropertiesData extends DS4ActorDataPropertiesDataBase {
baseInfo: DS4CreatureDataSourceDataBaseInfo;
}
interface DS4CharacterDataPropertiesData extends DS4ActorDataPropertiesDataBase {
baseInfo: DS4CharacterDataSourceDataBaseInfo;
progression: DS4CharacterDataSourceDataProgression;
profile: DS4CharacterDataSourceDataProfile;
currency: DS4CharacterDataSourceDataCurrency;
slayerPoints: DS4CharacterDataPropertiesDataSlayerPoints;
}
export interface DS4CharacterDataPropertiesDataSlayerPoints extends DS4CharacterDataSourceDataSlayerPoints {
max: number;
}

View file

@ -7,50 +7,56 @@
import { ModifiableData, ModifiableDataBase, ResourceData, UsableResource } from "../common/common-data";
import { DS4 } from "../config";
import { DS4ItemData } from "../item/item-data";
export type DS4ActorData = DS4CharacterData | DS4CreatureData;
type ActorType = keyof typeof DS4.i18n.actorTypes;
export interface DS4ActorDataHelper<T, U extends ActorType> extends Actor.Data<T, DS4ItemData> {
type: U;
declare global {
interface SourceConfig {
Actor: DS4ActorDataSource;
}
}
type DS4CharacterData = DS4ActorDataHelper<DS4CharacterDataData, "character">;
type DS4CreatureData = DS4ActorDataHelper<DS4CreatureDataData, "creature">;
export type DS4ActorDataSource = DS4CharacterDataSource | DS4CreatureDataSource;
interface DS4CharacterDataSource {
type: "character";
data: DS4CharacterDataSourceData;
}
interface DS4CreatureDataSource {
type: "creature";
data: DS4CreatureDataSourceData;
}
// templates
interface DS4ActorDataDataBase {
attributes: DS4ActorDataDataAttributes;
traits: DS4ActorDataDataTraits;
combatValues: DS4ActorDataDataCombatValues;
interface DS4ActorDataSourceDataBase {
attributes: DS4ActorDataSourceDataAttributes;
traits: DS4ActorDataSourceDataTraits;
combatValues: DS4ActorDataSourceDataCombatValues;
}
type DS4ActorDataDataAttributes = { [Key in keyof typeof DS4.i18n.attributes]: ModifiableDataBase<number> };
type DS4ActorDataSourceDataAttributes = { [Key in keyof typeof DS4.i18n.attributes]: ModifiableDataBase<number> };
type Attribute = keyof DS4ActorDataDataAttributes;
type Attribute = keyof DS4ActorDataSourceDataAttributes;
export function isAttribute(value: unknown): value is Attribute {
return (Object.keys(DS4.i18n.attributes) as Array<unknown>).includes(value);
}
type DS4ActorDataDataTraits = { [Key in keyof typeof DS4.i18n.traits]: ModifiableDataBase<number> };
type DS4ActorDataSourceDataTraits = { [Key in keyof typeof DS4.i18n.traits]: ModifiableDataBase<number> };
type Trait = keyof DS4ActorDataDataTraits;
type Trait = keyof DS4ActorDataSourceDataTraits;
export function isTrait(value: unknown): value is Trait {
return (Object.keys(DS4.i18n.traits) as Array<unknown>).includes(value);
}
type DS4ActorDataDataCombatValues = {
type DS4ActorDataSourceDataCombatValues = {
[Key in keyof typeof DS4.i18n.combatValues]: Key extends "hitPoints"
? ResourceData<number>
: ModifiableData<number>;
};
type CombatValue = keyof DS4ActorDataDataCombatValues;
type CombatValue = keyof DS4ActorDataSourceDataCombatValues;
export function isCombatValue(value: string): value is CombatValue {
return (Object.keys(DS4.i18n.combatValues) as Array<unknown>).includes(value);
@ -58,33 +64,44 @@ export function isCombatValue(value: string): value is CombatValue {
// types
interface DS4CharacterDataData extends DS4ActorDataDataBase {
baseInfo: DS4CharacterDataDataBaseInfo;
progression: DS4CharacterDataDataProgression;
language: DS4CharacterDataDataLanguage;
profile: DS4CharacterDataDataProfile;
currency: DS4CharacterDataDataCurrency;
slayerPoints: DS4CharacterDataDataSlayerPoints;
interface DS4CreatureDataSourceData extends DS4ActorDataSourceDataBase {
baseInfo: DS4CreatureDataSourceDataBaseInfo;
}
export interface DS4CharacterDataDataBaseInfo {
export interface DS4CreatureDataSourceDataBaseInfo {
loot: string;
foeFactor: number;
creatureType: CreatureType;
sizeCategory: SizeCategory;
experiencePoints: number;
description: string;
}
type CreatureType = keyof typeof DS4.i18n.creatureTypes;
type SizeCategory = keyof typeof DS4.i18n.creatureSizeCategories;
interface DS4CharacterDataSourceData extends DS4ActorDataSourceDataBase {
baseInfo: DS4CharacterDataSourceDataBaseInfo;
progression: DS4CharacterDataSourceDataProgression;
profile: DS4CharacterDataSourceDataProfile;
currency: DS4CharacterDataSourceDataCurrency;
slayerPoints: DS4CharacterDataSourceDataSlayerPoints;
}
export interface DS4CharacterDataSourceDataBaseInfo {
race: string;
class: string;
heroClass: string;
culture: string;
}
export interface DS4CharacterDataDataProgression {
export interface DS4CharacterDataSourceDataProgression {
level: number;
experiencePoints: number;
talentPoints: UsableResource<number>;
progressPoints: UsableResource<number>;
}
export interface DS4CharacterDataDataLanguage {
languages: string;
alphabets: string;
}
export interface DS4CharacterDataDataProfile {
export interface DS4CharacterDataSourceDataProfile {
biography: string;
gender: string;
birthday: string;
@ -97,29 +114,12 @@ export interface DS4CharacterDataDataProfile {
specialCharacteristics: string;
}
export interface DS4CharacterDataDataCurrency {
export interface DS4CharacterDataSourceDataCurrency {
gold: number;
silver: number;
copper: number;
}
export interface DS4CharacterDataDataSlayerPoints {
export interface DS4CharacterDataSourceDataSlayerPoints {
value: number;
}
interface DS4CreatureDataData extends DS4ActorDataDataBase {
baseInfo: DS4CreatureDataDataBaseInfo;
}
export interface DS4CreatureDataDataBaseInfo {
loot: string;
foeFactor: number;
creatureType: CreatureType;
sizeCategory: SizeCategory;
experiencePoints: number;
description: string;
}
type CreatureType = keyof typeof DS4.i18n.creatureTypes;
type SizeCategory = keyof typeof DS4.i18n.creatureSizeCategories;

View file

@ -1,77 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { ModifiableDataBaseTotal, ResourceDataBaseTotalMax } from "../common/common-data";
import { DS4 } from "../config";
import {
DS4ActorDataHelper,
DS4CharacterDataDataBaseInfo,
DS4CharacterDataDataCurrency,
DS4CharacterDataDataLanguage,
DS4CharacterDataDataProfile,
DS4CharacterDataDataProgression,
DS4CharacterDataDataSlayerPoints,
DS4CreatureDataDataBaseInfo,
} from "./actor-data";
export type DS4ActorPreparedData = DS4CharacterPreparedData | DS4CreaturePreparedData;
type DS4CharacterPreparedData = DS4ActorDataHelper<DS4CharacterPreparedDataData, "character">;
type DS4CreaturePreparedData = DS4ActorDataHelper<DS4CreaturePreparedDataData, "creature">;
// templates
interface DS4ActorPreparedDataDataBase {
attributes: DS4ActorPreparedDataDataAttributes;
traits: DS4ActorPreparedDataDataTraits;
combatValues: DS4ActorPreparedDataDataCombatValues;
rolling: DS4ActorPreparedDataDataRolling;
checks: DS4ActorPreparedDataDataChecks;
}
type DS4ActorPreparedDataDataAttributes = {
[Key in keyof typeof DS4.i18n.attributes]: ModifiableDataBaseTotal<number>;
};
type DS4ActorPreparedDataDataTraits = { [Key in keyof typeof DS4.i18n.traits]: ModifiableDataBaseTotal<number> };
type DS4ActorPreparedDataDataCombatValues = {
[Key in keyof typeof DS4.i18n.combatValues]: Key extends "hitPoints"
? ResourceDataBaseTotalMax<number>
: ModifiableDataBaseTotal<number>;
};
interface DS4ActorPreparedDataDataRolling {
maximumCoupResult: number;
minimumFumbleResult: number;
}
export type Check = keyof typeof DS4.i18n.checks;
export function isCheck(value: string): value is Check {
return Object.keys(DS4.i18n.checks).includes(value);
}
type DS4ActorPreparedDataDataChecks = {
[key in Check]: number;
};
// types
interface DS4CharacterPreparedDataData extends DS4ActorPreparedDataDataBase {
baseInfo: DS4CharacterDataDataBaseInfo;
progression: DS4CharacterDataDataProgression;
language: DS4CharacterDataDataLanguage;
profile: DS4CharacterDataDataProfile;
currency: DS4CharacterDataDataCurrency;
slayerPoints: DS4CharacterPreparedDataDataSlayerPoints;
}
export interface DS4CharacterPreparedDataDataSlayerPoints extends DS4CharacterDataDataSlayerPoints {
max: number;
}
interface DS4CreaturePreparedDataData extends DS4ActorPreparedDataDataBase {
baseInfo: DS4CreatureDataDataBaseInfo;
}

View file

@ -5,22 +5,27 @@
import { ModifiableDataBaseTotal } from "../common/common-data";
import { DS4 } from "../config";
import { getGame } from "../helpers";
import { DS4Item } from "../item/item";
import { ItemType } from "../item/item-data";
import { DS4ArmorPreparedData, DS4ShieldPreparedData } from "../item/item-prepared-data";
import { DS4ArmorDataProperties, DS4ShieldDataProperties } from "../item/item-data-properties";
import { ItemType } from "../item/item-data-source";
import { createCheckRoll } from "../rolls/check-factory";
import { DS4ActorData, isAttribute, isTrait } from "./actor-data";
import { Check, DS4ActorPreparedData } from "./actor-prepared-data";
import { Check } from "./actor-data-properties";
import { isAttribute, isTrait } from "./actor-data-source";
declare global {
interface DocumentClassConfig {
Actor: typeof DS4Actor;
}
}
/**
* The Actor class for DS4
*/
export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData> {
export class DS4Actor extends Actor {
/** @override */
prepareData(): void {
this.data = duplicate(this._data) as DS4ActorPreparedData;
if (!this.data.img) this.data.img = CONST.DEFAULT_TOKEN;
if (!this.data.name) this.data.name = "New " + this.entity;
this.data.reset();
this.prepareBaseData();
this.prepareEmbeddedEntities();
this.applyActiveEffectsToBaseData();
@ -49,6 +54,15 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
);
}
/**
* @override
* We override this with an empty implementation because we have our own custom way of applying
* {@link ActiveEffect}s and {@link Actor#prepareEmbeddedEntities} calls this.
*/
applyActiveEffects(): void {
return;
}
applyActiveEffectsToBaseData(): void {
// reset overrides because our variant of applying active effects does not set them, it only adds overrides
this.overrides = {};
@ -68,32 +82,30 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
*
* @param predicate - The predicate that ActiveEffectChanges need to satisfy in order to be applied
*/
applyActiveEffectsFiltered(predicate: (change: ActiveEffectChange) => boolean): void {
applyActiveEffectsFiltered(predicate: (change: foundry.data.ActiveEffectData["changes"][number]) => boolean): void {
const overrides: Record<string, unknown> = {};
// Organize non-disabled effects by their application priority
const changes = this.effects.reduce(
(changes: Array<ActiveEffectChange & { effect: ActiveEffect<DS4Actor> }>, e) => {
if (e.data.disabled) return changes;
const item = this._getOriginatingItemOfActiveEffect(e);
if (item?.isNonEquippedEuipable()) return changes;
const changes: (foundry.data.ActiveEffectData["changes"][number] & { effect: ActiveEffect })[] =
this.effects.reduce(
(changes: (foundry.data.ActiveEffectData["changes"][number] & { effect: ActiveEffect })[], e) => {
if (e.data.disabled) return changes;
const item = this.getOriginatingItemOfActiveEffect(e);
if (item?.isNonEquippedEuipable()) return changes;
const factor = item?.activeEffectFactor ?? 1;
const factor = item?.activeEffectFactor ?? 1;
return changes.concat(
e.data.changes.filter(predicate).flatMap((c) => {
const duplicatedChange = duplicate(c);
duplicatedChange.priority = duplicatedChange.priority ?? duplicatedChange.mode * 10;
return Array(factor).fill({
...duplicatedChange,
effect: e,
});
}),
);
},
[],
);
changes.sort((a, b) => a.priority - b.priority);
const newChanges = e.data.changes.filter(predicate).flatMap((c) => {
const changeSource = c.toObject();
changeSource.priority = changeSource.priority ?? changeSource.mode * 10;
return Array(factor).fill({ ...changeSource, effect: e });
});
return changes.concat(newChanges);
},
[],
);
changes.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
// Apply all changes
for (const change of changes) {
@ -102,11 +114,11 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
}
// Expand the set of final overrides
this.overrides = expandObject({ ...flattenObject(this.overrides), ...overrides });
this.overrides = foundry.utils.expandObject({ ...foundry.utils.flattenObject(this.overrides), ...overrides });
}
protected _getOriginatingItemOfActiveEffect(effect: ActiveEffect<DS4Actor>): DS4Item | undefined {
return this.items.find((item) => item.uuid === effect.data.origin) ?? undefined;
protected getOriginatingItemOfActiveEffect(effect: ActiveEffect): DS4Item | undefined {
return this.items.find((item) => item.uuid === effect.data.origin);
}
/**
@ -216,7 +228,7 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
return this.items
.map((item) => item.data)
.filter(
(data): data is DS4ArmorPreparedData | DS4ShieldPreparedData =>
(data): data is foundry.data.ItemData & (DS4ArmorDataProperties | DS4ShieldDataProperties) =>
data.type === "armor" || data.type === "shield",
)
.filter((data) => data.data.equipped)
@ -267,8 +279,13 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
* This only differs from the base implementation by also allowing negative values.
* @override
*/
async modifyTokenAttribute(attribute: string, value: number, isDelta = false, isBar = true): Promise<this> {
const current = getProperty(this.data.data, attribute);
async modifyTokenAttribute(
attribute: string,
value: number,
isDelta = false,
isBar = true,
): Promise<this | undefined> {
const current = foundry.utils.getProperty(this.data.data, attribute);
// Determine the updates to make to the actor data
let updates: Record<string, number>;
@ -291,10 +308,10 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
*/
async rollCheck(check: Check): Promise<void> {
await createCheckRoll(this.data.data.checks[check], {
rollMode: game.settings.get("core", "rollMode") as Const.DiceRollMode, // TODO(types): Type this setting in upstream
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: this.data.data.rolling.maximumCoupResult,
minimumFumbleResult: this.data.data.rolling.minimumFumbleResult,
flavor: game.i18n.format("DS4.ActorCheckFlavor", { actor: this.name, check: DS4.i18n.checks[check] }),
flavor: getGame().i18n.format("DS4.ActorCheckFlavor", { actor: this.name, check: DS4.i18n.checks[check] }),
});
}
@ -306,10 +323,10 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
const { attribute, trait } = await this.selectAttributeAndTrait();
const checkTargetNumber = this.data.data.attributes[attribute].total + this.data.data.traits[trait].total;
await createCheckRoll(checkTargetNumber, {
rollMode: game.settings.get("core", "rollMode") as Const.DiceRollMode, // TODO(types): Type this setting in upstream
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: this.data.data.rolling.maximumCoupResult,
minimumFumbleResult: this.data.data.rolling.minimumFumbleResult,
flavor: game.i18n.format("DS4.ActorGenericCheckFlavor", {
flavor: getGame().i18n.format("DS4.ActorGenericCheckFlavor", {
actor: this.name,
attribute: DS4.i18n.attributes[attribute],
trait: DS4.i18n.traits[trait],
@ -324,27 +341,27 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
const attributeIdentifier = "attribute-trait-selection-attribute";
const traitIdentifier = "attribute-trait-selection-trait";
return Dialog.prompt({
title: game.i18n.localize("DS4.DialogAttributeTraitSelection"),
title: getGame().i18n.localize("DS4.DialogAttributeTraitSelection"),
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
selects: [
{
label: game.i18n.localize("DS4.Attribute"),
label: getGame().i18n.localize("DS4.Attribute"),
identifier: attributeIdentifier,
options: DS4.i18n.attributes,
},
{
label: game.i18n.localize("DS4.Trait"),
label: getGame().i18n.localize("DS4.Trait"),
identifier: traitIdentifier,
options: DS4.i18n.traits,
},
],
}),
label: game.i18n.localize("DS4.GenericOkButton"),
label: getGame().i18n.localize("DS4.GenericOkButton"),
callback: (html) => {
const selectedAttribute = html.find(`#${attributeIdentifier}`).val();
if (!isAttribute(selectedAttribute)) {
throw new Error(
game.i18n.format("DS4.ErrorUnexpectedAttribute", {
getGame().i18n.format("DS4.ErrorUnexpectedAttribute", {
actualAttribute: selectedAttribute,
expectedTypes: Object.keys(DS4.i18n.attributes)
.map((attribute) => `'${attribute}'`)
@ -355,7 +372,7 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
const selectedTrait = html.find(`#${traitIdentifier}`).val();
if (!isTrait(selectedTrait)) {
throw new Error(
game.i18n.format("DS4.ErrorUnexpectedTrait", {
getGame().i18n.format("DS4.ErrorUnexpectedTrait", {
actualTrait: selectedTrait,
expectedTypes: Object.keys(DS4.i18n.traits)
.map((attribute) => `'${attribute}'`)

View file

@ -7,31 +7,26 @@
import { ModifiableDataBaseTotal } from "../../common/common-data";
import { DS4 } from "../../config";
import { getCanvas } from "../../helpers";
import { getCanvas, getGame } from "../../helpers";
import { DS4Item } from "../../item/item";
import { DS4ItemData } from "../../item/item-data";
import { getDS4Settings } from "../../settings";
import { DS4Settings, getDS4Settings } from "../../settings";
import notifications from "../../ui/notifications";
import { DS4Actor } from "../actor";
import { isCheck } from "../actor-prepared-data";
import { isCheck } from "../actor-data-properties";
/**
* The base Sheet class for all DS4 Actors
*/
export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
// TODO(types): Improve mergeObject in upstream so that it isn't necessary to provide all parameters (see https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/272)
export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetData> {
/** @override */
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
...superDefaultOptions,
static get defaultOptions(): ActorSheet.Options {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["ds4", "sheet", "actor"],
height: 620,
scrollY: [
".values",
".inventory",
".spells",
".talents-abilities",
".abilities",
".profile",
".biography",
".special-creature-abilities",
@ -57,14 +52,14 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
* object itemsByType.
* @returns The data fed to the template of the actor sheet
*/
async getData(): Promise<ActorSheet.Data<DS4Actor>> {
async getData(): Promise<DS4ActorSheetData> {
const itemsByType = Object.fromEntries(
Object.entries(this.actor.itemTypes).map(([itemType, items]) => {
return [itemType, items.map((item) => item.data).sort((a, b) => (a.sort || 0) - (b.sort || 0))];
}),
);
const data = {
...this._addTooltipsToData(await super.getData()),
...this.addTooltipsToData(await super.getData()),
// Add the localization config to the data:
config: DS4,
// Add the items explicitly sorted by type to the data:
@ -74,21 +69,23 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
return data;
}
protected _addTooltipsToData(data: ActorSheet.Data<DS4Actor>): ActorSheet.Data<DS4Actor> {
const valueGroups = [data.data.attributes, data.data.traits, data.data.combatValues];
protected addTooltipsToData(data: ActorSheet.Data): ActorSheet.Data {
const valueGroups = [data.data.data.attributes, data.data.data.traits, data.data.data.combatValues];
valueGroups.forEach((valueGroup) => {
Object.values(valueGroup).forEach((attribute: ModifiableDataBaseTotal<number> & { tooltip?: string }) => {
attribute.tooltip = this._getTooltipForValue(attribute);
attribute.tooltip = this.getTooltipForValue(attribute);
});
});
return data;
}
protected _getTooltipForValue(value: ModifiableDataBaseTotal<number>): string {
return `${value.base} (${game.i18n.localize("DS4.TooltipBaseValue")}) + ${value.mod} (${game.i18n.localize(
"DS4.TooltipModifier",
)}) ${game.i18n.localize("DS4.TooltipEffects")} ${value.total}`;
protected getTooltipForValue(value: ModifiableDataBaseTotal<number>): string {
return `${value.base} (${getGame().i18n.localize("DS4.TooltipBaseValue")}) + ${
value.mod
} (${getGame().i18n.localize("DS4.TooltipModifier")}) ${getGame().i18n.localize("DS4.TooltipEffects")} ${
value.total
}`;
}
/** @override */
@ -99,18 +96,18 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
if (!this.options.editable) return;
// Add Inventory Item
html.find(".item-create").on("click", this._onItemCreate.bind(this));
html.find(".item-create").on("click", this.onItemCreate.bind(this));
// Update Inventory Item
html.find(".item-edit").on("click", (ev) => {
const li = $(ev.currentTarget).parents(".item");
const id = li.data("itemId");
const item = this.actor.getOwnedItem(id);
const item = this.actor.items.get(id);
if (!item) {
throw new Error(game.i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
throw new Error(getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
}
if (!item.sheet) {
throw new Error(game.i18n.localize("DS4.ErrorUnexpectedError"));
throw new Error(getGame().i18n.localize("DS4.ErrorUnexpectedError"));
}
item.sheet.render(true);
});
@ -118,38 +115,36 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
// Delete Inventory Item
html.find(".item-delete").on("click", (ev) => {
const li = $(ev.currentTarget).parents(".item");
this.actor.deleteOwnedItem(li.data("itemId"));
this.actor.deleteEmbeddedDocuments("Item", [li.data("itemId")]);
li.slideUp(200, () => this.render(false));
});
html.find(".item-change").on("change", this._onItemChange.bind(this));
html.find(".item-change").on("change", this.onItemChange.bind(this));
html.find(".rollable-item").on("click", this._onRollItem.bind(this));
html.find(".rollable-item").on("click", this.onRollItem.bind(this));
html.find(".rollable-check").on("click", this._onRollCheck.bind(this));
html.find(".rollable-check").on("click", this.onRollCheck.bind(this));
}
/**
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
* Handle creating a new embedded Item for the actor using initial data defined in the HTML dataset
* @param event - The originating click event
*/
protected _onItemCreate(event: JQuery.ClickEvent): Promise<DS4ItemData> {
protected onItemCreate(event: JQuery.ClickEvent): void {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
// Grab any data associated with this control.
const { type, ...data } = duplicate(header.dataset);
// Initialize a default name.
const { type, ...data } = foundry.utils.deepClone(header.dataset);
const name = `New ${type.capitalize()}`;
// Prepare the item object.
const itemData = {
name: name,
type: type,
data: data,
};
// Finally, create the item!
return this.actor.createOwnedItem(itemData);
DS4Item.create(itemData, { parent: this.actor });
}
/**
@ -158,11 +153,15 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
* Assumes the item property is given as the value of the HTML element property 'data-property'.
* @param ev - The originating change event
*/
protected _onItemChange(ev: JQuery.ChangeEvent): void {
protected onItemChange(ev: JQuery.ChangeEvent): void {
ev.preventDefault();
const el: HTMLFormElement = $(ev.currentTarget).get(0);
const id = $(ev.currentTarget).parents(".item").data("itemId");
const item = duplicate(this.actor.getOwnedItem(id));
const item = this.actor.items.get(id);
if (!item) {
throw new Error(getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
}
const itemObject = item.toObject();
const property: string | undefined = $(ev.currentTarget).data("property");
// Early return:
@ -175,8 +174,8 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
// Set new value
const newValue = this.getValue(el);
setProperty(item, property, newValue);
this.actor.updateOwnedItem(item);
foundry.utils.setProperty(itemObject, property, newValue);
item.update(itemObject);
}
/**
@ -238,10 +237,13 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
* Handle clickable item rolls.
* @param event - The originating click event
*/
protected _onRollItem(event: JQuery.ClickEvent): void {
protected onRollItem(event: JQuery.ClickEvent): void {
event.preventDefault();
const id = $(event.currentTarget).parents(".item").data("itemId");
const item = this.actor.getOwnedItem(id);
const item = this.actor.items.get(id);
if (!item) {
throw new Error(getGame().i18n.format("DS4.ErrorActorDoesNotHaveItem", { id, actor: this.actor.name }));
}
item.roll().catch((e) => notifications.error(e, { log: true }));
}
@ -249,7 +251,7 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
* Handle clickable check rolls.
* @param event - The originating click event
*/
protected _onRollCheck(event: JQuery.ClickEvent): void {
protected onRollCheck(event: JQuery.ClickEvent): void {
event.preventDefault();
const check = event.currentTarget.dataset["check"];
this.actor.rollCheck(check).catch((e) => notifications.error(e, { log: true }));
@ -263,7 +265,7 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
const check = target.dataset.check;
if (!check) return super._onDragStart(event);
if (!isCheck(check)) throw new Error(game.i18n.format("DS4.ErrorCannotDragMissingCheck", { check }));
if (!isCheck(check)) throw new Error(getGame().i18n.format("DS4.ErrorCannotDragMissingCheck", { check }));
const dragData = {
actorId: this.actor.id,
@ -277,18 +279,11 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
}
/** @override */
protected async _onDropItem(
event: DragEvent,
data: { type: "Item" } & (
| { data: DeepPartial<ActorSheet.OwnedItemData<DS4Actor>> }
| { pack: string }
| { id: string }
),
): Promise<boolean | undefined | ActorSheet.OwnedItemData<DS4Actor>> {
protected async _onDropItem(event: DragEvent, data: ActorSheet.DropData.Item): Promise<unknown> {
const item = await DS4Item.fromDropData(data);
if (item && !this.actor.canOwnItemType(item.data.type)) {
notifications.warn(
game.i18n.format("DS4.WarningActorCannotOwnItem", {
getGame().i18n.format("DS4.WarningActorCannotOwnItem", {
actorName: this.actor.name,
actorType: this.actor.data.type,
itemName: item.name,
@ -300,3 +295,9 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
return super._onDropItem(event, data);
}
}
interface DS4ActorSheetData extends ActorSheet.Data<ActorSheet.Options> {
config: typeof DS4;
itemsByType: Record<string, foundry.data.ItemData[]>;
settings: DS4Settings;
}

View file

@ -9,10 +9,8 @@ import { DS4ActorSheet } from "./actor-sheet";
*/
export class DS4CharacterActorSheet extends DS4ActorSheet {
/** @override */
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
...superDefaultOptions,
static get defaultOptions(): ActorSheet.Options {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["ds4", "sheet", "actor", "character"],
});
}

View file

@ -9,10 +9,8 @@ import { DS4ActorSheet } from "./actor-sheet";
*/
export class DS4CreatureActorSheet extends DS4ActorSheet {
/** @override */
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
...superDefaultOptions,
static get defaultOptions(): ActorSheet.Options {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["ds4", "sheet", "actor", "creature"],
});
}

View file

@ -15,8 +15,6 @@ export interface HasTotal<T> {
total: T;
}
export interface ModifiableDataTotal<T> extends ModifiableData<T>, HasTotal<T> {}
export interface ModifiableDataBaseTotal<T> extends ModifiableDataBase<T>, HasTotal<T> {}
export interface ResourceData<T> extends ModifiableData<T> {
@ -27,6 +25,10 @@ export interface HasMax<T> {
max: T;
}
export interface ModifiableDataBaseMax<T> extends ModifiableDataBase<T>, HasMax<T> {}
export interface ModifiableDataBaseTotalMax<T> extends ModifiableDataBaseMax<T>, HasTotal<T> {}
export interface ResourceDataBaseTotalMax<T> extends ResourceData<T>, HasBase<T>, HasTotal<T>, HasMax<T> {}
export interface UsableResource<T> {

View file

@ -162,6 +162,20 @@ export const DS4 = {
targetedSpellcasting: "DS4.CombatValuesTargetedSpellcasting",
},
/**
* The what do display in the actor sheets for the combat value text (in some languages, abbreviations are necessary)
*/
combatValuesSheet: {
hitPoints: "DS4.CombatValuesHitPointsSheet",
defense: "DS4.CombatValuesDefenseSheet",
initiative: "DS4.CombatValuesInitiativeSheet",
movement: "DS4.CombatValuesMovementSheet",
meleeAttack: "DS4.CombatValuesMeleeAttackSheet",
rangedAttack: "DS4.CombatValuesRangedAttackSheet",
spellcasting: "DS4.CombatValuesSpellcastingSheet",
targetedSpellcasting: "DS4.CombatValuesTargetedSpellcastingSheet",
},
/**
* Define the base info of a character
*/

41
src/module/fonts.ts Normal file
View file

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
type CSSOMString = string;
type FontFaceLoadStatus = "unloaded" | "loading" | "loaded" | "error";
type FontFaceSetStatus = "loading" | "loaded";
interface FontFace {
family: CSSOMString;
style: CSSOMString;
weight: CSSOMString;
stretch: CSSOMString;
unicodeRange: CSSOMString;
variant: CSSOMString;
featureSettings: CSSOMString;
variationSettings: CSSOMString;
display: CSSOMString;
readonly status: FontFaceLoadStatus;
readonly loaded: Promise<FontFace>;
load(): Promise<FontFace>;
}
interface FontFaceSet {
readonly status: FontFaceSetStatus;
readonly ready: Promise<FontFaceSet>;
check(font: string, text?: string): boolean;
load(font: string, text?: string): Promise<FontFace[]>;
}
declare global {
interface Document {
fonts: FontFaceSet;
}
}
const fonts = ["Lora", "Wood Stamp"];
export async function preloadFonts(): Promise<FontFace[][]> {
return Promise.all(fonts.map((font) => document.fonts.load(`1rem ${font}`)));
}

View file

@ -2,10 +2,20 @@
//
// SPDX-License-Identifier: MIT
declare namespace ClientSettings {
interface Values {
"ds4.systemMigrationVersion": number;
"ds4.useSlayingDiceForAutomatedChecks": boolean;
"ds4.showSlayerPoints": boolean;
declare global {
namespace ClientSettings {
interface Values {
"ds4.systemMigrationVersion": number;
"ds4.useSlayingDiceForAutomatedChecks": boolean;
"ds4.showSlayerPoints": boolean;
}
}
namespace PoolTerm {
interface Modifiers {
x: (this: PoolTerm, modifier: string) => void;
}
}
}
export {};

View file

@ -6,29 +6,33 @@
export default async function registerHandlebarsPartials(): Promise<void> {
const templatePaths = [
"systems/ds4/templates/sheets/actor/components/character-progression.hbs",
"systems/ds4/templates/sheets/actor/components/actor-header.hbs",
"systems/ds4/templates/sheets/actor/components/actor-progression.hbs",
"systems/ds4/templates/sheets/actor/components/biography.hbs",
"systems/ds4/templates/sheets/actor/components/character-properties.hbs",
"systems/ds4/templates/sheets/actor/components/check.hbs",
"systems/ds4/templates/sheets/actor/components/checks.hbs",
"systems/ds4/templates/sheets/actor/components/combat-value.hbs",
"systems/ds4/templates/sheets/actor/components/combat-values.hbs",
"systems/ds4/templates/sheets/actor/components/core-value.hbs",
"systems/ds4/templates/sheets/actor/components/core-values.hbs",
"systems/ds4/templates/sheets/actor/components/creature-properties.hbs",
"systems/ds4/templates/sheets/actor/components/currency.hbs",
"systems/ds4/templates/sheets/actor/components/item-list-entry.hbs",
"systems/ds4/templates/sheets/actor/components/item-list-header.hbs",
"systems/ds4/templates/sheets/actor/components/items-overview.hbs",
"systems/ds4/templates/sheets/actor/components/overview-add-button.hbs",
"systems/ds4/templates/sheets/actor/components/overview-control-buttons.hbs",
"systems/ds4/templates/sheets/actor/components/profile.hbs",
"systems/ds4/templates/sheets/actor/components/rollable-image.hbs",
"systems/ds4/templates/sheets/actor/components/talent-rank-equation.hbs",
"systems/ds4/templates/sheets/actor/tabs/abilities.hbs",
"systems/ds4/templates/sheets/actor/tabs/biography.hbs",
"systems/ds4/templates/sheets/actor/tabs/character-inventory.hbs",
"systems/ds4/templates/sheets/actor/tabs/creature-inventory.hbs",
"systems/ds4/templates/sheets/actor/tabs/description.hbs",
"systems/ds4/templates/sheets/actor/tabs/profile.hbs",
"systems/ds4/templates/sheets/actor/tabs/special-creature-abilities.hbs",
"systems/ds4/templates/sheets/actor/tabs/spells.hbs",
"systems/ds4/templates/sheets/actor/tabs/talents-abilities.hbs",
"systems/ds4/templates/sheets/actor/tabs/values.hbs",
"systems/ds4/templates/sheets/item/components/body.hbs",
"systems/ds4/templates/sheets/item/components/sheet-header.hbs",

View file

@ -4,7 +4,14 @@
export function getCanvas(): Canvas {
if (!(canvas instanceof Canvas) || !canvas.ready) {
throw new Error(game.i18n.localize("DS4.ErrorCanvasIsNotInitialized"));
throw new Error(getGame().i18n.localize("DS4.ErrorCanvasIsNotInitialized"));
}
return canvas;
}
export function getGame(): Game {
if (!(game instanceof Game)) {
throw new Error("Game is not initialized yet."); // Cannot localize this as we would need to access game to do this.
}
return game;
}

View file

@ -2,25 +2,27 @@
//
// SPDX-License-Identifier: MIT
import { isCheck } from "../actor/actor-prepared-data";
import { isCheck } from "../actor/actor-data-properties";
import { getGame } from "../helpers";
import { DS4Item } from "../item/item";
import { DS4ItemData } from "../item/item-data";
import { createRollCheckMacro } from "../macros/roll-check";
import { createRollItemMacro } from "../macros/roll-item";
import notifications from "../ui/notifications";
export default function registerForHotbarDropHook(): void {
Hooks.on("hotbarDrop", async (hotbar: Hotbar, data: { type: string } & Record<string, unknown>, slot: string) => {
Hooks.on("hotbarDrop", async (hotbar: Hotbar, data: HotbarDropData, slot: string) => {
switch (data.type) {
case "Item": {
if (!("data" in data)) {
return notifications.warn(game.i18n.localize("DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems"));
if (!isItemDropData(data) || !("data" in data)) {
return notifications.warn(
getGame().i18n.localize("DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems"),
);
}
const itemData = data.data as DS4ItemData;
const itemData = data.data;
if (!DS4Item.rollableItemTypes.includes(itemData.type)) {
return notifications.warn(
game.i18n.format("DS4.WarningItemIsNotRollable", {
getGame().i18n.format("DS4.WarningItemIsNotRollable", {
name: itemData.name,
id: itemData._id,
type: itemData.type,
@ -31,10 +33,16 @@ export default function registerForHotbarDropHook(): void {
}
case "Check": {
if (!("data" in data) || typeof data.data !== "string" || !isCheck(data.data)) {
return notifications.warn(game.i18n.localize("DS4.WarningInvalidCheckDropped"));
return notifications.warn(getGame().i18n.localize("DS4.WarningInvalidCheckDropped"));
}
return createRollCheckMacro(data.data, slot);
}
}
});
}
type HotbarDropData = ActorSheet.DropData.Item | ({ type: string } & Partial<Record<string, unknown>>);
function isItemDropData(dropData: HotbarDropData): dropData is ActorSheet.DropData.Item {
return dropData.type === "Item";
}

View file

@ -4,12 +4,15 @@
//
// SPDX-License-Identifier: MIT
import { DS4ActiveEffect } from "../active-effect";
import { DS4Actor } from "../actor/actor";
import { DS4CharacterActorSheet } from "../actor/sheets/character-sheet";
import { DS4CreatureActorSheet } from "../actor/sheets/creature-sheet";
import { DS4 } from "../config";
import { preloadFonts as preloadFonts } from "../fonts";
import registerHandlebarsHelpers from "../handlebars/handlebars-helpers";
import registerHandlebarsPartials from "../handlebars/handlebars-partials";
import { getGame } from "../helpers";
import { DS4Item } from "../item/item";
import { DS4ItemSheet } from "../item/item-sheet";
import logger from "../logger";
@ -28,7 +31,7 @@ export default function registerForInitHook(): void {
async function init() {
logger.info(`Initializing the DS4 Game System\n${DS4.ASCII}`);
game.ds4 = {
getGame().ds4 = {
DS4Actor,
DS4Item,
DS4,
@ -39,8 +42,9 @@ async function init() {
CONFIG.DS4 = DS4;
CONFIG.Actor.entityClass = DS4Actor;
CONFIG.Item.entityClass = DS4Item;
CONFIG.Actor.documentClass = DS4Actor;
CONFIG.Item.documentClass = DS4Item;
CONFIG.ActiveEffect.documentClass = DS4ActiveEffect;
CONFIG.Actor.typeLabels = DS4.i18n.actorTypes;
CONFIG.Item.typeLabels = DS4.i18n.itemTypes;
@ -60,6 +64,24 @@ async function init() {
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("ds4", DS4ItemSheet, { makeDefault: true });
preloadFonts();
await registerHandlebarsPartials();
registerHandlebarsHelpers();
}
declare global {
interface Game {
ds4: {
DS4Actor: typeof DS4Actor;
DS4Item: typeof DS4Item;
DS4: typeof DS4;
createCheckRoll: typeof createCheckRoll;
migration: typeof migration;
macros: typeof macros;
};
}
interface CONFIG {
DS4: typeof DS4;
}
}

View file

@ -6,6 +6,7 @@
// SPDX-License-Identifier: MIT
import { DS4 } from "../config";
import { getGame } from "../helpers";
export default function registerForSetupHooks(): void {
Hooks.once("setup", () => {
@ -21,7 +22,7 @@ function localizeAndSortConfigObjects() {
const localizeObject = <T extends { [s: string]: string }>(obj: T, sort = true): T => {
const localized = Object.entries(obj).map(([key, value]) => {
return [key, game.i18n.localize(value)];
return [key, getGame().i18n.localize(value)];
});
if (sort) localized.sort((a, b) => a[1].localeCompare(b[1]));
return Object.fromEntries(localized);

View file

@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { ModifiableDataBaseTotalMax } from "../common/common-data";
import {
DS4AlphabetDataSourceData,
DS4ArmorDataSourceData,
DS4EquipmentDataSourceData,
DS4LanguageDataSourceData,
DS4LootDataSourceData,
DS4RacialAbilityDataSourceData,
DS4ShieldDataSourceData,
DS4SpecialCreatureAbilityDataSourceData,
DS4SpellDataSourceData,
DS4TalentDataSourceData,
DS4WeaponDataSourceData,
} from "./item-data-source";
declare global {
interface DataConfig {
Item: DS4ItemDataProperties;
}
}
export type DS4ItemDataProperties =
| DS4WeaponDataProperties
| DS4ArmorDataProperties
| DS4ShieldDataProperties
| DS4SpellDataProperties
| DS4EquipmentDataProperties
| DS4LootDataProperties
| DS4TalentDataProperties
| DS4RacialAbilityDataProperties
| DS4LanguageDataProperties
| DS4AlphabetDataProperties
| DS4SpecialCreatureAbilityDataProperties;
export interface DS4WeaponDataProperties {
type: "weapon";
data: DS4WeaponDataPropertiesData;
}
export interface DS4ArmorDataProperties {
type: "armor";
data: DS4ArmorDataPropertiesData;
}
export interface DS4ShieldDataProperties {
type: "shield";
data: DS4ShieldDataPropertiesData;
}
export interface DS4SpellDataProperties {
type: "spell";
data: DS4SpellDataPropertiesData;
}
export interface DS4EquipmentDataProperties {
type: "equipment";
data: DS4EquipmentDataPropertiesData;
}
export interface DS4LootDataProperties {
type: "loot";
data: DS4LootDataPropertiesData;
}
export interface DS4TalentDataProperties {
type: "talent";
data: DS4TalentDataPropertiesData;
}
export interface DS4RacialAbilityDataProperties {
type: "racialAbility";
data: DS4RacialAbilityDataPropertiesData;
}
export interface DS4LanguageDataProperties {
type: "language";
data: DS4LanguageDataPropertiesData;
}
export interface DS4AlphabetDataProperties {
type: "alphabet";
data: DS4AlphabetDataPropertiesData;
}
export interface DS4SpecialCreatureAbilityDataProperties {
type: "specialCreatureAbility";
data: DS4SpecialCreatureAbilityDataPropertiesData;
}
// templates
interface DS4ItemDataPropertiesDataRollable {
rollable: boolean;
}
//types
interface DS4WeaponDataPropertiesData extends DS4WeaponDataSourceData, DS4ItemDataPropertiesDataRollable {}
interface DS4ArmorDataPropertiesData extends DS4ArmorDataSourceData, DS4ItemDataPropertiesDataRollable {}
interface DS4ShieldDataPropertiesData extends DS4ShieldDataSourceData, DS4ItemDataPropertiesDataRollable {}
interface DS4SpellDataPropertiesData extends DS4SpellDataSourceData, DS4ItemDataPropertiesDataRollable {
price: number | null;
}
interface DS4EquipmentDataPropertiesData extends DS4EquipmentDataSourceData, DS4ItemDataPropertiesDataRollable {}
interface DS4LootDataPropertiesData extends DS4LootDataSourceData, DS4ItemDataPropertiesDataRollable {}
interface DS4TalentDataPropertiesData extends DS4TalentDataSourceData, DS4ItemDataPropertiesDataRollable {
rank: ModifiableDataBaseTotalMax<number>;
}
interface DS4RacialAbilityDataPropertiesData
extends DS4RacialAbilityDataSourceData,
DS4ItemDataPropertiesDataRollable {}
interface DS4LanguageDataPropertiesData extends DS4LanguageDataSourceData, DS4ItemDataPropertiesDataRollable {}
interface DS4AlphabetDataPropertiesData extends DS4AlphabetDataSourceData, DS4ItemDataPropertiesDataRollable {}
interface DS4SpecialCreatureAbilityDataPropertiesData
extends DS4SpecialCreatureAbilityDataSourceData,
DS4ItemDataPropertiesDataRollable {}

View file

@ -0,0 +1,184 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
//
// SPDX-License-Identifier: MIT
import { ModifiableDataBaseMax } from "../common/common-data";
import { DS4 } from "../config";
declare global {
interface SourceConfig {
Item: DS4ItemDataSource;
}
}
export type ItemType = keyof typeof DS4.i18n.itemTypes;
export type DS4ItemDataSource =
| DS4WeaponDataSource
| DS4ArmorDataSource
| DS4ShieldDataSource
| DS4SpellDataSource
| DS4EquipmentDataSource
| DS4LootDataSource
| DS4TalentDataSource
| DS4RacialAbilityDataSource
| DS4LanguageDataSource
| DS4AlphabetDataSource
| DS4SpecialCreatureAbilityDataSource;
interface DS4WeaponDataSource {
type: "weapon";
data: DS4WeaponDataSourceData;
}
interface DS4ArmorDataSource {
type: "armor";
data: DS4ArmorDataSourceData;
}
interface DS4ShieldDataSource {
type: "shield";
data: DS4ShieldDataSourceData;
}
interface DS4SpellDataSource {
type: "spell";
data: DS4SpellDataSourceData;
}
interface DS4EquipmentDataSource {
type: "equipment";
data: DS4EquipmentDataSourceData;
}
interface DS4LootDataSource {
type: "loot";
data: DS4LootDataSourceData;
}
interface DS4TalentDataSource {
type: "talent";
data: DS4TalentDataSourceData;
}
interface DS4RacialAbilityDataSource {
type: "racialAbility";
data: DS4RacialAbilityDataSourceData;
}
interface DS4LanguageDataSource {
type: "language";
data: DS4LanguageDataSourceData;
}
interface DS4AlphabetDataSource {
type: "alphabet";
data: DS4AlphabetDataSourceData;
}
interface DS4SpecialCreatureAbilityDataSource {
type: "specialCreatureAbility";
data: DS4SpecialCreatureAbilityDataSourceData;
}
// templates
interface DS4ItemDataSourceDataBase {
description: string;
}
interface DS4ItemDataSourceDataPhysical {
quantity: number;
price: number;
availability: keyof typeof DS4.i18n.itemAvailabilities;
storageLocation: string;
}
export function isDS4ItemDataTypePhysical(input: foundry.data.ItemData["data"]): boolean {
return "quantity" in input && "price" in input && "availability" in input && "storageLocation" in input;
}
interface DS4ItemDataSourceDataEquipable {
equipped: boolean;
}
interface DS4ItemDataSourceDataProtective {
armorValue: number;
}
// types
export interface DS4WeaponDataSourceData
extends DS4ItemDataSourceDataBase,
DS4ItemDataSourceDataPhysical,
DS4ItemDataSourceDataEquipable {
attackType: AttackType;
weaponBonus: number;
opponentDefense: number;
}
export type AttackType = keyof typeof DS4.i18n.attackTypes;
export interface DS4ArmorDataSourceData
extends DS4ItemDataSourceDataBase,
DS4ItemDataSourceDataPhysical,
DS4ItemDataSourceDataEquipable,
DS4ItemDataSourceDataProtective {
armorMaterialType: keyof typeof DS4.i18n.armorMaterialTypes;
armorType: keyof typeof DS4.i18n.armorTypes;
}
export interface DS4ShieldDataSourceData
extends DS4ItemDataSourceDataBase,
DS4ItemDataSourceDataPhysical,
DS4ItemDataSourceDataEquipable,
DS4ItemDataSourceDataProtective {}
export interface DS4SpellDataSourceData extends DS4ItemDataSourceDataBase, DS4ItemDataSourceDataEquipable {
spellType: keyof typeof DS4.i18n.spellTypes;
bonus: string;
spellCategory: keyof typeof DS4.i18n.spellCategories;
maxDistance: UnitData<DistanceUnit>;
effectRadius: UnitData<DistanceUnit>;
duration: UnitData<CustomTemporalUnit>;
cooldownDuration: UnitData<TemporalUnit>;
minimumLevels: {
healer: number | null;
wizard: number | null;
sorcerer: number | null;
};
}
export interface UnitData<UnitType> {
value: string;
unit: UnitType;
}
type DistanceUnit = keyof typeof DS4.i18n.distanceUnits;
type CustomTemporalUnit = keyof typeof DS4.i18n.customTemporalUnits;
export type TemporalUnit = keyof typeof DS4.i18n.temporalUnits;
export interface DS4EquipmentDataSourceData
extends DS4ItemDataSourceDataBase,
DS4ItemDataSourceDataPhysical,
DS4ItemDataSourceDataEquipable {}
export interface DS4LootDataSourceData extends DS4ItemDataSourceDataBase, DS4ItemDataSourceDataPhysical {}
export interface DS4TalentDataSourceData extends DS4ItemDataSourceDataBase {
rank: ModifiableDataBaseMax<number>;
}
export type DS4RacialAbilityDataSourceData = DS4ItemDataSourceDataBase;
export type DS4LanguageDataSourceData = DS4ItemDataSourceDataBase;
export type DS4AlphabetDataSourceData = DS4ItemDataSourceDataBase;
export interface DS4SpecialCreatureAbilityDataSourceData extends DS4ItemDataSourceDataBase {
experiencePoints: number;
}

View file

@ -1,133 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
//
// SPDX-License-Identifier: MIT
import { ModifiableDataBase } from "../common/common-data";
import { DS4 } from "../config";
export type ItemType = keyof typeof DS4.i18n.itemTypes;
export type DS4ItemData =
| DS4WeaponData
| DS4ArmorData
| DS4ShieldData
| DS4SpellData
| DS4EquipmentData
| DS4LootData
| DS4TalentData
| DS4RacialAbilityData
| DS4LanguageData
| DS4AlphabetData
| DS4SpecialCreatureAbilityData;
export interface DS4ItemDataHelper<T, U extends ItemType> extends Item.Data<T> {
type: U;
}
type DS4WeaponData = DS4ItemDataHelper<DS4WeaponDataData, "weapon">;
type DS4ArmorData = DS4ItemDataHelper<DS4ArmorDataData, "armor">;
type DS4ShieldData = DS4ItemDataHelper<DS4ShieldDataData, "shield">;
type DS4SpellData = DS4ItemDataHelper<DS4SpellDataData, "spell">;
type DS4EquipmentData = DS4ItemDataHelper<DS4EquipmentDataData, "equipment">;
type DS4LootData = DS4ItemDataHelper<DS4LootDataData, "loot">;
type DS4TalentData = DS4ItemDataHelper<DS4TalentDataData, "talent">;
type DS4RacialAbilityData = DS4ItemDataHelper<DS4RacialAbilityDataData, "racialAbility">;
type DS4LanguageData = DS4ItemDataHelper<DS4LanguageDataData, "language">;
type DS4AlphabetData = DS4ItemDataHelper<DS4AlphabetDataData, "alphabet">;
type DS4SpecialCreatureAbilityData = DS4ItemDataHelper<DS4SpecialCreatureAbilityDataData, "specialCreatureAbility">;
// templates
interface DS4ItemDataDataBase {
description: string;
}
interface DS4ItemDataDataPhysical {
quantity: number;
price: number;
availability: keyof typeof DS4.i18n.itemAvailabilities;
storageLocation: string;
}
export function isDS4ItemDataTypePhysical(input: DS4ItemData["data"]): boolean {
return "quantity" in input && "price" in input && "availability" in input && "storageLocation" in input;
}
interface DS4ItemDataDataEquipable {
equipped: boolean;
}
interface DS4ItemDataDataProtective {
armorValue: number;
}
export interface UnitData<UnitType> {
value: string;
unit: UnitType;
}
export type TemporalUnit = keyof typeof DS4.i18n.temporalUnits;
type CustomTemporalUnit = keyof typeof DS4.i18n.customTemporalUnits;
type DistanceUnit = keyof typeof DS4.i18n.distanceUnits;
// types
export interface DS4WeaponDataData extends DS4ItemDataDataBase, DS4ItemDataDataPhysical, DS4ItemDataDataEquipable {
attackType: AttackType;
weaponBonus: number;
opponentDefense: number;
}
export type AttackType = keyof typeof DS4.i18n.attackTypes;
export interface DS4ArmorDataData
extends DS4ItemDataDataBase,
DS4ItemDataDataPhysical,
DS4ItemDataDataEquipable,
DS4ItemDataDataProtective {
armorMaterialType: keyof typeof DS4.i18n.armorMaterialTypes;
armorType: keyof typeof DS4.i18n.armorTypes;
}
export interface DS4TalentDataData extends DS4ItemDataDataBase {
rank: DS4TalentRank;
}
export interface DS4TalentRank extends ModifiableDataBase<number> {
max: number;
}
export interface DS4SpellDataData extends DS4ItemDataDataBase, DS4ItemDataDataEquipable {
spellType: keyof typeof DS4.i18n.spellTypes;
bonus: string;
spellCategory: keyof typeof DS4.i18n.spellCategories;
maxDistance: UnitData<DistanceUnit>;
effectRadius: UnitData<DistanceUnit>;
duration: UnitData<CustomTemporalUnit>;
cooldownDuration: UnitData<TemporalUnit>;
minimumLevels: {
healer: number | null;
wizard: number | null;
sorcerer: number | null;
};
}
export interface DS4ShieldDataData
extends DS4ItemDataDataBase,
DS4ItemDataDataPhysical,
DS4ItemDataDataEquipable,
DS4ItemDataDataProtective {}
export interface DS4EquipmentDataData extends DS4ItemDataDataBase, DS4ItemDataDataPhysical, DS4ItemDataDataEquipable {}
export interface DS4LootDataData extends DS4ItemDataDataBase, DS4ItemDataDataPhysical {}
export type DS4RacialAbilityDataData = DS4ItemDataDataBase;
export type DS4LanguageDataData = DS4ItemDataDataBase;
export type DS4AlphabetDataData = DS4ItemDataDataBase;
export interface DS4SpecialCreatureAbilityDataData extends DS4ItemDataDataBase {
experiencePoints: number;
}

View file

@ -1,86 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { HasTotal } from "../common/common-data";
import {
DS4AlphabetDataData,
DS4ArmorDataData,
DS4EquipmentDataData,
DS4ItemDataHelper,
DS4LanguageDataData,
DS4LootDataData,
DS4RacialAbilityDataData,
DS4ShieldDataData,
DS4SpecialCreatureAbilityDataData,
DS4SpellDataData,
DS4TalentDataData,
DS4TalentRank,
DS4WeaponDataData,
} from "./item-data";
export type DS4ItemPreparedData =
| DS4WeaponPreparedData
| DS4ArmorPreparedData
| DS4ShieldPreparedData
| DS4SpellPreparedData
| DS4EquipmentPreparedData
| DS4LootPreparedData
| DS4TalentPreparedData
| DS4RacialAbilityPreparedData
| DS4LanguagePreparedData
| DS4AlphabetPreparedData
| DS4SpecialCreatureAbilityPreparedData;
export type DS4WeaponPreparedData = DS4ItemDataHelper<DS4WeaponPreparedDataData, "weapon">;
export type DS4ArmorPreparedData = DS4ItemDataHelper<DS4ArmorPreparedDataData, "armor">;
export type DS4ShieldPreparedData = DS4ItemDataHelper<DS4ShieldPreparedDataData, "shield">;
export type DS4SpellPreparedData = DS4ItemDataHelper<DS4SpellPreparedDataData, "spell">;
export type DS4EquipmentPreparedData = DS4ItemDataHelper<DS4EquipmentPreparedDataData, "equipment">;
export type DS4LootPreparedData = DS4ItemDataHelper<DS4LootPreparedDataData, "loot">;
export type DS4TalentPreparedData = DS4ItemDataHelper<DS4TalentPreparedDataData, "talent">;
export type DS4RacialAbilityPreparedData = DS4ItemDataHelper<DS4RacialAbilityPreparedDataData, "racialAbility">;
export type DS4LanguagePreparedData = DS4ItemDataHelper<DS4LanguagePreparedDataData, "language">;
export type DS4AlphabetPreparedData = DS4ItemDataHelper<DS4AlphabetPreparedDataData, "alphabet">;
export type DS4SpecialCreatureAbilityPreparedData = DS4ItemDataHelper<
DS4SpecialCreatureAbilityPreparedDataData,
"specialCreatureAbility"
>;
// templates
interface DS4ItemPreparedDataDataRollable {
rollable: boolean;
}
//types
interface DS4WeaponPreparedDataData extends DS4WeaponDataData, DS4ItemPreparedDataDataRollable {}
interface DS4ArmorPreparedDataData extends DS4ArmorDataData, DS4ItemPreparedDataDataRollable {}
interface DS4ShieldPreparedDataData extends DS4ShieldDataData, DS4ItemPreparedDataDataRollable {}
interface DS4SpellPreparedDataData extends DS4SpellDataData, DS4ItemPreparedDataDataRollable {
price: number | null;
}
interface DS4EquipmentPreparedDataData extends DS4EquipmentDataData, DS4ItemPreparedDataDataRollable {}
interface DS4LootPreparedDataData extends DS4LootDataData, DS4ItemPreparedDataDataRollable {}
interface DS4TalentPreparedDataData extends DS4TalentDataData, DS4ItemPreparedDataDataRollable {
rank: DS4TalentPreparedRank;
}
interface DS4TalentPreparedRank extends DS4TalentRank, HasTotal<number> {}
interface DS4RacialAbilityPreparedDataData extends DS4RacialAbilityDataData, DS4ItemPreparedDataDataRollable {}
interface DS4LanguagePreparedDataData extends DS4LanguageDataData, DS4ItemPreparedDataDataRollable {}
interface DS4AlphabetPreparedDataData extends DS4AlphabetDataData, DS4ItemPreparedDataDataRollable {}
interface DS4SpecialCreatureAbilityPreparedDataData
extends DS4SpecialCreatureAbilityDataData,
DS4ItemPreparedDataDataRollable {}

View file

@ -5,19 +5,17 @@
// SPDX-License-Identifier: MIT
import { DS4 } from "../config";
import { getGame } from "../helpers";
import notifications from "../ui/notifications";
import { DS4Item } from "./item";
import { isDS4ItemDataTypePhysical } from "./item-data";
import { isDS4ItemDataTypePhysical } from "./item-data-source";
/**
* The Sheet class for DS4 Items
*/
export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData> {
/** @override */
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
...superDefaultOptions,
static get defaultOptions(): ItemSheet.Options {
return foundry.utils.mergeObject(super.defaultOptions, {
width: 540,
height: 400,
classes: ["ds4", "sheet", "item"],
@ -33,7 +31,7 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
}
/** @override */
async getData(): Promise<ItemSheet.Data<DS4Item>> {
async getData(): Promise<DS4ItemSheetData> {
const data = {
...(await super.getData()),
config: DS4,
@ -45,11 +43,14 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
}
/** @override */
setPosition(options: Partial<Application.Position> = {}): Application.Position & { height: number } {
setPosition(options: Partial<Application.Position> = {}): (Application.Position & { height: number }) | undefined {
const position = super.setPosition(options);
const sheetBody = this.element.find(".sheet-body");
const bodyHeight = position.height - 192;
sheetBody.css("height", bodyHeight);
if (position) {
const sheetBody = this.element.find(".sheet-body");
const bodyHeight = position.height - 192;
sheetBody.css("height", bodyHeight);
}
return position;
}
@ -70,23 +71,25 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
event.preventDefault();
if (this.item.isOwned) {
return notifications.warn(game.i18n.localize("DS4.WarningManageActiveEffectOnOwnedItem"));
return notifications.warn(getGame().i18n.localize("DS4.WarningManageActiveEffectOnOwnedItem"));
}
const a = event.currentTarget;
const li = $(a).parents(".effect");
switch (a.dataset["action"]) {
case "create":
return this._createActiveEffect();
return this.createActiveEffect();
case "edit":
const id = li.data("effectId");
const effect = this.item.effects.get(id);
if (!effect) {
throw new Error(game.i18n.format("DS4.ErrorItemDoesNotHaveEffect", { id, item: this.item.name }));
throw new Error(
getGame().i18n.format("DS4.ErrorItemDoesNotHaveEffect", { id, item: this.item.name }),
);
}
return effect.sheet.render(true);
case "delete": {
return this.item.deleteEmbeddedEntity("ActiveEffect", li.data("effectId"));
return this.item.deleteEmbeddedDocuments("ActiveEffect", [li.data("effectId")]);
}
}
}
@ -94,17 +97,19 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
/**
* Create a new ActiveEffect for the item using default data.
*/
protected async _createActiveEffect(): Promise<ActiveEffect.Data> {
const label = `New Effect`;
protected async createActiveEffect(): Promise<ActiveEffect | undefined> {
const createData = {
label: label,
changes: [],
duration: {},
transfer: true,
label: "New Effect",
icon: "icons/svg/aura.svg",
};
const effect = ActiveEffect.create(createData, this.item);
return effect.create({});
return ActiveEffect.create(createData, { parent: this.item });
}
}
interface DS4ItemSheetData extends ItemSheet.Data<ItemSheet.Options> {
config: typeof DS4;
isOwned: boolean;
actor: DS4ItemSheet["item"]["actor"];
isPhysical: boolean;
}

View file

@ -3,26 +3,29 @@
//
// SPDX-License-Identifier: MIT
import { DS4Actor } from "../actor/actor";
import { DS4 } from "../config";
import { getGame } from "../helpers";
import { createCheckRoll } from "../rolls/check-factory";
import notifications from "../ui/notifications";
import { AttackType, DS4ItemData, ItemType } from "./item-data";
import { DS4ItemPreparedData } from "./item-prepared-data";
import { AttackType, ItemType } from "./item-data-source";
import { calculateSpellPrice } from "./type-specific-helpers/spell";
declare global {
interface DocumentClassConfig {
Item: typeof DS4Item;
}
}
/**
* The Item class for DS4
*/
export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
/**
* @override
*/
export class DS4Item extends Item {
/** @override */
prepareData(): void {
super.prepareData();
this.prepareDerivedData();
}
/** @override */
prepareDerivedData(): void {
if (this.data.type === "talent") {
const data = this.data.data;
@ -63,24 +66,22 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
* Roll a check for an action with this item.
*/
async roll(): Promise<void> {
if (!this.isOwnedItem()) {
throw new Error(game.i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id }));
}
switch (this.data.type) {
case "weapon":
return this.rollWeapon();
case "spell":
return this.rollSpell();
default:
throw new Error(game.i18n.format("DS4.ErrorRollingForItemTypeNotPossible", { type: this.data.type }));
throw new Error(
getGame().i18n.format("DS4.ErrorRollingForItemTypeNotPossible", { type: this.data.type }),
);
}
}
protected async rollWeapon(this: this & { readonly isOwned: true }): Promise<void> {
protected async rollWeapon(): Promise<void> {
if (!(this.data.type === "weapon")) {
throw new Error(
game.i18n.format("DS4.ErrorWrongItemType", {
getGame().i18n.format("DS4.ErrorWrongItemType", {
actualType: this.data.type,
expectedType: "weapon",
id: this.id,
@ -91,7 +92,7 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
if (!this.data.data.equipped) {
return notifications.warn(
game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
getGame().i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
name: this.name,
id: this.id,
type: this.data.type,
@ -99,24 +100,27 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
);
}
const actor = this.actor as unknown as DS4Actor; // TODO(types): Improve so that the concrete Actor type is known here
const ownerDataData = actor.data.data;
if (!this.actor) {
throw new Error(getGame().i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id }));
}
const ownerDataData = this.actor.data.data;
const weaponBonus = this.data.data.weaponBonus;
const combatValue = await this.getCombatValueKeyForAttackType(this.data.data.attackType);
const checkTargetNumber = ownerDataData.combatValues[combatValue].total + weaponBonus;
await createCheckRoll(checkTargetNumber, {
rollMode: game.settings.get("core", "rollMode") as Const.DiceRollMode, // TODO(types): Type this setting in upstream
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
flavor: game.i18n.format("DS4.ItemWeaponCheckFlavor", { actor: actor.name, weapon: this.name }),
flavor: getGame().i18n.format("DS4.ItemWeaponCheckFlavor", { actor: this.actor.name, weapon: this.name }),
});
}
protected async rollSpell(): Promise<void> {
if (!(this.data.type === "spell")) {
throw new Error(
game.i18n.format("DS4.ErrorWrongItemType", {
getGame().i18n.format("DS4.ErrorWrongItemType", {
actualType: this.data.type,
expectedType: "spell",
id: this.id,
@ -127,7 +131,7 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
if (!this.data.data.equipped) {
return notifications.warn(
game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
getGame().i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
name: this.name,
id: this.id,
type: this.data.type,
@ -135,12 +139,15 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
);
}
const actor = this.actor as unknown as DS4Actor; // TODO(types): Improve so that the concrete Actor type is known here
const ownerDataData = actor.data.data;
if (!this.actor) {
throw new Error(getGame().i18n.format("DS4.ErrorCannotRollUnownedItem", { name: this.name, id: this.id }));
}
const ownerDataData = this.actor.data.data;
const spellBonus = Number.isNumeric(this.data.data.bonus) ? parseInt(this.data.data.bonus) : undefined;
if (spellBonus === undefined) {
notifications.info(
game.i18n.format("DS4.InfoManuallyEnterSpellBonus", {
getGame().i18n.format("DS4.InfoManuallyEnterSpellBonus", {
name: this.name,
spellBonus: this.data.data.bonus,
}),
@ -150,10 +157,10 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
const checkTargetNumber = ownerDataData.combatValues[spellType].total + (spellBonus ?? 0);
await createCheckRoll(checkTargetNumber, {
rollMode: game.settings.get("core", "rollMode") as Const.DiceRollMode, // TODO(types): Type this setting in upstream
rollMode: getGame().settings.get("core", "rollMode"),
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
flavor: game.i18n.format("DS4.ItemSpellCheckFlavor", { actor: actor.name, spell: this.name }),
flavor: getGame().i18n.format("DS4.ItemSpellCheckFlavor", { actor: this.actor.name, spell: this.name }),
});
}
@ -162,22 +169,22 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
const { melee, ranged } = { ...DS4.i18n.attackTypes };
const identifier = "attack-type-selection";
return Dialog.prompt({
title: game.i18n.localize("DS4.DialogAttackTypeSelection"),
title: getGame().i18n.localize("DS4.DialogAttackTypeSelection"),
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
selects: [
{
label: game.i18n.localize("DS4.AttackType"),
label: getGame().i18n.localize("DS4.AttackType"),
identifier,
options: { melee, ranged },
},
],
}),
label: game.i18n.localize("DS4.GenericOkButton"),
label: getGame().i18n.localize("DS4.GenericOkButton"),
callback: (html) => {
const selectedAttackType = html.find(`#${identifier}`).val();
if (selectedAttackType !== "melee" && selectedAttackType !== "ranged") {
throw new Error(
game.i18n.format("DS4.ErrorUnexpectedAttackType", {
getGame().i18n.format("DS4.ErrorUnexpectedAttackType", {
actualType: selectedAttackType,
expectedTypes: "'melee', 'ranged'",
}),
@ -190,11 +197,4 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
return `${attackType}Attack` as const;
}
}
/**
* Type-guarding variant to check if the item is owned.
*/
isOwnedItem(): this is this & { readonly isOwned: true } {
return this.isOwned;
}
}

View file

@ -3,9 +3,9 @@
// SPDX-License-Identifier: MIT
import { hoursPerDay, minutesPerHour, secondsPerMinute, secondsPerRound } from "../../common/time-helpers";
import { DS4SpellDataData, TemporalUnit, UnitData } from "../item-data";
import { DS4SpellDataSourceData, TemporalUnit, UnitData } from "../item-data-source";
export function calculateSpellPrice(data: DS4SpellDataData): number | null {
export function calculateSpellPrice(data: DS4SpellDataSourceData): number | null {
const spellPriceFactor = calculateSpellPriceFactor(data.cooldownDuration);
const baseSpellPrices = [
data.minimumLevels.healer !== null ? 10 + (data.minimumLevels.healer - 1) * 35 : null,

View file

@ -8,24 +8,17 @@ const loggingSeparator = "|";
type LogLevel = "debug" | "info" | "warning" | "error";
type LoggingFunction = (...data: unknown[]) => void;
class Logger {
readonly debug: LoggingFunction;
readonly info: LoggingFunction;
readonly warn: LoggingFunction;
readonly error: LoggingFunction;
const getLoggingFunction = (type: LogLevel = "info"): LoggingFunction => {
const log = { debug: console.debug, info: console.info, warning: console.warn, error: console.error }[type];
return (...data: unknown[]) => log(loggingContext, loggingSeparator, ...data);
};
constructor() {
this.debug = this.getLoggingFunction("debug");
this.info = this.getLoggingFunction("info");
this.warn = this.getLoggingFunction("warning");
this.error = this.getLoggingFunction("error");
}
const logger = Object.freeze({
debug: getLoggingFunction("debug"),
info: getLoggingFunction("info"),
warn: getLoggingFunction("warning"),
error: getLoggingFunction("error"),
getLoggingFunction,
});
getLoggingFunction(type: LogLevel = "info") {
const log = { debug: console.debug, info: console.info, warning: console.warn, error: console.error }[type];
return (...data: unknown[]) => log(loggingContext, loggingSeparator, ...data);
}
}
const logger = new Logger();
export default logger;

View file

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
import { DS4Actor } from "../actor/actor";
import { getCanvas } from "../helpers";
import { getCanvas, getGame } from "../helpers";
/**
* Gets the currently active actor based on how {@link ChatMessage} determines
@ -13,13 +13,13 @@ import { getCanvas } from "../helpers";
export function getActiveActor(): DS4Actor | undefined {
const speaker = ChatMessage.getSpeaker();
const speakerToken = speaker.token ? getCanvas().tokens.get(speaker.token) : undefined;
const speakerToken = speaker.token ? getCanvas().tokens?.get(speaker.token) : undefined;
if (speakerToken) {
return speakerToken.actor as DS4Actor;
return speakerToken.actor ?? undefined;
}
const speakerActor = speaker.actor ? game.actors?.get(speaker.actor) : undefined;
const speakerActor = speaker.actor ? getGame().actors?.get(speaker.actor) : undefined;
if (speakerActor) {
return speakerActor as DS4Actor;
return speakerActor;
}
}

View file

@ -2,8 +2,9 @@
//
// SPDX-License-Identifier: MIT
import { Check } from "../actor/actor-prepared-data";
import { Check } from "../actor/actor-data-properties";
import { DS4 } from "../config";
import { getGame } from "../helpers";
import notifications from "../ui/notifications";
import { getActiveActor } from "./helpers";
@ -15,13 +16,13 @@ import { getActiveActor } from "./helpers";
*/
export async function createRollCheckMacro(check: Check, slot: string): Promise<void> {
const macro = await getOrCreateRollCheckMacro(check);
game.user?.assignHotbarMacro(macro, slot);
getGame().user?.assignHotbarMacro(macro ?? null, slot);
}
async function getOrCreateRollCheckMacro(check: Check): Promise<Macro | null> {
async function getOrCreateRollCheckMacro(check: Check): Promise<Macro | undefined> {
const command = `game.ds4.macros.rollCheck("${check}");`;
const existingMacro = game.macros?.entities.find(
const existingMacro = getGame().macros?.find(
(m) => m.name === DS4.i18n.checks[check] && m.data.command === command,
);
if (existingMacro) {
@ -36,7 +37,7 @@ async function getOrCreateRollCheckMacro(check: Check): Promise<Macro | null> {
img: DS4.icons.checks[check],
flags: { "ds4.checkMacro": true },
},
{ displaySheet: false },
{ renderSheet: false },
);
}
@ -46,7 +47,7 @@ async function getOrCreateRollCheckMacro(check: Check): Promise<Macro | null> {
export async function rollCheck(check: Check): Promise<void> {
const actor = getActiveActor();
if (!actor) {
return notifications.warn(game.i18n.localize("DS4.WarningMustControlActorToUseRollCheckMacro"));
return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollCheckMacro"));
}
return actor.rollCheck(check).catch((e) => notifications.error(e, { log: true }));

View file

@ -2,15 +2,17 @@
//
// SPDX-License-Identifier: MIT
import { getGame } from "../helpers";
import notifications from "../ui/notifications";
import { getActiveActor } from "./helpers";
/**
* Executes the roll generic check macro.
*/
export async function rollGenericCheck(): Promise<void> {
const actor = getActiveActor();
if (!actor) {
return notifications.warn(game.i18n.localize("DS4.WarningMustControlActorToUseRollCheckMacro"));
return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollCheckMacro"));
}
return actor.rollGenericCheck().catch((e) => notifications.error(e, { log: true }));

View file

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
import { DS4ItemData } from "../item/item-data";
import { getGame } from "../helpers";
import notifications from "../ui/notifications";
import { getActiveActor } from "./helpers";
@ -12,15 +12,15 @@ import { getActiveActor } from "./helpers";
* @param itemData - The item data
* @param slot - The hotbar slot to use
*/
export async function createRollItemMacro(itemData: DS4ItemData, slot: string): Promise<void> {
export async function createRollItemMacro(itemData: foundry.data.ItemData["_source"], slot: string): Promise<void> {
const macro = await getOrCreateRollItemMacro(itemData);
game.user?.assignHotbarMacro(macro, slot);
getGame().user?.assignHotbarMacro(macro ?? null, slot);
}
async function getOrCreateRollItemMacro(itemData: DS4ItemData): Promise<Macro | null> {
async function getOrCreateRollItemMacro(itemData: foundry.data.ItemData["_source"]): Promise<Macro | undefined> {
const command = `game.ds4.macros.rollItem("${itemData._id}");`;
const existingMacro = game.macros?.entities.find((m) => m.name === itemData.name && m.data.command === command);
const existingMacro = getGame().macros?.find((m) => m.name === itemData.name && m.data.command === command);
if (existingMacro) {
return existingMacro;
}
@ -33,7 +33,7 @@ async function getOrCreateRollItemMacro(itemData: DS4ItemData): Promise<Macro |
img: itemData.img,
flags: { "ds4.itemMacro": true },
},
{ displaySheet: false },
{ renderSheet: false },
);
}
@ -43,13 +43,13 @@ async function getOrCreateRollItemMacro(itemData: DS4ItemData): Promise<Macro |
export async function rollItem(itemId: string): Promise<void> {
const actor = getActiveActor();
if (!actor) {
return notifications.warn(game.i18n.localize("DS4.WarningMustControlActorToUseRollItemMacro"));
return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollItemMacro"));
}
const item = actor.items?.get(itemId);
if (!item) {
return notifications.warn(
game.i18n.format("DS4.WarningControlledActorDoesNotHaveItem", {
getGame().i18n.format("DS4.WarningControlledActorDoesNotHaveItem", {
actorName: actor.name,
actorId: actor.id,
itemId,

View file

@ -2,25 +2,25 @@
//
// SPDX-License-Identifier: MIT
import { getGame } from "./helpers";
import logger from "./logger";
import { migrate as migrate001 } from "./migrations/001";
import { migrate as migrate002 } from "./migrations/002";
import { migrate as migrate003 } from "./migrations/003";
import { migrate as migrate004 } from "./migrations/004";
import notifications from "./ui/notifications";
async function migrate(): Promise<void> {
if (!game.user?.isGM) {
if (!getGame().user?.isGM) {
return;
}
const oldMigrationVersion = game.settings.get("ds4", "systemMigrationVersion");
const oldMigrationVersion = getGame().settings.get("ds4", "systemMigrationVersion");
const targetMigrationVersion = migrations.length;
if (isFirstWorldStart(oldMigrationVersion)) {
game.settings.set("ds4", "systemMigrationVersion", targetMigrationVersion);
getGame().settings.set("ds4", "systemMigrationVersion", targetMigrationVersion);
return;
}
@ -28,7 +28,7 @@ async function migrate(): Promise<void> {
}
async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion: number): Promise<void> {
if (!game.user?.isGM) {
if (!getGame().user?.isGM) {
return;
}
@ -36,7 +36,7 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
if (migrationsToExecute.length > 0) {
notifications.info(
game.i18n.format("DS4.InfoSystemUpdateStart", {
getGame().i18n.format("DS4.InfoSystemUpdateStart", {
currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion,
}),
@ -48,10 +48,10 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
logger.info("executing migration script ", currentMigrationVersion);
try {
await migration();
game.settings.set("ds4", "systemMigrationVersion", currentMigrationVersion);
getGame().settings.set("ds4", "systemMigrationVersion", currentMigrationVersion);
} catch (err) {
notifications.error(
game.i18n.format("DS4.ErrorDuringMigration", {
getGame().i18n.format("DS4.ErrorDuringMigration", {
currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion,
migrationVersion: currentMigrationVersion,
@ -65,7 +65,7 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
}
notifications.info(
game.i18n.format("DS4.InfoSystemUpdateCompleted", {
getGame().i18n.format("DS4.InfoSystemUpdateCompleted", {
currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion,
}),

View file

@ -2,14 +2,18 @@
//
// SPDX-License-Identifier: MIT
import logger from "../logger";
import {
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> {
for (const a of game.actors?.entities ?? []) {
const updateData = getActorUpdateData();
logger.info(`Migrating actor ${a.name}`);
await a.update(updateData, { enforceTypes: false });
}
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium);
}
function getActorUpdateData(): Record<string, unknown> {
@ -32,3 +36,6 @@ function getActorUpdateData(): Record<string, unknown> {
};
return updateData;
}
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getActorUpdateData, getSceneUpdateData });

View file

@ -2,142 +2,33 @@
//
// SPDX-License-Identifier: MIT
import logger from "../logger";
import {
getActorUpdateDataGetter,
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> {
await migrateItems();
await migrateActors();
await migrateScenes();
await migrateCompendiums();
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium);
}
async function migrateItems() {
for (const item of game.items?.entities ?? []) {
try {
const updateData = getItemUpdateData(item._data);
if (updateData) {
logger.info(`Migrating Item entity ${item.name} (${item.id})`);
await item.update(updateData), { enforceTypes: false };
}
} catch (err) {
err.message = `Error during migration of Item entity ${item.name} (${item.id}), continuing anyways.`;
logger.error(err);
}
}
}
function getItemUpdateData(itemData: DeepPartial<Item.Data>) {
function getItemUpdateData(
itemData: Partial<foundry.data.ItemData["_source"]>,
): DeepPartial<foundry.data.ItemData["_source"]> | undefined {
if (!["equipment", "trinket"].includes(itemData.type ?? "")) return undefined;
return { type: itemData.type === "equipment" ? "loot" : "equipment" };
return { type: itemData.type === "equipment" ? ("loot" as const) : ("equipment" as const) };
}
async function migrateActors() {
for (const actor of game.actors?.entities ?? []) {
try {
const updateData = getActorUpdateData(actor._data);
if (updateData) {
logger.info(`Migrating Actor entity ${actor.name} (${actor.id})`);
await actor.update(updateData, { enforceTypes: false });
}
} catch (err) {
err.message = `Error during migration of Actor entity ${actor.name} (${actor.id}), continuing anyways.`;
logger.error(err);
}
}
}
function getActorUpdateData(actorData: DeepPartial<Actor.Data>) {
let hasItemUpdates = false;
const items = actorData.items?.map((itemData) => {
const update = itemData ? getItemUpdateData(itemData) : undefined;
if (update) {
hasItemUpdates = true;
return { ...itemData, ...update };
} else {
return itemData;
}
});
return hasItemUpdates ? { items } : undefined;
}
async function migrateScenes() {
for (const scene of game.scenes?.entities ?? []) {
try {
const updateData = getSceneUpdateData(scene._data);
if (updateData) {
logger.info(`Migrating Scene entity ${scene.name} (${scene.id})`);
await scene.update(updateData, { enforceTypes: false });
}
} catch (err) {
err.message = `Error during migration of Scene entity ${scene.name} (${scene.id}), continuing anyways.`;
logger.error(err);
}
}
}
function getSceneUpdateData(sceneData: Scene.Data) {
let hasTokenUpdates = false;
const tokens = sceneData.tokens.map((tokenData) => {
if (!tokenData.actorId || tokenData.actorLink || tokenData.actorData.data) {
tokenData.actorData = {};
hasTokenUpdates = true;
return tokenData;
}
const token = new Token(tokenData);
if (!token.actor) {
tokenData.actorId = null as unknown as string;
tokenData.actorData = {};
hasTokenUpdates = true;
} else if (!tokenData.actorLink) {
const actorUpdateData = getActorUpdateData(token.data.actorData);
tokenData.actorData = mergeObject(token.data.actorData, actorUpdateData);
hasTokenUpdates = true;
}
return tokenData;
});
if (!hasTokenUpdates) return undefined;
return hasTokenUpdates ? { tokens } : undefined;
}
async function migrateCompendiums() {
for (const compendium of game.packs ?? []) {
if (compendium.metadata.package !== "world") continue;
if (!["Actor", "Item", "Scene"].includes(compendium.metadata.entity)) continue;
await migrateCompendium(compendium);
}
}
async function migrateCompendium(compendium: Compendium) {
const entityName = compendium.metadata.entity;
if (!["Actor", "Item", "Scene"].includes(entityName)) return;
const wasLocked = compendium.locked;
await compendium.configure({ locked: false });
const content = await compendium.getContent();
for (const entity of content) {
try {
const getUpdateData = (entity: Entity) => {
switch (entityName) {
case "Item":
return getItemUpdateData(entity._data);
case "Actor":
return getActorUpdateData(entity._data);
case "Scene":
return getSceneUpdateData(entity._data as Scene.Data);
}
};
const updateData = getUpdateData(entity);
if (updateData) {
logger.info(`Migrating entity ${entity.name} (${entity.id}) in compendium ${compendium.collection}`);
await compendium.updateEntity({ ...updateData, _id: entity._id });
}
} catch (err) {
err.message = `Error during migration of entity ${entity.name} (${entity.id}) in compendium ${compendium.collection}, continuing anyways.`;
logger.error(err);
}
}
await compendium.migrate({});
await compendium.configure({ locked: wasLocked });
}
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator(
{ getItemUpdateData, getActorUpdateData, getSceneUpdateData },
{ migrateToTemplateEarly: false },
);

View file

@ -2,31 +2,24 @@
//
// SPDX-License-Identifier: MIT
import logger from "../logger";
import {
getActorUpdateDataGetter,
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> {
await migrateItems();
await migrateActors();
await migrateScenes();
await migrateCompendiums();
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium);
}
async function migrateItems() {
for (const item of game.items?.entities ?? []) {
try {
const updateData = getItemUpdateData(item._data);
if (updateData) {
logger.info(`Migrating Item entity ${item.name} (${item.id})`);
await item.update(updateData), { enforceTypes: false };
}
} catch (err) {
err.message = `Error during migration of Item entity ${item.name} (${item.id}), continuing anyways.`;
logger.error(err);
}
}
}
function getItemUpdateData(itemData: DeepPartial<Item.Data>) {
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) {
if (!["loot"].includes(itemData.type ?? "")) return undefined;
return {
data: {
@ -35,113 +28,9 @@ function getItemUpdateData(itemData: DeepPartial<Item.Data>) {
};
}
async function migrateActors() {
for (const actor of game.actors?.entities ?? []) {
try {
const updateData = getActorUpdateData(actor._data);
if (updateData) {
logger.info(`Migrating Actor entity ${actor.name} (${actor.id})`);
await actor.update(updateData, { enforceTypes: false });
}
} catch (err) {
err.message = `Error during migration of Actor entity ${actor.name} (${actor.id}), continuing anyways.`;
logger.error(err);
}
}
}
function getActorUpdateData(actorData: DeepPartial<Actor.Data>) {
let hasItemUpdates = false;
const items = actorData.items?.map((itemData) => {
const update = itemData ? getItemUpdateData(itemData) : undefined;
if (update) {
hasItemUpdates = true;
return mergeObject(itemData, update, { enforceTypes: false, inplace: false });
} else {
return itemData;
}
});
return hasItemUpdates ? { items } : undefined;
}
async function migrateScenes() {
for (const scene of game.scenes?.entities ?? []) {
try {
const updateData = getSceneUpdateData(scene._data);
if (updateData) {
logger.info(`Migrating Scene entity ${scene.name} (${scene.id})`);
await scene.update(updateData, { enforceTypes: false });
}
} catch (err) {
err.message = `Error during migration of Scene entity ${scene.name} (${scene.id}), continuing anyways.`;
logger.error(err);
}
}
}
function getSceneUpdateData(sceneData: Scene.Data) {
let hasTokenUpdates = false;
const tokens = sceneData.tokens.map((tokenData) => {
if (!tokenData.actorId || tokenData.actorLink || tokenData.actorData.data) {
tokenData.actorData = {};
hasTokenUpdates = true;
return tokenData;
}
const token = new Token(tokenData);
if (!token.actor) {
tokenData.actorId = null as unknown as string;
tokenData.actorData = {};
hasTokenUpdates = true;
} else if (!tokenData.actorLink) {
const actorUpdateData = getActorUpdateData(token.data.actorData);
tokenData.actorData = mergeObject(token.data.actorData, actorUpdateData);
hasTokenUpdates = true;
}
return tokenData;
});
if (!hasTokenUpdates) return undefined;
return hasTokenUpdates ? { tokens } : undefined;
}
async function migrateCompendiums() {
for (const compendium of game.packs ?? []) {
if (compendium.metadata.package !== "world") continue;
if (!["Actor", "Item", "Scene"].includes(compendium.metadata.entity)) continue;
await migrateCompendium(compendium);
}
}
async function migrateCompendium(compendium: Compendium) {
const entityName = compendium.metadata.entity;
if (!["Actor", "Item", "Scene"].includes(entityName)) return;
const wasLocked = compendium.locked;
await compendium.configure({ locked: false });
const content = await compendium.getContent();
for (const entity of content) {
try {
const getUpdateData = (entity: Entity) => {
switch (entityName) {
case "Item":
return getItemUpdateData(entity._data);
case "Actor":
return getActorUpdateData(entity._data);
case "Scene":
return getSceneUpdateData(entity._data as Scene.Data);
}
};
const updateData = getUpdateData(entity);
if (updateData) {
logger.info(`Migrating entity ${entity.name} (${entity.id}) in compendium ${compendium.collection}`);
await compendium.updateEntity({ ...updateData, _id: entity._id });
}
} catch (err) {
err.message = `Error during migration of entity ${entity.name} (${entity.id}) in compendium ${compendium.collection}, continuing anyways.`;
logger.error(err);
}
}
await compendium.migrate({});
await compendium.configure({ locked: wasLocked });
}
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator(
{ getItemUpdateData, getActorUpdateData },
{ migrateToTemplateEarly: false },
);

View file

@ -2,157 +2,39 @@
//
// SPDX-License-Identifier: MIT
import { DS4SpellDataData } from "../item/item-data";
import logger from "../logger";
import {
getActorUpdateDataGetter,
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> {
await migrateItems();
await migrateActors();
await migrateScenes();
await migrateCompendiums();
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium);
}
async function migrateItems() {
for (const item of game.items?.entities ?? []) {
try {
const updateData = getItemUpdateData(item._data);
if (updateData) {
logger.info(`Migrating Item entity ${item.name} (${item.id})`);
await item.update(updateData), { enforceTypes: false };
}
} catch (err) {
err.message = `Error during migration of Item entity ${item.name} (${item.id}), continuing anyways.`;
logger.error(err);
}
}
}
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) {
if (itemData.type !== "spell") return;
const cooldownDurationUnit: string | undefined = itemData.data?.cooldownDuration.unit;
function getItemUpdateData(itemData: DeepPartial<Item.Data>) {
if (!["spell"].includes(itemData.type ?? "")) return undefined;
const updateData: Record<string, unknown> = {
"-=data.scrollPrice": null,
"data.minimumLevels": { healer: null, wizard: null, sorcerer: null },
data: {
"-=scrollPrice": null,
minimumLevels: { healer: null, wizard: null, sorcerer: null },
cooldownDuration: {
unit: cooldownDurationUnit === "custom" ? "rounds" : cooldownDurationUnit,
},
},
};
if (((itemData.data as DS4SpellDataData).cooldownDuration.unit as string) === "custom") {
updateData["data.cooldownDuration.unit"] = "rounds";
}
return updateData;
}
async function migrateActors() {
for (const actor of game.actors?.entities ?? []) {
try {
const updateData = getActorUpdateData(actor._data);
if (updateData) {
logger.info(`Migrating Actor entity ${actor.name} (${actor.id})`);
await actor.update(updateData, { enforceTypes: false });
}
} catch (err) {
err.message = `Error during migration of Actor entity ${actor.name} (${actor.id}), continuing anyways.`;
logger.error(err);
}
}
}
function getActorUpdateData(actorData: DeepPartial<Actor.Data>) {
let hasItemUpdates = false;
const items = actorData.items?.map((itemData) => {
const update = itemData ? getItemUpdateData(itemData) : undefined;
if (update) {
hasItemUpdates = true;
return mergeObject(itemData, update, { enforceTypes: false, inplace: false });
} else {
return itemData;
}
});
const updateData: Record<string, unknown> = {};
if (actorData.type === "character") {
updateData["data.slayerPoints"] = { value: 0 };
}
if (hasItemUpdates) {
updateData["items"] = items;
}
return updateData;
}
async function migrateScenes() {
for (const scene of game.scenes?.entities ?? []) {
try {
const updateData = getSceneUpdateData(scene._data);
if (updateData) {
logger.info(`Migrating Scene entity ${scene.name} (${scene.id})`);
await scene.update(updateData, { enforceTypes: false });
}
} catch (err) {
err.message = `Error during migration of Scene entity ${scene.name} (${scene.id}), continuing anyways.`;
logger.error(err);
}
}
}
function getSceneUpdateData(sceneData: Scene.Data) {
let hasTokenUpdates = false;
const tokens = sceneData.tokens.map((tokenData) => {
if (!tokenData.actorId || tokenData.actorLink || tokenData.actorData.data) {
tokenData.actorData = {};
hasTokenUpdates = true;
return tokenData;
}
const token = new Token(tokenData);
if (!token.actor) {
tokenData.actorId = null as unknown as string;
tokenData.actorData = {};
hasTokenUpdates = true;
} else if (!tokenData.actorLink) {
const actorUpdateData = getActorUpdateData(token.data.actorData);
tokenData.actorData = mergeObject(token.data.actorData, actorUpdateData);
hasTokenUpdates = true;
}
return tokenData;
});
if (!hasTokenUpdates) return undefined;
return hasTokenUpdates ? { tokens } : undefined;
}
async function migrateCompendiums() {
for (const compendium of game.packs ?? []) {
if (compendium.metadata.package !== "world") continue;
if (!["Actor", "Item", "Scene"].includes(compendium.metadata.entity)) continue;
await migrateCompendium(compendium);
}
}
async function migrateCompendium(compendium: Compendium) {
const entityName = compendium.metadata.entity;
if (!["Actor", "Item", "Scene"].includes(entityName)) return;
const wasLocked = compendium.locked;
await compendium.configure({ locked: false });
await compendium.migrate({});
const content = await compendium.getContent();
for (const entity of content) {
try {
const getUpdateData = (entity: Entity) => {
switch (entityName) {
case "Item":
return getItemUpdateData(entity._data);
case "Actor":
return getActorUpdateData(entity._data);
case "Scene":
return getSceneUpdateData(entity._data as Scene.Data);
}
};
const updateData = getUpdateData(entity);
if (updateData) {
logger.info(`Migrating entity ${entity.name} (${entity.id}) in compendium ${compendium.collection}`);
await compendium.updateEntity({ ...updateData, _id: entity._id });
}
} catch (err) {
err.message = `Error during migration of entity ${entity.name} (${entity.id}) in compendium ${compendium.collection}, continuing anyways.`;
logger.error(err);
}
}
await compendium.configure({ locked: wasLocked });
}
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });

View file

@ -0,0 +1,178 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4Actor } from "../actor/actor";
import { getGame } from "../helpers";
import { DS4Item } from "../item/item";
import logger from "../logger";
type ItemUpdateDataGetter = (
itemData: Partial<foundry.data.ItemData["_source"]>,
) => DeepPartial<foundry.data.ItemData["_source"]> | Record<string, unknown> | undefined;
export async function migrateItems(getItemUpdateData: ItemUpdateDataGetter): Promise<void> {
for (const item of getGame().items ?? []) {
try {
const updateData = getItemUpdateData(item.toObject());
if (updateData) {
logger.info(`Migrating Item document ${item.name} (${item.id})`);
await item.update(updateData), { enforceTypes: false };
}
} catch (err) {
err.message = `Error during migration of Item document ${item.name} (${item.id}), continuing anyways.`;
logger.error(err);
}
}
}
type ActorUpdateDataGetter = (
itemData: Partial<foundry.data.ActorData["_source"]>,
) => DeepPartial<foundry.data.ActorData["_source"]> | undefined;
export async function migrateActors(getActorUpdateData: ActorUpdateDataGetter): Promise<void> {
for (const actor of getGame().actors ?? []) {
try {
const updateData = getActorUpdateData(actor.toObject());
if (updateData) {
logger.info(`Migrating Actor document ${actor.name} (${actor.id})`);
await actor.update(updateData);
}
} catch (err) {
err.message = `Error during migration of Actor document ${actor.name} (${actor.id}), continuing anyways.`;
logger.error(err);
}
}
}
type SceneUpdateDataGetter = (
sceneData: foundry.documents.BaseScene["data"],
) => DeepPartial<foundry.documents.BaseScene["data"]["_source"]>;
export async function migrateScenes(getSceneUpdateData: SceneUpdateDataGetter): Promise<void> {
for (const scene of getGame().scenes ?? []) {
try {
const updateData = getSceneUpdateData(scene.data);
if (updateData) {
logger.info(`Migrating Scene document ${scene.name} (${scene.id})`);
await scene.update(updateData);
}
} catch (err) {
err.message = `Error during migration of Scene document ${scene.name} (${scene.id}), continuing anyways.`;
logger.error(err);
}
}
}
type CompendiumMigrator = (compendium: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>;
export async function migrateCompendiums(migrateCompendium: CompendiumMigrator): Promise<void> {
for (const compendium of getGame().packs ?? []) {
if (compendium.metadata.package !== "world") continue;
if (!["Actor", "Item", "Scene"].includes(compendium.metadata.entity)) continue;
await migrateCompendium(compendium);
}
}
export function getActorUpdateDataGetter(getItemUpdateData: ItemUpdateDataGetter): ActorUpdateDataGetter {
return (
actorData: Partial<foundry.data.ActorData["_source"]>,
): DeepPartial<foundry.data.ActorData["_source"]> | undefined => {
let hasItemUpdates = false;
const items = actorData.items?.map((itemData) => {
const update = getItemUpdateData(itemData);
if (update) {
hasItemUpdates = true;
return { ...itemData, ...update };
} else {
return itemData;
}
});
return hasItemUpdates ? { items } : undefined;
};
}
export function getSceneUpdateDataGetter(getActorUpdateData: ActorUpdateDataGetter): SceneUpdateDataGetter {
return (sceneData: foundry.documents.BaseScene["data"]) => {
const tokens = (sceneData.tokens as Collection<TokenDocument>).map((token: TokenDocument) => {
const t = token.toObject();
if (!t.actorId || t.actorLink) {
t.actorData = {};
} else if (!getGame().actors?.has(t.actorId)) {
t.actorId = null;
t.actorData = {};
} else if (!t.actorLink) {
const actorData = foundry.utils.deepClone(t.actorData);
actorData.type = token.actor?.type;
const update = getActorUpdateData(actorData);
if (update !== undefined) {
["items" as const, "effects" as const].forEach((embeddedName) => {
const embeddedUpdates = update[embeddedName];
if (embeddedUpdates === undefined || !embeddedUpdates.length) return;
const updates = new Map(embeddedUpdates.flatMap((u) => (u && u._id ? [[u._id, u]] : [])));
const originals = t.actorData[embeddedName];
if (!originals) return;
originals.forEach((original) => {
if (!original._id) return;
const update = updates.get(original._id);
if (update) foundry.utils.mergeObject(original, update);
});
delete update[embeddedName];
});
foundry.utils.mergeObject(t.actorData, update);
}
}
return t;
});
return { tokens };
};
}
export function getCompendiumMigrator(
{
getItemUpdateData,
getActorUpdateData,
getSceneUpdateData,
}: {
getItemUpdateData?: ItemUpdateDataGetter;
getActorUpdateData?: ActorUpdateDataGetter;
getSceneUpdateData?: SceneUpdateDataGetter;
} = {},
{ migrateToTemplateEarly = true } = {},
) {
return async (compendium: CompendiumCollection<CompendiumCollection.Metadata>): Promise<void> => {
const entityName = compendium.metadata.entity;
if (!["Actor", "Item", "Scene"].includes(entityName)) return;
const wasLocked = compendium.locked;
await compendium.configure({ locked: false });
if (migrateToTemplateEarly) {
await compendium.migrate();
}
const documents = await compendium.getDocuments();
for (const doc of documents) {
try {
logger.info(`Migrating document ${doc.name} (${doc.id}) in compendium ${compendium.collection}`);
if (doc instanceof DS4Item && getItemUpdateData) {
const updateData = getItemUpdateData(doc.toObject());
updateData && (await doc.update(updateData));
} else if (doc instanceof DS4Actor && getActorUpdateData) {
const updateData = getActorUpdateData(doc.toObject());
updateData && (await doc.update(updateData));
} else if (doc instanceof Scene && getSceneUpdateData) {
const updateData = getSceneUpdateData(doc.data);
updateData && (await doc.update(updateData));
}
} catch (err) {
err.message = `Error during migration of document ${doc.name} (${doc.id}) in compendium ${compendium.collection}, continuing anyways.`;
logger.error(err);
}
}
if (!migrateToTemplateEarly) {
await compendium.migrate();
}
await compendium.configure({ locked: wasLocked });
};
}

View file

@ -3,6 +3,8 @@
//
// SPDX-License-Identifier: MIT
import { getGame } from "../helpers";
export default function evaluateCheck(
dice: number[],
checkTargetNumber: number,
@ -39,7 +41,7 @@ function assignSubChecksToDice(
const requiredNumberOfDice = getRequiredNumberOfDice(checkTargetNumber);
if (dice.length !== requiredNumberOfDice || requiredNumberOfDice < 1) {
throw new Error(game.i18n.localize("DS4.ErrorInvalidNumberOfDice"));
throw new Error(getGame().i18n.localize("DS4.ErrorInvalidNumberOfDice"));
}
const checkTargetNumberForLastSubCheck = checkTargetNumber - 20 * (requiredNumberOfDice - 1);
@ -86,11 +88,7 @@ function shouldUseCoupForLastSubCheck(
);
}
interface SubCheckResult extends DieWithSubCheck, DiceTerm.Result {
success?: boolean;
failure?: boolean;
count?: number;
}
interface SubCheckResult extends DieWithSubCheck, DiceTerm.Result {}
function evaluateDiceWithSubChecks(
results: DieWithSubCheck[],

View file

@ -3,6 +3,8 @@
//
// SPDX-License-Identifier: MIT
import { getGame } from "../helpers";
/**
* Provides default values for all arguments the `CheckFactory` expects.
*/
@ -10,7 +12,7 @@ class DefaultCheckOptions implements DS4CheckFactoryOptions {
readonly maximumCoupResult = 1;
readonly minimumFumbleResult = 20;
readonly useSlayingDice = false;
readonly rollMode: Const.DiceRollMode = "roll";
readonly rollMode: foundry.CONST.DiceRollMode = "roll";
readonly flavor: undefined;
mergeWith(other: Partial<DS4CheckFactoryOptions>): DS4CheckFactoryOptions {
@ -37,15 +39,16 @@ class CheckFactory {
private options: DS4CheckFactoryOptions;
async execute(): Promise<ChatMessage> {
async execute(): Promise<ChatMessage | undefined> {
const innerFormula = ["ds", this.createCheckTargetNumberModifier(), this.createCoupFumbleModifier()].filterJoin(
"",
);
const formula = this.options.useSlayingDice ? `{${innerFormula}}x` : innerFormula;
const roll = Roll.create(formula);
const speaker = ChatMessage.getSpeaker();
return roll.toMessage(
{ speaker: ChatMessage.getSpeaker(), flavor: this.options.flavor },
{ speaker, flavor: this.options.flavor },
{ rollMode: this.options.rollMode, create: true },
);
}
@ -85,7 +88,7 @@ export async function createCheckRoll(
const newOptions: Partial<DS4CheckFactoryOptions> = {
maximumCoupResult: gmModifierData.maximumCoupResult ?? options.maximumCoupResult,
minimumFumbleResult: gmModifierData.minimumFumbleResult ?? options.minimumFumbleResult,
useSlayingDice: game.settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
useSlayingDice: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
rollMode: gmModifierData.rollMode ?? options.rollMode,
flavor: options.flavor,
};
@ -113,13 +116,13 @@ async function askGmModifier(
{ template, title }: { template?: string; title?: string } = {},
): Promise<Partial<IntermediateGmModifierData>> {
const usedTemplate = template ?? "systems/ds4/templates/dialogs/roll-options.hbs";
const usedTitle = title ?? game.i18n.localize("DS4.DialogRollOptionsDefaultTitle");
const usedTitle = title ?? getGame().i18n.localize("DS4.DialogRollOptionsDefaultTitle");
const templateData = {
title: usedTitle,
checkTargetNumber: checkTargetNumber,
maximumCoupResult: options.maximumCoupResult ?? defaultCheckOptions.maximumCoupResult,
minimumFumbleResult: options.minimumFumbleResult ?? defaultCheckOptions.minimumFumbleResult,
rollMode: options.rollMode ?? game.settings.get("core", "rollMode"),
rollMode: options.rollMode ?? getGame().settings.get("core", "rollMode"),
rollModes: CONFIG.Dice.rollModes,
};
const renderedHtml = await renderTemplate(usedTemplate, templateData);
@ -131,11 +134,11 @@ async function askGmModifier(
buttons: {
ok: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("DS4.GenericOkButton"),
label: getGame().i18n.localize("DS4.GenericOkButton"),
callback: (html) => {
if (!("jquery" in html)) {
throw new Error(
game.i18n.format("DS4.ErrorUnexpectedHtmlType", {
getGame().i18n.format("DS4.ErrorUnexpectedHtmlType", {
exType: "JQuery",
realType: "HTMLElement",
}),
@ -144,7 +147,7 @@ async function askGmModifier(
const innerForm = html[0].querySelector("form");
if (!innerForm) {
throw new Error(
game.i18n.format("DS4.ErrorCouldNotFindHtmlElement", { htmlElement: "form" }),
getGame().i18n.format("DS4.ErrorCouldNotFindHtmlElement", { htmlElement: "form" }),
);
}
resolve(innerForm);
@ -153,7 +156,7 @@ async function askGmModifier(
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("DS4.GenericCancelButton"),
label: getGame().i18n.localize("DS4.GenericCancelButton"),
},
},
default: "ok",
@ -174,13 +177,11 @@ function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateGmM
const chosenMinimumFumbleResult = parseInt(formData["minimum-fumble-result"]?.value);
const chosenRollMode = formData["roll-mode"]?.value;
const invalidNumbers = [NaN, Infinity, -Infinity];
return {
checkTargetNumber: invalidNumbers.includes(chosenCheckTargetNumber) ? undefined : chosenCheckTargetNumber,
gmModifier: invalidNumbers.includes(chosenGMModifier) ? undefined : chosenGMModifier,
maximumCoupResult: invalidNumbers.includes(chosenMaximumCoupResult) ? undefined : chosenMaximumCoupResult,
minimumFumbleResult: invalidNumbers.includes(chosenMinimumFumbleResult) ? undefined : chosenMinimumFumbleResult,
checkTargetNumber: Number.isSafeInteger(chosenCheckTargetNumber) ? chosenCheckTargetNumber : undefined,
gmModifier: Number.isSafeInteger(chosenGMModifier) ? chosenGMModifier : undefined,
maximumCoupResult: Number.isSafeInteger(chosenMaximumCoupResult) ? chosenMaximumCoupResult : undefined,
minimumFumbleResult: Number.isSafeInteger(chosenMinimumFumbleResult) ? chosenMinimumFumbleResult : undefined,
rollMode: Object.values(CONST.DICE_ROLL_MODES).includes(chosenRollMode) ? chosenRollMode : undefined,
};
}
@ -190,7 +191,7 @@ function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateGmM
*/
interface GmModifierData {
gmModifier: number;
rollMode: Const.DiceRollMode;
rollMode: foundry.CONST.DiceRollMode;
}
/**
@ -221,6 +222,6 @@ export interface DS4CheckFactoryOptions {
maximumCoupResult: number;
minimumFumbleResult: number;
useSlayingDice: boolean;
rollMode: Const.DiceRollMode;
rollMode: foundry.CONST.DiceRollMode;
flavor?: string;
}

View file

@ -3,6 +3,7 @@
//
// SPDX-License-Identifier: MIT
import { getGame } from "../helpers";
import evaluateCheck, { getRequiredNumberOfDice } from "./check-evaluation";
/**
@ -15,11 +16,12 @@ import evaluateCheck, { getRequiredNumberOfDice } from "./check-evaluation";
* - Roll a check with a racial ability that makes `5` a coup and default fumble: `/r dsv19c5`
*/
export class DS4Check extends DiceTerm {
constructor({ modifiers = [], options }: Partial<DiceTerm.TermData> = {}) {
constructor({ modifiers = [], results = [], options }: Partial<DiceTerm.TermData> = {}) {
super({
faces: 20,
modifiers: modifiers,
options: options,
results,
modifiers,
options,
});
// Parse and store check target number
@ -49,7 +51,7 @@ export class DS4Check extends DiceTerm {
? parseInt(parseMinimumFumbleResult)
: DS4Check.DEFAULT_MINIMUM_FUMBLE_RESULT;
if (this.minimumFumbleResult <= this.maximumCoupResult)
throw new SyntaxError(game.i18n.localize("DS4.ErrorDiceCoupFumbleOverlap"));
throw new SyntaxError(getGame().i18n.localize("DS4.ErrorDiceCoupFumbleOverlap"));
}
// Parse and store no fumble
@ -57,6 +59,10 @@ export class DS4Check extends DiceTerm {
if (noFumbleModifier) {
this.canFumble = false;
}
if (this.results.length > 0) {
this.evaluateResults();
}
}
coup: boolean | null = null;
@ -72,14 +78,14 @@ export class DS4Check extends DiceTerm {
}
/** @override */
get total(): number | null {
get total(): string | number | null | undefined {
if (this.fumble) return 0;
return super.total;
}
/** @override */
evaluate({ minimize = false, maximize = false } = {}): this {
super.evaluate({ minimize, maximize });
_evaluateSync({ minimize = false, maximize = false } = {}): this {
super._evaluateSync({ minimize, maximize });
this.evaluateResults();
return this;
}
@ -102,17 +108,14 @@ export class DS4Check extends DiceTerm {
this.fumble = results[0].failure ?? false;
}
/** @override */
static fromResults<T extends DS4Check>(
this: ConstructorOf<T>,
options: Partial<DiceTerm.TermData>,
results: DiceTerm.Result[],
): T {
const term = new this(options);
term.results = results;
term.evaluateResults();
term._evaluated = true;
return term;
/**
* @override
* @remarks "min" and "max" are filtered out because they are irrelevant for
* {@link DS4Check}s and only result in some dice rolls being highlighted
* incorrectly.
*/
getResultCSS(result: DiceTerm.Result): (string | null)[] {
return super.getResultCSS(result).filter((cssClass) => cssClass !== "min" && cssClass !== "max");
}
static readonly DEFAULT_CHECK_TARGET_NUMBER = 10;

View file

@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: MIT
import { getGame } from "../helpers";
import { DS4Check } from "./check";
export class DS4Roll<D extends Record<string, unknown> = Record<string, unknown>> extends Roll<D> {
@ -12,10 +13,10 @@ export class DS4Roll<D extends Record<string, unknown> = Record<string, unknown>
* template if the first dice term is a ds4 check.
* @override
*/
async render(chatOptions: Roll.ChatOptions = {}): Promise<string> {
chatOptions = mergeObject(
async render(chatOptions: Parameters<Roll["render"]>[0] = {}): Promise<string> {
chatOptions = foundry.utils.mergeObject(
{
user: game.user?._id,
user: getGame().user?.id,
flavor: null,
template: DS4Roll.CHAT_TEMPLATE,
blind: false,
@ -25,7 +26,7 @@ export class DS4Roll<D extends Record<string, unknown> = Record<string, unknown>
const isPrivate = chatOptions.isPrivate;
// Execute the roll, if needed
if (!this._rolled) this.roll();
if (!this._evaluated) this.evaluate();
// Define chat data
const firstDiceTerm = this.dice[0];

View file

@ -3,14 +3,14 @@
//
// SPDX-License-Identifier: MIT
import { getGame } from "../helpers";
import { DS4Check } from "./check";
export default function registerSlayingDiceModifier(): void {
DicePool.MODIFIERS.x = slay;
DicePool.POOL_REGEX = /^{([^}]+)}([A-z]([A-z0-9<=>]+)?)?$/;
PoolTerm.MODIFIERS.x = slay;
}
function slay(this: DicePool, modifier: string): void {
function slay(this: PoolTerm, modifier: string): void {
const rgx = /[xX]/;
const match = modifier.match(rgx);
if (!match || !this.rolls) return;
@ -21,11 +21,12 @@ function slay(this: DicePool, modifier: string): void {
checked++;
if (diceTerm instanceof DS4Check && diceTerm.coup) {
const formula = `dsv${diceTerm.checkTargetNumber}c${diceTerm.maximumCoupResult}:${diceTerm.minimumFumbleResult}n`;
const additionalRoll = Roll.create(formula).evaluate();
const additionalRoll = Roll.create(formula).evaluate({ async: false });
this.rolls.push(additionalRoll);
this.results.push({ result: additionalRoll.total ?? 0, active: true });
this.terms.push(formula);
}
if (checked > 1000) throw new Error(game.i18n.localize("DS4.ErrorSlayingDiceRecursionLimitExceeded"));
if (checked > 1000) throw new Error(getGame().i18n.localize("DS4.ErrorSlayingDiceRecursionLimitExceeded"));
}
}

View file

@ -2,11 +2,13 @@
//
// SPDX-License-Identifier: MIT
import { getGame } from "./helpers";
export function registerSystemSettings(): void {
/**
* Track the migrations version of the latest migration that has been applied
*/
game.settings.register("ds4", "systemMigrationVersion", {
getGame().settings.register("ds4", "systemMigrationVersion", {
name: "System Migration Version",
scope: "world",
config: false,
@ -14,7 +16,7 @@ export function registerSystemSettings(): void {
default: -1,
});
game.settings.register("ds4", "useSlayingDiceForAutomatedChecks", {
getGame().settings.register("ds4", "useSlayingDiceForAutomatedChecks", {
name: "DS4.SettingUseSlayingDiceForAutomatedChecksName",
hint: "DS4.SettingUseSlayingDiceForAutomatedChecksHint",
scope: "world",
@ -23,7 +25,7 @@ export function registerSystemSettings(): void {
default: false,
});
game.settings.register("ds4", "showSlayerPoints", {
getGame().settings.register("ds4", "showSlayerPoints", {
name: "DS4.SettingShowSlayerPointsName",
hint: "DS4.SettingShowSlayerPointsHint",
scope: "world",
@ -33,7 +35,7 @@ export function registerSystemSettings(): void {
});
}
interface DS4Settings {
export interface DS4Settings {
systemMigrationVersion: number;
useSlayingDiceForAutomatedChecks: boolean;
showSlayerPoints: boolean;
@ -41,8 +43,8 @@ interface DS4Settings {
export function getDS4Settings(): DS4Settings {
return {
systemMigrationVersion: game.settings.get("ds4", "systemMigrationVersion"),
useSlayingDiceForAutomatedChecks: game.settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
showSlayerPoints: game.settings.get("ds4", "showSlayerPoints"),
systemMigrationVersion: getGame().settings.get("ds4", "systemMigrationVersion"),
useSlayingDiceForAutomatedChecks: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
showSlayerPoints: getGame().settings.get("ds4", "showSlayerPoints"),
};
}

View file

@ -12,18 +12,18 @@
{"_id":"0vIgZkHBeEPut73w","name":"Elfenbogen +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative +1, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":3325,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":6,"opponentDefense":-3},"flags":{},"img":"icons/weapons/bows/longbow-recurve.webp","effects":[{"_id":"QScLkDv6gysh119m","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"azjxgNJkYNvxg622","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"0wgXMtaVpVJabEun","name":"Dolch +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Initiative +1</p>","quantity":1,"price":1752,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-3},"flags":{},"img":"icons/weapons/daggers/dagger-double-engraved-black.webp","effects":[{"_id":"9jtH6ER0s0I8SPyi","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"JLc9UYdHy4aAeqA2","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"12WbnUt5h84JQxMp","name":"Kettenpanzer +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":null,"quantity":1,"price":4260,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":2,"armorMaterialType":"chain","armorType":"body"},"flags":{},"img":"icons/equipment/chest/breastplate-scale-grey.webp","effects":[{"_id":"gPN9UcowmdjdyGyn","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true}]}
{"name":"Heiltrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieses oftmals rote Getr&auml;nk heilt W20 Lebenskraft.</p>","quantity":1,"price":10,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"19bmt5UJrT3T36wE"}
{"name":"Unverwundbartrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Der Charakter erh&auml;lt f&uuml;r W20 Runden +20 auf seine Abwehr durch diesen meist roten, flockigen Trank. Dieser Bonus gilt auch bei Schaden, gegen den normalerweise keine Abwehr zul&auml;ssig ist.</p>","quantity":1,"price":1000,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"1HjmUAR5mf59yXlw"}
{"_id":"19bmt5UJrT3T36wE","name":"Heiltrank","type":"loot","img":"icons/consumables/potions/bottle-round-corked-red.webp","data":{"description":"<p>Dieses oftmals rote Getr&auml;nk heilt W20 Lebenskraft.</p>","quantity":1,"price":10,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"1HjmUAR5mf59yXlw","name":"Unverwundbartrank","type":"loot","img":"icons/consumables/potions/potion-flask-corked-shiny-red.webp","data":{"description":"<p>Der Charakter erh&auml;lt f&uuml;r W20 Runden +20 auf seine Abwehr durch diesen meist roten, flockigen Trank. Dieser Bonus gilt auch bei Schaden, gegen den normalerweise keine Abwehr zul&auml;ssig ist.</p>","quantity":1,"price":1000,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"1IWsAaMSnz1Q9ZWd","name":"Schleuder +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Distanzmalus -1 pro 2m</p>","quantity":1,"price":1250.1,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":2,"opponentDefense":-2},"flags":{},"img":"icons/weapons/slings/slingshot-wood.webp","effects":[{"_id":"aKfE4S2oocgJMro8","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"1hmprC7XVhIPemy5","name":"Kurzbogen +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative +1</p>","quantity":1,"price":1756,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":3,"opponentDefense":-2},"flags":{},"img":"icons/weapons/bows/shortbow-recurve.webp","effects":[{"_id":"zgiIGlRMVCgAzrn7","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"VkuGm7hES83WX4HD","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"name":"Teleporttrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieser rauchige, wirbelnde Trank wirkt den Zauber <em>Teleport</em> auf den Trinker (keine Probe notwendig), nicht jedoch auf weitere Charaktere.</p>","quantity":1,"price":1000,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"1uHuQJcCjjxzvP4C"}
{"_id":"1uHuQJcCjjxzvP4C","name":"Teleporttrank","type":"loot","img":"icons/consumables/potions/potion-bottle-fumes-blue.webp","data":{"description":"<p>Dieser rauchige, wirbelnde Trank wirkt den Zauber <em>Teleport</em> auf den Trinker (keine Probe notwendig), nicht jedoch auf weitere Charaktere.</p>","quantity":1,"price":1000,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"name":"Rüstung des Löwen","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Eine Plattenr&uuml;stung +2 mit verzierten L&ouml;wenk&ouml;pfen, die <em>Laufen</em> +1,5m gew&auml;hrt.</p>","quantity":1,"price":6800,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":3,"armorMaterialType":"plate","armorType":"body"},"flags":{},"img":"icons/equipment/chest/breastplate-layered-gold.webp","effects":[{"_id":"uOEN4xXL3IcebbJO","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true},{"_id":"iIT1kOsyMJn0mIte","flags":{},"changes":[{"key":"data.combatValues.movement.total","value":1.5,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Laufen +1,5 (magisch)","tint":"","transfer":true}],"_id":"1uYooTtDWgzB9FI9"}
{"name":"Wechselring","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Mittels <strong>Wechsler +V</strong> verleiht dieser Ring +10 auf Proben, um die eigenen Zauber zu wechseln.</p>","quantity":1,"price":1502,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/finger/ring-cabochon-white-blue.webp","effects":[],"_id":"1vrVO2sqFqC4AA1k"}
{"_id":"2BNzuiU6wc3r9ByF","name":"Plattenarmschienen +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":null,"quantity":1,"price":4257,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"plate","armorType":"vambrace"},"flags":{},"img":"icons/equipment/wrist/bracer-armored-steel-blue.webp","effects":[{"_id":"PIRBFfHOrmdREhnH","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":3,"mode":2}],"duration":{},"label":"Panzerung +3 (magisch)","transfer":true}]}
{"_id":"2C0GH1sYXj8QtRTK","name":"Krummsäbel +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1756,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-2},"flags":{},"img":"icons/weapons/swords/scimitar-guard-brown.webp","effects":[{"_id":"xjUL1B0P5jhze3vQ","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"2JQowFF6ZjF90OFI","name":"Hellebarde","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -2, Zerbricht bei Schlagen-Patzer</p>","quantity":1,"price":1754,"availability":"village","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":0},"flags":{},"img":"icons/weapons/polearms/halberd-engraved-black.webp","effects":[{"_id":"APXje5Ppu0d75HNw","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"duration":{},"label":"Initiative -2","transfer":true}]}
{"name":"Abklingring","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Dieser einfache Abklingring senkt die Abklingzeit s&auml;mtlicher Zauber seines Tr&auml;gers um 1.</p>","quantity":1,"price":5252,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/finger/ring-band-engraved-scrolls-silver.webp","effects":[],"_id":"2XfoxOYNOTar9OAt"}
{"name":"Atemfreitrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Der Trinker dieses sprudelnden Trankes braucht f&uuml;r K&Ouml;R in Stunden nicht zu atmen.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"2jgIyVHZYJroSUFY"}
{"_id":"2jgIyVHZYJroSUFY","name":"Atemfreitrank","type":"loot","img":"icons/consumables/potions/bottle-conical-bubbling-blue.webp","data":{"description":"<p>Der Trinker dieses sprudelnden Trankes braucht f&uuml;r K&Ouml;R in Stunden nicht zu atmen.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"2le5COwoh45Pc4oD","name":"Flegel +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Initiative -2</p>","quantity":1,"price":1758,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-1},"flags":{},"img":"icons/weapons/maces/flail-cube-grey.webp","effects":[{"_id":"yXvt3CT4FbXYjIfc","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"duration":{},"label":"Initiative -2","transfer":true},{"_id":"rUye8ORwMGGtWPrr","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"2ydkhz5gDjxAiaYy","name":"Bihänder +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -2, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":2260,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-5},"flags":{},"img":"icons/weapons/swords/greatsword-crossguard-engraved-green.webp","effects":[{"_id":"DaKTtdhRO45QZuDJ","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative -2","tint":"","transfer":true},{"_id":"qdSiJ4l0AuTd68CB","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"34fD45Yzi3s2cSgy","name":"Lederpanzer (Für Reittiere) +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"","quantity":1,"price":4262,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"leather","armorType":"body"},"flags":{},"img":"icons/commodities/leather/leather-leaf-tan.webp","effects":[{"_id":"DY0fXwaK8RHbyo75","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":3,"mode":2}],"duration":{},"label":"Panzerung +3 (magisch)","transfer":true}]}
@ -31,8 +31,8 @@
{"_id":"3pdw4CN8Wc9oCKX5","name":"Schloss: Einfach (SW: 0)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":1,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/containers/chest/chest-reinforced-steel-oak-tan.webp","effects":[]}
{"_id":"3zqSBuiQWIsIov4h","name":"Kletterausrüstung","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":1,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/sundries/survival/rope-wrapped-loops-grey.webp","effects":[]}
{"_id":"4E9WdEs1JaWrCYim","name":"Wagen (4 Räder)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":35,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/commodities/wood/wood-wheel-brown.webp","effects":[]}
{"name":"Rüstung des Kriegers","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Diese aufwendig verzierte Plattenr&uuml;stung +2 gew&auml;hrt ihrem Tr&auml;ger +1 auf <em>K&ouml;rper</em>.</p>","quantity":1,"price":7300,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":3,"armorMaterialType":"plate","armorType":"body"},"flags":{},"img":"icons/equipment/chest/breastplate-metal-white-02.webp","effects":[{"_id":"uOEN4xXL3IcebbJO","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true},{"_id":"TZoEpatdi8z1nreX","flags":{},"changes":[{"key":"data.attributes.body.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Körper +1 (magisch)","tint":"","transfer":true}],"_id":"55AkLjiaIn0SWO9k"}
{"_id":"5DY52CR03xXJHG6m","name":"Plattenpanzer +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":null,"quantity":1,"price":6300,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":3,"armorMaterialType":"plate","armorType":"body"},"flags":{},"img":"icons/equipment/chest/breastplate-metal-pieced-grey-02.webp","effects":[{"_id":"wz2krJzwVba18XvV","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":3,"mode":2}],"duration":{},"label":"Panzerung +3 (magisch)","transfer":true}]}
{"_id":"55AkLjiaIn0SWO9k","name":"Rüstung des Kriegers","type":"armor","img":"icons/equipment/chest/breastplate-collared-steel.webp","data":{"description":"<p>Diese aufwendig verzierte Plattenr&uuml;stung +2 gew&auml;hrt ihrem Tr&auml;ger +1 auf <em>K&ouml;rper</em>.</p>","quantity":1,"price":7300,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":3,"armorMaterialType":"plate","armorType":"body"},"effects":[{"_id":"uOEN4xXL3IcebbJO","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":"2","mode":2}],"duration":{"startTime":null},"label":"Panzerung +2 (magisch)","transfer":true,"disabled":false},{"_id":"TZoEpatdi8z1nreX","flags":{},"changes":[{"key":"data.attributes.body.total","value":"1","mode":2}],"disabled":false,"duration":{"startTime":null},"icon":null,"label":"Körper +1 (magisch)","tint":null,"transfer":true}],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"5DY52CR03xXJHG6m","name":"Plattenpanzer +3","type":"armor","img":"icons/equipment/chest/breastplate-gorget-steel.webp","data":{"description":null,"quantity":1,"price":6300,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":3,"armorMaterialType":"plate","armorType":"body"},"effects":[{"_id":"wz2krJzwVba18XvV","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":"3","mode":2}],"duration":{"startTime":null},"label":"Panzerung +3 (magisch)","transfer":true,"disabled":false}],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"5KxdKllRXuau0Uhm","name":"Breitschwert","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":8,"availability":"hamlet","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":1,"opponentDefense":-2},"flags":{},"img":"icons/weapons/swords/sword-broad-worn.webp","effects":[]}
{"_id":"5MrsKOS4sAxpMv2U","name":"Lanze +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Nur im Trab (WB +1) oder Galopp (WB +4)</p>","quantity":1,"price":1752,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":-2},"flags":{},"img":"icons/weapons/polearms/spear-flared-blue.webp","effects":[{"_id":"iLA1AQrTlmA0BFnC","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"name":"Immertreff","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Ein t&ouml;dlicher Langbogen +2 mit <strong>Fieser Schuss +II</strong> und <strong>Scharfsch&uuml;tze +II</strong>.</p>\n<p>Zweih&auml;ndig, Initiative +1, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":10660,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":4,"opponentDefense":-2},"flags":{},"img":"icons/weapons/bows/longbow-gold-pink.webp","effects":[{"_id":"XsqzwEX1AvYJyczG","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"iUXn0CQKZw6Rr1yH","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}],"_id":"5cqP2SvMe5j0BD3t"}
@ -40,7 +40,7 @@
{"_id":"6QehiJpVqqA9bW3P","name":"Hammer +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1757,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-3},"flags":{},"img":"icons/weapons/hammers/shorthammer-double-steel-embossed.webp","effects":[{"_id":"xQlGUPOpB6Me9OgF","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"name":"Gewänder des Adlers","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Diese hellbeige, mit Adlerfedern verzierte Lederr&uuml;stung +1 gew&auml;hrt +1 auf <em>Geist</em>.</p>","quantity":1,"price":4254,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"leather","armorType":"body"},"flags":{},"img":"icons/equipment/chest/breastplate-banded-simple-leather-brown.webp","effects":[{"_id":"CUa4rA1A1cwWac46","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true},{"_id":"cl2PqWeAtDsBjz8k","flags":{},"changes":[{"key":"data.attributes.mind.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Geist +1 (magisch)","tint":"","transfer":true}],"_id":"6UBvjMJd6n5P5YWv"}
{"_id":"6WqPqjMQMMPjV99U","name":"Kurzbogen +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative +1</p>","quantity":1,"price":1256,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":2,"opponentDefense":-1},"flags":{},"img":"icons/weapons/bows/shortbow-recurve-leather.webp","effects":[{"_id":"zgiIGlRMVCgAzrn7","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"8iny3Tt6i6g5EcYh","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Trank der Lebenskraft","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Diese meist blutroten Tr&auml;nke erh&ouml;hen die Lebenskraft um W20 f&uuml;r W20 Stunden.</p>","quantity":1,"price":500,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"74iFRkzvOwLxOxOq"}
{"_id":"74iFRkzvOwLxOxOq","name":"Trank der Lebenskraft","type":"loot","img":"icons/consumables/potions/potion-bottle-corked-labeled-red.webp","data":{"description":"<p>Diese meist blutroten Tr&auml;nke erh&ouml;hen die Lebenskraft um W20 f&uuml;r W20 Stunden.</p>","quantity":1,"price":500,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"7CCoTap4GzN1uSNw","name":"Krummschwert +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1757,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-1},"flags":{},"img":"icons/weapons/swords/scimitar-guard-wood.webp","effects":[{"_id":"Vg3Q9A7QIXHrABFk","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"7JCc96rbTbTSASbT","name":"Metallbesteck","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":4,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/tools/cooking/fork-steel-grey.webp","effects":[]}
{"_id":"7g4vNrJOoX0v4Byu","name":"Keule +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":null,"quantity":1,"price":2250.2,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-3},"flags":{},"img":"icons/weapons/clubs/club-banded-brown.webp","effects":[{"_id":"8y8e8BsFQZZkwUXk","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
@ -54,7 +54,7 @@
{"_id":"99xGE9jkmXEXyf4U","name":"Plattenpanzer (Für Reittiere) +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Laufen -0,5m</p>","quantity":1,"price":4400,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":3,"armorMaterialType":"plate","armorType":"body"},"flags":{},"img":"icons/commodities/metal/mail-plate-steel.webp","effects":[{"_id":"pAxAXam5JBN6Acz9","flags":{},"changes":[{"key":"data.combatValues.movement.total","value":-0.5,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Laufen -0,5m","tint":"","transfer":true},{"_id":"Jkhqmke6gzWU8vPZ","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true}]}
{"_id":"9H3CBGOLUJ5JCHGJ","name":"Plattenbeinschienen","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Laufen -0,5m</p>","quantity":1,"price":8,"availability":"village","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"plate","armorType":"greaves"},"flags":{},"img":"icons/equipment/leg/pants-armored-tasset-steel.webp","effects":[{"_id":"sIbiQW6tSjZyfCzk","flags":{},"changes":[{"key":"data.combatValues.movement.total","value":-0.5,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Laufen -0,5m","tint":"","transfer":true}]}
{"_id":"9MsBlnMhvAMe4zTk","name":"Leichte Armbrust +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -2</p>","quantity":1,"price":1758,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":3,"opponentDefense":-1},"flags":{},"img":"icons/weapons/crossbows/crossbow-simple-brown.webp","effects":[{"_id":"gMm2PnBADqXBrCi1","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative -2","tint":"","transfer":true},{"_id":"sACwF2e5qLRs6c1a","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Andauernder Heiltrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieses oft purpurne Getr&auml;nk heilt 2W20 Runden lang 1 Lebenskraft/Runde.</p>","quantity":1,"price":20,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"9NUM3H7oTfrHhFpD"}
{"_id":"9NUM3H7oTfrHhFpD","name":"Andauernder Heiltrank","type":"loot","img":"icons/consumables/potions/bottle-round-corked-pink.webp","data":{"description":"<p>Dieses oft purpurne Getr&auml;nk heilt 2W20 Runden lang 1 Lebenskraft/Runde.</p>","quantity":1,"price":20,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"9PdQv9CRy4ckaF0j","name":"Metallhelm +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":null,"quantity":1,"price":3256,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"plate","armorType":"helmet"},"flags":{},"img":"icons/equipment/head/helm-barbute-engraved.webp","effects":[{"_id":"9IOcWr1CbRpF3lya","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true}]}
{"_id":"9aY1zWfD5RwZlAUH","name":"Tee (10 Tassen)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.05,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/commodities/flowers/buds-red-green.webp","effects":[]}
{"name":"Smaragd-Schlüssel","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"loot","data":{"description":"<p>Einmal alle 24h kann man damit den Zauber <em>&Ouml;ffnen</em> auf ein Schloss wirken.</p>","quantity":1,"price":null,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/sundries/misc/key-jeweled-gold-purple.webp","effects":[],"_id":"9xQRXWUYj3giHVRC"}
@ -68,7 +68,7 @@
{"_id":"Aw9aoumlI69gYv75","name":"Lederschienen +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>An Arm &amp; Bein</p>","quantity":1,"price":4254,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"leather","armorType":"vambraceGreaves"},"flags":{},"img":"icons/equipment/wrist/bracer-banded-leather-black.webp","effects":[{"_id":"LE8TRjZF13O6kDB8","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":3,"mode":2}],"duration":{},"label":"Panzerung +3 (magisch)","transfer":true}]}
{"_id":"BeXHrv1TQzgfV6Mc","name":"Umhängetasche","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.5,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/containers/bags/pack-leather-embossed-brown.webp","effects":[]}
{"_id":"BrsnuGuOEfolt9VV","name":"Flegel +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Initiative -2</p>","quantity":1,"price":2758,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":5,"opponentDefense":-3},"flags":{},"img":"icons/weapons/maces/flail-studded-grey.webp","effects":[{"_id":"yXvt3CT4FbXYjIfc","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"duration":{},"label":"Initiative -2","transfer":true},{"_id":"98YoU1PGqcSm47Zw","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"C2ggZE3ifOXlonkj","name":"Laternenöl (brennt 4h)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.05,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/containers/kitchenware/vase-009.webp","effects":[]}
{"_id":"C2ggZE3ifOXlonkj","name":"Laternenöl (brennt 4h)","type":"loot","img":"icons/containers/kitchenware/vase-clay-cracked-white.webp","data":{"description":"","quantity":1,"price":0.05,"availability":"hamlet","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"CHRqMQxkgz3jad9J","name":"Schlagring +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Wie waffenlos, Gegner aber kein Abwehr-Bonus</p>","quantity":1,"price":1751,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-3},"flags":{},"img":"icons/weapons/fist/fist-knuckles-brass.webp","effects":[{"_id":"K3m0tLhoW9vT6e19","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"name":"Blutrüstung","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>In diese rotgef&auml;rbte Plattenr&uuml;stung +1 ist das Talent <strong>Verletzen +I</strong> eingebettet.</p>\n<p>Laufen -0,5m</p>","quantity":1,"price":5300,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":3,"armorMaterialType":"plate","armorType":"body"},"flags":{},"img":"icons/equipment/chest/breastplate-rivited-red.webp","effects":[{"_id":"pAxAXam5JBN6Acz9","flags":{},"changes":[{"key":"data.combatValues.movement.total","value":-0.5,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Laufen -0,5m","tint":"","transfer":true},{"_id":"CXouU4P0kZYU6oXR","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true}],"_id":"CQZtlL8zUdOVUVrU"}
{"_id":"CsUnbnytOapKsjuW","name":"Kartenspiel","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":1,"availability":"village","storageLocation":"-"},"flags":{},"img":"icons/sundries/gaming/playing-cards-black.webp","effects":[]}
@ -109,7 +109,7 @@
{"_id":"JZDsSUkQGVf0aOjH","name":"Flegel +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Initiative -2</p>","quantity":1,"price":2258,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-2},"flags":{},"img":"icons/weapons/maces/flail-spiked-grey.webp","effects":[{"_id":"yXvt3CT4FbXYjIfc","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"duration":{},"label":"Initiative -2","transfer":true},{"_id":"PD6vgViNBP4QaxMK","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"JZkzRagRS8TKZplw","name":"Zwergenaxt +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -1</p>","quantity":1,"price":2310,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-3},"flags":{},"img":"icons/weapons/axes/axe-double-engraved-runes.webp","effects":[{"_id":"Ytio5tOcCUO91MQ0","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-1,"mode":2}],"duration":{},"label":"Initiative -1","transfer":true},{"_id":"armx56Psy1qosJib","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"JjM6nTZzV28ioIFq","name":"Feuerstein & Zunder","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.05,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/commodities/stone/geode-raw-white.webp","effects":[]}
{"name":"Stärketrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieser nach Schwei&szlig; riechende Trank verdoppelt ST f&uuml;r ST in Runden.</p>","quantity":1,"price":150,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"JlcYB53S1wQRfmUG"}
{"_id":"JlcYB53S1wQRfmUG","name":"Stärketrank","type":"loot","img":"icons/consumables/potions/potion-bottle-corked-stopper-yellow.webp","data":{"description":"<p>Dieser nach Schwei&szlig; riechende Trank verdoppelt ST f&uuml;r ST in Runden.</p>","quantity":1,"price":150,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"name":"Skrupelloser Bogen","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Ein Kurzbogen +1 mit <em>Kleiner Terror</em>, der Feinde angeblich immer in den R&uuml;cken trifft.</p>\n<p>Zweih&auml;ndig, Initiative +1</p>","quantity":1,"price":2006,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":2,"opponentDefense":-1},"flags":{},"img":"icons/weapons/bows/shortbow-recurve-red.webp","effects":[{"_id":"zgiIGlRMVCgAzrn7","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"8iny3Tt6i6g5EcYh","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}],"_id":"K4fd3AlpMoK1EZeg"}
{"name":"Robe der Macht","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Diese violette Robe +3 verleiht ihrem Tr&auml;ger +1 auf <em>Geist</em>.</p>","quantity":1,"price":5251,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":0,"armorMaterialType":"cloth","armorType":"body"},"flags":{},"img":"icons/equipment/chest/robe-collared-pink.webp","effects":[{"_id":"Qpy3XcHOokbU7ZaS","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":3,"mode":2}],"duration":{},"label":"Panzerung +3 (magisch)","transfer":true},{"_id":"tGAxxMZu2cj0Pzs2","flags":{},"changes":[{"key":"data.attributes.mind.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Geist +1 (magisch)","tint":"","transfer":true}],"_id":"KGk7UFwLwrsdPuQe"}
{"_id":"KJsCiqvtPqjyu65Y","name":"Plattenbeinschienen +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":null,"quantity":1,"price":2258,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"plate","armorType":"greaves"},"flags":{},"img":"icons/equipment/leg/cuisses-reticulated-steel.webp","effects":[{"_id":"I32OnPe7rgPEHtEk","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true}]}
@ -117,18 +117,18 @@
{"_id":"KefO4lxHxwddpFFu","name":"Holzschild +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"shield","data":{"description":null,"quantity":1,"price":4251,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1},"flags":{},"img":"icons/equipment/shield/round-wooden-boss-gold-brown.webp","effects":[{"_id":"AkDi2TNzgpT2ZfrH","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":3,"mode":2}],"duration":{},"label":"Panzerung +3 (magisch)","transfer":true}]}
{"_id":"KyhMB9Jn8Vaxs4eF","name":"Axt +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":2256,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-3},"flags":{},"img":"icons/weapons/axes/shortaxe-yellow.webp","effects":[{"_id":"yaR4SKssj8gYJB91","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"L2ZE2l98snBZw5DZ","name":"Federkiel","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.1,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/tools/scribal/ink-quill-pink.webp","effects":[]}
{"name":"Verkleinerungstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Verkleinert den Trinkenden auf ein Zehntel seiner normalen Gr&ouml;&szlig;e f&uuml;r W20 Minuten. K&Ouml;R, ST und H&Auml; werden solange halbiert und die Kampfwerte entsprechend angepasst.</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"LAI81qZlbkr7MlGY"}
{"_id":"LAI81qZlbkr7MlGY","name":"Verkleinerungstrank","type":"loot","img":"icons/consumables/potions/potion-flask-corked-yellow.webp","data":{"description":"<p>Verkleinert den Trinkenden auf ein Zehntel seiner normalen Gr&ouml;&szlig;e f&uuml;r W20 Minuten. K&Ouml;R, ST und H&Auml; werden solange halbiert und die Kampfwerte entsprechend angepasst.</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"Li5rC0lvytKRAc31","name":"Turmschild +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"shield","data":{"description":null,"quantity":1,"price":4265,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":2},"flags":{},"img":"icons/equipment/shield/heater-steel-gold.webp","effects":[{"_id":"yfHGGfxxGvmHG6b9","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true}]}
{"name":"Zaubertrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Erh&ouml;ht die Werte von <em>Zaubern</em> und <em>Zielzauber</em> f&uuml;r die Dauer eines Kampfes um +1.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"LoY2CnEEWfxbvjEt"}
{"_id":"LoY2CnEEWfxbvjEt","name":"Zaubertrank","type":"loot","img":"icons/consumables/potions/bottle-pear-corked-pink.webp","data":{"description":"<p>Erh&ouml;ht die Werte von <em>Zaubern</em> und <em>Zielzauber</em> f&uuml;r die Dauer eines Kampfes um +1.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"MO1ga2aLjjkBZRzt","name":"Kurzschwert +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":null,"quantity":1,"price":1756,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-2},"flags":{},"img":"icons/weapons/swords/shortsword-guard-silver.webp","effects":[{"_id":"qkDy1MWD6Fkk97e6","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"N3RcggWJuKGtKZyP","name":"Schlachtbeil +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -6, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":2770,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":5,"opponentDefense":-5},"flags":{},"img":"icons/weapons/axes/axe-battle-blackened.webp","effects":[{"_id":"atmWPMxgNoeole7n","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-6,"mode":2}],"duration":{},"label":"Initiative -6","transfer":true},{"_id":"aHr33jsfG78BJ7m2","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"NHV9ho8tGutv0mrS","name":"Zwergenaxt +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -1</p>","quantity":1,"price":2810,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":5,"opponentDefense":-4},"flags":{},"img":"icons/weapons/axes/axe-double-engraved-black.webp","effects":[{"_id":"Ytio5tOcCUO91MQ0","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-1,"mode":2}],"duration":{},"label":"Initiative -1","transfer":true},{"_id":"WsibIQWwcjabH8QS","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"NMmZ3Uu9uwAHc5IP","name":"Schwere Armbrust +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -4</p>","quantity":1,"price":2765,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":5,"opponentDefense":-4},"flags":{},"img":"icons/weapons/crossbows/crossbow-blue.webp","effects":[{"_id":"N4vxVFNLW9Q9QsIp","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative -4","tint":"","transfer":true},{"_id":"sOBnfFviucXKykt1","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"NSg4SdEHDuCfwXji","name":"Robe +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"","quantity":1,"price":2251,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":0,"armorMaterialType":"cloth","armorType":"body"},"flags":{},"img":"icons/equipment/chest/robe-layered-red.webp","effects":[{"_id":"0GJ943ZfeGKcsb2l","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true}]}
{"name":"Abklingtrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Diese meist hellblauen Tr&auml;nke halbieren (abrunden) die Abklingzeit aller Zauber f&uuml;r die Dauer eines Kampfes.</p>","quantity":1,"price":50,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"NZPp8EGEg1JmXdNd"}
{"_id":"NZPp8EGEg1JmXdNd","name":"Abklingtrank","type":"loot","img":"icons/consumables/potions/bottle-round-corked-blue.webp","data":{"description":"<p>Diese meist hellblauen Tr&auml;nke halbieren (abrunden) die Abklingzeit aller Zauber f&uuml;r die Dauer eines Kampfes.</p>","quantity":1,"price":50,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"Nb65CmtFiiWPpnNZ","name":"Schlachtbeil +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -6, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":3770,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":7,"opponentDefense":-7},"flags":{},"img":"icons/weapons/axes/axe-battle-heavy-black.webp","effects":[{"_id":"atmWPMxgNoeole7n","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-6,"mode":2}],"duration":{},"label":"Initiative -6","transfer":true},{"_id":"DJQ1hHJRJuraxIym","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"Nz6gFGSHzHVO3QxA","name":"Breitschwert +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1258,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":-3},"flags":{},"img":"icons/weapons/swords/sword-broad-red.webp","effects":[{"_id":"M7zvcLqDr9HuzqTV","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Schwebentrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieses meist gr&uuml;nliche Getr&auml;nk wirkt den Zauber <em>Schweben</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"OZU7ietSpnivKPVt"}
{"_id":"OZU7ietSpnivKPVt","name":"Schwebentrank","type":"loot","img":"icons/consumables/potions/bottle-bulb-corked-green.webp","data":{"description":"<p>Dieses meist gr&uuml;nliche Getr&auml;nk wirkt den Zauber <em>Schweben</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"name":"Elfenstiefel","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Diese bequemen Stiefel erh&ouml;hen den <em>Laufen</em>-Wert um 1.</p>","quantity":1,"price":1251.5,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/feet/boots-leather-green.webp","effects":[{"_id":"mdjigDqsRTZyFNmC","flags":{},"changes":[{"key":"data.combatValues.movement.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Laufen +1 (magisch)","tint":"","transfer":true}],"_id":"OaG6IhVfS6EmHRux"}
{"_id":"OvxWaEJrElas3EUL","name":"Waffenlos","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":0,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":0,"opponentDefense":5},"flags":{},"img":"icons/equipment/hand/gauntlet-tooled-leather-brown.webp","effects":[]}
{"name":"Elfischer Tarnumhang","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Dieser Umhang, in den Feengarn eingewebt wurde, verleiht zus&auml;tzlich zu seiner eingebetteten <strong>Heimlichkeit +III</strong> auf Verbergen-Proben +3.</p>","quantity":1,"price":875.5,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/back/cloak-collared-leaves-green.webp","effects":[],"_id":"PE3eWmsGcd5dVVh4"}
@ -150,10 +150,10 @@
{"name":"Orkspalter","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Eine alte, legend&auml;re Zwergenaxt +1 mit <strong>Brutaler Hieb +II</strong>.</p>\n<p>Zweih&auml;ndig, Initiative -1</p>","quantity":1,"price":4310,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-3},"flags":{},"img":"icons/weapons/axes/axe-double-engraved-runes.webp","effects":[{"_id":"Ytio5tOcCUO91MQ0","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-1,"mode":2}],"duration":{},"label":"Initiative -1","transfer":true},{"_id":"armx56Psy1qosJib","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}],"_id":"RGyfv3y0LxHORAzA"}
{"_id":"RKKhoedvylYWFBN3","name":"Kurzbogen +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative +1</p>","quantity":1,"price":2256,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":4,"opponentDefense":-3},"flags":{},"img":"icons/weapons/bows/shortbow-recurve-blue.webp","effects":[{"_id":"zgiIGlRMVCgAzrn7","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"OQr5POyJ0Azklftu","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"RWSRxi9np1UrEi7N","name":"Schwere Armbrust +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -4</p>","quantity":1,"price":3265,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":6,"opponentDefense":-5},"flags":{},"img":"icons/weapons/crossbows/crossbow-ornamental-black.webp","effects":[{"_id":"N4vxVFNLW9Q9QsIp","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative -4","tint":"","transfer":true},{"_id":"SlJV88ZHTV0VORzk","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"name":"Verjüngungstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Der Trinkende wird augenblicklich W20 Jahre j&uuml;nger.</p>","quantity":1,"price":5000,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"RlA4PIa9hnsqoqFi"}
{"_id":"RoXGTPdisjn6AdYK","name":"Weihwasser (1/2 Liter)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":null,"quantity":1,"price":0.1,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/containers/kitchenware/vase-005.webp","effects":[]}
{"_id":"RlA4PIa9hnsqoqFi","name":"Verjüngungstrank","type":"loot","img":"icons/consumables/potions/potion-tube-corked-orange.webp","data":{"description":"<p>Der Trinkende wird augenblicklich W20 Jahre j&uuml;nger.</p>","quantity":1,"price":5000,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"RoXGTPdisjn6AdYK","name":"Weihwasser (1/2 Liter)","type":"loot","img":"icons/consumables/potions/bottle-conical-corked-labeled-shell-cyan.webp","data":{"description":null,"quantity":1,"price":0.1,"availability":"hamlet","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"RsKimH7snoPFlg4L","name":"Plattenarmschienen","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Laufen -0,5m</p>","quantity":1,"price":7,"availability":"village","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"plate","armorType":"vambrace"},"flags":{},"img":"icons/equipment/wrist/bracer-armored-steel.webp","effects":[{"_id":"DxcTuYT1mUpj9RdQ","flags":{},"changes":[{"key":"data.combatValues.movement.total","value":-0.5,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Laufen -0,5m","tint":"","transfer":true}]}
{"name":"Großer Schutztrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Erh&ouml;ht f&uuml;r W20 Runden die Abwehr um +3.</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"Ryv745YriIZNKXG5"}
{"_id":"Ryv745YriIZNKXG5","name":"Großer Schutztrank","type":"loot","img":"icons/consumables/potions/bottle-conical-corked-tied-blue.webp","data":{"description":"<p>Erh&ouml;ht f&uuml;r W20 Runden die Abwehr um +3.</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"S0EV25lf5TbN6COJ","name":"Schwere Armbrust +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -4</p>","quantity":1,"price":2265,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":4,"opponentDefense":-3},"flags":{},"img":"icons/weapons/crossbows/crossbow-loaded-black.webp","effects":[{"_id":"N4vxVFNLW9Q9QsIp","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative -4","tint":"","transfer":true},{"_id":"ZBJldrSO6rRHmgnI","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"S204KfhmoRdMDVpZ","name":"Wachskerze (brennt 10h)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.02,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/sundries/lights/candle-unlit-tan.webp","effects":[]}
{"_id":"S4pc70BxmRU0DCsW","name":"Schleuder +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Distanzmalus -1 pro 2m</p>","quantity":1,"price":1750.1,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":3,"opponentDefense":-3},"flags":{},"img":"icons/weapons/slings/slingshot-wood.webp","effects":[{"_id":"kM7ZknhuQS8uIJbu","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
@ -166,7 +166,7 @@
{"_id":"TSoSy8ck9XCUa1SM","name":"Morgenstern +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1757,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-3},"flags":{},"img":"icons/weapons/maces/mace-round-spiked-grey.webp","effects":[{"_id":"fZ3VByauGosUUN3D","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"TcyE0faEebPLoLOD","name":"Kettenpanzer +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":null,"quantity":1,"price":3260,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":2,"armorMaterialType":"chain","armorType":"body"},"flags":{},"img":"icons/equipment/chest/breastplate-scale-grey.webp","effects":[{"_id":"C7jqj8julpambLpm","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true}]}
{"_id":"TjB6AWIwGY5Q8xln","name":"Dietrich","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":1,"availability":"village","storageLocation":"-"},"flags":{},"img":"icons/tools/hand/lockpicks-steel-grey.webp","effects":[]}
{"name":"Schnelligkeitstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>F&uuml;r W20 Runden erh&ouml;ht sich der Laufen- Wert des Trinkenden um 100%.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"Tlfjrxlm9NpmVR0L"}
{"_id":"Tlfjrxlm9NpmVR0L","name":"Schnelligkeitstrank","type":"loot","img":"icons/consumables/potions/potion-bottle-corked-fancy-orange.webp","data":{"description":"<p>F&uuml;r W20 Runden erh&ouml;ht sich der Laufen- Wert des Trinkenden um 100%.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"Trl2ljtHi1kRYHTX","name":"Streitkolben +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1757,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-3},"flags":{},"img":"icons/weapons/maces/mace-studded-steel.webp","effects":[{"_id":"PeyncDD5OoPJkkTO","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"Tu0Mf3Qib68wxpRs","name":"Zwergenaxt +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -1</p>","quantity":1,"price":3310,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":6,"opponentDefense":-5},"flags":{},"img":"icons/weapons/axes/axe-double-engraved-blue.webp","effects":[{"_id":"Ytio5tOcCUO91MQ0","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-1,"mode":2}],"duration":{},"label":"Initiative -1","transfer":true},{"_id":"ulhp9EsUM5Tf0iCd","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"U89qHZqIfVM8xvaJ","name":"Tagesration (3 Mahlzeiten)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.5,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/tools/cooking/can.webp","effects":[]}
@ -175,23 +175,23 @@
{"_id":"UNd96UN0NQo8I0SI","name":"Turmschild +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"shield","data":{"description":null,"quantity":1,"price":5265,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":2},"flags":{},"img":"icons/equipment/shield/heater-embossed-gold.webp","effects":[{"_id":"ZXSBIh1UotdZuHbj","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":3,"mode":2}],"duration":{},"label":"Panzerung +3 (magisch)","transfer":true}]}
{"name":"Satteltasche","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"loot","data":{"description":"","quantity":1,"price":4,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/containers/bags/pouch-simple-leather-tan.webp","effects":[],"_id":"UpkQTtuxNS1eOIRu"}
{"name":"Elfischer Sattel","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>In diesen &auml;u&szlig;erst fein gearbeiteten Sattel ist <strong>Reiten +I</strong> eingebettet.</p>","quantity":1,"price":505,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/commodities/leather/leather-studded-tan.webp","effects":[],"_id":"V3D7u9LDumvBwJQI"}
{"name":"Fliegentrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieser oft gelbe Trank wirkt auf den Trinker den Zauber <em>Fliegen</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"V6ywiDBc1Son1XrQ"}
{"_id":"V6ywiDBc1Son1XrQ","name":"Fliegentrank","type":"loot","img":"icons/consumables/potions/bottle-conical-corked-yellow.webp","data":{"description":"<p>Dieser oft gelbe Trank wirkt auf den Trinker den Zauber <em>Fliegen</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"name":"Armreif des Bogners","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Verleiht auf <em>Schie&szlig;en</em>-Proben mit B&ouml;gen einen Bonus von +2.</p>","quantity":1,"price":1260,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/wrist/bracer-belted-leather-brown.webp","effects":[],"_id":"VJftG703v7db0Cqf"}
{"_id":"VQtcpPYhzifN9Lqr","name":"Plattenarmschienen +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":null,"quantity":1,"price":3257,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"plate","armorType":"vambrace"},"flags":{},"img":"icons/equipment/wrist/bracer-armored-steel-worn.webp","effects":[{"_id":"pPtRJMUFA3gbNZXx","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true}]}
{"_id":"Van6Sze8TZl8Y88o","name":"Zelt (2 Mann)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":4,"availability":"village","storageLocation":"-"},"flags":{},"img":"icons/environment/settlement/tent.webp","effects":[]}
{"name":"Kette der Regeneration","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Diese schlichte Silberkette heilt in jeder Kamfprunde 1 LK.</p>","quantity":1,"price":null,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/neck/choker-chain-thick-silver.webp","effects":[],"_id":"VfjAtfQv5Ga8hoHu"}
{"_id":"VxyrCBfVdbRD5nj8","name":"Wurfmesser +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Distanzmalus -1 pro 2m</p>","quantity":1,"price":1752,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"meleeRanged","weaponBonus":3,"opponentDefense":-3},"flags":{},"img":"icons/weapons/thrown/dagger-ringed-blue.webp","effects":[{"_id":"hQfQjfXJhE4yDXQU","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"name":"Konzentrationstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieser oft graue Trank verdoppelt GEI f&uuml;r GEI in Runden.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"W2dHT0OENc5kNwnZ"}
{"_id":"W2dHT0OENc5kNwnZ","name":"Konzentrationstrank","type":"loot","img":"icons/consumables/potions/potion-bottle-corked-white.webp","data":{"description":"<p>Dieser oft graue Trank verdoppelt GEI f&uuml;r GEI in Runden.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"WP62N2sjGz3eIN18","name":"Robe","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"","quantity":1,"price":1,"availability":"hamlet","storageLocation":"-","equipped":false,"armorValue":0,"armorMaterialType":"cloth","armorType":"body"},"flags":{},"img":"icons/equipment/chest/robe-layered-teal.webp","effects":[]}
{"_id":"WiW9Sad1D2HjkNMz","name":"Langbogen +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative +1, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":2760,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":5,"opponentDefense":-3},"flags":{},"img":"icons/weapons/bows/bow-recurve-black.webp","effects":[{"_id":"XsqzwEX1AvYJyczG","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"B6tNz4B208qZf3JU","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"name":"Vergrößerungstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Vergr&ouml;&szlig;ert den Trinkenden auf das Doppelte seiner normalen Gr&ouml;&szlig;e f&uuml;r W20/2 Minuten. K&Ouml;R, ST und H&Auml; werden verdoppelt und die Kampfwerte entsprechend angepasst.</p>","quantity":1,"price":1000,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"Wjcv3WZW3j4jg9XY"}
{"_id":"Wjcv3WZW3j4jg9XY","name":"Vergrößerungstrank","type":"loot","img":"icons/consumables/potions/bottle-bulb-corked-purple.webp","data":{"description":"<p>Vergr&ouml;&szlig;ert den Trinkenden auf das Doppelte seiner normalen Gr&ouml;&szlig;e f&uuml;r W20/2 Minuten. K&Ouml;R, ST und H&Auml; werden verdoppelt und die Kampfwerte entsprechend angepasst.</p>","quantity":1,"price":1000,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"XBCdDIpdR1weOhiy","name":"Plattenarmschienen +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":null,"quantity":1,"price":2257,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"plate","armorType":"vambrace"},"flags":{},"img":"icons/equipment/wrist/bracer-armored-steel-white.webp","effects":[{"_id":"IwrLITAgM3vLzkwA","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true}]}
{"_id":"XSXEDiMN27cuEoOx","name":"Leichte Armbrust +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -2</p>","quantity":1,"price":2758,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":5,"opponentDefense":-3},"flags":{},"img":"icons/weapons/crossbows/crossbow-purple.webp","effects":[{"_id":"gMm2PnBADqXBrCi1","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative -2","tint":"","transfer":true},{"_id":"z2uPOzDQ47VbWmt5","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"name":"Schutztrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Erh&ouml;ht f&uuml;r W20 Runden die Abwehr um +2.</p>","quantity":1,"price":50,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"XiW3k793840i0rdo"}
{"_id":"XiW3k793840i0rdo","name":"Schutztrank","type":"loot","img":"icons/consumables/potions/bottle-pear-corked-blue.webp","data":{"description":"<p>Erh&ouml;ht f&uuml;r W20 Runden die Abwehr um +2.</p>","quantity":1,"price":50,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"XyDt7MThnLkDCldV","name":"Metallschild +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"shield","data":{"description":null,"quantity":1,"price":3258,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1},"flags":{},"img":"icons/equipment/shield/heater-steel-sword-yellow-black.webp","effects":[{"_id":"MidC9f4kBbIz7Z7S","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true}]}
{"_id":"YSl6qdVC6fDYVkFQ","name":"Schlachtgeißel +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Initiative -4, Bei Schlagen-Patzer trifft Angreifer sich selbst (Patzer ausgeschlossen)</p>","quantity":1,"price":2766,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":5,"opponentDefense":-6},"flags":{},"img":"icons/weapons/maces/flail-triple-grey.webp","effects":[{"_id":"BdejYg7Bq6tM2ROu","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"duration":{},"label":"Initiative -4","transfer":true},{"_id":"OF7IAa9tUBoz23TD","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"name":"Kundschafterpanzer","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Diese mit braunen Fellschulterpolstern verzierte Kettenr&uuml;stung gew&auml;hrt ihrem Tr&auml;ger +1 auf <em>Bewegung</em>.</p>\n<p>Laufen -0,5m</p>","quantity":1,"price":1260,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":2,"armorMaterialType":"chain","armorType":"body"},"flags":{},"img":"icons/equipment/chest/breastplate-scale-leather.webp","effects":[{"_id":"EkJB0kpYFHRMYSgl","flags":{},"changes":[{"key":"data.combatValues.movement.total","value":-0.5,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Laufen -0,5m","tint":"","transfer":true},{"_id":"mq2ViimrfXr7sGC7","flags":{},"changes":[{"key":"data.traits.agility.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Bewegung +1 (magisch)","tint":"","transfer":true}],"_id":"ZLDJNdtwxbdiJOP2"}
{"_id":"Zl8ZkPrwHibRYWPh","name":"Allsichttrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>F&uuml;r W20 Minuten kann der Trinker Magie, Unsichtbares und Verborgenes (Fallen, Geheimt&uuml;ren usw.) automatisch erkennen.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[]}
{"_id":"Zl8ZkPrwHibRYWPh","name":"Allsichttrank","type":"loot","img":"icons/consumables/potions/potion-vial-corked-labeled-purple.webp","data":{"description":"<p>F&uuml;r W20 Minuten kann der Trinker Magie, Unsichtbares und Verborgenes (Fallen, Geheimt&uuml;ren usw.) automatisch erkennen.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"ZyML0QPctIXK8A4c","name":"Laterne","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":5,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/sundries/lights/lantern-iron-yellow.webp","effects":[]}
{"_id":"aJ9xkECmqeBUhING","name":"Schlagring","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Wie waffenlos, Gegner aber kein Abwehr-Bonus</p>","quantity":1,"price":1,"availability":"village","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":0,"opponentDefense":0},"flags":{},"img":"icons/weapons/fist/fist-knuckles-spiked-grey.webp","effects":[]}
{"_id":"aNGn2tplXGSikyV7","name":"Lederpanzer (Für Reittiere)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"","quantity":1,"price":12,"availability":"hamlet","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"leather","armorType":"body"},"flags":{},"img":"icons/commodities/leather/leather-leaf-tan.webp","effects":[]}
@ -212,7 +212,7 @@
{"_id":"dDhfIuRomMfy2kkP","name":"Anhänger mit heiligem Symbol","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"equipment","data":{"description":"","quantity":1,"price":1,"availability":"village","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/neck/amulet-carved-stone-spiral.webp","effects":[]}
{"_id":"dIA53f3kvU9JgGvg","name":"Bihänder +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -2, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":3260,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":6,"opponentDefense":-7},"flags":{},"img":"icons/weapons/swords/greatsword-guard-gem-blue.webp","effects":[{"_id":"DaKTtdhRO45QZuDJ","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative -2","tint":"","transfer":true},{"_id":"n4mZBHWKSQLQcHHD","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"dMbjx675yI2F4rUg","name":"Langschwert +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1757,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-1},"flags":{},"img":"icons/weapons/swords/sword-guard-embossed-green.webp","effects":[{"_id":"l7qeGLLyuz7VXAAd","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Trank der Gasgestalt","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieser meist rauchige Trank wirkt auf den Trinker den Zauber <em>Gasgestalt</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":500,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"dUQPe5X6ka3HJLuF"}
{"_id":"dUQPe5X6ka3HJLuF","name":"Trank der Gasgestalt","type":"loot","img":"icons/consumables/potions/potion-bottle-corked-fancy-blue.webp","data":{"description":"<p>Dieser meist rauchige Trank wirkt auf den Trinker den Zauber <em>Gasgestalt</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":500,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"dW8OETKY21O3GNHf","name":"Schloss: Solide (SW: 4)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":10,"availability":"village","storageLocation":"-"},"flags":{},"img":"icons/containers/chest/chest-reinforced-steel-oak-tan.webp","effects":[]}
{"_id":"dq4wK5bmMvOfwoWH","name":"Kampfstab +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Zielzaubern +1</p>","quantity":1,"price":1250.5,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":-1},"flags":{},"img":"icons/weapons/staves/staff-simple-gold.webp","effects":[{"_id":"1aNTAQBai6qAcWGM","flags":{},"changes":[{"key":"data.combatValues.targetedSpellcasting.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Zielzaubern +1","tint":"","transfer":true},{"_id":"ve7iQmkxC6l5WmTE","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Fäustlinge der Verstümmelung","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Diese blutverschmutzten Wildlederhandschuhe gew&auml;hren ihrem Tr&auml;ger <strong>Brutaler Hieb +I</strong> und <strong>Verletzen +I</strong>.</p>","quantity":1,"price":3050.1,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/hand/glove-tooled-leather-brown.webp","effects":[],"_id":"dvVhgqCv9VDpFW0l"}
@ -223,7 +223,7 @@
{"_id":"eLTOIKuFGf3M9HYt","name":"Decke","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.1,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/sundries/survival/bedroll-worn-beige.webp","effects":[]}
{"_id":"eMeS2JSyiRJ5xO48","name":"Streithammer +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -4</p>","quantity":1,"price":2756,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":5,"opponentDefense":-2},"flags":{},"img":"icons/weapons/hammers/hammer-war-rounding.webp","effects":[{"_id":"dcGiLPK2AIDA7NHH","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"duration":{},"label":"Initiative -4","transfer":true},{"_id":"1I5R9xQlug0aSVTZ","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"eMyZ5EJ9SsiXaeKK","name":"Streithammer +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -4</p>","quantity":1,"price":2256,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-1},"flags":{},"img":"icons/weapons/hammers/hammer-war-spiked-simple.webp","effects":[{"_id":"dcGiLPK2AIDA7NHH","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"duration":{},"label":"Initiative -4","transfer":true},{"_id":"bPdVsWIS1AC54ZA8","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Allheilungstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Von milchiger F&auml;rbung, wirkt dieser Trank den Zauber <em>Allheilung</em> auf den Trinkenden (keine Probe n&ouml;tig).</p>","quantity":1,"price":1000,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"eRQ9LUNWbnAuuVX6"}
{"_id":"eRQ9LUNWbnAuuVX6","name":"Allheilungstrank","type":"loot","img":"icons/consumables/potions/potion-bottle-corked-white.webp","data":{"description":"<p>Von milchiger F&auml;rbung, wirkt dieser Trank den Zauber <em>Allheilung</em> auf den Trinkenden (keine Probe n&ouml;tig).</p>","quantity":1,"price":1000,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"ec2Aft0epeFhdg9B","name":"Bärenfalle","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Schlagen 30</p>","quantity":1,"price":10,"availability":"village","storageLocation":"-"},"flags":{},"img":"icons/environment/traps/trap-jaw-green.webp","effects":[]}
{"_id":"egleYygLWn8IfeuF","name":"Metallkrug","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":1,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/containers/kitchenware/mug-steel-wood-brown.webp","effects":[]}
{"_id":"eqGo6VKETI1UTxP1","name":"Schlachtgeißel +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Initiative -4, Bei Schlagen-Patzer trifft Angreifer sich selbst (Patzer ausgeschlossen)</p>","quantity":1,"price":3266,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":6,"opponentDefense":-7},"flags":{},"img":"icons/weapons/maces/flail-triple-grey.webp","effects":[{"_id":"BdejYg7Bq6tM2ROu","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"duration":{},"label":"Initiative -4","transfer":true},{"_id":"uhpIAR3T0P3bJw1C","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
@ -236,45 +236,45 @@
{"_id":"fziU7tEIQPowqBJj","name":"Schlagring +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Wie waffenlos, Gegner aber kein Abwehr-Bonus</p>","quantity":1,"price":751,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":1,"opponentDefense":-1},"flags":{},"img":"icons/weapons/fist/fist-knuckles-spiked-blue.webp","effects":[{"_id":"ph5sqT08zd8lSi7s","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Runenbestickte Feurrobe","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Eine feuerrote Robe +3 mit aufgestickten Flammenrunen und <strong>Feuermagier +V</strong>.</p>","quantity":1,"price":4501,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":0,"armorMaterialType":"cloth","armorType":"body"},"flags":{},"img":"icons/equipment/chest/robe-layered-red.webp","effects":[{"_id":"Qpy3XcHOokbU7ZaS","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":3,"mode":2}],"duration":{},"label":"Panzerung +3 (magisch)","transfer":true}],"_id":"gK76RPRhQF7T4AQC"}
{"_id":"gVwoOpuHkmwvcUow","name":"Langbogen +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative +1, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":2260,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":4,"opponentDefense":-2},"flags":{},"img":"icons/weapons/bows/bow-ornamental-gold-blue.webp","effects":[{"_id":"XsqzwEX1AvYJyczG","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"iUXn0CQKZw6Rr1yH","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"name":"Wachsamkeitstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieses meist klare Getr&auml;nk gew&auml;hrt f&uuml;r W20 Stunden auf alle Bemerken-Proben einen Bonus von +5.</p>","quantity":1,"price":15,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"gXr3lLQmlHeDMuv5"}
{"_id":"gXr3lLQmlHeDMuv5","name":"Wachsamkeitstrank","type":"loot","img":"icons/consumables/potions/potion-tube-corked-labeled-cyan.webp","data":{"description":"<p>Dieses meist klare Getr&auml;nk gew&auml;hrt f&uuml;r W20 Stunden auf alle Bemerken-Proben einen Bonus von +5.</p>","quantity":1,"price":15,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"grAnIWqeXTAIGXmN","name":"Wurfmesser","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Distanzmalus -1 pro 2m</p>","quantity":1,"price":2,"availability":"hamlet","storageLocation":"-","equipped":false,"attackType":"meleeRanged","weaponBonus":0,"opponentDefense":0},"flags":{},"img":"icons/weapons/thrown/dagger-simple.webp","effects":[]}
{"_id":"gyfU78OLQj8qH3Zh","name":"Metallschild","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"shield","data":{"description":"<p>Laufen -0,5m</p>","quantity":1,"price":8,"availability":"hamlet","storageLocation":"-","equipped":false,"armorValue":1},"flags":{},"img":"icons/equipment/shield/heater-steel-worn.webp","effects":[{"_id":"kDcyXkLx75K2YRCl","flags":{},"changes":[{"key":"data.combatValues.movement.total","value":-0.5,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Laufen -0,5m","tint":"","transfer":true}]}
{"_id":"hBemmfRcQLFU7PSt","name":"Schlachtgeißel +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Initiative -4, Bei Schlagen-Patzer trifft Angreifer sich selbst (Patzer ausgeschlossen)</p>","quantity":1,"price":2266,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-5},"flags":{},"img":"icons/weapons/maces/flail-triple-grey.webp","effects":[{"_id":"BdejYg7Bq6tM2ROu","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"duration":{},"label":"Initiative -4","transfer":true},{"_id":"oOQJt7Ub0PGGjdzW","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"hGiaAJyEUHbPl5pf","name":"Holzbesteck","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.2,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/tools/cooking/fork-steel-tan.webp","effects":[]}
{"_id":"hfxblADLXdGaRMAA","name":"Wurfmesser +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Distanzmalus -1 pro 2m</p>","quantity":1,"price":752,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"meleeRanged","weaponBonus":1,"opponentDefense":-1},"flags":{},"img":"icons/weapons/thrown/dagger-ringed-steel.webp","effects":[{"_id":"c2P4Qt8b6GonPFv7","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Glückstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Der Trinkende kann f&uuml;r W20 Stunden alle Patzer ignorieren.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"hrAyguSjO6JhTE5m"}
{"_id":"hrAyguSjO6JhTE5m","name":"Glückstrank","type":"loot","img":"icons/consumables/potions/bottle-round-corked-green.webp","data":{"description":"<p>Der Trinkende kann f&uuml;r W20 Stunden alle Patzer ignorieren.</p>","quantity":1,"price":200,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"htmQWmMCQN620KrE","name":"Langschwert","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":7,"availability":"hamlet","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":0},"flags":{},"img":"icons/weapons/swords/sword-guard-blue.webp","effects":[]}
{"_id":"i1ZcbKzviqz0nxjV","name":"Langschwert +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":2257,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-2},"flags":{},"img":"icons/weapons/swords/sword-guard-purple.webp","effects":[{"_id":"5JZ001rLJuZMJSrD","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"iGiZU77IGQQqlYFr","name":"Metallschild +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"shield","data":{"description":null,"quantity":1,"price":2258,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1},"flags":{},"img":"icons/equipment/shield/heater-steel-grey.webp","effects":[{"_id":"9xMdH6XQxlulL7wv","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true}]}
{"_id":"iMX7YuWPHnCnbeh8","name":"Talgkerze (brennt 6h)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.01,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/sundries/lights/candle-unlit-grey.webp","effects":[]}
{"_id":"iOAmrK7t7ZyrtGy1","name":"Pfeife","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.5,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/sundries/misc/pipe-wooden-02.webp","effects":[]}
{"_id":"iOAmrK7t7ZyrtGy1","name":"Pfeife","type":"loot","img":"icons/sundries/misc/pipe-wooden-straight-brown.webp","data":{"description":"","quantity":1,"price":0.5,"availability":"hamlet","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"iORLpplub64kuxb4","name":"Streithammer +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -4</p>","quantity":1,"price":3256,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":6,"opponentDefense":-3},"flags":{},"img":"icons/weapons/hammers/hammer-war-spiked.webp","effects":[{"_id":"dcGiLPK2AIDA7NHH","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"duration":{},"label":"Initiative -4","transfer":true},{"_id":"RWRk7pwQf2vs1Aqv","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"iWOrlMcGVAXTvFEg","name":"Holzwürfel (sechsseitig)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.02,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/sundries/gaming/dice-runed-tan.webp","effects":[]}
{"_id":"iWOrlMcGVAXTvFEg","name":"Holzwürfel (sechsseitig)","type":"loot","img":"icons/sundries/gaming/dice-runed-brown.webp","data":{"description":"","quantity":1,"price":0.02,"availability":"hamlet","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"ibiHqm4rH8meeu9m","name":"Turmschild +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"shield","data":{"description":"<p>Laufen -0,5m</p>","quantity":1,"price":3265,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":2},"flags":{},"img":"icons/equipment/shield/heater-steel-engraved-lance-rest.webp","effects":[{"_id":"ScAKi1eiWow9Y1ZZ","flags":{},"changes":[{"key":"data.combatValues.movement.total","value":-0.5,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Laufen -0,5m","tint":"","transfer":true},{"_id":"TeyRcwja5lQWHICW","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true}]}
{"_id":"j3wcDmBNiDx7sK0n","name":"Runenbestickte Robe +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Aura +1</p>","quantity":1,"price":2258,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":0,"armorMaterialType":"cloth","armorType":"body"},"flags":{},"img":"icons/equipment/chest/robe-layered-blue.webp","effects":[{"_id":"vgJWV95OeyzrjyFw","flags":{},"changes":[{"key":"data.traits.aura.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Aura +1","tint":"","transfer":true},{"_id":"a2GXKNKHJWdyKb7h","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true}]}
{"_id":"jRzgvvygxk5IjXYB","name":"Speer +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":null,"quantity":1,"price":1251,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"meleeRanged","weaponBonus":2,"opponentDefense":-1},"flags":{},"img":"icons/weapons/polearms/spear-hooked-red.webp","effects":[{"_id":"r0XGSAypTjvBmHHC","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"jsANSjzxRPKO3AyG","name":"Axt +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1256,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":-1},"flags":{},"img":"icons/weapons/axes/shortaxe-black.webp","effects":[{"_id":"wh12XwprBMtY3BTB","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Schutzring +1","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Erh&ouml;ht die Abwehr um 1, ohne dabei Panzerungsmalus zu verursachen.</p>","quantity":1,"price":752,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/finger/ring-band-engraved-lines-bronze.webp","effects":[{"_id":"yH3Ujo3jKNCYI4n9","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Abwehr +1 (magisch)","tint":"","transfer":true}],"_id":"kGTB9f2zPrmedHq4"}
{"name":"Kampftrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Meist von oranger Farbe erh&ouml;ht solch ein Trank <em>Schlagen</em> und <em>Abwehr</em> f&uuml;r die Dauer eines Kampfes um +1.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"kIiDbrtAPno14O85"}
{"_id":"kIiDbrtAPno14O85","name":"Kampftrank","type":"loot","img":"icons/consumables/potions/bottle-round-corked-orange.webp","data":{"description":"<p>Meist von oranger Farbe erh&ouml;ht solch ein Trank <em>Schlagen</em> und <em>Abwehr</em> f&uuml;r die Dauer eines Kampfes um +1.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"name":"Robe des Heilers","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Eine wei&szlig;e Robe, die ihrem Tr&auml;ger +1 auf Heilzauber gew&auml;hrt.</p>","quantity":1,"price":751,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":0,"armorMaterialType":"cloth","armorType":"body"},"flags":{},"img":"icons/equipment/chest/robe-layered-white.webp","effects":[],"_id":"kVRybb0knZkoJLlj"}
{"name":"Unsichtbarkeitsring","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>B&ouml;se Zungen behaupten, dass dieser Ring, in den <em>Unsichtbarkeit</em> eingebettet ist, seinen Tr&auml;ger zu seinem abh&auml;ngigen Sklaven macht.</p>","quantity":1,"price":6972,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/finger/ring-band-rounded-gold.webp","effects":[],"_id":"kurEYTP9rqk8YnND"}
{"name":"Grausame Axt","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Antike Axt +1 mit <strong>Verletzen +III</strong></p>","quantity":1,"price":4256,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":-1},"flags":{},"img":"icons/weapons/axes/shortaxe-simple-black.webp","effects":[{"_id":"wh12XwprBMtY3BTB","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}],"_id":"lHlwYWlgC2iKjDFM"}
{"name":"Waffenweih","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>&Uuml;ber eine Waffe gesch&uuml;ttet, verleiht dieser meist silberne Trank dieser f&uuml;r die Dauer eines Kampfes den Effekt des Zaubers <em>Magische Waffe</em>.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"luYRwVP5oR6cdDMQ"}
{"_id":"luYRwVP5oR6cdDMQ","name":"Waffenweih","type":"loot","img":"icons/consumables/potions/potion-jar-capped-teal.webp","data":{"description":"<p>&Uuml;ber eine Waffe gesch&uuml;ttet, verleiht dieser meist silberne Trank dieser f&uuml;r die Dauer eines Kampfes den Effekt des Zaubers <em>Magische Waffe</em>.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"mjBwhnK5gf9MIdlm","name":"Streitkolben","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":7,"availability":"hamlet","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":1,"opponentDefense":-1},"flags":{},"img":"icons/weapons/maces/mace-spiked-steel-wood.webp","effects":[]}
{"name":"Berserkertrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieses von verr&uuml;ckten Orkschamanen entwickelte, dampfende Getr&auml;nk heilt drei Kampfrunden jeweils W20 Lebenskraft. In der vierten Runde explodiert der Trinker und verursacht in 2m Radius Schaden in H&ouml;he der erw&uuml;rfelten Heilung (Abwehr gilt).</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"nH1Vfhhx2jlGoQ6i"}
{"_id":"nH1Vfhhx2jlGoQ6i","name":"Berserkertrank","type":"loot","img":"icons/consumables/potions/bottle-round-flask-fumes-purple.webp","data":{"description":"<p>Dieses von verr&uuml;ckten Orkschamanen entwickelte, dampfende Getr&auml;nk heilt drei Kampfrunden jeweils W20 Lebenskraft. In der vierten Runde explodiert der Trinker und verursacht in 2m Radius Schaden in H&ouml;he der erw&uuml;rfelten Heilung (Abwehr gilt).</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"name":"Karten des Schummlers","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"loot","data":{"description":"<p>In dieses wundersch&ouml;ne Spielkarten-Set ist der Zauber <em>Zeitstop</em> eingebettet.</p>","quantity":1,"price":13031,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/sundries/gaming/playing-cards-grey.webp","effects":[],"_id":"nff3XieL5dvOZga9"}
{"name":"Trank der Zwergensicht","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieses meist schwarze Getr&auml;nk gew&auml;hrt f&uuml;r W20 Stunden dem Trinker die zwergische Volksf&auml;higkeit Dunkelsicht (siehe GRW Seite 83).</p>","quantity":1,"price":15,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"nixhgFSQ7jaZrvxD"}
{"_id":"nixhgFSQ7jaZrvxD","name":"Trank der Zwergensicht","type":"loot","img":"icons/consumables/potions/potion-bottle-labeled-medicine-capped-red-black.webp","data":{"description":"<p>Dieses meist schwarze Getr&auml;nk gew&auml;hrt f&uuml;r W20 Stunden dem Trinker die zwergische Volksf&auml;higkeit Dunkelsicht (siehe GRW Seite 83).</p>","quantity":1,"price":15,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"name":"Sattel","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"","quantity":1,"price":5,"availability":"hamlet","storageLocation":"-","equipped":false},"flags":{},"img":"icons/commodities/leather/leather-scrap-brown.webp","effects":[],"_id":"nslQfc441x1GG0SL"}
{"name":"Spruchspeicherring","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Einmal pro Tag kann der Tr&auml;ger des Ringes zu einem vorher festgelegten Zauber aktionsfrei wechseln, ohne daf&uuml;r w&uuml;rfeln zu m&uuml;ssen, da in den Ring <em>Wechselzauber</em> eingebettet wurde.</p>","quantity":1,"price":4992,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/finger/ring-cabochon-engraved-gold-pink.webp","effects":[],"_id":"oJbpYZlvfJ6kpudj"}
{"name":"Giftbanntrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Wirkt den Zauber <em>Giftbann</em> auf den Trinkenden (keine Probe n&ouml;tig).</p>","quantity":1,"price":150,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"oPUlB9rz5rvRKrq8"}
{"_id":"oPUlB9rz5rvRKrq8","name":"Giftbanntrank","type":"loot","img":"icons/consumables/potions/bottle-bulb-corked-glowing-red.webp","data":{"description":"<p>Wirkt den Zauber <em>Giftbann</em> auf den Trinkenden (keine Probe n&ouml;tig).</p>","quantity":1,"price":150,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"oWvJfxEBr83QxO9Q","name":"Speer","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zerbricht bei Schie&szlig;en-Patzer</p>","quantity":1,"price":1,"availability":"hamlet","storageLocation":"-","equipped":false,"attackType":"meleeRanged","weaponBonus":1,"opponentDefense":0},"flags":{},"img":"icons/weapons/polearms/spear-hooked-simple.webp","effects":[]}
{"name":"Alterungstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Der Trinkende wird augenblicklich W20 Jahre &auml;lter, Haare und N&auml;gel wachsen entsprechend.</p>","quantity":1,"price":500,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"oeyhSfAQQPUbm10p"}
{"_id":"oeyhSfAQQPUbm10p","name":"Alterungstrank","type":"loot","img":"icons/consumables/potions/potion-vial-corked-purple.webp","data":{"description":"<p>Der Trinkende wird augenblicklich W20 Jahre &auml;lter, Haare und N&auml;gel wachsen entsprechend.</p>","quantity":1,"price":500,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"opq2AakrpM9gLSa0","name":"Krummschwert +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":2757,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":5,"opponentDefense":-3},"flags":{},"img":"icons/weapons/swords/scimitar-blue.webp","effects":[{"_id":"RGYzuIow1nDLd681","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"oqnI982dhCya94Tu","name":"Elfenbogen +2","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative +1, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":2825,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":5,"opponentDefense":-2},"flags":{},"img":"icons/weapons/bows/longbow-recurve-leather-red.webp","effects":[{"_id":"QScLkDv6gysh119m","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"l9ZRRSrd6yBhQUJL","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":2,"mode":2}],"duration":{},"label":"Initiative +2 (magisch)","transfer":true}]}
{"_id":"ozPRhUPx9Y9u3GNE","name":"Seife (1 Stück)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.5,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/sundries/survival/soap.webp","effects":[]}
{"name":"Zornhammer","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Ein schwerer, schlichter Streithammer +3, der einst einem Zwergenhelden geh&ouml;rte.</p>\n<p>Zweih&auml;ndig, Initiative -4</p>","quantity":1,"price":3256,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":6,"opponentDefense":-3},"flags":{},"img":"icons/weapons/hammers/hammer-double-steel-embossed.webp","effects":[{"_id":"dcGiLPK2AIDA7NHH","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-4,"mode":2}],"duration":{},"label":"Initiative -4","transfer":true},{"_id":"RWRk7pwQf2vs1Aqv","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}],"_id":"pQqbXD5ELmjcu4Dd"}
{"_id":"pYP26CskxKade3GF","name":"Pfanne","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":1,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/tools/cooking/pot-camping-iron-black.webp","effects":[]}
{"name":"Talenttrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Metallisch riechend, erh&ouml;ht dieser Trank f&uuml;r W20 Runden ein vom Trinkenden bereits beherrschtes Talent um +I.</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"pljOii88ltzuQNsu"}
{"_id":"pljOii88ltzuQNsu","name":"Talenttrank","type":"loot","img":"icons/consumables/potions/potion-flask-corked-labeled-pink.webp","data":{"description":"<p>Metallisch riechend, erh&ouml;ht dieser Trank f&uuml;r W20 Runden ein vom Trinkenden bereits beherrschtes Talent um +I.</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"pzD6fcJa1Hk4Duwu","name":"Keule +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":null,"quantity":1,"price":1250.2,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":-1},"flags":{},"img":"icons/weapons/clubs/club-simple-barbed.webp","effects":[{"_id":"9ZyXiW0n9WJ5WQOv","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"pzjZv0HhCA15wy1i","name":"Holzbecher","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":0.2,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/containers/kitchenware/mug-simple-wooden-brown.webp","effects":[]}
{"name":"Schutzring +3","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"equipment","data":{"description":"<p>Erh&ouml;ht die Abwehr um 3, ohne dabei Panzerungsmalus zu verursachen.</p>","quantity":1,"price":1752,"availability":"unset","storageLocation":"-","equipped":false},"flags":{},"img":"icons/equipment/finger/ring-band-engraved-scrolls-gold.webp","effects":[{"_id":"yH3Ujo3jKNCYI4n9","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":3,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Abwehr +3 (magisch)","tint":"","transfer":true}],"_id":"qlBIUI00RTYZzclX"}
@ -282,7 +282,7 @@
{"_id":"rdOU1FmBq1nqQKW5","name":"Streitkolben +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":2257,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-4},"flags":{},"img":"icons/weapons/maces/mace-spiked-steel-grey.webp","effects":[{"_id":"dgH1Fyz2pVFcmhFL","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"name":"Fellmantel des Heilers","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Diese aus wei&szlig;em Fell geschneiderte Lederr&uuml;stung gew&auml;hrt +2 auf alle Heilzauber.</p>","quantity":1,"price":4254,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":1,"armorMaterialType":"leather","armorType":"body"},"flags":{},"img":"icons/equipment/chest/vest-leather-tattered-white.webp","effects":[{"_id":"5vJF9ck6rDsBPnKl","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":2,"mode":2}],"duration":{},"label":"Panzerung +2 (magisch)","transfer":true}],"_id":"rvDTHq5UoRK6acm6"}
{"_id":"s1xaEPPKJGMImZ9m","name":"Streitaxt +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -2</p>","quantity":1,"price":2257,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-1},"flags":{},"img":"icons/weapons/axes/axe-double-chopping-black.webp","effects":[{"_id":"DPS3CaNXapsBzzsj","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"duration":{},"label":"Initiative -2","transfer":true},{"_id":"fQgMSANHWdQrzmlJ","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Wasserwandeltrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieser oft braune Trank wirkt auf den Trinker den Zauber <em>Wasserwandeln</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"s47J9CEdTELiRlPT"}
{"_id":"s47J9CEdTELiRlPT","name":"Wasserwandeltrank","type":"loot","img":"icons/consumables/potions/potion-jar-corked-orange.webp","data":{"description":"<p>Dieser oft braune Trank wirkt auf den Trinker den Zauber <em>Wasserwandeln</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":100,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"sMJw9EQtd2pYsgYE","name":"Hammer","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":7,"availability":"hamlet","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":1,"opponentDefense":-1},"flags":{},"img":"icons/weapons/hammers/shorthammer-simple-iron-black.webp","effects":[]}
{"_id":"sUHJSIG8aTs0do67","name":"Runenbestickte Robe +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Aura +1</p>","quantity":1,"price":1258,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":0,"armorMaterialType":"cloth","armorType":"body"},"flags":{},"img":"icons/equipment/chest/robe-collared-blue.webp","effects":[{"_id":"vgJWV95OeyzrjyFw","flags":{},"changes":[{"key":"data.traits.aura.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Aura +1","tint":"","transfer":true},{"_id":"6NLQSkPxsA3H8Gc8","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true}]}
{"_id":"siJAzGmpHVegUKBF","name":"Keule","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zerbricht bei Schlagen-Patzer</p>","quantity":1,"price":0.2,"availability":"hamlet","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":1,"opponentDefense":0},"flags":{},"img":"icons/weapons/clubs/club-simple-black.webp","effects":[]}
@ -290,17 +290,17 @@
{"_id":"tAdNTxSRq9hARm1p","name":"Hammer +3","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":2257,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":4,"opponentDefense":-4},"flags":{},"img":"icons/weapons/hammers/shorthammer-embossed-white.webp","effects":[{"_id":"aLeVPvqVvSHnDTsp","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":3,"mode":2}],"duration":{},"label":"Initiative +3 (magisch)","transfer":true}]}
{"_id":"tPlXSWQ4mP0dqnOa","name":"Schloss: Zwergenarbeit (SW: 12)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"","quantity":1,"price":250,"availability":"city","storageLocation":"-"},"flags":{},"img":"icons/containers/chest/chest-reinforced-steel-oak-tan.webp","effects":[]}
{"name":"Rüstung des Schützen","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Diese mit dunkelgr&uuml;nen Stoffr&auml;ndern ges&auml;umte Kettenr&uuml;stung +1 gew&auml;hrt ihrem Tr&auml;ger +3 auf <em>Schie&szlig;en</em>.</p>","quantity":1,"price":4760,"availability":"unset","storageLocation":"-","equipped":false,"armorValue":2,"armorMaterialType":"chain","armorType":"body"},"flags":{},"img":"icons/equipment/chest/breastplate-scale-grey.webp","effects":[{"_id":"C7jqj8julpambLpm","flags":{},"changes":[{"key":"data.combatValues.defense.total","value":1,"mode":2}],"duration":{},"label":"Panzerung +1 (magisch)","transfer":true},{"_id":"fwNP4w1u7JP3OFEb","flags":{},"changes":[{"key":"data.combatValues.rangedAttack.total","value":3,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Schießen +3 (magisch)","tint":"","transfer":true}],"_id":"tQ1LUX7in4xuuR12"}
{"name":"Großer Heiltrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Diese meist weinroten Tr&auml;nke heilen 2W20 Lebenskraft.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"tW53rAmCXx6rqKMP"}
{"_id":"tW53rAmCXx6rqKMP","name":"Großer Heiltrank","type":"loot","img":"icons/consumables/potions/potion-flask-stopped-red.webp","data":{"description":"<p>Diese meist weinroten Tr&auml;nke heilen 2W20 Lebenskraft.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"trop6WmDZEhv3gRA","name":"Dolch +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Initiative +1</p>","quantity":1,"price":752,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":1,"opponentDefense":-1},"flags":{},"img":"icons/weapons/daggers/dagger-curved-blue.webp","effects":[{"_id":"9jtH6ER0s0I8SPyi","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true},{"_id":"aDUESIHIetT9xLbD","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"u1ycDWee8nHPfZHA","name":"Morgenstern +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1257,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":-2},"flags":{},"img":"icons/weapons/maces/mace-round-studded.webp","effects":[{"_id":"nTOl8E9qA6stLcEJ","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"_id":"uDdLTyyNqeXzCssp","name":"Elfenbogen","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative +1, F&uuml;r Zwerge auf Grund der Gr&ouml;&szlig;e zu unhandlich</p>","quantity":1,"price":75,"availability":"elves","storageLocation":"-","equipped":false,"attackType":"ranged","weaponBonus":3,"opponentDefense":0},"flags":{},"img":"icons/weapons/bows/longbow-recurve-brown.webp","effects":[{"_id":"QScLkDv6gysh119m","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Initiative +1","tint":"","transfer":true}]}
{"_id":"uVYJY3A8kLne2ZkQ","name":"Parfüm (50 x benutzbar)","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Gibt 4 Stunden lang +1 auf Proben sozialer Interaktion mit anderem Geschlecht</p>","quantity":1,"price":5,"availability":"village","storageLocation":"-"},"flags":{},"img":"icons/tools/laboratory/vial-orange.webp","effects":[]}
{"name":"Unsichtbarkeitstrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Dieser oft klare, farblose Trank wirkt auf den Trinker den Zauber <em>Unsichtbarkeit</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":500,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"uYcNgFz5PkaOmK6b"}
{"_id":"uYcNgFz5PkaOmK6b","name":"Unsichtbarkeitstrank","type":"loot","img":"icons/consumables/potions/potion-flask-corked-tied-necklace-teal.webp","data":{"description":"<p>Dieser oft klare, farblose Trank wirkt auf den Trinker den Zauber <em>Unsichtbarkeit</em> (Probenwert 20; Patzer ausgeschlossen).</p>","quantity":1,"price":500,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"name":"Stab des Magus","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Ein Kampfstab +1 mit Zielzaubern +3 (insgesamt also mit dem normalen Bonus Zielzaubern +4).</p>\n<p>Zweih&auml;ndig, Zielzaubern +1</p>","quantity":1,"price":2750.5,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":2,"opponentDefense":-1},"flags":{},"img":"icons/weapons/staves/staff-ornate-purple.webp","effects":[{"_id":"1aNTAQBai6qAcWGM","flags":{},"changes":[{"key":"data.combatValues.targetedSpellcasting.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Zielzaubern +1","tint":"","transfer":true},{"_id":"ve7iQmkxC6l5WmTE","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true},{"_id":"XD3tGbvi1S03diuz","flags":{},"changes":[{"key":"data.combatValues.targetedSpellcasting.total","value":3,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Zielzaubern +3 (magisch)","tint":"","transfer":true}],"_id":"uafOWinH9nnRO3lr"}
{"name":"Klettertrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Der Charakter kann f&uuml;r W20 Runden mit seinem normalen Laufen-Wert wie eine Spinne klettern, selbst kopf&uuml;ber an der Decke.</p>","quantity":1,"price":50,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"udsNOh5h0TQmOYBl"}
{"_id":"udsNOh5h0TQmOYBl","name":"Klettertrank","type":"loot","img":"icons/consumables/potions/bottle-conical-corked-cyan.webp","data":{"description":"<p>Der Charakter kann f&uuml;r W20 Runden mit seinem normalen Laufen-Wert wie eine Spinne klettern, selbst kopf&uuml;ber an der Decke.</p>","quantity":1,"price":50,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"upLe2iticb6YKsIR","name":"Brechstange","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"","quantity":1,"price":1.5,"availability":"hamlet","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":1,"opponentDefense":0},"flags":{},"img":"icons/tools/hand/wrench-iron-grey.webp","effects":[]}
{"_id":"upkDR02zMILTPib6","name":"Lanze +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Nur im Trab (WB +1) oder Galopp (WB +4)</p>","quantity":1,"price":1252,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":1,"opponentDefense":-1},"flags":{},"img":"icons/weapons/polearms/spear-flared-green.webp","effects":[{"_id":"d8N240qBKaRGqkcf","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Zauberwechseltrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Diese meist blauen Tr&auml;nke gew&auml;hren f&uuml;r die Dauer eines Kampfes +10 auf Zauber wechseln.</p>","quantity":1,"price":10,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"uqIQjhDGgbEuHYmd"}
{"_id":"uqIQjhDGgbEuHYmd","name":"Zauberwechseltrank","type":"loot","img":"icons/consumables/potions/potion-bottle-corked-blue.webp","data":{"description":"<p>Diese meist blauen Tr&auml;nke gew&auml;hren f&uuml;r die Dauer eines Kampfes +10 auf Zauber wechseln.</p>","quantity":1,"price":10,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"name":"Geisterbote","permission":{"default":0,"TAIf4a1vtFG928pw":3},"type":"loot","data":{"description":"<p>Jeder dieser mit Rauch gef&uuml;llten Beh&auml;lter enth&auml;lt eine Ladung des Zaubers <em>Botschaft</em>.</p>","quantity":1,"price":454,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/commodities/materials/glass-orb-blue.webp","effects":[],"_id":"v7WiMqH1XylGqNOy"}
{"_id":"vqKLn65gjoced8jV","name":"Streitaxt","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -2</p>","quantity":1,"price":7,"availability":"hamlet","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":0},"flags":{},"img":"icons/weapons/axes/axe-double-brown.webp","effects":[{"_id":"DPS3CaNXapsBzzsj","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"duration":{},"label":"Initiative -2","transfer":true}]}
{"_id":"w1bZ2431gdOQumWK","name":"Runenbestickte Robe","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"armor","data":{"description":"<p>Aura +1</p>","quantity":1,"price":8,"availability":"village","storageLocation":"-","equipped":false,"armorValue":0,"armorMaterialType":"cloth","armorType":"body"},"flags":{},"img":"icons/equipment/chest/coat-leather-blue.webp","effects":[{"_id":"vgJWV95OeyzrjyFw","flags":{},"changes":[{"key":"data.traits.aura.total","value":1,"mode":2}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"","label":"Aura +1","tint":"","transfer":true}]}
@ -320,6 +320,6 @@
{"_id":"zXgxu2gCkaTSSSMU","name":"Waffenpaste","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Macht WB+1; h&auml;lt W20 Nahkampfangriffe bzw. reicht f&uuml;r W20 Fernkampfgeschosse</p>","quantity":1,"price":0.5,"availability":"village","storageLocation":"-"},"flags":{},"img":"icons/tools/laboratory/bowl-liquid-black.webp","effects":[]}
{"_id":"zjefX9KYvMphtVwX","name":"Handschellen","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Preis f&uuml;r beide Schl&ouml;sser extra ermitteln</p>","quantity":1,"price":8,"availability":"village","storageLocation":"-"},"flags":{},"img":"icons/sundries/survival/cuffs-shackles-steel.webp","effects":[]}
{"_id":"zoPPqqDyRTvmV2do","name":"Hellebarde +1","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"weapon","data":{"description":"<p>Zweih&auml;ndig, Initiative -2</p>","quantity":1,"price":4,"availability":"unset","storageLocation":"-","equipped":false,"attackType":"melee","weaponBonus":3,"opponentDefense":-1},"flags":{},"img":"icons/weapons/polearms/halberd-crescent-engraved-steel.webp","effects":[{"_id":"APXje5Ppu0d75HNw","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":-2,"mode":2}],"duration":{},"label":"Initiative -2","transfer":true},{"_id":"FuPTO8plsKR3lq8Z","flags":{},"changes":[{"key":"data.combatValues.initiative.total","value":1,"mode":2}],"duration":{},"label":"Initiative +1 (magisch)","transfer":true}]}
{"name":"Zieltrank","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Erh&ouml;ht die Werte von <em>Schie&szlig;en</em> und <em>Zielzauber</em> f&uuml;r die Dauer eines Kampfes um +1.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"flags":{},"img":"icons/svg/mystery-man.svg","effects":[],"_id":"zqlc3bq1ZfneLeYx"}
{"_id":"zqlc3bq1ZfneLeYx","name":"Zieltrank","type":"loot","img":"icons/consumables/potions/potion-flask-capped-yellow-green.webp","data":{"description":"<p>Erh&ouml;ht die Werte von <em>Schie&szlig;en</em> und <em>Zielzauber</em> f&uuml;r die Dauer eines Kampfes um +1.</p>","quantity":1,"price":25,"availability":"unset","storageLocation":"-"},"effects":[],"folder":null,"sort":0,"permission":{"default":0,"onhXSUZqbwjdEiuE":3},"flags":{}}
{"_id":"zquQpOOVr5lIbnmL","name":"Heilkraut","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":"<p>Probenwert 10: 1-10 heilt LK in Ergebnish&ouml;he, 11+ kein Heileffekt</p>","quantity":1,"price":2.5,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/tools/laboratory/bowl-herbs-green.webp","effects":[]}
{"_id":"zx3FKwvrLw147ARX","name":"Pergamentblatt","permission":{"default":0,"onhXSUZqbwjdEiuE":3},"type":"loot","data":{"description":null,"quantity":1,"price":0.5,"availability":"hamlet","storageLocation":"-"},"flags":{},"img":"icons/sundries/documents/paper-plain-white.webp","effects":[]}

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2021 Johannes Loher
*
* SPDX-License-Identifier: MIT
*/
@use "../utils/typography";
.ds4-actor-header {
display: flex;
flex-grow: 0;
flex-shrink: 0;
gap: 1em;
&__img {
border: none;
cursor: pointer;
height: 100px;
width: 100px;
}
&__data {
display: flex;
flex-direction: column;
}
&__data-row {
align-content: center;
display: flex;
flex: 1;
flex-direction: row;
gap: 0.5em;
> * {
flex: 1;
}
}
&__name {
display: flex;
align-items: center;
border-bottom: 0;
margin: 0;
}
&__name-input {
@include typography.font-heading-upper;
background-color: transparent;
border: none;
flex: 1;
font-size: 1.25em;
height: auto;
}
}

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2021 Johannes Loher
* SPDX-FileCopyrightText: 2021 Oliver Rümpelein
*
* SPDX-License-Identifier: MIT
*/
@use "../utils/colors";
@use "../utils/typography";
@use "../utils/variables";
@use "../utils/mixins";
.ds4-actor-progression {
@include mixins.mark-invalid-or-disabled-input;
display: flex;
gap: 0.5em;
&__entry {
align-items: center;
display: flex;
flex: 1;
gap: 0.25em;
justify-content: flex-end;
}
&__label {
@include typography.font-heading-upper;
border: none;
color: colors.$c-light-grey;
margin: 0;
padding: 0;
text-align: right;
}
&__input {
flex: 0 0 5ch;
&--slayer-points {
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: auto;
}
&:hover,
&:focus {
-moz-appearance: auto;
}
}
}
}

View file

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2021 Johannes Loher
* SPDX-FileCopyrightText: 2021 Oliver Rümpelein
* SPDX-FileCopyrightText: 2021 Gesina Schwalbe
*
* SPDX-License-Identifier: MIT
*/
@use "../utils/mixins";
@use "../utils/variables";
.ds4-actor-properties {
@include mixins.mark-invalid-or-disabled-input;
display: flex;
gap: 0.25em;
&__property {
flex: 1;
}
&__property-label {
font-size: 0.9em;
font-weight: bold;
white-space: nowrap;
}
&__property-select {
width: 100%;
height: variables.$default-input-height;
}
&__property-multi-input {
display: flex;
gap: 0.125em;
}
}

View file

@ -19,6 +19,7 @@
padding-right: 1px;
& > label {
font-size: 0.9em;
font-weight: bold;
}

View file

@ -1,52 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 Johannes Loher
* SPDX-FileCopyrightText: 2021 Oliver Rümpelein
*
* SPDX-License-Identifier: MIT
*/
@use "../utils/colors";
@use "../utils/typography";
@use "../utils/variables";
@use "../utils/mixins";
.progression {
.progression-entry {
@include mixins.mark-invalid-or-disabled-input;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
padding-right: 3px;
h2.progression-label {
@include typography.font-heading-upper;
display: block;
height: 50px;
padding: 0;
color: colors.$c-light-grey;
border: none;
line-height: 50px;
margin: variables.$margin-sm 0;
text-align: right;
}
input.progression-value {
margin-left: 5px;
flex: 0 0 40px;
text-align: left;
&--slayer-points {
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: auto;
}
&:hover,
&:focus {
-moz-appearance: auto;
}
}
}
}
}

View file

@ -6,13 +6,14 @@
@use "../utils/mixins";
@use "../utils/variables";
@use "../utils/typography";
.ds4-combat-value {
$size: 3.75rem;
display: grid;
place-items: center;
row-gap: 0.5em;
row-gap: 0.125em;
&__value {
$combat-values-icons-path: "#{variables.$official-icons-path}/combat-values";
@ -51,6 +52,12 @@
}
}
&__label {
@include typography.font-heading-upper;
font-size: 1.2em;
white-space: nowrap;
}
&__formula {
align-items: center;
justify-content: space-between;

View file

@ -75,4 +75,10 @@
.tox-edit-area {
padding: 0 8px;
}
.tox-toolbar-overlord {
background-color: transparent;
.tox-toolbar__primary {
background: transparent;
}
}
}

View file

@ -25,6 +25,7 @@ header.sheet-header {
flex: 0 0 100px;
height: 100px;
margin: variables.$margin-sm 10px variables.$margin-sm 0;
border: none;
}
.header-fields {

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2021 Johannes Loher
*
* SPDX-License-Identifier: MIT
*/
.ds4-profile {
display: flex;
flex-direction: column;
gap: 0.5em;
&__entry {
display: flex;
flex-direction: column;
align-items: center;
}
&__entry-input {
&--multiline {
resize: none;
}
}
&__entry-label {
font-size: 0.8em;
font-weight: bold;
}
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: MIT
*/
header.sheet-header {
.character-values {
flex: 0 0 100%;
}
.ds4-sheet {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
}

View file

@ -6,16 +6,13 @@
@use "../utils/variables";
nav.tabs {
height: auto;
border-top: variables.$border-groove;
.ds4-sheet-tab-nav {
border-bottom: variables.$border-groove;
.item {
border-top: variables.$border-groove;
height: auto;
&__item {
font-weight: bold;
white-space: nowrap;
}
.item.active {
text-decoration: none;
}
}

View file

@ -20,10 +20,11 @@
/* Styles limited to ds4 sheets */
.ds4 {
@include meta.load-css("components/actor_header");
@include meta.load-css("components/actor_progression");
@include meta.load-css("components/actor_properties");
@include meta.load-css("components/apps");
@include meta.load-css("components/basic_property");
@include meta.load-css("components/character_progression");
@include meta.load-css("components/character_values");
@include meta.load-css("components/check");
@include meta.load-css("components/checks");
@include meta.load-css("components/combat_value");
@ -34,7 +35,10 @@
@include meta.load-css("components/description");
@include meta.load-css("components/forms");
@include meta.load-css("components/item_list");
@include meta.load-css("components/profile");
@include meta.load-css("components/rollable_image");
@include meta.load-css("components/tabs");
@include meta.load-css("components/sheet_tab_nav");
@include meta.load-css("components/sheet");
@include meta.load-css("components/talent_rank_equation");
@include meta.load-css("tabs/biography");
}

View file

@ -5,6 +5,7 @@
*/
@font-face {
font-display: swap;
font-family: "Lora";
font-style: normal;
font-weight: normal;
@ -12,6 +13,7 @@
}
@font-face {
font-display: swap;
font-family: "Lora";
font-style: normal;
font-weight: bold;
@ -19,6 +21,7 @@
}
@font-face {
font-display: swap;
font-family: "Lora";
font-style: italic;
font-weight: normal;
@ -26,6 +29,7 @@
}
@font-face {
font-display: swap;
font-family: "Lora";
font-style: italic;
font-weight: bold;
@ -33,6 +37,7 @@
}
@font-face {
font-display: swap;
font-family: "Wood Stamp";
font-style: normal;
font-weight: normal;

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2021 Johannes Loher
*
* SPDX-License-Identifier: MIT
*/
.ds4-biography-tab-content {
display: grid;
grid-template-columns: 1fr 3fr;
column-gap: 1em;
}

View file

@ -2,9 +2,9 @@
"name": "ds4",
"title": "Dungeonslayers 4",
"description": "The Dungeonslayers 4 system for FoundryVTT. Dungeonslayers by Christian Kennig is licensed under CC BY-NC-SA 3.0 (https://creativecommons.org/licenses/by-nc-sa/3.0/). The icons by the authors of Game-icons.net are licensed under CC BY 3.0 (https://creativecommons.org/licenses/by/3.0/). The creature images by Devin Night (https://immortalnights.com/) and are licensed under the terms described at https://immortalnights.com/tokens/token-usage-rights/.",
"version": "0.8.0",
"minimumCoreVersion": "0.7.9",
"compatibleCoreVersion": "0.7.10",
"version": "1.1.3",
"minimumCoreVersion": "0.8.8",
"compatibleCoreVersion": "0.8.8",
"templateVersion": 6,
"author": "Johannes Loher, Gesina Schwalbe, Oliver Rümpelein, Siegfried Krug, Max Tharr, Sascha Martens",
"authors": [
@ -93,7 +93,7 @@
"primaryTokenAttribute": "combatValues.hitPoints",
"url": "https://git.f3l.de/dungeonslayers/ds4",
"manifest": "https://git.f3l.de/dungeonslayers/ds4/-/raw/latest/src/system.json?inline=false",
"download": "https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/0.8.0/download?job=build",
"download": "https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/1.1.3/download?job=build",
"license": "https://git.f3l.de/dungeonslayers/ds4#licensing",
"initiative": "@combatValues.initiative.total",
"manifestPlusVersion": "1.0.0",

View file

@ -170,29 +170,6 @@
"shield": {
"templates": ["base", "physical", "equipable", "protective"]
},
"equipment": {
"templates": ["base", "physical", "equipable"]
},
"loot": {
"templates": ["base", "physical"]
},
"talent": {
"templates": ["base"],
"rank": {
"base": 0,
"max": 0,
"mod": 0
}
},
"racialAbility": {
"templates": ["base"]
},
"language": {
"templates": ["base"]
},
"alphabet": {
"templates": ["base"]
},
"spell": {
"templates": ["base", "equipable"],
"spellType": "spellcasting",
@ -220,6 +197,29 @@
"sorcerer": null
}
},
"equipment": {
"templates": ["base", "physical", "equipable"]
},
"loot": {
"templates": ["base", "physical"]
},
"talent": {
"templates": ["base"],
"rank": {
"base": 0,
"max": 0,
"mod": 0
}
},
"racialAbility": {
"templates": ["base"]
},
"language": {
"templates": ["base"]
},
"alphabet": {
"templates": ["base"]
},
"specialCreatureAbility": {
"templates": ["base"],
"experiencePoints": 0

View file

@ -6,84 +6,19 @@ SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
<form class="{{cssClass}} flexcol" autocomplete="off">
{{!-- Sheet Header --}}
<header class="sheet-header">
<img class="profile-img" src="{{actor.img}}" data-edit="img" alt="Actor Icon" title="{{actor.name}}"
height="100" width="100" />
<div class="header-fields flexrow">
<h1 class="charname">
<label for="actor.name" class="hidden">Name</label>
<input name="name" type="text" id="actor.name" value="{{actor.name}}" placeholder="Name" />
</h1>
{{> systems/ds4/templates/sheets/actor/components/character-progression.hbs}}
<div class="flexrow basic-properties">
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.race">{{config.i18n.characterBaseInfo.race}}</label>
<input type="text" name="data.baseInfo.race" id="data.baseInfo.race" value="{{data.baseInfo.race}}"
data-dtype="String" />
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.culture">{{config.i18n.characterBaseInfo.culture}}</label>
<input id="data.baseInfo.culture" type="text" name="data.baseInfo.culture"
value="{{data.baseInfo.culture}}" data-dtype="String" />
</div>
<div class="basic-property flex125">
<label class="basic-property-label"
for="data.progression.progressPoints.used">{{config.i18n.characterProgression.progressPoints}}</label>
<div class="flexrow">
<input id="data.progression.progressPoints.used" type="number"
name="data.progression.progressPoints.used" value="{{data.progression.progressPoints.used}}"
data-dtype="Number" />
<span class="input-divider"> / </span>
<label class="hidden" for="data.progression.progressPoints.total">Total
Progression Points</label>
<input type="number" id="data.progression.progressPoints.total"
name="data.progression.progressPoints.total"
value="{{data.progression.progressPoints.total}}" data-dtype="Number" />
</div>
</div>
<div class="basic-property flex125">
<label class="basic-property-label"
for="data.progression.talentPoints.used">{{config.i18n.characterProgression.talentPoints}}</label>
<div class="flexrow">
<input type="number" name="data.progression.talentPoints.used"
id="data.progression.talentPoints.used" value="{{data.progression.talentPoints.used}}"
data-dtype="Number" />
<span class="input-divider"> / </span>
<label for="data.progression.talentPoints.total" class="hidden">Total Talent Points</label>
<input type="number" name="data.progression.talentPoints.total"
id="data.progression.talentPoints.total" value="{{data.progression.talentPoints.total}}"
data-dtype="Number" />
</div>
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.class">{{config.i18n.characterBaseInfo.class}}</label>
<input type="text" id="data.baseInfo.class" name="data.baseInfo.class"
value="{{data.baseInfo.class}}" data-dtype="String" />
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.heroClass">{{config.i18n.characterBaseInfo.heroClass}}</label>
<input type="text" id="data.baseInfo.heroClass" name="data.baseInfo.heroClass"
value="{{data.baseInfo.heroClass}}" data-dtype="String" />
</div>
</div>
</div>
</header>
<form class="{{cssClass}} ds4-sheet" autocomplete="off">
{{!-- Header --}}
{{#> systems/ds4/templates/sheets/actor/components/actor-header.hbs}}
{{> systems/ds4/templates/sheets/actor/components/character-properties.hbs}}
{{/systems/ds4/templates/sheets/actor/components/actor-header.hbs}}
{{!-- Sheet Tab Navigation --}}
<nav class="sheet-tabs tabs" data-group="primary">
<a class="item" data-tab="values">{{localize 'DS4.HeadingValues'}}</a>
<a class="item" data-tab="inventory">{{localize 'DS4.HeadingInventory'}}</a>
<a class="item" data-tab="spells">{{localize 'DS4.HeadingSpells'}}</a>
<a class="item" data-tab="talents-abilities">{{localize 'DS4.HeadingTalentsAbilities'}}</a>
<a class="item" data-tab="profile">{{localize "DS4.HeadingProfile"}}</a>
<a class="item" data-tab="biography">{{localize 'DS4.HeadingBiography'}}</a>
<nav class="ds4-sheet-tab-nav sheet-tabs tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item item" data-tab="values">{{localize 'DS4.HeadingValues'}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="inventory">{{localize 'DS4.HeadingInventory'}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="spells">{{localize 'DS4.HeadingSpells'}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="abilities">{{localize 'DS4.HeadingAbilities'}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="biography">{{localize 'DS4.HeadingBiography'}}</a>
</nav>
<!-- beautify ignore:start -->
@ -99,11 +34,8 @@ SPDX-License-Identifier: MIT
{{!-- Spells Tab --}}
{{> systems/ds4/templates/sheets/actor/tabs/spells.hbs}}
{{!-- Talents Tab --}}
{{> systems/ds4/templates/sheets/actor/tabs/talents-abilities.hbs}}
{{! Profile Tab --}}
{{> systems/ds4/templates/sheets/actor/tabs/profile.hbs}}
{{!-- Abilities Tab --}}
{{> systems/ds4/templates/sheets/actor/tabs/abilities.hbs}}
{{!-- Biography Tab --}}
{{> systems/ds4/templates/sheets/actor/tabs/biography.hbs}}

View file

@ -0,0 +1,29 @@
{{!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Oliver Rümpelein
SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
{{!--
!-- Render an actor sheet header.
!-- @param @partial-block: Properties to render in the second header row.
--}}
<header class="ds4-actor-header">
<img class="ds4-actor-header__img" src="{{data.img}}" data-edit="img" alt="{{localize 'DS4.ActorImageAltText'}}"
title="{{data.name}}" height="100" width="100" />
<div class="ds4-actor-header__data">
<div class="ds4-actor-header__data-row">
<h1 class="ds4-actor-header__name">
<label for="name-{{data._id}}" class="hidden">{{localize 'DS4.ActorName'}}</label>
<input class="ds4-actor-header__name-input" name="name" type="text" id="name-{{data._id}}"
value="{{data.name}}" placeholder="{{localize 'DS4.ActorName'}}" />
</h1>
{{> systems/ds4/templates/sheets/actor/components/actor-progression.hbs}}
</div>
<div class="ds4-actor-header__data-row">
{{> @partial-block}}
</div>
</div>
</header>

View file

@ -0,0 +1,48 @@
{{!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Oliver Rümpelein
SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
<div class="ds4-actor-progression">
<div class="ds4-actor-progression__entry">
<h2 class="ds4-actor-progression__label"><label for="data.combatValues.hitPoints.value-{{data._id}}"
title="{{localize 'DS4.CombatValuesHitPointsCurrent'}}">{{localize
"DS4.CombatValuesHitPointsCurrentAbbr"}}</label>
</h2>
<input class="ds4-actor-progression__input" type="number" name="data.combatValues.hitPoints.value"
id="data.combatValues.hitPoints.value-{{data._id}}" value="{{data.data.combatValues.hitPoints.value}}"
data-dtype="Number" />
</div>
{{#if (eq data.type "character")}}
{{#if settings.showSlayerPoints}}
<div class="ds4-actor-progression__entry">
<h2 class="ds4-actor-progression__label"><label for="data.slayersPoints.value-{{data._id}}"
title="{{localize 'DS4.CharacterSlayerPoints'}}">{{localize "DS4.CharacterSlayerPointsAbbr"}}</label>
</h2>
<input class="ds4-actor-progression__input ds4-actor-progression__input--slayer-points" type="number"
max="{{data.data.slayerPoints.max}}" min="0" step="1" name="data.slayerPoints.value"
id="data.slayersPoints.value-{{data._id}}" value="{{data.data.slayerPoints.value}}" data-dtype="Number" />
</div>
{{/if}}
<div class="ds4-actor-progression__entry">
<h2 class="ds4-actor-progression__label"><label for="data.progression.level-{{data._id}}"
title="{{localize 'DS4.CharacterProgressionLevel'}}">{{localize
"DS4.CharacterProgressionLevelAbbr"}}</label>
</h2>
<input class="ds4-actor-progression__input" type="number" min="0" name="data.progression.level"
id="data.progression.level-{{data._id}}" value="{{data.data.progression.level}}" data-dtype="Number" />
</div>
<div class="ds4-actor-progression__entry">
<h2 class="ds4-actor-progression__label"><label for="data.progression.experiencePoints-{{data._id}}"
title="{{localize 'DS4.CharacterProgressionExperiencePoints'}}">{{localize
"DS4.CharacterProgressionExperiencePointsAbbr"}}</label>
</h2>
<input class="ds4-actor-progression__input" type="number" min="0" name="data.progression.experiencePoints"
id="data.progression.experiencePoints-{{data._id}}" value="{{data.data.progression.experiencePoints}}"
data-dtype="Number" />
</div>
{{/if}}
</div>

View file

@ -0,0 +1,10 @@
{{!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-License-Identifier: MIT
--}}
<div class="ds4-biography">
{{editor content=data.data.profile.biography target="data.profile.biography" button=true owner=owner
editable=editable}}
</div>

View file

@ -1,46 +0,0 @@
{{!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Oliver Rümpelein
SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
<div class="progression flexrow">
<div class="progression-entry">
<h2 class="progression-label"><label for="data.combatValues.hitPoints.value"
title="{{localize 'DS4.CombatValuesHitPointsCurrent'}}">{{localize
"DS4.CombatValuesHitPointsCurrentAbbr"}}</label>
</h2>
<input class="progression-value" type="number" name="data.combatValues.hitPoints.value"
id="data.combatValues.hitPoints.value" value="{{data.combatValues.hitPoints.value}}" data-dtype="Number" />
</div>
{{#if (eq actor.type "character")}}
{{#if settings.showSlayerPoints}}
<div class="progression-entry">
<h2 class="progression-label"><label for="data.slayersPoints.value"
title="{{localize 'DS4.CharacterSlayerPoints'}}">{{localize "DS4.CharacterSlayerPointsAbbr"}}</label>
</h2>
<input class="progression-value progression-value--slayer-points" type="number" max="{{data.slayerPoints.max}}"
min="0" step="1" name="data.slayerPoints.value" id="data.slayersPoints.value"
value="{{data.slayerPoints.value}}" data-dtype="Number" />
</div>
{{/if}}
<div class="progression-entry">
<h2 class="progression-label"><label for="data.progression.level"
title="{{localize 'DS4.CharacterProgressionLevel'}}">{{localize
"DS4.CharacterProgressionLevelAbbr"}}</label>
</h2>
<input class="progression-value" type="number" min="0" name="data.progression.level" id="data.progression.level"
value="{{data.progression.level}}" data-dtype="Number" />
</div>
<div class="progression-entry">
<h2 class="progression-label"><label for="data.progression.experiencePoints"
title="{{localize 'DS4.CharacterProgressionExperiencePoints'}}">{{localize
"DS4.CharacterProgressionExperiencePointsAbbr"}}</label>
</h2>
<input class="progression-value" type="number" min="0" name="data.progression.experiencePoints"
id="data.progression.experiencePoints" value="{{data.progression.experiencePoints}}" data-dtype="Number" />
</div>
{{/if}}
</div>

View file

@ -0,0 +1,63 @@
{{!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Oliver Rümpelein
SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
<div class="ds4-actor-properties">
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.race-{{data._id}}">{{config.i18n.characterBaseInfo.race}}</label>
<input type="text" name="data.baseInfo.race" id="data.baseInfo.race-{{data._id}}"
value="{{data.data.baseInfo.race}}" data-dtype="String" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.culture-{{data._id}}">{{config.i18n.characterBaseInfo.culture}}</label>
<input id="data.baseInfo.culture-{{data._id}}" type="text" name="data.baseInfo.culture"
value="{{data.data.baseInfo.culture}}" data-dtype="String" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.progression.progressPoints.used-{{data._id}}">{{config.i18n.characterProgression.progressPoints}}</label>
<div class="ds4-actor-properties__property-multi-input">
<input id="data.progression.progressPoints.used-{{data._id}}" type="number"
name="data.progression.progressPoints.used" value="{{data.data.progression.progressPoints.used}}"
data-dtype="Number" />
<span class="input-divider"> / </span>
<label class="hidden" for="data.progression.progressPoints.total-{{data._id}}">Total
Progression Points</label>
<input type="number" id="data.progression.progressPoints.total-{{data._id}}"
name="data.progression.progressPoints.total" value="{{data.data.progression.progressPoints.total}}"
data-dtype="Number" />
</div>
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.progression.talentPoints.used-{{data._id}}">{{config.i18n.characterProgression.talentPoints}}</label>
<div class="ds4-actor-properties__property-multi-input">
<input type="number" name="data.progression.talentPoints.used"
id="data.progression.talentPoints.used-{{data._id}}" value="{{data.data.progression.talentPoints.used}}"
data-dtype="Number" />
<span class="input-divider"> / </span>
<label for="data.progression.talentPoints.total-{{data._id}}" class="hidden">Total Talent Points</label>
<input type="number" name="data.progression.talentPoints.total"
id="data.progression.talentPoints.total-{{data._id}}"
value="{{data.data.progression.talentPoints.total}}" data-dtype="Number" />
</div>
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.class-{{data._id}}">{{config.i18n.characterBaseInfo.class}}</label>
<input type="text" id="data.baseInfo.class-{{data._id}}" name="data.baseInfo.class"
value="{{data.data.baseInfo.class}}" data-dtype="String" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.heroClass-{{data._id}}">{{config.i18n.characterBaseInfo.heroClass}}</label>
<input type="text" id="data.baseInfo.heroClass-{{data._id}}" name="data.baseInfo.heroClass"
value="{{data.data.baseInfo.heroClass}}" data-dtype="String" />
</div>
</div>

View file

@ -7,6 +7,6 @@ SPDX-License-Identifier: MIT
<div class="ds4-checks">
{{#each config.i18n.checks as |check-label check-key|}}
{{> systems/ds4/templates/sheets/actor/components/check.hbs check-key=check-key check-target-number=(lookup
../data.checks check-key) check-label=check-label}}
../data.data.checks check-key) check-label=check-label}}
{{/each}}
</div>

View file

@ -9,20 +9,24 @@ SPDX-License-Identifier: MIT
!--
!-- @param combat-value-key: The key of the combat value
!-- @param combat-value-data: The data for the combat value
!-- @param combat-value-label: The label for the combat value
!-- @param combat-value-title: The title for the combat value
!-- @param combat-value-label: The label for the combat value (possibly an abbreviation)
!-- @param actor-id: The id of the actor the core value belongs to
--}}
<div class="ds4-combat-value">
<div class="ds4-combat-value__value ds4-combat-value__value--{{combat-value-key}}"
title="{{combat-value-label}}: {{combat-value-data.tooltip}}">
title="{{combat-value-title}}: {{combat-value-data.tooltip}}">
{{combat-value-data.total}}
</div>
<span title="{{combat-value-title}}" class="ds4-combat-value__label">{{combat-value-label}}</span>
<div class="ds4-combat-value__formula">
<span class="ds4-combat-value__formula-base"
title="{{combat-value-label}} {{localize 'DS4.TooltipBaseValue'}}">{{combat-value-data.base}}</span>
title="{{combat-value-title}} {{localize 'DS4.TooltipBaseValue'}}">{{combat-value-data.base}}</span>
<span>+</span>
<input class="ds4-combat-value__formula-modifier" type="number" name="data.combatValues.{{combat-value-key}}.mod"
value='{{combat-value-data.mod}}' data-dtype="Number"
title="{{combat-value-label}} {{localize 'DS4.TooltipModifier'}}" />
<input class="ds4-combat-value__formula-modifier" type="number"
id="data.combatValues.{{combat-value-key}}.mod-{{actor-id}}"
name="data.combatValues.{{combat-value-key}}.mod" value='{{combat-value-data.mod}}' data-dtype="Number"
title="{{combat-value-title}} {{localize 'DS4.TooltipModifier'}}" />
</div>
</div>

View file

@ -6,8 +6,10 @@ SPDX-License-Identifier: MIT
--}}
<div class="ds4-combat-values">
{{#each config.i18n.combatValues as |combat-value-label combat-value-key|}}
{{#each config.i18n.combatValues as |combat-value-title combat-value-key|}}
{{> systems/ds4/templates/sheets/actor/components/combat-value.hbs combat-value-key=combat-value-key
combat-value-data=(lookup ../data.combatValues combat-value-key) combat-value-label=combat-value-label}}
combat-value-data=(lookup ../data.data.combatValues combat-value-key) combat-value-label=(lookup
../config.i18n.combatValuesSheet combat-value-key) combat-value-title=combat-value-title
actor-id=../data._id}}
{{/each}}
</div>

View file

@ -11,21 +11,22 @@ SPDX-License-Identifier: MIT
!-- @param core-value-key: The key of the core value
!-- @param core-value-data: The data for the core value
!-- @param core-value-variant: The variant of the core value, i.e. attribute or trait
!-- @param actor-id: The id of the actor the core value belongs to
--}}
<div class="ds4-core-value ds4-core-value--{{core-value-variant}}">
<label for="data.{{core-value-variant}}s.{{core-value-key}}.base"
<label for="data.{{core-value-variant}}s.{{core-value-key}}.base-{{actor-id}}"
class="ds4-core-value__label">{{core-value-label}}</label>
<div class="ds4-core-value__value">
<input class="ds4-core-value__value-input" type="number"
name="data.{{core-value-variant}}s.{{core-value-key}}.base"
id="data.{{core-value-variant}}s.{{core-value-key}}.base" value='{{core-value-data.base}}'
id="data.{{core-value-variant}}s.{{core-value-key}}.base-{{actor-id}}" value='{{core-value-data.base}}'
data-dtype="Number" title="{{core-value-label}} {{localize 'DS4.TooltipBaseValue'}}" />
<span>+</span>
<input class="ds4-core-value__value-input" type="number"
name="data.{{core-value-variant}}s.{{core-value-key}}.mod"
id="data.{{core-value-variant}}s.{{core-value-key}}.mod" value='{{core-value-data.mod}}' data-dtype="Number"
title="{{core-value-label}} {{localize 'DS4.TooltipModifier'}}" />
id="data.{{core-value-variant}}s.{{core-value-key}}.mod-{{actor-id}}" value='{{core-value-data.mod}}'
data-dtype="Number" title="{{core-value-label}} {{localize 'DS4.TooltipModifier'}}" />
<span class="ds4-core-value__value-arrow">➞</span>
<span class="ds4-core-value__value-total"
title="{{core-value-label}}: {{core-value-data.tooltip}}">{{core-value-data.total}}</span>

View file

@ -8,12 +8,12 @@ SPDX-License-Identifier: MIT
<div class="ds4-core-values">
{{#each config.i18n.attributes as |attribute-label attribute-key|}}
{{> systems/ds4/templates/sheets/actor/components/core-value.hbs core-value-label=attribute-label
core-value-key=attribute-key core-value-data=(lookup ../data.attributes
attribute-key) core-value-variant="attribute"}}
core-value-key=attribute-key core-value-data=(lookup ../data.data.attributes
attribute-key) core-value-variant="attribute" actor-id=../data._id}}
{{/each}}
{{#each config.i18n.traits as |trait-label trait-key|}}
{{> systems/ds4/templates/sheets/actor/components/core-value.hbs core-value-label=trait-label
core-value-key=trait-key
core-value-data=(lookup ../data.traits trait-key) core-value-variant="trait"}}
core-value-data=(lookup ../data.data.traits trait-key) core-value-variant="trait" actor-id=../data._id}}
{{/each}}
</div>

View file

@ -0,0 +1,52 @@
{{!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Oliver Rümpelein
SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
<div class="ds4-actor-properties">
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.creatureType-{{data._id}}">{{config.i18n.creatureBaseInfo.creatureType}}</label>
<select class="ds4-actor-properties__property-select" id="data.baseInfo.creatureType-{{data._id}}"
name="data.baseInfo.creatureType" data-type="String">
{{#select data.data.baseInfo.creatureType}}
{{#each config.i18n.creatureTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.loot-{{data._id}}">{{config.i18n.creatureBaseInfo.loot}}</label>
<input type="text" id="data.baseInfo.loot-{{data._id}}" name="data.baseInfo.loot"
value="{{data.data.baseInfo.loot}}" data-dtype="String" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.foeFactor-{{data._id}}">{{config.i18n.creatureBaseInfo.foeFactor}}</label>
<input type="text" id="data.baseInfo.foeFactor-{{data._id}}" name="data.baseInfo.foeFactor"
value="{{data.data.baseInfo.foeFactor}}" data-dtype="Number" />
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.sizeCategory-{{data._id}}">{{config.i18n.creatureBaseInfo.sizeCategory}}</label>
<select class="ds4-actor-properties__property-select" id="data.baseInfo.sizeCategory-{{data._id}}"
name="data.baseInfo.sizeCategory" data-type="String">
{{#select data.data.baseInfo.sizeCategory}}
{{#each config.i18n.creatureSizeCategories as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.experiencePoints-{{data._id}}">{{config.i18n.creatureBaseInfo.experiencePoints}}</label>
<input type="text" id="data.baseInfo.experiencePoints-{{data._id}}" name="data.baseInfo.experiencePoints"
value="{{data.data.baseInfo.experiencePoints}}" data-dtype="Number" />
</div>
</div>

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: MIT
<h4 class="ds4-currency-title">{{localize 'DS4.CharacterCurrency'}}</h4>
<div class="ds4-currency">
{{#each data.currency as |value key|}}
{{#each data.data.currency as |value key|}}
<label for="data.currency.{{key}}" class="flex05">{{lookup ../config.i18n.characterCurrency key}}</label>
<input class="ds4-currency__value ds4-currency__value--{{key}} item-change" type="number" min="0" step="1"
name="data.currency.{{key}}" id="data.currency.{{key}}" value="{{value}}" data-dtype="Number" />

View file

@ -25,7 +25,7 @@ SPDX-License-Identifier: MIT
{{!-- image --}}
{{> systems/ds4/templates/sheets/actor/components/rollable-image.hbs rollable=itemData.data.rollable
src=itemData.img alt=(localize "DS4.EntityImageAltText" name=itemData.name) title=itemData.name
src=itemData.img alt=(localize "DS4.DocumentImageAltText" name=itemData.name) title=itemData.name
rollableTitle=(localize "DS4.RollableImageRollableTitle" name=itemData.name) rollableClass="rollable-item"}}
{{!-- amount --}}

View file

@ -69,8 +69,8 @@ SPDX-License-Identifier: MIT
</div>
{{!-- armor type --}}
<div title="{{lookup ../../config.i18n.armorTypes itemData.dataData.armorType}}">
{{lookup ../../config.i18n.armorTypesAbbr itemData.dataData.armorType}}
<div title="{{lookup ../../config.i18n.armorTypes itemData.data.armorType}}">
{{lookup ../../config.i18n.armorTypesAbbr itemData.data.armorType}}
</div>
{{!-- armor value --}}

View file

@ -0,0 +1,29 @@
{{!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
<div class="ds4-profile">
{{#each data.data.profile as |profile-data-value profile-data-key|}}
{{#if (and (ne profile-data-key 'biography') (ne profile-data-key 'specialCharacteristics'))}}
<div class="ds4-profile__entry">
<label class="ds4-profile__entry-label" for="data.profile.{{profile-data-key}}">
{{lookup ../config.i18n.characterProfile profile-data-key}}
</label>
<input class="ds4-profile__entry-input" type="text" name="data.profile.{{profile-data-key}}"
value="{{profile-data-value}}"
data-dtype="{{lookup ../config.i18n.characterProfileDTypes profile-data-key}}" />
</div>
{{/if}}
{{/each}}
<div class="ds4-profile__entry">
<label class="ds4-profile__entry-label" for="data.profile.specialCharacteristics">
{{lookup config.i18n.characterProfile 'specialCharacteristics'}}
</label>
<textarea class="ds4-profile__entry-input ds4-profile__entry-input--multiline"
name="data.profile.specialCharacteristics" data-dtype="String"
rows="4">{{data.data.profile.specialCharacteristics}}</textarea>
</div>
</div>

View file

@ -6,66 +6,21 @@ SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
<form class="{{cssClass}} flexcol" autocomplete="off">
{{!-- Sheet Header --}}
<header class="sheet-header">
<img class="profile-img" src="{{actor.img}}" data-edit="img" title="{{actor.name}}" height="100" width="100" />
<div class="header-fields flexrow">
<h1 class="charname">
<label for="actor.name" class="hidden">Name</label>
<input name="name" type="text" id="actor.name" value="{{actor.name}}" placeholder="Name" />
</h1>
{{> systems/ds4/templates/sheets/actor/components/character-progression.hbs}}
<form class="{{cssClass}} ds4-sheet" autocomplete="off">
{{!-- Header --}}
{{#> systems/ds4/templates/sheets/actor/components/actor-header.hbs}}
{{> systems/ds4/templates/sheets/actor/components/creature-properties.hbs}}
{{/systems/ds4/templates/sheets/actor/components/actor-header.hbs}}
<div class="flexrow basic-properties">
<div class="basic-property">
<label>{{config.i18n.creatureBaseInfo.creatureType}}</label>
<select name="data.baseInfo.creatureType" data-type="String">
{{#select data.baseInfo.creatureType}}
{{#each config.i18n.creatureTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.loot">{{config.i18n.creatureBaseInfo.loot}}</label>
<input type="text" name="data.baseInfo.loot" value="{{data.baseInfo.loot}}" data-dtype="String" />
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.foeFactor">{{config.i18n.creatureBaseInfo.foeFactor}}</label>
<input type="text" name="data.baseInfo.foeFactor" value="{{data.baseInfo.foeFactor}}"
data-dtype="Number" />
</div>
<div class="basic-property">
<label>{{config.i18n.creatureBaseInfo.sizeCategory}}</label>
<select name="data.baseInfo.sizeCategory" data-type="String">
{{#select data.baseInfo.sizeCategory}}
{{#each config.i18n.creatureSizeCategories as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.experiencePoints">{{config.i18n.creatureBaseInfo.experiencePoints}}</label>
<input type="text" name="data.baseInfo.experiencePoints" value="{{data.baseInfo.experiencePoints}}"
data-dtype="Number" />
</div>
</div>
</div>
</header>
{{!-- Sheet Tab Navigation --}}
<nav class="sheet-tabs tabs" data-group="primary">
<a class="item" data-tab="values">{{localize 'DS4.HeadingValues'}}</a>
<a class="item" data-tab="inventory">{{localize 'DS4.HeadingInventory'}}</a>
<a class="item" data-tab="special-creature-abilities">{{localize 'DS4.HeadingSpecialCreatureAbilities'}}</a>
<a class="item" data-tab="spells">{{localize 'DS4.HeadingSpells'}}</a>
<a class="item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
<nav class="ds4-sheet-tab-nav sheet-tabs tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item item" data-tab="values">{{localize 'DS4.HeadingValues'}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="inventory">{{localize 'DS4.HeadingInventory'}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="special-creature-abilities">{{localize
'DS4.HeadingSpecialCreatureAbilities'}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="spells">{{localize 'DS4.HeadingSpells'}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="description">{{localize 'DS4.HeadingDescription'}}</a>
</nav>
{{!-- Sheet Body --}}

View file

@ -5,7 +5,7 @@ SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
<div class="tab talents-abilities" data-group="primary" data-tab="talents-abilities">
<div class="tab abilities" data-group="primary" data-tab="abilities">
{{!-- TALENT --}}
<h4 class="ds4-item-list-title">{{localize 'DS4.ItemTypeTalentPlural'}}</h4>
{{#unless (isEmpty itemsByType.talent)}}

View file

@ -5,6 +5,13 @@ SPDX-License-Identifier: MIT
--}}
<div class="tab biography" data-group="primary" data-tab="biography">
{{editor content=data.profile.biography target="data.profile.biography" button=true owner=owner
editable=editable}}
<div class="ds4-biography-tab-content">
<!-- beautify ignore:start -->
<!-- prettier-ignore-start -->
{{!-- remove indentation to avoid annoying Handlebars auto-indent --}}
{{> systems/ds4/templates/sheets/actor/components/profile.hbs}}
{{> systems/ds4/templates/sheets/actor/components/biography.hbs}}
<!-- beautify ignore:end -->
<!-- prettier-ignore-end -->
</div>
</div>

View file

@ -5,6 +5,6 @@ SPDX-License-Identifier: MIT
--}}
<div class="tab description" data-group="primary" data-tab="description">
{{editor content=data.baseInfo.description target="data.baseInfo.description" button=true owner=owner
{{editor content=data.data.baseInfo.description target="data.baseInfo.description" button=true owner=owner
editable=editable}}
</div>

View file

@ -1,29 +0,0 @@
{{!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-License-Identifier: MIT
--}}
<div class="tab profile" data-group="primary" data-tab="profile">
<div class="grid grid-2col">
{{#each data.profile as |profile-data-value profile-data-key|}}
{{#if (and (ne profile-data-key 'biography') (ne profile-data-key 'specialCharacteristics'))}}
<div class="profile-entry">
<label for="data.profile.{{profile-data-key}}">
{{lookup ../config.i18n.characterProfile profile-data-key}}
</label>
<input type="text" name="data.profile.{{profile-data-key}}" value="{{profile-data-value}}"
data-dtype="{{lookup ../config.i18n.characterProfileDTypes profile-data-key}}" />
</div>
{{/if}}
{{/each}}
<div>
<label for="data.profile.specialCharacteristics">
{{lookup config.i18n.characterProfile 'specialCharacteristics'}}
</label>
<textarea name="data.profile.specialCharacteristics" data-dtype="String"
rows="4">{{data.profile.specialCharacteristics}}</textarea>
</div>
</div>
</div>

View file

@ -7,35 +7,36 @@ SPDX-License-Identifier: MIT
<form class="{{cssClass}}" autocomplete="off">
{{#> systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
<div class="grid grid-3col basic-properties">
<div class="basic-property">
<label>{{localize "DS4.ArmorType"}}</label>
<select name="data.armorType" data-type="String">
{{#select data.armorType}}
{{#each config.i18n.armorTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="basic-property">
<label for="data.armorMaterialType">{{localize "DS4.ArmorMaterialType"}}</label>
<select name="data.armorMaterialType" data-type="String">
{{#select data.armorMaterialType}}
{{#each config.i18n.armorMaterialTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="basic-property">
<label>{{localize "DS4.ArmorValue"}}</label>
<input type="text" name="data.armorValue" value="{{data.armorValue}}"
placeholder="0" data-dtype="Number" />
</div>
</div>
<div class="grid grid-3col basic-properties">
<div class="basic-property">
<label>{{localize "DS4.ArmorType"}}</label>
<select name="data.armorType" data-type="String">
{{#select data.data.armorType}}
{{#each config.i18n.armorTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="basic-property">
<label for="data.armorMaterialType">{{localize "DS4.ArmorMaterialType"}}</label>
<select name="data.armorMaterialType" data-type="String">
{{#select data.data.armorMaterialType}}
{{#each config.i18n.armorMaterialTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="basic-property">
<label>{{localize "DS4.ArmorValue"}}</label>
<input type="text" name="data.armorValue" value="{{data.data.armorValue}}" placeholder="0"
data-dtype="Number" />
</div>
</div>
{{/systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
{{!-- Common Item body --}}
{{#> systems/ds4/templates/sheets/item/components/body.hbs}}{{/systems/ds4/templates/sheets/item/components/body.hbs}}
{{#>
systems/ds4/templates/sheets/item/components/body.hbs}}{{/systems/ds4/templates/sheets/item/components/body.hbs}}
</form>

View file

@ -8,11 +8,11 @@ SPDX-License-Identifier: MIT
{{!-- Template for the common body (navigation & body sections) of all items. --}}
{{!-- Sheet Tab Navigation --}}
<nav class="sheet-tabs tabs" data-group="primary">
<a class="item" data-tab="description">{{localize "DS4.HeadingDescription"}}</a>
<a class="item" data-tab="effects">{{localize "DS4.HeadingEffects"}}</a>
<nav class="ds4-sheet-tab-nav sheet-tabs tabs" data-group="primary">
<a class="ds4-sheet-tab-nav__item item" data-tab="description">{{localize "DS4.HeadingDescription"}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="effects">{{localize "DS4.HeadingEffects"}}</a>
{{#if isPhysical}}
<a class="item" data-tab="details">{{localize "DS4.HeadingDetails"}}</a>
<a class="ds4-sheet-tab-nav__item item" data-tab="details">{{localize "DS4.HeadingDetails"}}</a>
{{/if}}
</nav>

View file

@ -6,9 +6,9 @@ SPDX-License-Identifier: MIT
--}}
<header class="sheet-header">
<img class="profile-img" src="{{item.img}}" data-edit="img" title="{{item.name}}" />
<img class="profile-img" src="{{data.img}}" data-edit="img" title="{{data.name}}" />
<div class="header-fields flexrow">
<h1 class="charname"><input name="name" type="text" value="{{item.name}}" placeholder="Name" /></h1>
<h1 class="charname"><input name="name" type="text" value="{{data.name}}" placeholder="Name" /></h1>
<h2 class="item-type">{{lookup config.i18n.itemTypes item.type}}</h2>
{{> @partial-block}}
</div>

View file

@ -7,15 +7,16 @@ SPDX-License-Identifier: MIT
<form class="{{cssClass}}" autocomplete="off">
{{#> systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
<div class="grid grid-1col basic-properties">
<div class="basic-property">
<label>{{localize "DS4.ArmorValue"}}</label>
<input type="text" name="data.armorValue" value="{{data.armorValue}}"
placeholder="0" data-dtype="Number" />
</div>
<div class="grid grid-1col basic-properties">
<div class="basic-property">
<label>{{localize "DS4.ArmorValue"}}</label>
<input type="text" name="data.armorValue" value="{{data.data.armorValue}}" placeholder="0"
data-dtype="Number" />
</div>
</div>
{{/systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
{{!-- Common Item body --}}
{{#> systems/ds4/templates/sheets/item/components/body.hbs}}{{/systems/ds4/templates/sheets/item/components/body.hbs}}
{{#>
systems/ds4/templates/sheets/item/components/body.hbs}}{{/systems/ds4/templates/sheets/item/components/body.hbs}}
</form>

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: MIT
<div class="grid grid-3col basic-properties">
<div class="basic-property">
<label>{{localize "DS4.SpecialCreatureAbilityExperiencePoints"}}</label>
<input type="number" min="0" step="1" name="data.experiencePoints" value="{{data.experiencePoints}}"
<input type="number" min="0" step="1" name="data.experiencePoints" value="{{data.data.experiencePoints}}"
placeholder="0" data-dtype="Number" />
</div>
</div>

View file

@ -16,9 +16,9 @@ SPDX-License-Identifier: MIT
<label>{{localize localizeString}}</label>
<div class="unit-data-pair">
<input class="item-num-val" type="text" data-dtype="String" name="data.{{property}}.value"
value="{{lookup (lookup data property) 'value'}}" />
value="{{lookup (lookup data.data property) 'value'}}" />
<select name="data.{{property}}.unit" data-type="String">
{{#select (lookup (lookup data property) 'unit')}}
{{#select (lookup (lookup data.data property) 'unit')}}
{{#if (eq unitType 'temporal')}}
{{#each (lookup config.i18n 'temporalUnitsAbbr') as |value key|}}
<option value="{{key}}">{{value}}</option>
@ -48,7 +48,7 @@ SPDX-License-Identifier: MIT
<div class="basic-property">
<label for="data.spellType">{{localize "DS4.SpellType"}}</label>
<select id="data.spellType" name="data.spellType" data-type="String">
{{#select data.spellType}}
{{#select data.data.spellType}}
{{#each config.i18n.spellTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
@ -57,7 +57,7 @@ SPDX-License-Identifier: MIT
</div>
<div class="basic-property">
<label for="data.bonus">{{localize "DS4.SpellBonus"}}</label>
<input id="data.bonus" type="text" name="data.bonus" value="{{data.bonus}}" data-dtype="String" />
<input id="data.bonus" type="text" name="data.bonus" value="{{data.data.bonus}}" data-dtype="String" />
</div>
</div>
{{/systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
@ -67,7 +67,7 @@ SPDX-License-Identifier: MIT
<div class="side-property">
<label for="data.spellCategory">{{localize "DS4.SpellCategory"}}</label>
<select id="data.spellCategory" name="data.spellCategory" data-type="String">
{{#select data.spellCategory}}
{{#select data.data.spellCategory}}
{{#each config.i18n.spellCategories as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
@ -82,21 +82,21 @@ SPDX-License-Identifier: MIT
<div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsHealer'}}">
<label for="data.minimumLevels.healer">{{localize "DS4.SpellMinimumLevelsHealerAbbr"}}</label>
<input type="number" min="0" step="1" data-dtype="Number" name="data.minimumLevels.healer"
id="data.minimumLevels.healer" value="{{data.minimumLevels.healer}}" />
id="data.minimumLevels.healer" value="{{data.data.minimumLevels.healer}}" />
</div>
<div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsWizard'}}">
<label for="data.minimumLevels.wizard">{{localize "DS4.SpellMinimumLevelsWizardAbbr"}}</label>
<input type="number" min="0" step="1" data-dtype="Number" name="data.minimumLevels.wizard"
id="data.minimumLevels.wizard" value="{{data.minimumLevels.wizard}}" />
id="data.minimumLevels.wizard" value="{{data.data.minimumLevels.wizard}}" />
</div>
<div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsSorcerer'}}">
<label for="data.minimumLevels.sorcerer">{{localize "DS4.SpellMinimumLevelsSorcererAbbr"}}</label>
<input type="number" min="0" step="1" data-dtype="Number" name="data.minimumLevels.sorcerer"
id="data.minimumLevels.sorcerer" value="{{data.minimumLevels.sorcerer}}" />
id="data.minimumLevels.sorcerer" value="{{data.data.minimumLevels.sorcerer}}" />
</div>
<div class="side-property">
<label for="data.price">{{localize "DS4.SpellPrice"}}</label>
<span name="data.price" id="data.price">{{data.price}}</span>
<span name="data.price" id="data.price">{{data.data.price}}</span>
</div>
{{/systems/ds4/templates/sheets/item/components/body.hbs}}

View file

@ -13,25 +13,27 @@ Additional elements of the side-properties div can be handed over via the @parti
<div class="tab flexrow description" data-group="primary" data-tab="description">
<div class="side-properties">
{{#if isOwned}}
{{#if (ne data.equipped undefined)}}<div class="side-property">
{{#if (ne data.data.equipped undefined)}}<div class="side-property">
<label for="data.equipped">{{localize 'DS4.ItemEquipped'}}</label>
<input type="checkbox" name="data.equipped" data-dtype="Boolean" {{checked data.equipped}} title="{{localize 'DS4.ItemEquipped'}}">
<input type="checkbox" name="data.equipped" data-dtype="Boolean" {{checked data.data.equipped}}
title="{{localize 'DS4.ItemEquipped'}}">
</div>
{{/if}}
<div class="side-property">
<label for="data.actor">{{localize 'DS4.ItemOwner'}}</label>
<a class="entity-link" draggable="true" data-entity="Actor" data-id="{{actor._id}}"><i
<a class="entity-link" draggable="true" data-entity="Actor" data-id="{{actor.id}}"><i
class="fas fa-user"></i>{{actor.name}}</a>
</div>
{{#if isPhysical}}
<div class="side-property">
<label for="data.quantity">{{localize 'DS4.Quantity'}}</label>
<input type="number" min="0" step="1" data-dtype="Number" name="data.quantity" value="{{data.quantity}}" />
</div>
<div class="side-property">
<label for="data.storageLocation">{{localize 'DS4.StorageLocation'}}</label>
<input type="text" data-dtype="String" name="data.storageLocation" value="{{data.storageLocation}}" />
</div>
<div class="side-property">
<label for="data.quantity">{{localize 'DS4.Quantity'}}</label>
<input type="number" min="0" step="1" data-dtype="Number" name="data.quantity"
value="{{data.data.quantity}}" />
</div>
<div class="side-property">
<label for="data.storageLocation">{{localize 'DS4.StorageLocation'}}</label>
<input type="text" data-dtype="String" name="data.storageLocation" value="{{data.data.storageLocation}}" />
</div>
{{/if}}
{{else}}
<span>{{localize "DS4.NotOwned"}}</span>
@ -39,6 +41,6 @@ Additional elements of the side-properties div can be handed over via the @parti
{{> @partial-block}}
</div>
<div class="description" title="{{localize 'DS4.HeadingDescription'}}">
{{editor content=data.description target="data.description" button=true owner=owner editable=editable}}
{{editor content=data.data.description target="data.description" button=true owner=owner editable=editable}}
</div>
</div>

View file

@ -11,13 +11,13 @@ SPDX-License-Identifier: MIT
<div class="side-properties">
<div class="side-property">
<label for="data.price">{{localize "DS4.PriceGold"}}</label>
<input type="number" min="0" max="99999" step="0.01" data-dtype="Number"
name="data.price" value="{{data.price}}" />
<input type="number" min="0" max="99999" step="0.01" data-dtype="Number" name="data.price"
value="{{data.data.price}}" />
</div>
<div class="side-property">
<label for="data.availability">{{localize "DS4.ItemAvailability"}}</label>
<select name="data.availability" data-type="String">
{{#select data.availability}}
{{#select data.data.availability}}
{{#each config.i18n.itemAvailabilities as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}

View file

@ -17,8 +17,8 @@ SPDX-License-Identifier: MIT
</div>
</li>
{{#each item.effects as |effect id|}}
<li class="effect flexrow" data-effect-id="{{effect._id}}">
<h4 class="effect-name">{{effect.label}}</h4>
<li class="effect flexrow" data-effect-id="{{effect.id}}">
<h4 class="effect-name">{{effect.data.label}}</h4>
<div class="effect-controls">
<a class="effect-control" data-action="edit" title="{{localize 'DS4.UserInteractionEditEffect'}}">
<i class="fas fa-edit"></i></a>

View file

@ -13,9 +13,9 @@ SPDX-License-Identifier: MIT
{{#*inline "talentRankBasicProperty" }}
<div class="basic-property">
<label for="data.rank.{{property}}">{{localize localizeString}}</label>
<input type="number" min="0" step="1" data-dtype="Number" {{disabled}}
{{#if (eq property 'base') }}max="{{data.rank.max}}"{{/if}}
name="data.rank.{{property}}" value="{{lookup data.rank property}}" />
<input type="number" min="0" step="1" data-dtype="Number" {{disabled}} {{#if (eq property 'base' )
}}max="{{data.data.rank.max}}" {{/if}} name="data.rank.{{property}}"
value="{{lookup data.data.rank property}}" />
</div>
{{/inline}}
@ -25,15 +25,17 @@ SPDX-License-Identifier: MIT
<form class="{{cssClass}}" autocomplete="off">
{{#> systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
<div class="grid grid-4col basic-properties">
{{> talentRankBasicProperty data=data property='base' localizeString='DS4.TalentRankBase' }}
{{> talentRankBasicProperty data=data property='max' localizeString='DS4.TalentRankMax'}}
{{> talentRankBasicProperty data=data property='mod' localizeString='DS4.TalentRankMod'}}
{{> talentRankBasicProperty data=data property='total' localizeString='DS4.TalentRankTotal' disabled='disabled'}}
</div>
<div class="grid grid-4col basic-properties">
{{> talentRankBasicProperty data=data property='base' localizeString='DS4.TalentRankBase' }}
{{> talentRankBasicProperty data=data property='max' localizeString='DS4.TalentRankMax'}}
{{> talentRankBasicProperty data=data property='mod' localizeString='DS4.TalentRankMod'}}
{{> talentRankBasicProperty data=data property='total' localizeString='DS4.TalentRankTotal'
disabled='disabled'}}
</div>
{{/systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
{{!-- Common Item body --}}
{{#> systems/ds4/templates/sheets/item/components/body.hbs}}{{/systems/ds4/templates/sheets/item/components/body.hbs}}
{{#>
systems/ds4/templates/sheets/item/components/body.hbs}}{{/systems/ds4/templates/sheets/item/components/body.hbs}}
</form>

View file

@ -7,30 +7,31 @@ SPDX-License-Identifier: MIT
<form class="{{cssClass}}" autocomplete="off">
{{#> systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
<div class="grid grid-3col basic-properties">
<div class="basic-property">
<label>{{localize "DS4.AttackType"}}</label>
<select name="data.attackType" data-type="String">
{{#select data.attackType}}
{{#each config.i18n.attackTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="basic-property">
<label>{{localize "DS4.WeaponBonus"}}</label>
<input type="number" name="data.weaponBonus" value="{{data.weaponBonus}}"
placeholder="0" data-dtype="Number" />
</div>
<div class="basic-property">
<label>{{localize "DS4.OpponentDefense"}}</label>
<input type="number" name="data.opponentDefense"
value="{{data.opponentDefense}}" placeholder="0" data-dtype="Number" />
</div>
<div class="grid grid-3col basic-properties">
<div class="basic-property">
<label>{{localize "DS4.AttackType"}}</label>
<select name="data.attackType" data-type="String">
{{#select data.data.attackType}}
{{#each config.i18n.attackTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="basic-property">
<label>{{localize "DS4.WeaponBonus"}}</label>
<input type="number" name="data.weaponBonus" value="{{data.data.weaponBonus}}" placeholder="0"
data-dtype="Number" />
</div>
<div class="basic-property">
<label>{{localize "DS4.OpponentDefense"}}</label>
<input type="number" name="data.opponentDefense" value="{{data.data.opponentDefense}}" placeholder="0"
data-dtype="Number" />
</div>
</div>
{{/systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
{{!-- Common Item body --}}
{{#> systems/ds4/templates/sheets/item/components/body.hbs}}{{/systems/ds4/templates/sheets/item/components/body.hbs}}
{{#>
systems/ds4/templates/sheets/item/components/body.hbs}}{{/systems/ds4/templates/sheets/item/components/body.hbs}}
</form>

2881
yarn.lock

File diff suppressed because it is too large Load diff