Merge branch 'spell-groups' into 'main'

feat: replace spell category by spell groups

See merge request dungeonslayers/ds4!214
This commit is contained in:
Johannes Loher 2022-11-04 20:18:43 +00:00
commit b74919b75b
25 changed files with 3675 additions and 487 deletions

View file

@ -118,20 +118,31 @@
"DS4.SortBySpellType": "Nach Zauberspruchtyp sortieren",
"DS4.SpellTypeSpellcasting": "Zaubern",
"DS4.SpellTypeTargetedSpellcasting": "Zielzaubern",
"DS4.SpellCategory": "Kategorie",
"DS4.SpellCategoryDescription": "Eine Kategorie, der der Zauberspruch zugehörig ist.",
"DS4.SpellCategoryHealing": "Heilung",
"DS4.SpellCategoryFire": "Feuer",
"DS4.SpellCategoryIce": "Eis",
"DS4.SpellCategoryLight": "Licht",
"DS4.SpellCategoryDarkness": "Schatten",
"DS4.SpellCategoryMindAffecting": "Geistesbeeinflussend",
"DS4.SpellCategoryElectricity": "Elektrizität",
"DS4.SpellCategoryNone": "Keine",
"DS4.SpellCategoryUnset": "Nicht gesetzt",
"DS4.SpellGroups": "Zaubergruppen",
"DS4.SpellGroupsDescription": "Zaubergruppen, denen der Zauberspruch zugehörig ist.",
"DS4.SpellGroupLightning": "Blitz",
"DS4.SpellGroupEarth": "Erde, Fels, Stein",
"DS4.SpellGroupWater": "Wasser",
"DS4.SpellGroupIce": "Eis, Frost",
"DS4.SpellGroupFire": "Feuer",
"DS4.SpellGroupHealing": "Heilung",
"DS4.SpellGroupLight": "Licht",
"DS4.SpellGroupAir": "Luft",
"DS4.SpellGroupTransport": "Transport",
"DS4.SpellGroupDamage": "Schaden",
"DS4.SpellGroupShadow": "Schatten",
"DS4.SpellGroupProtection": "Schutz",
"DS4.SpellGroupMindAffecting": "Geistesbeeinflussend",
"DS4.SpellGroupDemonology": "Dämonologie",
"DS4.SpellGroupNecromancy": "Nekromantie",
"DS4.SpellGroupTransmutation": "Verwandlung",
"DS4.SpellGroupArea": "Fläche",
"DS4.SpellModifier": "Zauberbonus",
"DS4.SpellModifierNumerical": "Zauberbonus (numerisch)",
"DS4.SpellModifierComplex": "Zauberbonus (komplex)",
"DS4.SpellModifierAbbr": "ZB",
"DS4.SpellModifierDescription": "Der Zauberbonus auf die Probe.",
"DS4.SpellModifierNumericalDescription": "Der numerische Zauberbonus auf die Probe.",
"DS4.SpellModifierComplexDescription": "Ein komplexer Zauberbonus auf die Probe (zum Beispiel abhängig von Werten des Ziels). Wenn diese Art von Zauberbonus angegeben ist, wird der numerische ignoriert.",
"DS4.SortBySpellModifier": "Nach Zauberbonus sortieren",
"DS4.SpellDistance": "Distanz",
"DS4.SpellDistanceDescription": "Die maximale Entfernung zum Ziel. „Selbst“ bedeutet, dass nur der Zauberwirker selbst das Ziel des Zaubers sein kann.",
@ -371,5 +382,5 @@
"DS4.ActiveEffectApplyToItems": "Auf Items Andwenden",
"DS4.ActiveEffectItemName": "Itemname",
"DS4.ActiveEffectItemCondition": "Bedingung",
"DS4.TooltipDisabledDueToEffects": "inaktiv, weil von Aktiven Effekten beeinflusst"
"DS4.TooltipNotEditableDueToEffects": "Feld nicht bearbeitbar, weil von Aktiven Effekten beeinflusst"
}

