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" "postinstall": "husky install"
}, },
"devDependencies": { "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", "@rollup/plugin-node-resolve": "^13.0.0",
"@types/fs-extra": "^9.0.11", "@types/fs-extra": "^9.0.11",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",

View file

@ -5,7 +5,23 @@
import evaluateCheck from "../../src/module/rolls/check-evaluation"; 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", () => { describe("evaluateCheck with no dice", () => {
it("should throw an error.", () => { it("should throw an error.", () => {

View file

@ -5,7 +5,7 @@
"DS4.UserInteractionAddEffect": "Neuer Effekt", "DS4.UserInteractionAddEffect": "Neuer Effekt",
"DS4.UserInteractionEditEffect": "Effekt bearbeiten", "DS4.UserInteractionEditEffect": "Effekt bearbeiten",
"DS4.UserInteractionDeleteEffect": "Effekt löschen", "DS4.UserInteractionDeleteEffect": "Effekt löschen",
"DS4.EntityImageAltText": "Bild von {name}", "DS4.DocumentImageAltText": "Bild von {name}",
"DS4.RollableImageRollableTitle": "Für {name} würfeln", "DS4.RollableImageRollableTitle": "Für {name} würfeln",
"DS4.DiceOverlayImageAltText": "Bild eines W20", "DS4.DiceOverlayImageAltText": "Bild eines W20",
"DS4.NotOwned": "Nicht besessen", "DS4.NotOwned": "Nicht besessen",

View file

@ -5,7 +5,7 @@
"DS4.UserInteractionAddEffect": "Add Effect", "DS4.UserInteractionAddEffect": "Add Effect",
"DS4.UserInteractionEditEffect": "Edit Effect", "DS4.UserInteractionEditEffect": "Edit Effect",
"DS4.UserInteractionDeleteEffect": "Delete Effect", "DS4.UserInteractionDeleteEffect": "Delete Effect",
"DS4.EntityImageAltText": "Image of {name}", "DS4.DocumentImageAltText": "Image of {name}",
"DS4.RollableImageRollableTitle": "Roll for {name}", "DS4.RollableImageRollableTitle": "Roll for {name}",
"DS4.DiceOverlayImageAltText": "Image of a d20", "DS4.DiceOverlayImageAltText": "Image of a d20",
"DS4.NotOwned": "No owner", "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 { ModifiableData, ModifiableDataBase, ResourceData, UsableResource } from "../common/common-data";
import { DS4 } from "../config"; import { DS4 } from "../config";
import { DS4ItemData } from "../item/item-data";
export type DS4ActorData = DS4CharacterData | DS4CreatureData; declare global {
interface SourceConfig {
type ActorType = keyof typeof DS4.i18n.actorTypes; Actor: DS4ActorDataSource;
}
export interface DS4ActorDataHelper<T, U extends ActorType> extends Actor.Data<T, DS4ItemData> {
type: U;
} }
type DS4CharacterData = DS4ActorDataHelper<DS4CharacterDataData, "character">; export type DS4ActorDataSource = DS4CharacterDataSource | DS4CreatureDataSource;
type DS4CreatureData = DS4ActorDataHelper<DS4CreatureDataData, "creature">;
interface DS4CharacterDataSource {
type: "character";
data: DS4CharacterDataSourceData;
}
interface DS4CreatureDataSource {
type: "creature";
data: DS4CreatureDataSourceData;
}
// templates // templates
interface DS4ActorDataDataBase { interface DS4ActorDataSourceDataBase {
attributes: DS4ActorDataDataAttributes; attributes: DS4ActorDataSourceDataAttributes;
traits: DS4ActorDataDataTraits; traits: DS4ActorDataSourceDataTraits;
combatValues: DS4ActorDataDataCombatValues; 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 { export function isAttribute(value: unknown): value is Attribute {
return (Object.keys(DS4.i18n.attributes) as Array<unknown>).includes(value); 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 { export function isTrait(value: unknown): value is Trait {
return (Object.keys(DS4.i18n.traits) as Array<unknown>).includes(value); 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" [Key in keyof typeof DS4.i18n.combatValues]: Key extends "hitPoints"
? ResourceData<number> ? ResourceData<number>
: ModifiableData<number>; : ModifiableData<number>;
}; };
type CombatValue = keyof DS4ActorDataDataCombatValues; type CombatValue = keyof DS4ActorDataSourceDataCombatValues;
export function isCombatValue(value: string): value is CombatValue { export function isCombatValue(value: string): value is CombatValue {
return (Object.keys(DS4.i18n.combatValues) as Array<unknown>).includes(value); return (Object.keys(DS4.i18n.combatValues) as Array<unknown>).includes(value);
@ -58,33 +64,44 @@ export function isCombatValue(value: string): value is CombatValue {
// types // types
interface DS4CharacterDataData extends DS4ActorDataDataBase { interface DS4CreatureDataSourceData extends DS4ActorDataSourceDataBase {
baseInfo: DS4CharacterDataDataBaseInfo; baseInfo: DS4CreatureDataSourceDataBaseInfo;
progression: DS4CharacterDataDataProgression;
language: DS4CharacterDataDataLanguage;
profile: DS4CharacterDataDataProfile;
currency: DS4CharacterDataDataCurrency;
slayerPoints: DS4CharacterDataDataSlayerPoints;
} }
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; race: string;
class: string; class: string;
heroClass: string; heroClass: string;
culture: string; culture: string;
} }
export interface DS4CharacterDataDataProgression { export interface DS4CharacterDataSourceDataProgression {
level: number; level: number;
experiencePoints: number; experiencePoints: number;
talentPoints: UsableResource<number>; talentPoints: UsableResource<number>;
progressPoints: UsableResource<number>; progressPoints: UsableResource<number>;
} }
export interface DS4CharacterDataDataLanguage { export interface DS4CharacterDataSourceDataProfile {
languages: string;
alphabets: string;
}
export interface DS4CharacterDataDataProfile {
biography: string; biography: string;
gender: string; gender: string;
birthday: string; birthday: string;
@ -97,29 +114,12 @@ export interface DS4CharacterDataDataProfile {
specialCharacteristics: string; specialCharacteristics: string;
} }
export interface DS4CharacterDataDataCurrency { export interface DS4CharacterDataSourceDataCurrency {
gold: number; gold: number;
silver: number; silver: number;
copper: number; copper: number;
} }
export interface DS4CharacterDataDataSlayerPoints { export interface DS4CharacterDataSourceDataSlayerPoints {
value: number; 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 { ModifiableDataBaseTotal } from "../common/common-data";
import { DS4 } from "../config"; import { DS4 } from "../config";
import { getGame } from "../helpers";
import { DS4Item } from "../item/item"; import { DS4Item } from "../item/item";
import { ItemType } from "../item/item-data"; import { DS4ArmorDataProperties, DS4ShieldDataProperties } from "../item/item-data-properties";
import { DS4ArmorPreparedData, DS4ShieldPreparedData } from "../item/item-prepared-data"; import { ItemType } from "../item/item-data-source";
import { createCheckRoll } from "../rolls/check-factory"; import { createCheckRoll } from "../rolls/check-factory";
import { DS4ActorData, isAttribute, isTrait } from "./actor-data"; import { Check } from "./actor-data-properties";
import { Check, DS4ActorPreparedData } from "./actor-prepared-data"; import { isAttribute, isTrait } from "./actor-data-source";
declare global {
interface DocumentClassConfig {
Actor: typeof DS4Actor;
}
}
/** /**
* The Actor class for DS4 * The Actor class for DS4
*/ */
export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData> { export class DS4Actor extends Actor {
/** @override */ /** @override */
prepareData(): void { prepareData(): void {
this.data = duplicate(this._data) as DS4ActorPreparedData; this.data.reset();
if (!this.data.img) this.data.img = CONST.DEFAULT_TOKEN;
if (!this.data.name) this.data.name = "New " + this.entity;
this.prepareBaseData(); this.prepareBaseData();
this.prepareEmbeddedEntities(); this.prepareEmbeddedEntities();
this.applyActiveEffectsToBaseData(); 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 { applyActiveEffectsToBaseData(): void {
// reset overrides because our variant of applying active effects does not set them, it only adds overrides // reset overrides because our variant of applying active effects does not set them, it only adds overrides
this.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 * @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> = {}; const overrides: Record<string, unknown> = {};
// Organize non-disabled effects by their application priority // Organize non-disabled effects by their application priority
const changes = this.effects.reduce( const changes: (foundry.data.ActiveEffectData["changes"][number] & { effect: ActiveEffect })[] =
(changes: Array<ActiveEffectChange & { effect: ActiveEffect<DS4Actor> }>, e) => { this.effects.reduce(
if (e.data.disabled) return changes; (changes: (foundry.data.ActiveEffectData["changes"][number] & { effect: ActiveEffect })[], e) => {
const item = this._getOriginatingItemOfActiveEffect(e); if (e.data.disabled) return changes;
if (item?.isNonEquippedEuipable()) 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( const newChanges = e.data.changes.filter(predicate).flatMap((c) => {
e.data.changes.filter(predicate).flatMap((c) => { const changeSource = c.toObject();
const duplicatedChange = duplicate(c); changeSource.priority = changeSource.priority ?? changeSource.mode * 10;
duplicatedChange.priority = duplicatedChange.priority ?? duplicatedChange.mode * 10; return Array(factor).fill({ ...changeSource, effect: e });
return Array(factor).fill({ });
...duplicatedChange,
effect: e, return changes.concat(newChanges);
}); },
}), [],
); );
}, changes.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
[],
);
changes.sort((a, b) => a.priority - b.priority);
// Apply all changes // Apply all changes
for (const change of changes) { for (const change of changes) {
@ -102,11 +114,11 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
} }
// Expand the set of final overrides // 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 { protected getOriginatingItemOfActiveEffect(effect: ActiveEffect): DS4Item | undefined {
return this.items.find((item) => item.uuid === effect.data.origin) ?? 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 return this.items
.map((item) => item.data) .map((item) => item.data)
.filter( .filter(
(data): data is DS4ArmorPreparedData | DS4ShieldPreparedData => (data): data is foundry.data.ItemData & (DS4ArmorDataProperties | DS4ShieldDataProperties) =>
data.type === "armor" || data.type === "shield", data.type === "armor" || data.type === "shield",
) )
.filter((data) => data.data.equipped) .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. * This only differs from the base implementation by also allowing negative values.
* @override * @override
*/ */
async modifyTokenAttribute(attribute: string, value: number, isDelta = false, isBar = true): Promise<this> { async modifyTokenAttribute(
const current = getProperty(this.data.data, attribute); 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 // Determine the updates to make to the actor data
let updates: Record<string, number>; let updates: Record<string, number>;
@ -291,10 +308,10 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
*/ */
async rollCheck(check: Check): Promise<void> { async rollCheck(check: Check): Promise<void> {
await createCheckRoll(this.data.data.checks[check], { 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, maximumCoupResult: this.data.data.rolling.maximumCoupResult,
minimumFumbleResult: this.data.data.rolling.minimumFumbleResult, 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 { attribute, trait } = await this.selectAttributeAndTrait();
const checkTargetNumber = this.data.data.attributes[attribute].total + this.data.data.traits[trait].total; const checkTargetNumber = this.data.data.attributes[attribute].total + this.data.data.traits[trait].total;
await createCheckRoll(checkTargetNumber, { 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, maximumCoupResult: this.data.data.rolling.maximumCoupResult,
minimumFumbleResult: this.data.data.rolling.minimumFumbleResult, minimumFumbleResult: this.data.data.rolling.minimumFumbleResult,
flavor: game.i18n.format("DS4.ActorGenericCheckFlavor", { flavor: getGame().i18n.format("DS4.ActorGenericCheckFlavor", {
actor: this.name, actor: this.name,
attribute: DS4.i18n.attributes[attribute], attribute: DS4.i18n.attributes[attribute],
trait: DS4.i18n.traits[trait], trait: DS4.i18n.traits[trait],
@ -324,27 +341,27 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
const attributeIdentifier = "attribute-trait-selection-attribute"; const attributeIdentifier = "attribute-trait-selection-attribute";
const traitIdentifier = "attribute-trait-selection-trait"; const traitIdentifier = "attribute-trait-selection-trait";
return Dialog.prompt({ 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", { content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
selects: [ selects: [
{ {
label: game.i18n.localize("DS4.Attribute"), label: getGame().i18n.localize("DS4.Attribute"),
identifier: attributeIdentifier, identifier: attributeIdentifier,
options: DS4.i18n.attributes, options: DS4.i18n.attributes,
}, },
{ {
label: game.i18n.localize("DS4.Trait"), label: getGame().i18n.localize("DS4.Trait"),
identifier: traitIdentifier, identifier: traitIdentifier,
options: DS4.i18n.traits, options: DS4.i18n.traits,
}, },
], ],
}), }),
label: game.i18n.localize("DS4.GenericOkButton"), label: getGame().i18n.localize("DS4.GenericOkButton"),
callback: (html) => { callback: (html) => {
const selectedAttribute = html.find(`#${attributeIdentifier}`).val(); const selectedAttribute = html.find(`#${attributeIdentifier}`).val();
if (!isAttribute(selectedAttribute)) { if (!isAttribute(selectedAttribute)) {
throw new Error( throw new Error(
game.i18n.format("DS4.ErrorUnexpectedAttribute", { getGame().i18n.format("DS4.ErrorUnexpectedAttribute", {
actualAttribute: selectedAttribute, actualAttribute: selectedAttribute,
expectedTypes: Object.keys(DS4.i18n.attributes) expectedTypes: Object.keys(DS4.i18n.attributes)
.map((attribute) => `'${attribute}'`) .map((attribute) => `'${attribute}'`)
@ -355,7 +372,7 @@ export class DS4Actor extends Actor<DS4ActorData, DS4Item, DS4ActorPreparedData>
const selectedTrait = html.find(`#${traitIdentifier}`).val(); const selectedTrait = html.find(`#${traitIdentifier}`).val();
if (!isTrait(selectedTrait)) { if (!isTrait(selectedTrait)) {
throw new Error( throw new Error(
game.i18n.format("DS4.ErrorUnexpectedTrait", { getGame().i18n.format("DS4.ErrorUnexpectedTrait", {
actualTrait: selectedTrait, actualTrait: selectedTrait,
expectedTypes: Object.keys(DS4.i18n.traits) expectedTypes: Object.keys(DS4.i18n.traits)
.map((attribute) => `'${attribute}'`) .map((attribute) => `'${attribute}'`)

View file

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

View file

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

View file

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

View file

@ -2,10 +2,20 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
declare namespace ClientSettings { declare global {
interface Values { namespace ClientSettings {
"ds4.systemMigrationVersion": number; interface Values {
"ds4.useSlayingDiceForAutomatedChecks": boolean; "ds4.systemMigrationVersion": number;
"ds4.showSlayerPoints": boolean; "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 { export function getCanvas(): Canvas {
if (!(canvas instanceof Canvas) || !canvas.ready) { if (!(canvas instanceof Canvas) || !canvas.ready) {
throw new Error(game.i18n.localize("DS4.ErrorCanvasIsNotInitialized")); throw new Error(getGame().i18n.localize("DS4.ErrorCanvasIsNotInitialized"));
} }
return canvas; 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 // 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 { DS4Item } from "../item/item";
import { DS4ItemData } from "../item/item-data";
import { createRollCheckMacro } from "../macros/roll-check"; import { createRollCheckMacro } from "../macros/roll-check";
import { createRollItemMacro } from "../macros/roll-item"; import { createRollItemMacro } from "../macros/roll-item";
import notifications from "../ui/notifications"; import notifications from "../ui/notifications";
export default function registerForHotbarDropHook(): void { 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) { switch (data.type) {
case "Item": { case "Item": {
if (!("data" in data)) { if (!isItemDropData(data) || !("data" in data)) {
return notifications.warn(game.i18n.localize("DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems")); return notifications.warn(
getGame().i18n.localize("DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems"),
);
} }
const itemData = data.data as DS4ItemData; const itemData = data.data;
if (!DS4Item.rollableItemTypes.includes(itemData.type)) { if (!DS4Item.rollableItemTypes.includes(itemData.type)) {
return notifications.warn( return notifications.warn(
game.i18n.format("DS4.WarningItemIsNotRollable", { getGame().i18n.format("DS4.WarningItemIsNotRollable", {
name: itemData.name, name: itemData.name,
id: itemData._id, id: itemData._id,
type: itemData.type, type: itemData.type,
@ -31,10 +33,16 @@ export default function registerForHotbarDropHook(): void {
} }
case "Check": { case "Check": {
if (!("data" in data) || typeof data.data !== "string" || !isCheck(data.data)) { 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); 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 { DS4 } from "../config";
import registerHandlebarsHelpers from "../handlebars/handlebars-helpers"; import registerHandlebarsHelpers from "../handlebars/handlebars-helpers";
import registerHandlebarsPartials from "../handlebars/handlebars-partials"; import registerHandlebarsPartials from "../handlebars/handlebars-partials";
import { getGame } from "../helpers";
import { DS4Item } from "../item/item"; import { DS4Item } from "../item/item";
import { DS4ItemSheet } from "../item/item-sheet"; import { DS4ItemSheet } from "../item/item-sheet";
import logger from "../logger"; import logger from "../logger";
@ -28,7 +29,7 @@ export default function registerForInitHook(): void {
async function init() { async function init() {
logger.info(`Initializing the DS4 Game System\n${DS4.ASCII}`); logger.info(`Initializing the DS4 Game System\n${DS4.ASCII}`);
game.ds4 = { getGame().ds4 = {
DS4Actor, DS4Actor,
DS4Item, DS4Item,
DS4, DS4,
@ -39,8 +40,8 @@ async function init() {
CONFIG.DS4 = DS4; CONFIG.DS4 = DS4;
CONFIG.Actor.entityClass = DS4Actor; CONFIG.Actor.documentClass = DS4Actor;
CONFIG.Item.entityClass = DS4Item; CONFIG.Item.documentClass = DS4Item;
CONFIG.Actor.typeLabels = DS4.i18n.actorTypes; CONFIG.Actor.typeLabels = DS4.i18n.actorTypes;
CONFIG.Item.typeLabels = DS4.i18n.itemTypes; CONFIG.Item.typeLabels = DS4.i18n.itemTypes;
@ -63,3 +64,20 @@ async function init() {
await registerHandlebarsPartials(); await registerHandlebarsPartials();
registerHandlebarsHelpers(); 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 // SPDX-License-Identifier: MIT
import { DS4 } from "../config"; import { DS4 } from "../config";
import { getGame } from "../helpers";
export default function registerForSetupHooks(): void { export default function registerForSetupHooks(): void {
Hooks.once("setup", () => { Hooks.once("setup", () => {
@ -21,7 +22,7 @@ function localizeAndSortConfigObjects() {
const localizeObject = <T extends { [s: string]: string }>(obj: T, sort = true): T => { const localizeObject = <T extends { [s: string]: string }>(obj: T, sort = true): T => {
const localized = Object.entries(obj).map(([key, value]) => { 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])); if (sort) localized.sort((a, b) => a[1].localeCompare(b[1]));
return Object.fromEntries(localized); 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 // SPDX-License-Identifier: MIT
import { DS4 } from "../config"; import { DS4 } from "../config";
import { getGame } from "../helpers";
import notifications from "../ui/notifications"; import notifications from "../ui/notifications";
import { DS4Item } from "./item"; import { isDS4ItemDataTypePhysical } from "./item-data-source";
import { isDS4ItemDataTypePhysical } from "./item-data";
/** /**
* The Sheet class for DS4 Items * The Sheet class for DS4 Items
*/ */
export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> { export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData> {
/** @override */ /** @override */
static get defaultOptions(): BaseEntitySheet.Options { static get defaultOptions(): ItemSheet.Options {
const superDefaultOptions = super.defaultOptions; return foundry.utils.mergeObject(super.defaultOptions, {
return mergeObject(superDefaultOptions, {
...superDefaultOptions,
width: 540, width: 540,
height: 400, height: 400,
classes: ["ds4", "sheet", "item"], classes: ["ds4", "sheet", "item"],
@ -33,7 +31,7 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
} }
/** @override */ /** @override */
async getData(): Promise<ItemSheet.Data<DS4Item>> { async getData(): Promise<DS4ItemSheetData> {
const data = { const data = {
...(await super.getData()), ...(await super.getData()),
config: DS4, config: DS4,
@ -45,11 +43,14 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
} }
/** @override */ /** @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 position = super.setPosition(options);
const sheetBody = this.element.find(".sheet-body"); if (position) {
const bodyHeight = position.height - 192; const sheetBody = this.element.find(".sheet-body");
sheetBody.css("height", bodyHeight); const bodyHeight = position.height - 192;
sheetBody.css("height", bodyHeight);
}
return position; return position;
} }
@ -70,23 +71,25 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
event.preventDefault(); event.preventDefault();
if (this.item.isOwned) { 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 a = event.currentTarget;
const li = $(a).parents(".effect"); const li = $(a).parents(".effect");
switch (a.dataset["action"]) { switch (a.dataset["action"]) {
case "create": case "create":
return this._createActiveEffect(); return this.createActiveEffect();
case "edit": case "edit":
const id = li.data("effectId"); const id = li.data("effectId");
const effect = this.item.effects.get(id); const effect = this.item.effects.get(id);
if (!effect) { 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); return effect.sheet.render(true);
case "delete": { 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. * Create a new ActiveEffect for the item using default data.
*/ */
protected async _createActiveEffect(): Promise<ActiveEffect.Data> { protected async createActiveEffect(): Promise<ActiveEffect | undefined> {
const label = `New Effect`;
const createData = { const createData = {
label: label, label: "New Effect",
changes: [], icon: "icons/svg/aura.svg",
duration: {},
transfer: true,
}; };
const effect = ActiveEffect.create(createData, this.item); return ActiveEffect.create({ ...createData }, { parent: this.item });
return effect.create({});
} }
} }
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 // SPDX-License-Identifier: MIT
import { DS4Actor } from "../actor/actor";
import { DS4 } from "../config"; import { DS4 } from "../config";
import { getGame } from "../helpers";
import { createCheckRoll } from "../rolls/check-factory"; import { createCheckRoll } from "../rolls/check-factory";
import notifications from "../ui/notifications"; import notifications from "../ui/notifications";
import { AttackType, DS4ItemData, ItemType } from "./item-data"; import { AttackType, ItemType } from "./item-data-source";
import { DS4ItemPreparedData } from "./item-prepared-data";
import { calculateSpellPrice } from "./type-specific-helpers/spell"; import { calculateSpellPrice } from "./type-specific-helpers/spell";
declare global {
interface DocumentClassConfig {
Item: typeof DS4Item;
}
}
/** /**
* The Item class for DS4 * The Item class for DS4
*/ */
export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> { export class DS4Item extends Item {
/** /** @override */
* @override
*/
prepareData(): void { prepareData(): void {
super.prepareData(); super.prepareData();
this.prepareDerivedData();
} }
/** @override */
prepareDerivedData(): void { prepareDerivedData(): void {
if (this.data.type === "talent") { if (this.data.type === "talent") {
const data = this.data.data; 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. * Roll a check for an action with this item.
*/ */
async roll(): Promise<void> { 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) { switch (this.data.type) {
case "weapon": case "weapon":
return this.rollWeapon(); return this.rollWeapon();
case "spell": case "spell":
return this.rollSpell(); return this.rollSpell();
default: 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")) { if (!(this.data.type === "weapon")) {
throw new Error( throw new Error(
game.i18n.format("DS4.ErrorWrongItemType", { getGame().i18n.format("DS4.ErrorWrongItemType", {
actualType: this.data.type, actualType: this.data.type,
expectedType: "weapon", expectedType: "weapon",
id: this.id, id: this.id,
@ -91,7 +92,7 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
if (!this.data.data.equipped) { if (!this.data.data.equipped) {
return notifications.warn( return notifications.warn(
game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", { getGame().i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
name: this.name, name: this.name,
id: this.id, id: this.id,
type: this.data.type, 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 if (!this.actor) {
const ownerDataData = actor.data.data; 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 weaponBonus = this.data.data.weaponBonus;
const combatValue = await this.getCombatValueKeyForAttackType(this.data.data.attackType); const combatValue = await this.getCombatValueKeyForAttackType(this.data.data.attackType);
const checkTargetNumber = ownerDataData.combatValues[combatValue].total + weaponBonus; const checkTargetNumber = ownerDataData.combatValues[combatValue].total + weaponBonus;
await createCheckRoll(checkTargetNumber, { 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, maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult, 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> { protected async rollSpell(): Promise<void> {
if (!(this.data.type === "spell")) { if (!(this.data.type === "spell")) {
throw new Error( throw new Error(
game.i18n.format("DS4.ErrorWrongItemType", { getGame().i18n.format("DS4.ErrorWrongItemType", {
actualType: this.data.type, actualType: this.data.type,
expectedType: "spell", expectedType: "spell",
id: this.id, id: this.id,
@ -127,7 +131,7 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
if (!this.data.data.equipped) { if (!this.data.data.equipped) {
return notifications.warn( return notifications.warn(
game.i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", { getGame().i18n.format("DS4.WarningItemMustBeEquippedToBeRolled", {
name: this.name, name: this.name,
id: this.id, id: this.id,
type: this.data.type, 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 if (!this.actor) {
const ownerDataData = actor.data.data; 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; const spellBonus = Number.isNumeric(this.data.data.bonus) ? parseInt(this.data.data.bonus) : undefined;
if (spellBonus === undefined) { if (spellBonus === undefined) {
notifications.info( notifications.info(
game.i18n.format("DS4.InfoManuallyEnterSpellBonus", { getGame().i18n.format("DS4.InfoManuallyEnterSpellBonus", {
name: this.name, name: this.name,
spellBonus: this.data.data.bonus, spellBonus: this.data.data.bonus,
}), }),
@ -150,10 +157,10 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
const checkTargetNumber = ownerDataData.combatValues[spellType].total + (spellBonus ?? 0); const checkTargetNumber = ownerDataData.combatValues[spellType].total + (spellBonus ?? 0);
await createCheckRoll(checkTargetNumber, { 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, maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult, 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 { melee, ranged } = { ...DS4.i18n.attackTypes };
const identifier = "attack-type-selection"; const identifier = "attack-type-selection";
return Dialog.prompt({ 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", { content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
selects: [ selects: [
{ {
label: game.i18n.localize("DS4.AttackType"), label: getGame().i18n.localize("DS4.AttackType"),
identifier, identifier,
options: { melee, ranged }, options: { melee, ranged },
}, },
], ],
}), }),
label: game.i18n.localize("DS4.GenericOkButton"), label: getGame().i18n.localize("DS4.GenericOkButton"),
callback: (html) => { callback: (html) => {
const selectedAttackType = html.find(`#${identifier}`).val(); const selectedAttackType = html.find(`#${identifier}`).val();
if (selectedAttackType !== "melee" && selectedAttackType !== "ranged") { if (selectedAttackType !== "melee" && selectedAttackType !== "ranged") {
throw new Error( throw new Error(
game.i18n.format("DS4.ErrorUnexpectedAttackType", { getGame().i18n.format("DS4.ErrorUnexpectedAttackType", {
actualType: selectedAttackType, actualType: selectedAttackType,
expectedTypes: "'melee', 'ranged'", expectedTypes: "'melee', 'ranged'",
}), }),
@ -190,11 +197,4 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
return `${attackType}Attack` as const; 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 // SPDX-License-Identifier: MIT
import { hoursPerDay, minutesPerHour, secondsPerMinute, secondsPerRound } from "../../common/time-helpers"; 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 spellPriceFactor = calculateSpellPriceFactor(data.cooldownDuration);
const baseSpellPrices = [ const baseSpellPrices = [
data.minimumLevels.healer !== null ? 10 + (data.minimumLevels.healer - 1) * 35 : null, 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 LogLevel = "debug" | "info" | "warning" | "error";
type LoggingFunction = (...data: unknown[]) => void; type LoggingFunction = (...data: unknown[]) => void;
class Logger { const getLoggingFunction = (type: LogLevel = "info"): LoggingFunction => {
readonly debug: LoggingFunction; const log = { debug: console.debug, info: console.info, warning: console.warn, error: console.error }[type];
readonly info: LoggingFunction; return (...data: unknown[]) => log(loggingContext, loggingSeparator, ...data);
readonly warn: LoggingFunction; };
readonly error: LoggingFunction;
constructor() { const logger = Object.freeze({
this.debug = this.getLoggingFunction("debug"); debug: getLoggingFunction("debug"),
this.info = this.getLoggingFunction("info"); info: getLoggingFunction("info"),
this.warn = this.getLoggingFunction("warning"); warn: getLoggingFunction("warning"),
this.error = this.getLoggingFunction("error"); 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; export default logger;

View file

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { DS4Actor } from "../actor/actor"; 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 * 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; const speakerToken = speaker.token ? getCanvas().tokens.get(speaker.token) : undefined;
if (speakerToken) { 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) { if (speakerActor) {
return speakerActor as DS4Actor; return speakerActor;
} }
} }

View file

@ -2,8 +2,9 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Check } from "../actor/actor-prepared-data"; import { Check } from "../actor/actor-data-properties";
import { DS4 } from "../config"; import { DS4 } from "../config";
import { getGame } from "../helpers";
import notifications from "../ui/notifications"; import notifications from "../ui/notifications";
import { getActiveActor } from "./helpers"; import { getActiveActor } from "./helpers";
@ -15,13 +16,13 @@ import { getActiveActor } from "./helpers";
*/ */
export async function createRollCheckMacro(check: Check, slot: string): Promise<void> { export async function createRollCheckMacro(check: Check, slot: string): Promise<void> {
const macro = await getOrCreateRollCheckMacro(check); 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 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, (m) => m.name === DS4.i18n.checks[check] && m.data.command === command,
); );
if (existingMacro) { if (existingMacro) {
@ -36,7 +37,7 @@ async function getOrCreateRollCheckMacro(check: Check): Promise<Macro | null> {
img: DS4.icons.checks[check], img: DS4.icons.checks[check],
flags: { "ds4.checkMacro": true }, 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> { export async function rollCheck(check: Check): Promise<void> {
const actor = getActiveActor(); const actor = getActiveActor();
if (!actor) { 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 })); return actor.rollCheck(check).catch((e) => notifications.error(e, { log: true }));

View file

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

View file

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { DS4ItemData } from "../item/item-data"; import { getGame } from "../helpers";
import notifications from "../ui/notifications"; import notifications from "../ui/notifications";
import { getActiveActor } from "./helpers"; import { getActiveActor } from "./helpers";
@ -12,15 +12,15 @@ import { getActiveActor } from "./helpers";
* @param itemData - The item data * @param itemData - The item data
* @param slot - The hotbar slot to use * @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); 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 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) { if (existingMacro) {
return existingMacro; return existingMacro;
} }
@ -33,7 +33,7 @@ async function getOrCreateRollItemMacro(itemData: DS4ItemData): Promise<Macro |
img: itemData.img, img: itemData.img,
flags: { "ds4.itemMacro": true }, 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> { export async function rollItem(itemId: string): Promise<void> {
const actor = getActiveActor(); const actor = getActiveActor();
if (!actor) { if (!actor) {
return notifications.warn(game.i18n.localize("DS4.WarningMustControlActorToUseRollItemMacro")); return notifications.warn(getGame().i18n.localize("DS4.WarningMustControlActorToUseRollItemMacro"));
} }
const item = actor.items?.get(itemId); const item = actor.items?.get(itemId);
if (!item) { if (!item) {
return notifications.warn( return notifications.warn(
game.i18n.format("DS4.WarningControlledActorDoesNotHaveItem", { getGame().i18n.format("DS4.WarningControlledActorDoesNotHaveItem", {
actorName: actor.name, actorName: actor.name,
actorId: actor.id, actorId: actor.id,
itemId, itemId,

View file

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

View file

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

View file

@ -2,142 +2,33 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import logger from "../logger"; import {
getActorUpdateDataGetter,
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> { export async function migrate(): Promise<void> {
await migrateItems(); await migrateItems(getItemUpdateData);
await migrateActors(); await migrateActors(getActorUpdateData);
await migrateScenes(); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(); await migrateCompendiums(migrateCompendium);
} }
async function migrateItems() { function getItemUpdateData(
for (const item of game.items?.entities ?? []) { itemData: Partial<foundry.data.ItemData["_source"]>,
try { ): DeepPartial<foundry.data.ItemData["_source"]> | undefined {
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>) {
if (!["equipment", "trinket"].includes(itemData.type ?? "")) return 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() { const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
for (const actor of game.actors?.entities ?? []) { const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
try { const migrateCompendium = getCompendiumMigrator(
const updateData = getActorUpdateData(actor._data); { getItemUpdateData, getActorUpdateData, getSceneUpdateData },
if (updateData) { { migrateToTemplateEarly: false },
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 });
}

View file

@ -2,31 +2,24 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import logger from "../logger"; import {
getActorUpdateDataGetter,
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> { export async function migrate(): Promise<void> {
await migrateItems(); await migrateItems(getItemUpdateData);
await migrateActors(); await migrateActors(getActorUpdateData);
await migrateScenes(); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(); await migrateCompendiums(migrateCompendium);
} }
async function migrateItems() { function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) {
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>) {
if (!["loot"].includes(itemData.type ?? "")) return undefined; if (!["loot"].includes(itemData.type ?? "")) return undefined;
return { return {
data: { data: {
@ -35,113 +28,9 @@ function getItemUpdateData(itemData: DeepPartial<Item.Data>) {
}; };
} }
async function migrateActors() { const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
for (const actor of game.actors?.entities ?? []) { const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
try { const migrateCompendium = getCompendiumMigrator(
const updateData = getActorUpdateData(actor._data); { getItemUpdateData, getActorUpdateData },
if (updateData) { { migrateToTemplateEarly: false },
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 });
}

View file

@ -2,157 +2,39 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { DS4SpellDataData } from "../item/item-data"; import {
import logger from "../logger"; getActorUpdateDataGetter,
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> { export async function migrate(): Promise<void> {
await migrateItems(); await migrateItems(getItemUpdateData);
await migrateActors(); await migrateActors(getActorUpdateData);
await migrateScenes(); await migrateScenes(getSceneUpdateData);
await migrateCompendiums(); await migrateCompendiums(migrateCompendium);
} }
async function migrateItems() { function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) {
for (const item of game.items?.entities ?? []) { if (itemData.type !== "spell") return;
try { const cooldownDurationUnit: string | undefined = itemData.data?.cooldownDuration.unit;
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>) {
if (!["spell"].includes(itemData.type ?? "")) return undefined;
const updateData: Record<string, unknown> = { const updateData: Record<string, unknown> = {
"-=data.scrollPrice": null, data: {
"data.minimumLevels": { healer: null, wizard: null, sorcerer: null }, "-=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; return updateData;
} }
async function migrateActors() { const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
for (const actor of game.actors?.entities ?? []) { const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
try { const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
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 });
}

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

View file

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

View file

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

View file

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

View file

@ -3,14 +3,14 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { getGame } from "../helpers";
import { DS4Check } from "./check"; import { DS4Check } from "./check";
export default function registerSlayingDiceModifier(): void { export default function registerSlayingDiceModifier(): void {
DicePool.MODIFIERS.x = slay; PoolTerm.MODIFIERS.x = slay;
DicePool.POOL_REGEX = /^{([^}]+)}([A-z]([A-z0-9<=>]+)?)?$/;
} }
function slay(this: DicePool, modifier: string): void { function slay(this: PoolTerm, modifier: string): void {
const rgx = /[xX]/; const rgx = /[xX]/;
const match = modifier.match(rgx); const match = modifier.match(rgx);
if (!match || !this.rolls) return; if (!match || !this.rolls) return;
@ -21,11 +21,12 @@ function slay(this: DicePool, modifier: string): void {
checked++; checked++;
if (diceTerm instanceof DS4Check && diceTerm.coup) { if (diceTerm instanceof DS4Check && diceTerm.coup) {
const formula = `dsv${diceTerm.checkTargetNumber}c${diceTerm.maximumCoupResult}:${diceTerm.minimumFumbleResult}n`; 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.rolls.push(additionalRoll);
this.results.push({ result: additionalRoll.total ?? 0, active: true }); 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 // SPDX-License-Identifier: MIT
import { getGame } from "./helpers";
export function registerSystemSettings(): void { export function registerSystemSettings(): void {
/** /**
* Track the migrations version of the latest migration that has been applied * 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", name: "System Migration Version",
scope: "world", scope: "world",
config: false, config: false,
@ -14,7 +16,7 @@ export function registerSystemSettings(): void {
default: -1, default: -1,
}); });
game.settings.register("ds4", "useSlayingDiceForAutomatedChecks", { getGame().settings.register("ds4", "useSlayingDiceForAutomatedChecks", {
name: "DS4.SettingUseSlayingDiceForAutomatedChecksName", name: "DS4.SettingUseSlayingDiceForAutomatedChecksName",
hint: "DS4.SettingUseSlayingDiceForAutomatedChecksHint", hint: "DS4.SettingUseSlayingDiceForAutomatedChecksHint",
scope: "world", scope: "world",
@ -23,7 +25,7 @@ export function registerSystemSettings(): void {
default: false, default: false,
}); });
game.settings.register("ds4", "showSlayerPoints", { getGame().settings.register("ds4", "showSlayerPoints", {
name: "DS4.SettingShowSlayerPointsName", name: "DS4.SettingShowSlayerPointsName",
hint: "DS4.SettingShowSlayerPointsHint", hint: "DS4.SettingShowSlayerPointsHint",
scope: "world", scope: "world",
@ -33,7 +35,7 @@ export function registerSystemSettings(): void {
}); });
} }
interface DS4Settings { export interface DS4Settings {
systemMigrationVersion: number; systemMigrationVersion: number;
useSlayingDiceForAutomatedChecks: boolean; useSlayingDiceForAutomatedChecks: boolean;
showSlayerPoints: boolean; showSlayerPoints: boolean;
@ -41,8 +43,8 @@ interface DS4Settings {
export function getDS4Settings(): DS4Settings { export function getDS4Settings(): DS4Settings {
return { return {
systemMigrationVersion: game.settings.get("ds4", "systemMigrationVersion"), systemMigrationVersion: getGame().settings.get("ds4", "systemMigrationVersion"),
useSlayingDiceForAutomatedChecks: game.settings.get("ds4", "useSlayingDiceForAutomatedChecks"), useSlayingDiceForAutomatedChecks: getGame().settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
showSlayerPoints: game.settings.get("ds4", "showSlayerPoints"), showSlayerPoints: getGame().settings.get("ds4", "showSlayerPoints"),
}; };
} }

View file

@ -3,8 +3,8 @@
"title": "Dungeonslayers 4", "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/).", "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", "version": "0.8.0",
"minimumCoreVersion": "0.7.9", "minimumCoreVersion": "0.8.8",
"compatibleCoreVersion": "0.7.10", "compatibleCoreVersion": "0.8.8",
"templateVersion": 6, "templateVersion": 6,
"author": "Johannes Loher, Gesina Schwalbe, Oliver Rümpelein, Siegfried Krug, Max Tharr, Sascha Martens", "author": "Johannes Loher, Gesina Schwalbe, Oliver Rümpelein, Siegfried Krug, Max Tharr, Sascha Martens",
"authors": [ "authors": [

View file

@ -170,29 +170,6 @@
"shield": { "shield": {
"templates": ["base", "physical", "equipable", "protective"] "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": { "spell": {
"templates": ["base", "equipable"], "templates": ["base", "equipable"],
"spellType": "spellcasting", "spellType": "spellcasting",
@ -220,6 +197,29 @@
"sorcerer": null "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": { "specialCreatureAbility": {
"templates": ["base"], "templates": ["base"],
"experiencePoints": 0 "experiencePoints": 0

View file

@ -9,12 +9,12 @@ SPDX-License-Identifier: MIT
<form class="{{cssClass}} flexcol" autocomplete="off"> <form class="{{cssClass}} flexcol" autocomplete="off">
{{!-- Sheet Header --}} {{!-- Sheet Header --}}
<header class="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" /> height="100" width="100" />
<div class="header-fields flexrow"> <div class="header-fields flexrow">
<h1 class="charname"> <h1 class="charname">
<label for="actor.name" class="hidden">Name</label> <label for="name" class="hidden">Name</label>
<input name="name" type="text" id="actor.name" value="{{actor.name}}" placeholder="Name" /> <input name="name" type="text" id="name" value="{{data.name}}" placeholder="Name" />
</h1> </h1>
{{> systems/ds4/templates/sheets/actor/components/character-progression.hbs}} {{> systems/ds4/templates/sheets/actor/components/character-progression.hbs}}
@ -22,28 +22,28 @@ SPDX-License-Identifier: MIT
<div class="basic-property"> <div class="basic-property">
<label class="basic-property-label" <label class="basic-property-label"
for="data.baseInfo.race">{{config.i18n.characterBaseInfo.race}}</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}}" <input type="text" name="data.baseInfo.race" id="data.baseInfo.race"
data-dtype="String" /> value="{{data.data.baseInfo.race}}" data-dtype="String" />
</div> </div>
<div class="basic-property"> <div class="basic-property">
<label class="basic-property-label" <label class="basic-property-label"
for="data.baseInfo.culture">{{config.i18n.characterBaseInfo.culture}}</label> for="data.baseInfo.culture">{{config.i18n.characterBaseInfo.culture}}</label>
<input id="data.baseInfo.culture" type="text" name="data.baseInfo.culture" <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>
<div class="basic-property flex125"> <div class="basic-property flex125">
<label class="basic-property-label" <label class="basic-property-label"
for="data.progression.progressPoints.used">{{config.i18n.characterProgression.progressPoints}}</label> for="data.progression.progressPoints.used">{{config.i18n.characterProgression.progressPoints}}</label>
<div class="flexrow"> <div class="flexrow">
<input id="data.progression.progressPoints.used" type="number" <input id="data.progression.progressPoints.used" type="number"
name="data.progression.progressPoints.used" value="{{data.progression.progressPoints.used}}" name="data.progression.progressPoints.used"
data-dtype="Number" /> value="{{data.data.progression.progressPoints.used}}" data-dtype="Number" />
<span class="input-divider"> / </span> <span class="input-divider"> / </span>
<label class="hidden" for="data.progression.progressPoints.total">Total <label class="hidden" for="data.progression.progressPoints.total">Total
Progression Points</label> Progression Points</label>
<input type="number" id="data.progression.progressPoints.total" <input type="number" id="data.progression.progressPoints.total"
name="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> </div>
<div class="basic-property flex125"> <div class="basic-property flex125">
@ -51,26 +51,26 @@ SPDX-License-Identifier: MIT
for="data.progression.talentPoints.used">{{config.i18n.characterProgression.talentPoints}}</label> for="data.progression.talentPoints.used">{{config.i18n.characterProgression.talentPoints}}</label>
<div class="flexrow"> <div class="flexrow">
<input type="number" name="data.progression.talentPoints.used" <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" /> data-dtype="Number" />
<span class="input-divider"> / </span> <span class="input-divider"> / </span>
<label for="data.progression.talentPoints.total" class="hidden">Total Talent Points</label> <label for="data.progression.talentPoints.total" class="hidden">Total Talent Points</label>
<input type="number" name="data.progression.talentPoints.total" <input type="number" name="data.progression.talentPoints.total"
id="data.progression.talentPoints.total" value="{{data.progression.talentPoints.total}}" id="data.progression.talentPoints.total"
data-dtype="Number" /> value="{{data.data.progression.talentPoints.total}}" data-dtype="Number" />
</div> </div>
</div> </div>
<div class="basic-property"> <div class="basic-property">
<label class="basic-property-label" <label class="basic-property-label"
for="data.baseInfo.class">{{config.i18n.characterBaseInfo.class}}</label> for="data.baseInfo.class">{{config.i18n.characterBaseInfo.class}}</label>
<input type="text" id="data.baseInfo.class" name="data.baseInfo.class" <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>
<div class="basic-property"> <div class="basic-property">
<label class="basic-property-label" <label class="basic-property-label"
for="data.baseInfo.heroClass">{{config.i18n.characterBaseInfo.heroClass}}</label> for="data.baseInfo.heroClass">{{config.i18n.characterBaseInfo.heroClass}}</label>
<input type="text" id="data.baseInfo.heroClass" name="data.baseInfo.heroClass" <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> </div>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: MIT
<h4 class="ds4-currency-title">{{localize 'DS4.CharacterCurrency'}}</h4> <h4 class="ds4-currency-title">{{localize 'DS4.CharacterCurrency'}}</h4>
<div class="ds4-currency"> <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> <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" <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" /> name="data.currency.{{key}}" id="data.currency.{{key}}" value="{{value}}" data-dtype="Number" />

View file

@ -25,7 +25,7 @@ SPDX-License-Identifier: MIT
{{!-- image --}} {{!-- image --}}
{{> systems/ds4/templates/sheets/actor/components/rollable-image.hbs rollable=itemData.data.rollable {{> 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"}} rollableTitle=(localize "DS4.RollableImageRollableTitle" name=itemData.name) rollableClass="rollable-item"}}
{{!-- amount --}} {{!-- amount --}}

View file

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

View file

@ -5,6 +5,6 @@ SPDX-License-Identifier: MIT
--}} --}}
<div class="tab biography" data-group="primary" data-tab="biography"> <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}} editable=editable}}
</div> </div>

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: MIT
<div class="tab profile" data-group="primary" data-tab="profile"> <div class="tab profile" data-group="primary" data-tab="profile">
<div class="grid grid-2col"> <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'))}} {{#if (and (ne profile-data-key 'biography') (ne profile-data-key 'specialCharacteristics'))}}
<div class="profile-entry"> <div class="profile-entry">
<label for="data.profile.{{profile-data-key}}"> <label for="data.profile.{{profile-data-key}}">
@ -23,7 +23,7 @@ SPDX-License-Identifier: MIT
{{lookup config.i18n.characterProfile 'specialCharacteristics'}} {{lookup config.i18n.characterProfile 'specialCharacteristics'}}
</label> </label>
<textarea name="data.profile.specialCharacteristics" data-dtype="String" <textarea name="data.profile.specialCharacteristics" data-dtype="String"
rows="4">{{data.profile.specialCharacteristics}}</textarea> rows="4">{{data.data.profile.specialCharacteristics}}</textarea>
</div> </div>
</div> </div>
</div> </div>

View file

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

View file

@ -6,9 +6,9 @@ SPDX-License-Identifier: MIT
--}} --}}
<header class="sheet-header"> <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"> <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> <h2 class="item-type">{{lookup config.i18n.itemTypes item.type}}</h2>
{{> @partial-block}} {{> @partial-block}}
</div> </div>

View file

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

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: MIT
<div class="grid grid-3col basic-properties"> <div class="grid grid-3col basic-properties">
<div class="basic-property"> <div class="basic-property">
<label>{{localize "DS4.SpecialCreatureAbilityExperiencePoints"}}</label> <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" /> placeholder="0" data-dtype="Number" />
</div> </div>
</div> </div>

View file

@ -16,9 +16,9 @@ SPDX-License-Identifier: MIT
<label>{{localize localizeString}}</label> <label>{{localize localizeString}}</label>
<div class="unit-data-pair"> <div class="unit-data-pair">
<input class="item-num-val" type="text" data-dtype="String" name="data.{{property}}.value" <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 name="data.{{property}}.unit" data-type="String">
{{#select (lookup (lookup data property) 'unit')}} {{#select (lookup (lookup data.data property) 'unit')}}
{{#if (eq unitType 'temporal')}} {{#if (eq unitType 'temporal')}}
{{#each (lookup config.i18n 'temporalUnitsAbbr') as |value key|}} {{#each (lookup config.i18n 'temporalUnitsAbbr') as |value key|}}
<option value="{{key}}">{{value}}</option> <option value="{{key}}">{{value}}</option>
@ -48,7 +48,7 @@ SPDX-License-Identifier: MIT
<div class="basic-property"> <div class="basic-property">
<label for="data.spellType">{{localize "DS4.SpellType"}}</label> <label for="data.spellType">{{localize "DS4.SpellType"}}</label>
<select id="data.spellType" name="data.spellType" data-type="String"> <select id="data.spellType" name="data.spellType" data-type="String">
{{#select data.spellType}} {{#select data.data.spellType}}
{{#each config.i18n.spellTypes as |value key|}} {{#each config.i18n.spellTypes as |value key|}}
<option value="{{key}}">{{value}}</option> <option value="{{key}}">{{value}}</option>
{{/each}} {{/each}}
@ -57,7 +57,7 @@ SPDX-License-Identifier: MIT
</div> </div>
<div class="basic-property"> <div class="basic-property">
<label for="data.bonus">{{localize "DS4.SpellBonus"}}</label> <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>
</div> </div>
{{/systems/ds4/templates/sheets/item/components/sheet-header.hbs}} {{/systems/ds4/templates/sheets/item/components/sheet-header.hbs}}
@ -67,7 +67,7 @@ SPDX-License-Identifier: MIT
<div class="side-property"> <div class="side-property">
<label for="data.spellCategory">{{localize "DS4.SpellCategory"}}</label> <label for="data.spellCategory">{{localize "DS4.SpellCategory"}}</label>
<select id="data.spellCategory" name="data.spellCategory" data-type="String"> <select id="data.spellCategory" name="data.spellCategory" data-type="String">
{{#select data.spellCategory}} {{#select data.data.spellCategory}}
{{#each config.i18n.spellCategories as |value key|}} {{#each config.i18n.spellCategories as |value key|}}
<option value="{{key}}">{{value}}</option> <option value="{{key}}">{{value}}</option>
{{/each}} {{/each}}
@ -82,21 +82,21 @@ SPDX-License-Identifier: MIT
<div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsHealer'}}"> <div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsHealer'}}">
<label for="data.minimumLevels.healer">{{localize "DS4.SpellMinimumLevelsHealerAbbr"}}</label> <label for="data.minimumLevels.healer">{{localize "DS4.SpellMinimumLevelsHealerAbbr"}}</label>
<input type="number" min="0" step="1" data-dtype="Number" name="data.minimumLevels.healer" <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>
<div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsWizard'}}"> <div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsWizard'}}">
<label for="data.minimumLevels.wizard">{{localize "DS4.SpellMinimumLevelsWizardAbbr"}}</label> <label for="data.minimumLevels.wizard">{{localize "DS4.SpellMinimumLevelsWizardAbbr"}}</label>
<input type="number" min="0" step="1" data-dtype="Number" name="data.minimumLevels.wizard" <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>
<div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsSorcerer'}}"> <div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsSorcerer'}}">
<label for="data.minimumLevels.sorcerer">{{localize "DS4.SpellMinimumLevelsSorcererAbbr"}}</label> <label for="data.minimumLevels.sorcerer">{{localize "DS4.SpellMinimumLevelsSorcererAbbr"}}</label>
<input type="number" min="0" step="1" data-dtype="Number" name="data.minimumLevels.sorcerer" <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>
<div class="side-property"> <div class="side-property">
<label for="data.price">{{localize "DS4.SpellPrice"}}</label> <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> </div>
{{/systems/ds4/templates/sheets/item/components/body.hbs}} {{/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="tab flexrow description" data-group="primary" data-tab="description">
<div class="side-properties"> <div class="side-properties">
{{#if isOwned}} {{#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> <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> </div>
{{/if}} {{/if}}
<div class="side-property"> <div class="side-property">
<label for="data.actor">{{localize 'DS4.ItemOwner'}}</label> <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> class="fas fa-user"></i>{{actor.name}}</a>
</div> </div>
{{#if isPhysical}} {{#if isPhysical}}
<div class="side-property"> <div class="side-property">
<label for="data.quantity">{{localize 'DS4.Quantity'}}</label> <label for="data.quantity">{{localize 'DS4.Quantity'}}</label>
<input type="number" min="0" step="1" data-dtype="Number" name="data.quantity" value="{{data.quantity}}" /> <input type="number" min="0" step="1" data-dtype="Number" name="data.quantity"
</div> value="{{data.data.quantity}}" />
<div class="side-property"> </div>
<label for="data.storageLocation">{{localize 'DS4.StorageLocation'}}</label> <div class="side-property">
<input type="text" data-dtype="String" name="data.storageLocation" value="{{data.storageLocation}}" /> <label for="data.storageLocation">{{localize 'DS4.StorageLocation'}}</label>
</div> <input type="text" data-dtype="String" name="data.storageLocation" value="{{data.data.storageLocation}}" />
</div>
{{/if}} {{/if}}
{{else}} {{else}}
<span>{{localize "DS4.NotOwned"}}</span> <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}} {{> @partial-block}}
</div> </div>
<div class="description" title="{{localize 'DS4.HeadingDescription'}}"> <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>
</div> </div>

View file

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

View file

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

View file

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

View file

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

2803
yarn.lock

File diff suppressed because it is too large Load diff