Merge branch 'spell-groups' into 'main'
feat: replace spell category by spell groups See merge request dungeonslayers/ds4!214
This commit is contained in:
commit
b74919b75b
25 changed files with 3675 additions and 487 deletions
37
lang/de.json
37
lang/de.json
|
@ -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"
|
||||
}
|
||||
|
|
37
lang/en.json
37
lang/en.json
|
@ -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 target’s 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"
|
||||
}
|
||||
|
|
|
@ -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ämonen und Untote nicht abwehrbaren Schaden. Jede Einheit Weihwasser hat einen anderen Angriffswert, der mit W20 ermittelt wird. Dieser Wert wird erst ausgewürfelt, wenn das Weihwasser den Dä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ötigt 1 Aktion) und dann einen normalen Angriff mit Schlagen bzw. Schießen würfeln. Ist dieser erfolgreich, wird bei Dämonen und Untoten neben dem normalen Schaden auch noch ein Angriff für das Weihwasser gewü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üllen und diese im Nah- oder Fernkampf gegen Dämonen und Untote einsetzen, wobei die zerbrechlichen Gefäße zerspringen. In solchen Fällen verursacht nur das Weihwasser Schaden, nicht die Schießen-Probe.</p>\n<p>Weihwasser kann außerdem dazu benutzt werden, in schützenden Linien oder Kreisen (1 m pro Einheit) auf den Boden geschüttet zu werden, um für eine gewisse Zeit Dämonen bzw. Untote aufzuhalten, die das Weihwasser nicht passieren können.</p>",
|
||||
"quantity": 1,
|
||||
"price": 0.1,
|
||||
"availability": "hamlet",
|
||||
|
|
3660
packs/spells.json
3660
packs/spells.json
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,6 @@
|
|||
*/
|
||||
|
||||
.ds4-actor-sheet {
|
||||
min-height: 625px;
|
||||
min-height: 635px;
|
||||
min-width: 650px;
|
||||
}
|
||||
|
|
27
scss/components/shared/_checkbox_grid.scss
Normal file
27
scss/components/shared/_checkbox_grid.scss
Normal 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;
|
||||
}
|
||||
}
|
27
scss/components/shared/_form_group.scss
Normal file
27
scss/components/shared/_form_group.scss
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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", "");
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -23,7 +23,7 @@ function localizeAndSortConfigObjects() {
|
|||
"combatValues",
|
||||
"cooldownDurations",
|
||||
"creatureSizeCategories",
|
||||
"spellCategories",
|
||||
"spellGroups",
|
||||
"traits",
|
||||
"checkModifiers",
|
||||
];
|
||||
|
|
|
@ -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}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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
117
src/migrations/006.ts
Normal 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,
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 });
|
||||
|
|
Loading…
Reference in a new issue