View file

@ -118,20 +118,31 @@
"DS4.SortBySpellType": "Sort by Spell Type",
"DS4.SpellTypeSpellcasting": "Spellcasting",
"DS4.SpellTypeTargetedSpellcasting": "Targeted Spellcasting",
"DS4.SpellCategory": "Category",
"DS4.SpellCategoryDescription": "A category which the spell belongs to.",
"DS4.SpellCategoryHealing": "Healing",
"DS4.SpellCategoryFire": "Fire",
"DS4.SpellCategoryIce": "Ice",
"DS4.SpellCategoryLight": "Light",
"DS4.SpellCategoryDarkness": "Darkness",
"DS4.SpellCategoryMindAffecting": "Mind Affecting",
"DS4.SpellCategoryElectricity": "Electricity",
"DS4.SpellCategoryNone": "None",
"DS4.SpellCategoryUnset": "Unset",
"DS4.SpellGroups": "Spell Groups",
"DS4.SpellGroupsDescription": "Spell groups which the spell belongs to.",
"DS4.SpellGroupLightning": "Lightning",
"DS4.SpellGroupEarth": "Earth, Rock, Stone",
"DS4.SpellGroupWater": "Water",
"DS4.SpellGroupIce": "Ice, Frost",
"DS4.SpellGroupFire": "Fire",
"DS4.SpellGroupHealing": "Healing",
"DS4.SpellGroupLight": "Light",
"DS4.SpellGroupAir": "Air",
"DS4.SpellGroupTransport": "Transport",
"DS4.SpellGroupDamage": "Damage",
"DS4.SpellGroupShadow": "Shadow",
"DS4.SpellGroupProtection": "Protection",
"DS4.SpellGroupMindAffecting": "Mind Affecting",
"DS4.SpellGroupDemonology": "Demonologie",
"DS4.SpellGroupNecromancy": "Necromancy",
"DS4.SpellGroupTransmutation": "Transmutation",
"DS4.SpellGroupArea": "Area",
"DS4.SpellModifier": "Spell Modifier",
"DS4.SpellModifierNumerical": "Spell Modifier (numerical)",
"DS4.SpellModifierComplex": "Spell Modifier (complex)",
"DS4.SpellModifierAbbr": "SM",
"DS4.SpellModifierDescription": "The spell modifier for the corresponding check.",
"DS4.SpellModifierNumericalDescription": "The numerical spell modifier for the corresponding check.",
"DS4.SpellModifierComplexDescription": "A complex spell modifier for the corresponding check (for example, dependent on the targets values). If given, the numerical spell bonus is ignored.",
"DS4.SortBySpellModifier": "Sort by Spell Modifier",
"DS4.SpellDistance": "Distance",
"DS4.SpellDistanceDescription": "The maximum distance to the target, “Self” meaning that only the caster can be the target of this spell.",
@ -371,5 +382,5 @@
"DS4.ActiveEffectApplyToItems": "Apply to Items",
"DS4.ActiveEffectItemName": "Item Name",
"DS4.ActiveEffectItemCondition": "Condition",
"DS4.TooltipDisabledDueToEffects": "disabled, because affected by Active Effects"
"DS4.TooltipNotEditableDueToEffects": "field not editable, because affected by Active Effects"
}

View file

