feat: replace spell category by spell groups

This also allows to assign a spell to multiple spell groups, which is the case for many spells in
the SRD.

Additionally, this makes many small improvements and fixes to the provided spell compendium.
This commit is contained in:
Johannes Loher 2022-11-04 21:08:23 +01:00
parent ab31450dd8
commit 9d7c570553
20 changed files with 3615 additions and 466 deletions

View file

@ -118,20 +118,31 @@
"DS4.SortBySpellType": "Nach Zauberspruchtyp sortieren", "DS4.SortBySpellType": "Nach Zauberspruchtyp sortieren",
"DS4.SpellTypeSpellcasting": "Zaubern", "DS4.SpellTypeSpellcasting": "Zaubern",
"DS4.SpellTypeTargetedSpellcasting": "Zielzaubern", "DS4.SpellTypeTargetedSpellcasting": "Zielzaubern",
"DS4.SpellCategory": "Kategorie", "DS4.SpellGroups": "Zaubergruppen",
"DS4.SpellCategoryDescription": "Eine Kategorie, der der Zauberspruch zugehörig ist.", "DS4.SpellGroupsDescription": "Zaubergruppen, denen der Zauberspruch zugehörig ist.",
"DS4.SpellCategoryHealing": "Heilung", "DS4.SpellGroupLightning": "Blitz",
"DS4.SpellCategoryFire": "Feuer", "DS4.SpellGroupEarth": "Erde, Fels, Stein",
"DS4.SpellCategoryIce": "Eis", "DS4.SpellGroupWater": "Wasser",
"DS4.SpellCategoryLight": "Licht", "DS4.SpellGroupIce": "Eis, Frost",
"DS4.SpellCategoryDarkness": "Schatten", "DS4.SpellGroupFire": "Feuer",
"DS4.SpellCategoryMindAffecting": "Geistesbeeinflussend", "DS4.SpellGroupHealing": "Heilung",
"DS4.SpellCategoryElectricity": "Elektrizität", "DS4.SpellGroupLight": "Licht",
"DS4.SpellCategoryNone": "Keine", "DS4.SpellGroupAir": "Luft",
"DS4.SpellCategoryUnset": "Nicht gesetzt", "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.SpellModifier": "Zauberbonus",
"DS4.SpellModifierNumerical": "Zauberbonus (numerisch)",
"DS4.SpellModifierComplex": "Zauberbonus (komplex)",
"DS4.SpellModifierAbbr": "ZB", "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.SortBySpellModifier": "Nach Zauberbonus sortieren",
"DS4.SpellDistance": "Distanz", "DS4.SpellDistance": "Distanz",
"DS4.SpellDistanceDescription": "Die maximale Entfernung zum Ziel. „Selbst“ bedeutet, dass nur der Zauberwirker selbst das Ziel des Zaubers sein kann.", "DS4.SpellDistanceDescription": "Die maximale Entfernung zum Ziel. „Selbst“ bedeutet, dass nur der Zauberwirker selbst das Ziel des Zaubers sein kann.",

View file

