Merge branch '074-update-to-foundry-0.8.x' into 'master'

Update to 0.8.x

Closes #74

See merge request dungeonslayers/ds4!117
This commit is contained in:
Johannes Loher 2021-07-08 06:16:12 +00:00
commit a07dd207c3
66 changed files with 2505 additions and 2792 deletions

4
.husky/.gitignore vendored
View file

@ -1,5 +1 @@
# SPDX-FileCopyrightText: 2021 Johannes Loher
#
# SPDX-License-Identifier: MIT
_

View file

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

View file

@ -52,7 +52,7 @@
"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-0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@types/fs-extra": "^9.0.11",
"@types/jest": "^26.0.23",

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",

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",

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,24 +7,19 @@
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: [
@ -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

@ -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

@ -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

@ -10,6 +10,7 @@ import { DS4CreatureActorSheet } from "../actor/sheets/creature-sheet";
import { DS4 } from "../config";
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 +29,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 +40,8 @@ async function init() {
CONFIG.DS4 = DS4;
CONFIG.Actor.entityClass = DS4Actor;
CONFIG.Item.entityClass = DS4Item;
CONFIG.Actor.documentClass = DS4Actor;
CONFIG.Item.documentClass = DS4Item;
CONFIG.Actor.typeLabels = DS4.i18n.actorTypes;
CONFIG.Item.typeLabels = DS4.i18n.itemTypes;
@ -63,3 +64,20 @@ async function init() {
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
@ -15,11 +15,11 @@ export function getActiveActor(): DS4Actor | 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

@ -3,8 +3,8 @@
"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/).",
"version": "0.8.0",
"minimumCoreVersion": "0.7.9",
"compatibleCoreVersion": "0.7.10",
"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": [

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

@ -9,12 +9,12 @@ 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}}"
<img class="profile-img" src="{{data.img}}" data-edit="img" alt="Actor Icon" title="{{data.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" />
<label for="name" class="hidden">Name</label>
<input name="name" type="text" id="name" value="{{data.name}}" placeholder="Name" />
</h1>
{{> systems/ds4/templates/sheets/actor/components/character-progression.hbs}}
@ -22,28 +22,28 @@ SPDX-License-Identifier: MIT
<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" />
<input type="text" name="data.baseInfo.race" id="data.baseInfo.race"
value="{{data.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" />
value="{{data.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" />
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">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" />
value="{{data.data.progression.progressPoints.total}}" data-dtype="Number" />
</div>
</div>
<div class="basic-property flex125">
@ -51,26 +51,26 @@ SPDX-License-Identifier: MIT
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}}"
id="data.progression.talentPoints.used" value="{{data.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" />
id="data.progression.talentPoints.total"
value="{{data.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" />
value="{{data.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" />
value="{{data.data.baseInfo.heroClass}}" data-dtype="String" />
</div>
</div>
</div>

View file

@ -13,7 +13,8 @@ SPDX-License-Identifier: MIT
"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" />
id="data.combatValues.hitPoints.value" value="{{data.data.combatValues.hitPoints.value}}"
data-dtype="Number" />
</div>
{{#if (eq actor.type "character")}}
{{#if settings.showSlayerPoints}}
@ -21,9 +22,9 @@ SPDX-License-Identifier: MIT
<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" />
<input class="progression-value progression-value--slayer-points" type="number"
max="{{data.data.slayerPoints.max}}" min="0" step="1" name="data.slayerPoints.value"
id="data.slayersPoints.value" value="{{data.data.slayerPoints.value}}" data-dtype="Number" />
</div>
{{/if}}
<div class="progression-entry">
@ -32,7 +33,7 @@ SPDX-License-Identifier: MIT
"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" />
value="{{data.data.progression.level}}" data-dtype="Number" />
</div>
<div class="progression-entry">
<h2 class="progression-label"><label for="data.progression.experiencePoints"
@ -40,7 +41,8 @@ SPDX-License-Identifier: MIT
"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" />
id="data.progression.experiencePoints" value="{{data.data.progression.experiencePoints}}"
data-dtype="Number" />
</div>
{{/if}}
</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

@ -8,6 +8,6 @@ SPDX-License-Identifier: MIT
<div class="ds4-combat-values">
{{#each config.i18n.combatValues as |combat-value-label 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=combat-value-label}}
{{/each}}
</div>

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
core-value-key=attribute-key core-value-data=(lookup ../data.data.attributes
attribute-key) core-value-variant="attribute"}}
{{/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"}}
{{/each}}
</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

@ -9,11 +9,11 @@ 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" />
<img class="profile-img" src="{{data.img}}" data-edit="img" title="{{data.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" />
<label for="name" class="hidden">Name</label>
<input name="name" type="text" id="name" value="{{data.name}}" placeholder="Name" />
</h1>
{{> systems/ds4/templates/sheets/actor/components/character-progression.hbs}}
@ -21,7 +21,7 @@ SPDX-License-Identifier: MIT
<div class="basic-property">
<label>{{config.i18n.creatureBaseInfo.creatureType}}</label>
<select name="data.baseInfo.creatureType" data-type="String">
{{#select data.baseInfo.creatureType}}
{{#select data.data.baseInfo.creatureType}}
{{#each config.i18n.creatureTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
@ -31,18 +31,19 @@ SPDX-License-Identifier: MIT
<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" />
<input type="text" name="data.baseInfo.loot" value="{{data.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}}"
<input type="text" name="data.baseInfo.foeFactor" value="{{data.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}}
{{#select data.data.baseInfo.sizeCategory}}
{{#each config.i18n.creatureSizeCategories as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
@ -52,8 +53,8 @@ SPDX-License-Identifier: MIT
<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" />
<input type="text" name="data.baseInfo.experiencePoints"
value="{{data.data.baseInfo.experiencePoints}}" data-dtype="Number" />
</div>
</div>
</div>

View file

@ -5,6 +5,6 @@ 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
{{editor content=data.data.profile.biography target="data.profile.biography" button=true owner=owner
editable=editable}}
</div>

View file

@ -7,7 +7,7 @@ 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|}}
{{#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="profile-entry">
<label for="data.profile.{{profile-data-key}}">
@ -23,7 +23,7 @@ SPDX-License-Identifier: MIT
{{lookup config.i18n.characterProfile 'specialCharacteristics'}}
</label>
<textarea name="data.profile.specialCharacteristics" data-dtype="String"
rows="4">{{data.profile.specialCharacteristics}}</textarea>
rows="4">{{data.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

@ -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>

2803
yarn.lock

File diff suppressed because it is too large Load diff