@ -6098,7 +6098,7 @@
"type": "loot",
"img": "icons/consumables/potions/bottle-conical-corked-labeled-shell-cyan.webp",
"data": {
"description": null,
"description": "<p>Weihwasser verursacht gegen D&auml;monen und Untote nicht abwehrbaren Schaden. Jede Einheit Weihwasser hat einen anderen Angriffswert, der mit W20 ermittelt wird. Dieser Wert wird erst ausgew&uuml;rfelt, wenn das Weihwasser den D&auml;monen bzw. Untoten trifft, es sei denn, es wird vorher in Bezug auf seinen Schadenswert von einem Zauberwirker mit GEI+AU, gefolgt von GEI+VE, erfolgreich analysiert.</p>\n<p>Eine Weihwassereinheit kann man auf eine Waffe/ein Geschoss auftragen (ben&ouml;tigt 1 Aktion) und dann einen normalen Angriff mit Schlagen bzw. Schie&szlig;en w&uuml;rfeln. Ist dieser erfolgreich, wird bei D&auml;monen und Untoten neben dem normalen Schaden auch noch ein Angriff f&uuml;r das Weihwasser gew&uuml;rfelt, der nicht abwehrbaren Schaden verursacht. Nach dem ersten Treffer ist die Einheit Weihwasser aufgebraucht.</p>\n<p>Alternativ kann man Weihwassereinheiten in zerbrechliche Phiolen (WB +0; 2 GM) f&uuml;llen und diese im Nah- oder Fernkampf gegen D&auml;monen und Untote einsetzen, wobei die zerbrechlichen Gef&auml;&szlig;e zerspringen. In solchen F&auml;llen verursacht nur das Weihwasser Schaden, nicht die Schie&szlig;en-Probe.</p>\n<p>Weihwasser kann au&szlig;erdem dazu benutzt werden, in sch&uuml;tzenden Linien oder Kreisen (1 m pro Einheit) auf den Boden gesch&uuml;ttet zu werden, um f&uuml;r eine gewisse Zeit D&auml;monen bzw. Untote aufzuhalten, die das Weihwasser nicht passieren k&ouml;nnen.</p>",
"quantity": 1,
"price": 0.1,
"availability": "hamlet",

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,6 @@
*/
.ds4-actor-sheet {
min-height: 625px;
min-height: 635px;
min-width: 650px;
}

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2022 Johannes Loher
*
* SPDX-License-Identifier: MIT
*/
.ds4-checkbox-grid {
$gap: 3px;
gap: $gap;
display: grid;
font-size: var(--font-size-12);
grid-template-columns: 1fr 1fr;
&__item {
align-items: center;
display: flex;
gap: $gap;
justify-content: flex-start;
> * {
flex: 0 0 auto;
}
}
&__checkbox[type="checkbox"] {
margin: 0;
}
}

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2022 Johannes Loher
*
* SPDX-License-Identifier: MIT
*/
.ds4-form-group {
clear: both;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 3px 0;
align-items: center;
&--start {
align-items: flex-start;
}
& > * {
flex: 3;
}
&__label {
flex: 2;
line-height: var(--form-field-height);
}
}

View file

@ -14,8 +14,10 @@
// shared
@use "components/shared/add_button";
@use "components/shared/control_button_group";
@use "components/shared/checkbox_grid";
@use "components/shared/editor";
@use "components/shared/embedded_document_list";
@use "components/shared/form_group";
@use "components/shared/rollable_image";
@use "components/shared/sheet_body";
@use "components/shared/sheet_form";

View file

@ -12,8 +12,29 @@ const defaultData: DS4SpellDataSourceData = {
description: "",
equipped: false,
spellType: "spellcasting",
bonus: "",
spellCategory: "unset",
spellModifier: {
numerical: 0,
complex: "",
},
spellGroups: {
lightning: false,
earth: false,
water: false,
ice: false,
fire: false,
healing: false,
light: false,
air: false,
transport: false,
damage: false,
shadow: false,
protection: false,
mindAffecting: false,
demonology: false,
necromancy: false,
transmutation: false,
area: false,
},
maxDistance: {
value: "",
unit: "meter",

View file

@ -76,7 +76,6 @@ export class DS4ActiveEffect extends ActiveEffect {
try {
change.value = DS4ActiveEffect.safeEval(change.value).toString();
} catch (e) {
logger.warn(e);
// this is a valid case, e.g., if the effect change simply is a string
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment

View file

@ -25,7 +25,7 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
static override get defaultOptions(): ActorSheet.Options {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-actor-sheet"],
height: 625,
height: 635,
scrollY: [".ds4-sheet-body"],
tabs: [{ navSelector: ".ds4-sheet-tab-nav", contentSelector: ".ds4-sheet-body", initial: "values" }],
dragDrop: [
@ -111,7 +111,14 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
html.find(".sort-items").on("click", this.onSortItems.bind(this));
disableOverriddenFields.call(this);
disableOverriddenFields(this.form, this.actor.overrides, (key) => `[name="${key}"]`);
for (const item of this.actor.items) {
disableOverriddenFields(
this.form,
item.overrides,
(key) => `[data-item-id="${item.id}"] .change-item[data-property="${key}"]`,
);
}
}
/**
@ -359,6 +366,7 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
enforce(type !== undefined, `Could not find property 'type' in the dataset of the parent of ${target}`);
const dataPath = target.dataset["dataPath"];
enforce(dataPath !== undefined, `Could not find property 'dataPath' in the dataset of ${target}`);
const dataPath2 = target.dataset["dataPath2"];
const items = this.actor.items.filter((item) => item.type === type);
items.sort((a, b) => a.data.sort - b.data.sort);
@ -367,13 +375,20 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
(a: DS4Item, b: DS4Item): number => {
const propertyA = getProperty(a.data, dataPath);
const propertyB = getProperty(b.data, dataPath);
if (typeof propertyA === "string" || typeof propertyB === "string") {
return invert
? (propertyB ?? "").localeCompare(propertyA ?? "")
: (propertyA ?? "").localeCompare(propertyB ?? "");
} else {
return invert ? propertyB - propertyA : propertyA - propertyB;
const comparison =
typeof propertyA === "string" || typeof propertyB === "string"
? compareAsStrings(propertyA, propertyB, invert)
: compareAsNumbers(propertyA, propertyB, invert);
if (comparison === 0 && dataPath2 !== undefined) {
const propertyA = getProperty(a.data, dataPath);
const propertyB = getProperty(b.data, dataPath);
return typeof propertyA === "string" || typeof propertyB === "string"
? compareAsStrings(propertyA, propertyB, invert)
: compareAsNumbers(propertyA, propertyB, invert);
}
return comparison;
};
const sortedItems = [...items].sort(sortFunction(false));
@ -434,3 +449,11 @@ const embeddedDocumentListEntryProperties = Object.freeze({
idDataAttribute: "itemId",
},
});
const compareAsStrings = (a: { toString(): string }, b: { toString(): string }, invert: boolean): number => {
return invert ? b.toString().localeCompare(a.toString()) : a.toString().localeCompare(b.toString());
};
const compareAsNumbers = (a: number, b: number, invert: boolean): number => {
return invert ? b - a : a - b;
};

View file

@ -79,12 +79,12 @@ export class DS4Actor extends Actor {
if (condition !== undefined && condition !== "") {
try {
const replacedCondition = Roll.replaceFormulaData(condition, {
const replacedCondition = DS4Actor.replaceFormulaData(condition, {
item: item.data,
actor: this.data,
effect: effect.data,
});
return Boolean(mathEvaluator.evaluate(replacedCondition));
return replacedCondition !== undefined ? Boolean(mathEvaluator.evaluate(replacedCondition)) : false;
} catch (error) {
logger.warn(error);
return false;
@ -95,6 +95,21 @@ export class DS4Actor extends Actor {
});
}
private static replaceFormulaData(formula: string, data: object): string | undefined {
const dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi);
try {
return formula.replace(dataRgx, (_, term) => {
const value = foundry.utils.getProperty(data, term);
if (value == null) {
throw new Error();
}
return String(value).trim();
});
} catch {
return undefined;
}
}
/**
* We override this with an empty implementation because we have our own custom way of applying
* {@link ActiveEffect}s and {@link Actor#prepareEmbeddedDocuments} calls this.

View file

@ -4,15 +4,16 @@
import { getGame } from "../helpers";
export function disableOverriddenFields<
Options extends FormApplicationOptions,
Data extends object,
ConcreteObject extends { overrides: Record<string, unknown> },
>(this: FormApplication<Options, Data, ConcreteObject>): void {
export function disableOverriddenFields(
form: HTMLElement | null,
overrides: Record<string, unknown>,
selector: (key: string) => string,
): void {
const inputs = ["INPUT", "SELECT", "TEXTAREA", "BUTTON"];
const titleAddition = `(${getGame().i18n.localize("DS4.TooltipDisabledDueToEffects")})`;
for (const key of Object.keys(foundry.utils.flattenObject(this.object.overrides))) {
const elements = this.form?.querySelectorAll(`[name="${key}"]`);
const titleAddition = `(${getGame().i18n.localize("DS4.TooltipNotEditableDueToEffects")})`;
for (const key of Object.keys(foundry.utils.flattenObject(overrides))) {
const elements = form?.querySelectorAll(selector(key));
elements?.forEach((element) => {
if (inputs.includes(element.tagName)) {
element.setAttribute("disabled", "");

View file

@ -94,16 +94,24 @@ const i18nKeys = {
targetedSpellcasting: "DS4.SpellTypeTargetedSpellcasting",
},
spellCategories: {
healing: "DS4.SpellCategoryHealing",
fire: "DS4.SpellCategoryFire",
ice: "DS4.SpellCategoryIce",
light: "DS4.SpellCategoryLight",
darkness: "DS4.SpellCategoryDarkness",
mindAffecting: "DS4.SpellCategoryMindAffecting",
electricity: "DS4.SpellCategoryElectricity",
none: "DS4.SpellCategoryNone",
unset: "DS4.SpellCategoryUnset",
spellGroups: {
lightning: "DS4.SpellGroupLightning",
earth: "DS4.SpellGroupEarth",
water: "DS4.SpellGroupWater",
ice: "DS4.SpellGroupIce",
fire: "DS4.SpellGroupFire",
healing: "DS4.SpellGroupHealing",
light: "DS4.SpellGroupLight",
air: "DS4.SpellGroupAir",
transport: "DS4.SpellGroupTransport",
damage: "DS4.SpellGroupDamage",
shadow: "DS4.SpellGroupShadow",
protection: "DS4.SpellGroupProtection",
mindAffecting: "DS4.SpellGroupMindAffecting",
demonology: "DS4.SpellGroupDemonology",
necromancy: "DS4.SpellGroupNecromancy",
transmutation: "DS4.SpellGroupTransmutation",
area: "DS4.SpellGroupArea",
},
cooldownDurations: {

View file

@ -23,7 +23,7 @@ function localizeAndSortConfigObjects() {
"combatValues",
"cooldownDurations",
"creatureSizeCategories",
"spellCategories",
"spellGroups",
"traits",
"checkModifiers",
];

View file

@ -72,7 +72,7 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
html.find(".control-effect").on("click", this.onControlEffect.bind(this));
disableOverriddenFields.call(this);
disableOverriddenFields(this.form, this.item.overrides, (key) => `[name="${key}"]`);
}
/**

View file

@ -12,8 +12,11 @@ export interface DS4SpellDataSource {
export interface DS4SpellDataSourceData extends DS4ItemDataSourceDataBase, DS4ItemDataSourceDataEquipable {
spellType: keyof typeof DS4.i18n.spellTypes;
bonus: string;
spellCategory: keyof typeof DS4.i18n.spellCategories;
spellModifier: {
numerical: number;
complex: string;
};
spellGroups: Record<keyof typeof DS4.i18n.spellGroups, boolean>;
maxDistance: UnitData<DistanceUnit>;
effectRadius: UnitData<DistanceUnit>;
duration: UnitData<TemporalUnit>;

View file

@ -32,17 +32,19 @@ export class DS4Spell extends DS4Item {
}
const ownerDataData = this.actor.data.data;
const spellModifier = Number.isNumeric(this.data.data.bonus) ? parseInt(this.data.data.bonus) : undefined;
if (spellModifier === undefined) {
const hasComplexModifier = this.data.data.spellModifier.complex !== "";
if (hasComplexModifier === undefined) {
notifications.info(
game.i18n.format("DS4.InfoManuallyEnterSpellModifier", {
name: this.name,
spellModifier: this.data.data.bonus,
spellModifier: this.data.data.spellModifier.complex,
}),
);
}
const spellType = this.data.data.spellType;
const checkTargetNumber = ownerDataData.combatValues[spellType].total + (spellModifier ?? 0);
const checkTargetNumber =
ownerDataData.combatValues[spellType].total +
(hasComplexModifier ? 0 : this.data.data.spellModifier.numerical);
const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
await createCheckRoll(checkTargetNumber, {

View file

@ -9,6 +9,7 @@ import { migration as migration002 } from "./migrations/002";
import { migration as migration003 } from "./migrations/003";
import { migration as migration004 } from "./migrations/004";
import { migration as migration005 } from "./migrations/005";
import { migration as migration006 } from "./migrations/006";
import notifications from "./ui/notifications";
async function migrate(): Promise<void> {
@ -135,7 +136,7 @@ interface Migration {
migrateCompendium: (pack: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>;
}
const migrations: Migration[] = [migration001, migration002, migration003, migration004, migration005];
const migrations: Migration[] = [migration001, migration002, migration003, migration004, migration005, migration006];
function isFirstWorldStart(migrationVersion: number): boolean {
return migrationVersion < 0;

117
src/migrations/006.ts Normal file
View file

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import {
getActorUpdateDataGetter,
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
import type { DS4SpellDataSourceData } from "../item/spell/spell-data-source";
async function migrate(): Promise<void> {
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium);
}
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) {
if (itemData.type !== "spell") return;
// @ts-expect-error spellCategory is removed with this migration
const spellCategory: string | undefined = itemData.data?.spellCategory;
const spellGroups = migrateSpellCategory(spellCategory);
// @ts-expect-error bonus is removed with this migration
const bonus: string | undefined = itemData.data?.bonus;
const spellModifier = migrateBonus(bonus);
const updateData: Record<string, unknown> = {
data: {
spellGroups,
"-=spellCategory": null,
spellModifier,
"-=bonus": null,
},
};
return updateData;
}
function migrateSpellCategory(spellCategory: string | undefined): DS4SpellDataSourceData["spellGroups"] {
const spellGroups = {
lightning: false,
earth: false,
water: false,
ice: false,
fire: false,
healing: false,
light: false,
air: false,
transport: false,
damage: false,
shadow: false,
protection: false,
mindAffecting: false,
demonology: false,
necromancy: false,
transmutation: false,
area: false,
};
switch (spellCategory) {
case "healing": {
spellGroups.healing = true;
break;
}
case "fire": {
spellGroups.fire = true;
break;
}
case "ice": {
spellGroups.ice = true;
break;
}
case "light": {
spellGroups.light = true;
break;
}
case "darkness": {
spellGroups.shadow = true;
break;
}
case "mindAffecting": {
spellGroups.mindAffecting = true;
break;
}
case "electricity": {
spellGroups.lightning = true;
break;
}
}
return spellGroups;
}
function migrateBonus(bonus: string | undefined): DS4SpellDataSourceData["spellModifier"] {
const spellModifier = { numerical: 0, complex: "" };
if (bonus) {
if (Number.isNumeric(bonus)) {
spellModifier.numerical = +bonus;
} else {
spellModifier.complex = bonus;
}
}
return spellModifier;
}
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
export const migration = {
migrate,
migrateCompendium,
};

View file

@ -72,7 +72,7 @@ type CompendiumMigrator = (compendium: CompendiumCollection<CompendiumCollection
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;
if (!["Actor", "Item", "Scene"].includes(compendium.metadata.type)) continue;
await migrateCompendium(compendium);
}
}
@ -144,8 +144,8 @@ export function getCompendiumMigrator(
{ migrateToTemplateEarly = true } = {},
) {
return async (compendium: CompendiumCollection<CompendiumCollection.Metadata>): Promise<void> => {
const entityName = compendium.metadata.entity;
if (!["Actor", "Item", "Scene"].includes(entityName)) return;
const type = compendium.metadata.type;
if (!["Actor", "Item", "Scene"].includes(type)) return;
const wasLocked = compendium.locked;
await compendium.configure({ locked: false });
if (migrateToTemplateEarly) {

View file

@ -173,8 +173,29 @@
"spell": {
"templates": ["base", "equipable"],
"spellType": "spellcasting",
"bonus": "",
"spellCategory": "unset",
"spellModifier": {
"numerical": 0,
"complex": ""
},
"spellGroups": {
"lightning": false,
"earth": false,
"water": false,
"ice": false,
"fire": false,
"healing": false,
"light": false,
"air": false,
"transport": false,
"damage": false,
"shadow": false,
"protection": false,
"mindAffecting": false,
"demonology": false,
"necromancy": false,
"transmutation": false,
"area": false
},
"maxDistance": {
"value": "",
"unit": "meter"

View file

@ -53,9 +53,10 @@ titleKey=titleKey}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.spellType"
title="{{localize 'DS4.SortBySpellType'}}">{{localize 'DS4.SpellTypeAbbr'}}</div>
{{!-- spell bonus --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.bonus"
title="{{localize 'DS4.SortBySpellModifier'}}">{{localize 'DS4.SpellModifierAbbr'}}</div>
{{!-- spell modifier --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.spellModifier.complex"
data-data-path2="data.spellModifier.numerical" title="{{localize 'DS4.SortBySpellModifier'}}">{{localize
'DS4.SpellModifierAbbr'}}</div>
{{!-- max. distance --}}
<div title="{{localize 'DS4.SpellDistance'}}"><i class="fas fa-ruler"></i></div>
@ -75,9 +76,9 @@ titleKey=titleKey}}
src="{{lookup @root/config.icons.spellTypes itemData.data.spellType}}"
title="{{lookup @root/config.i18n.spellTypes itemData.data.spellType}}" />
{{!-- spell bonus --}}
<input class="ds4-embedded-document-list__editable change-item" type="text" data-dtype="String"
data-property="data.bonus" value="{{itemData.data.bonus}}" title="{{localize 'DS4.SpellModifier'}}" />
{{!-- spell modifier --}}
<div title="{{localize 'DS4.SpellModifier'}}">{{#if (eq itemData.data.spellModifier.complex
'')}}{{itemData.data.spellModifier.numerical}}{{else}}{{itemData.data.spellModifier.complex}}{{/if}}</div>
{{!-- max. distance --}}
{{> distanceUnit titleKey='DS4.SpellDistance' unitDatum=itemData.data.maxDistance

View file

@ -7,11 +7,21 @@ SPDX-License-Identifier: MIT
<div class="ds4-item-properties ds4-item-properties--spell">
<h4 class="ds4-item-properties__title">{{localize 'DS4.ItemPropertiesSpell'}}</h4>
<div class="form-group">
<label for="data.bonus-{{data._id}}" title="{{localize 'DS4.SpellModifierDescription'}}">{{localize
"DS4.SpellModifier"}}</label>
<label for="data.spellModifier.numerical-{{data._id}}"
title="{{localize 'DS4.SpellModifierNumericalDescription'}}">{{localize
"DS4.SpellModifierNumerical"}}</label>
<div class="form-fields">
<input id="data.bonus-{{data._id}}" data-dtype="String" type="text" name="data.bonus" placeholder="0"
value="{{data.data.bonus}}" />
<input id="data.spellModifier.numerical-{{data._id}}" data-dtype="Number" type="number"
name="data.spellModifier.numerical" placeholder="0" value="{{data.data.spellModifier.numerical}}" />
</div>
</div>
<div class="form-group">
<label for="data.spellModifier.complex-{{data._id}}"
title="{{localize 'DS4.SpellModifierComplexDescription'}}">{{localize
"DS4.SpellModifierComplex"}}</label>
<div class="form-fields">
<input id="data.spellModifier.complex-{{data._id}}" data-dtype="String" type="text"
name="data.spellModifier.complex" value="{{data.data.spellModifier.complex}}" />
</div>
</div>
<div class="form-group">
@ -27,19 +37,6 @@ SPDX-License-Identifier: MIT
</select>
</div>
</div>
<div class="form-group">
<label for="data.spellCategory-{{data._id}}" title="{{localize 'DS4.SpellCategoryDescription'}}">{{localize
"DS4.SpellCategory"}}</label>
<div class="form-fields">
<select id="data.spellCategory-{{data._id}}" name="data.spellCategory" data-dtype="String">
{{#select data.data.spellCategory}}
{{#each config.i18n.spellCategories as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
</div>
<div class="form-group slim">
<label title="{{localize 'DS4.SpellDistanceDescription'}}">{{localize "DS4.SpellDistance"}}</label>
<div class="form-fields">
@ -94,18 +91,33 @@ SPDX-License-Identifier: MIT
</select>
</div>
</div>
<div class="ds4-form-group ds4-form-group--start">
<label class="ds4-form-group__label" title="{{localize 'DS4.SpellGroupsDescription'}}">{{localize
"DS4.SpellGroups"}}</label>
<div class="ds4-checkbox-grid">
{{#each config.i18n.spellGroups as |value key|}}
<div class="ds4-checkbox-grid__item">
<input class="ds4-checkbox-grid__checkbox" id="data.spellGroups.{{key}}-{{../data._id}}"
name="data.spellGroups.{{key}}" data-dtype="Boolean" type="checkbox" {{checked (lookup
../data.data.spellGroups key)}} />
<label for="data.spellGroups.{{key}}-{{../data._id}}">{{value}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group slim">
<label title="{{localize 'DS4.SpellMinimumLevelDescription'}}">{{localize "DS4.SpellMinimumLevel"}}</label>
<div class="form-fields">
<label for="data.minimumLevels.healer-{{data._id}}">{{localize "DS4.SpellCasterClassHealer"}}</label>
<input id="data.minimumLevels.healer-{{data._id}}" data-dtype="Number" type="number" min="0" step="1"
name="data.minimumLevels.healer" placeholder="" value="{{data.data.minimumLevels.healer}}" />
<label for="data.minimumLevels.sorcerer-{{data._id}}">{{localize "DS4.SpellCasterClassSorcerer"}}</label>
<input id="data.minimumLevels.sorcerer-{{data._id}}" data-dtype="Number" type="number" min="0" step="1"
name="data.minimumLevels.sorcerer" placeholder="" value="{{data.data.minimumLevels.sorcerer}}" />
<label for="data.minimumLevels.wizard-{{data._id}}">{{localize "DS4.SpellCasterClassWizard"}}</label>
<input id="data.minimumLevels.wizard-{{data._id}}" data-dtype="Number" type="number" min="0" step="1"
name="data.minimumLevels.wizard" placeholder="" value="{{data.data.minimumLevels.wizard}}" />
<label for="data.minimumLevels.sorcerer-{{data._id}}">{{localize
"DS4.SpellCasterClassSorcerer"}}</label>
<input id="data.minimumLevels.sorcerer-{{data._id}}" data-dtype="Number" type="number" min="0" step="1"
name="data.minimumLevels.sorcerer" placeholder="" value="{{data.data.minimumLevels.sorcerer}}" />
</div>
</div>
<div class="form-group">

View file

@ -57,7 +57,7 @@ export async function convertJSONToPack(contents) {
/**
* Converts a pack file (NeDB) to a JSON string.
* @param {string} filename The name of the pack file
* @returns {Promise<Array<unknown>>} A promise that resolves to an array of the documents in the pack file
* @returns {Promise<string>} A promise that resolves to an array of the documents in the pack file
*/
function convertPackFileToJSON(filename) {
const db = new Datastore({ filename, autoload: true });