@ -118,20 +118,31 @@
"DS4.SortBySpellType": "Sort by Spell Type", "DS4.SortBySpellType": "Sort by Spell Type",
"DS4.SpellTypeSpellcasting": "Spellcasting", "DS4.SpellTypeSpellcasting": "Spellcasting",
"DS4.SpellTypeTargetedSpellcasting": "Targeted Spellcasting", "DS4.SpellTypeTargetedSpellcasting": "Targeted Spellcasting",
"DS4.SpellCategory": "Category", "DS4.SpellGroups": "Spell Groups",
"DS4.SpellCategoryDescription": "A category which the spell belongs to.", "DS4.SpellGroupsDescription": "Spell groups which the spell belongs to.",
"DS4.SpellCategoryHealing": "Healing", "DS4.SpellGroupLightning": "Lightning",
"DS4.SpellCategoryFire": "Fire", "DS4.SpellGroupEarth": "Earth, Rock, Stone",
"DS4.SpellCategoryIce": "Ice", "DS4.SpellGroupWater": "Water",
"DS4.SpellCategoryLight": "Light", "DS4.SpellGroupIce": "Ice, Frost",
"DS4.SpellCategoryDarkness": "Darkness", "DS4.SpellGroupFire": "Fire",
"DS4.SpellCategoryMindAffecting": "Mind Affecting", "DS4.SpellGroupHealing": "Healing",
"DS4.SpellCategoryElectricity": "Electricity", "DS4.SpellGroupLight": "Light",
"DS4.SpellCategoryNone": "None", "DS4.SpellGroupAir": "Air",
"DS4.SpellCategoryUnset": "Unset", "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.SpellModifier": "Spell Modifier",
"DS4.SpellModifierNumerical": "Spell Modifier (numerical)",
"DS4.SpellModifierComplex": "Spell Modifier (complex)",
"DS4.SpellModifierAbbr": "SM", "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.SortBySpellModifier": "Sort by Spell Modifier",
"DS4.SpellDistance": "Distance", "DS4.SpellDistance": "Distance",
"DS4.SpellDistanceDescription": "The maximum distance to the target, “Self” meaning that only the caster can be the target of this spell.", "DS4.SpellDistanceDescription": "The maximum distance to the target, “Self” meaning that only the caster can be the target of this spell.",

View file

@ -6098,7 +6098,7 @@
"type": "loot", "type": "loot",
"img": "icons/consumables/potions/bottle-conical-corked-labeled-shell-cyan.webp", "img": "icons/consumables/potions/bottle-conical-corked-labeled-shell-cyan.webp",
"data": { "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, "quantity": 1,
"price": 0.1, "price": 0.1,
"availability": "hamlet", "availability": "hamlet",

File diff suppressed because one or more lines are too long

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 // shared
@use "components/shared/add_button"; @use "components/shared/add_button";
@use "components/shared/control_button_group"; @use "components/shared/control_button_group";
@use "components/shared/checkbox_grid";
@use "components/shared/editor"; @use "components/shared/editor";
@use "components/shared/embedded_document_list"; @use "components/shared/embedded_document_list";
@use "components/shared/form_group";
@use "components/shared/rollable_image"; @use "components/shared/rollable_image";
@use "components/shared/sheet_body"; @use "components/shared/sheet_body";
@use "components/shared/sheet_form"; @use "components/shared/sheet_form";

View file

@ -12,8 +12,29 @@ const defaultData: DS4SpellDataSourceData = {
description: "", description: "",
equipped: false, equipped: false,
spellType: "spellcasting", spellType: "spellcasting",
bonus: "", spellModifier: {
spellCategory: "unset", 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: { maxDistance: {
value: "", value: "",
unit: "meter", unit: "meter",

View file

@ -13,8 +13,7 @@ export function disableOverriddenFields(
const titleAddition = `(${getGame().i18n.localize("DS4.TooltipNotEditableDueToEffects")})`; const titleAddition = `(${getGame().i18n.localize("DS4.TooltipNotEditableDueToEffects")})`;
for (const key of Object.keys(foundry.utils.flattenObject(overrides))) { for (const key of Object.keys(foundry.utils.flattenObject(overrides))) {
const sel = selector(key); const elements = form?.querySelectorAll(selector(key));
const elements = form?.querySelectorAll(sel);
elements?.forEach((element) => { elements?.forEach((element) => {
if (inputs.includes(element.tagName)) { if (inputs.includes(element.tagName)) {
element.setAttribute("disabled", ""); element.setAttribute("disabled", "");

View file

@ -94,16 +94,24 @@ const i18nKeys = {
targetedSpellcasting: "DS4.SpellTypeTargetedSpellcasting", targetedSpellcasting: "DS4.SpellTypeTargetedSpellcasting",
}, },
spellCategories: { spellGroups: {
healing: "DS4.SpellCategoryHealing", lightning: "DS4.SpellGroupLightning",
fire: "DS4.SpellCategoryFire", earth: "DS4.SpellGroupEarth",
ice: "DS4.SpellCategoryIce", water: "DS4.SpellGroupWater",
light: "DS4.SpellCategoryLight", ice: "DS4.SpellGroupIce",
darkness: "DS4.SpellCategoryDarkness", fire: "DS4.SpellGroupFire",
mindAffecting: "DS4.SpellCategoryMindAffecting", healing: "DS4.SpellGroupHealing",
electricity: "DS4.SpellCategoryElectricity", light: "DS4.SpellGroupLight",
none: "DS4.SpellCategoryNone", air: "DS4.SpellGroupAir",
unset: "DS4.SpellCategoryUnset", 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: { cooldownDurations: {

View file

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

View file

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

View file

@ -32,17 +32,19 @@ export class DS4Spell extends DS4Item {
} }
const ownerDataData = this.actor.data.data; const ownerDataData = this.actor.data.data;
const spellModifier = Number.isNumeric(this.data.data.bonus) ? parseInt(this.data.data.bonus) : undefined; const hasComplexModifier = this.data.data.spellModifier.complex !== "";
if (spellModifier === undefined) { if (hasComplexModifier === undefined) {
notifications.info( notifications.info(
game.i18n.format("DS4.InfoManuallyEnterSpellModifier", { game.i18n.format("DS4.InfoManuallyEnterSpellModifier", {
name: this.name, name: this.name,
spellModifier: this.data.data.bonus, spellModifier: this.data.data.spellModifier.complex,
}), }),
); );
} }
const spellType = this.data.data.spellType; 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 }); const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
await createCheckRoll(checkTargetNumber, { 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 migration003 } from "./migrations/003";
import { migration as migration004 } from "./migrations/004"; import { migration as migration004 } from "./migrations/004";
import { migration as migration005 } from "./migrations/005"; import { migration as migration005 } from "./migrations/005";
import { migration as migration006 } from "./migrations/006";
import notifications from "./ui/notifications"; import notifications from "./ui/notifications";
async function migrate(): Promise<void> { async function migrate(): Promise<void> {
@ -135,7 +136,7 @@ interface Migration {
migrateCompendium: (pack: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>; 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 { function isFirstWorldStart(migrationVersion: number): boolean {
return migrationVersion < 0; 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> { export async function migrateCompendiums(migrateCompendium: CompendiumMigrator): Promise<void> {
for (const compendium of getGame().packs ?? []) { for (const compendium of getGame().packs ?? []) {
if (compendium.metadata.package !== "world") continue; 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); await migrateCompendium(compendium);
} }
} }
@ -144,8 +144,8 @@ export function getCompendiumMigrator(
{ migrateToTemplateEarly = true } = {}, { migrateToTemplateEarly = true } = {},
) { ) {
return async (compendium: CompendiumCollection<CompendiumCollection.Metadata>): Promise<void> => { return async (compendium: CompendiumCollection<CompendiumCollection.Metadata>): Promise<void> => {
const entityName = compendium.metadata.entity; const type = compendium.metadata.type;
if (!["Actor", "Item", "Scene"].includes(entityName)) return; if (!["Actor", "Item", "Scene"].includes(type)) return;
const wasLocked = compendium.locked; const wasLocked = compendium.locked;
await compendium.configure({ locked: false }); await compendium.configure({ locked: false });
if (migrateToTemplateEarly) { if (migrateToTemplateEarly) {

View file

@ -173,8 +173,29 @@
"spell": { "spell": {
"templates": ["base", "equipable"], "templates": ["base", "equipable"],
"spellType": "spellcasting", "spellType": "spellcasting",
"bonus": "", "spellModifier": {
"spellCategory": "unset", "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": { "maxDistance": {
"value": "", "value": "",
"unit": "meter" "unit": "meter"

View file

@ -53,9 +53,10 @@ titleKey=titleKey}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.spellType" <div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.spellType"
title="{{localize 'DS4.SortBySpellType'}}">{{localize 'DS4.SpellTypeAbbr'}}</div> title="{{localize 'DS4.SortBySpellType'}}">{{localize 'DS4.SpellTypeAbbr'}}</div>
{{!-- spell bonus --}} {{!-- spell modifier --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.bonus" <div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.spellModifier.complex"
title="{{localize 'DS4.SortBySpellModifier'}}">{{localize 'DS4.SpellModifierAbbr'}}</div> data-data-path2="data.spellModifier.numerical" title="{{localize 'DS4.SortBySpellModifier'}}">{{localize
'DS4.SpellModifierAbbr'}}</div>
{{!-- max. distance --}} {{!-- max. distance --}}
<div title="{{localize 'DS4.SpellDistance'}}"><i class="fas fa-ruler"></i></div> <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}}" src="{{lookup @root/config.icons.spellTypes itemData.data.spellType}}"
title="{{lookup @root/config.i18n.spellTypes itemData.data.spellType}}" /> title="{{lookup @root/config.i18n.spellTypes itemData.data.spellType}}" />
{{!-- spell bonus --}} {{!-- spell modifier --}}
<input class="ds4-embedded-document-list__editable change-item" type="text" data-dtype="String" <div title="{{localize 'DS4.SpellModifier'}}">{{#if (eq itemData.data.spellModifier.complex
data-property="data.bonus" value="{{itemData.data.bonus}}" title="{{localize 'DS4.SpellModifier'}}" /> '')}}{{itemData.data.spellModifier.numerical}}{{else}}{{itemData.data.spellModifier.complex}}{{/if}}</div>
{{!-- max. distance --}} {{!-- max. distance --}}
{{> distanceUnit titleKey='DS4.SpellDistance' unitDatum=itemData.data.maxDistance {{> 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"> <div class="ds4-item-properties ds4-item-properties--spell">
<h4 class="ds4-item-properties__title">{{localize 'DS4.ItemPropertiesSpell'}}</h4> <h4 class="ds4-item-properties__title">{{localize 'DS4.ItemPropertiesSpell'}}</h4>
<div class="form-group"> <div class="form-group">
<label for="data.bonus-{{data._id}}" title="{{localize 'DS4.SpellModifierDescription'}}">{{localize <label for="data.spellModifier.numerical-{{data._id}}"
"DS4.SpellModifier"}}</label> title="{{localize 'DS4.SpellModifierNumericalDescription'}}">{{localize
"DS4.SpellModifierNumerical"}}</label>
<div class="form-fields"> <div class="form-fields">
<input id="data.bonus-{{data._id}}" data-dtype="String" type="text" name="data.bonus" placeholder="0" <input id="data.spellModifier.numerical-{{data._id}}" data-dtype="Number" type="number"
value="{{data.data.bonus}}" /> 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> </div>
<div class="form-group"> <div class="form-group">
@ -27,19 +37,6 @@ SPDX-License-Identifier: MIT
</select> </select>
</div> </div>
</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"> <div class="form-group slim">
<label title="{{localize 'DS4.SpellDistanceDescription'}}">{{localize "DS4.SpellDistance"}}</label> <label title="{{localize 'DS4.SpellDistanceDescription'}}">{{localize "DS4.SpellDistance"}}</label>
<div class="form-fields"> <div class="form-fields">
@ -94,18 +91,33 @@ SPDX-License-Identifier: MIT
</select> </select>
</div> </div>
</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"> <div class="form-group slim">
<label title="{{localize 'DS4.SpellMinimumLevelDescription'}}">{{localize "DS4.SpellMinimumLevel"}}</label> <label title="{{localize 'DS4.SpellMinimumLevelDescription'}}">{{localize "DS4.SpellMinimumLevel"}}</label>
<div class="form-fields"> <div class="form-fields">
<label for="data.minimumLevels.healer-{{data._id}}">{{localize "DS4.SpellCasterClassHealer"}}</label> <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" <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}}" /> 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> <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" <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}}" /> 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> </div>
<div class="form-group"> <div class="form-group">

View file

@ -57,7 +57,7 @@ export async function convertJSONToPack(contents) {
/** /**
* Converts a pack file (NeDB) to a JSON string. * Converts a pack file (NeDB) to a JSON string.
* @param {string} filename The name of the pack file * @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) { function convertPackFileToJSON(filename) {
const db = new Datastore({ filename, autoload: true }); const db = new Datastore({ filename, autoload: true });