feat: only allow specific selectable values for the cooldown duration of spells

World data (including compendium packs) is migrated automatically. In order to also migrate packs
provided by modules, you can use the following macro:
```js
const pack = game.packs.get("<name-of-the-module>.<name-of-the-pack>");
game.ds4.migration.migrateCompendiumFromTo(pack, 4, 5);
```
This commit is contained in:
Johannes Loher 2022-02-14 00:58:23 +01:00
parent 73e2d44c55
commit da1f6999eb
20 changed files with 558 additions and 876 deletions

View file

@ -114,10 +114,12 @@
"DS4.ArmorMaterialTypeNaturalAbbr": "Natürlich",
"DS4.SpellType": "Zauberspruchtyp",
"DS4.SpellTypeAbbr": "T",
"DS4.SpellTypeDescription": "Der Typ des Zauberspruchs.",
"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",
@ -127,18 +129,33 @@
"DS4.SpellCategoryElectricity": "Elektrizität",
"DS4.SpellCategoryNone": "Keine",
"DS4.SpellCategoryUnset": "Nicht gesetzt",
"DS4.SpellBonus": "Zauberbonus",
"DS4.SpellBonusAbbr": "ZB",
"DS4.SortBySpellBonus": "Nach Zauberbonus sortieren",
"DS4.SpellMaxDistance": "Reichweite",
"DS4.SpellEffectRadius": "Effektradius",
"DS4.SpellDuration": "Wirkdauer",
"DS4.SpellCooldownDuration": "Abklingzeit",
"DS4.SpellModifier": "Zauberbonus",
"DS4.SpellModifierAbbr": "ZB",
"DS4.SpellModifierDescription": "Der Zauberbonus auf die Probe.",
"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.",
"DS4.SpellEffectRadius": "Wirkungsradius",
"DS4.SpellEffectRadiusDescription": "Der Wirkungsradius des Zaubers.",
"DS4.SpellDuration": "Dauer",
"DS4.SpellDurationDescription": "Die Wirkungszeit des Zaubers.",
"DS4.CooldownDuration": "Abklingzeit",
"DS4.CooldownDurationDescription": "Die Dauer, die der Zauber nach erfolgreichem Wirken nicht einsetzbar ist.",
"DS4.CooldownDuration0R": "0 Kampfrunden",
"DS4.CooldownDuration1R": "1 Kampfrunde",
"DS4.CooldownDuration2R": "2 Kampfrunden",
"DS4.CooldownDuration5R": "5 Kampfrunden",
"DS4.CooldownDuration10R": "10 Kampfrunden",
"DS4.CooldownDuration100R": "100 Kampfrunden",
"DS4.CooldownDuration1D": "1 Tag",
"DS4.CooldownDurationD20D": "W20 Tage",
"DS4.SpellMinimumLevel": "Zugangsstufe",
"DS4.SpellMinimumLevelDescription": "Die minimale Stufe, ab der ein Zauberwirker den Zauberspruch erlernen kann.",
"DS4.SpellCasterClassHealer": "Heiler",
"DS4.SpellCasterClassSorcerer": "Schwarzmagier",
"DS4.SpellCasterClassWizard": "Zauberer",
"DS4.SpellPrice": "Preis (Gold)",
"DS4.SpellPriceDescription": "Der Kaufpreis des Zauberspruchs.",
"DS4.EffectEnabled": "Aktiv",
"DS4.EffectEnabledAbbr": "A",
"DS4.EffectEffectivelyEnabled": "Effektiv Aktiv (unter Betrachtung, ob ein eventuelles Quellen-Item ausgerüstet ist usw.)",
@ -240,6 +257,7 @@
"DS4.ErrorSlayingDiceRecursionLimitExceeded": "Die maximale Rekursionstiefe für slayende Würfelwürfe wurde überschritten.",
"DS4.ErrorInvalidNumberOfDice": "Ungültige Anzahl an Würfeln.",
"DS4.ErrorDuringMigration": "Fehler während der Aktualisierung des DS4 Systems von Migrationsversion {currentVersion} auf {targetVersion}. Der Fehler trat während der Ausführung des Migrationsskripts mit der Version {migrationVersion} auf. Spätere Migrationsskripte wurden nicht ausgeführt. Mehr Details finden Sie in der Entwicklerkonsole (F12).",
"DS4.ErrorDuringCompendiumMigration": "Fehler während der Aktualisierung Kompendiums '{pack}' für DS4 von Migrationsversion {currentVersion} auf {targetVersion}. Der Fehler trat während der Ausführung des Migrationsskripts mit der Version {migrationVersion} auf. Spätere Migrationsskripte wurden nicht ausgeführt. Mehr Details finden Sie in der Entwicklerkonsole (F12).",
"DS4.ErrorCannotRollUnownedItem": "Für das Item '{name}' ({id}) kann nicht gewürfelt werden, da es keinem Aktor gehört.",
"DS4.ErrorRollingForItemTypeNotPossible": "Würfeln ist für Items vom Typ '{type}' nicht möglich.",
"DS4.ErrorWrongItemType": "Ein Item vom Type '{expectedType}' wurde erwartet aber das Item '{name}' ({id}) ist vom Typ '{actualType}'.",
@ -255,9 +273,11 @@
"DS4.WarningItemIsNotRollable": "Für das Item '{name}' ({id}) vom Typ '{type}' kann nicht gewürfelt werden.",
"DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems": "Makros können nur für besessene Items angelegt werden.",
"DS4.WarningInvalidCheckDropped": "Eine ungültige Probe wurde auf die Hotbar gezogen.",
"DS4.InfoManuallyEnterSpellBonus": "Der korrekte Wert für den Zauberbonus '{spellBonus}' des Zaubers '{name}' muss manuell angegeben werden.",
"DS4.InfoManuallyEnterSpellModifier": "Der korrekte Wert für den Zauberbonus '{spellModifier}' des Zaubers '{name}' muss manuell angegeben werden.",
"DS4.InfoSystemUpdateStart": "Aktualisiere DS4 System von Migrationsversion {currentVersion} auf {targetVersion}. Bitte haben Sie etwas Geduld, schließen Sie nicht das Spiel und fahren Sie nicht den Server herunter.",
"DS4.InfoSystemUpdateCompleted": "Aktualisierung des DS4 Systems von Migrationsversion {currentVersion} auf {targetVersion} erfolgreich!",
"DS4.InfoCompendiumMigrationStart": "Aktualisiere Kompendium '{pack}' für DS4 von Migrationsversion {currentVersion} auf {targetVersion}. Bitte haben Sie etwas Geduld, schließen Sie nicht das Spiel und fahren Sie nicht den Server herunter.",
"DS4.InfoCompendiumMigrationCompleted": "Aktualisierung des Kompendiums '{pack}' für DS4 von Migrationsversion {currentVersion} auf {targetVersion} erfolgreich!",
"DS4.UnitRounds": "Runden",
"DS4.UnitRoundsAbbr": "Rnd",
"DS4.UnitMinutes": "Minuten",

View file

@ -114,10 +114,12 @@
"DS4.ArmorMaterialTypeNaturalAbbr": "Natural",
"DS4.SpellType": "Spell Type",
"DS4.SpellTypeAbbr": "T",
"DS4.SpellTypeDescription": "The type of the spell.",
"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",
@ -127,18 +129,33 @@
"DS4.SpellCategoryElectricity": "Electricity",
"DS4.SpellCategoryNone": "None",
"DS4.SpellCategoryUnset": "Unset",
"DS4.SpellBonus": "Spell Bonus",
"DS4.SpellBonusAbbr": "SB",
"DS4.SortBySpellBonus": "Sort by Spell Bonus",
"DS4.SpellMaxDistance": "Range",
"DS4.SpellEffectRadius": "Radius",
"DS4.SpellModifier": "Spell Modifier",
"DS4.SpellModifierAbbr": "SM",
"DS4.SpellModifierDescription": "The spell modifier for the corresponding check.",
"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.",
"DS4.SpellEffectRadius": "Area of Effect Radius",
"DS4.SpellEffectRadiusDescription": "The radius of the area of effect of the spell.",
"DS4.SpellDuration": "Duration",
"DS4.SpellCooldownDuration": "Cooldown",
"DS4.SpellDurationDescription": "The spells duration.",
"DS4.CooldownDuration": "Cooldown Period",
"DS4.CooldownDurationDescription": "The length of time to wait after a successful casting before the spell can be cast again.",
"DS4.CooldownDuration0R": "0 Rounds",
"DS4.CooldownDuration1R": "1 Round",
"DS4.CooldownDuration2R": "2 Rounds",
"DS4.CooldownDuration5R": "5 Rounds",
"DS4.CooldownDuration10R": "10 Rounds",
"DS4.CooldownDuration100R": "100 Rounds",
"DS4.CooldownDuration1D": "1 Day",
"DS4.CooldownDurationD20D": "D20 Days",
"DS4.SpellMinimumLevel": "Minimum Level",
"DS4.SpellMinimumLevelDescription": "The minimum level at which a spell caster may learn the spell.",
"DS4.SpellCasterClassHealer": "Healer",
"DS4.SpellCasterClassSorcerer": "Sorcerer",
"DS4.SpellCasterClassWizard": "Wizard",
"DS4.SpellPrice": "Price (Gold)",
"DS4.SpellPriceDescription": "The price to purchase the spell.",
"DS4.EffectEnabled": "Enabled",
"DS4.EffectEnabledAbbr": "E",
"DS4.EffectEffectivelyEnabled": "Effectively Enabled (taking into account whether a potential source item is equipped etc.)",
@ -240,6 +257,7 @@
"DS4.ErrorSlayingDiceRecursionLimitExceeded": "Maximum recursion depth for slaying dice roll exceeded.",
"DS4.ErrorInvalidNumberOfDice": "Invalid number of dice.",
"DS4.ErrorDuringMigration": "Error while migrating DS4 system from migration version {currentVersion} to {targetVersion}. The error occurred during execution of migration script with version {migrationVersion}. Later migrations have not been executed. For more details, please look at the development console (F12).",
"DS4.ErrorDuringCompendiumMigration": "Error while migrating compendium '{pack}' for DS4 from migration version {currentVersion} to {targetVersion}. The error occurred during execution of migration script with version {migrationVersion}. Later migrations have not been executed. For more details, please look at the development console (F12).",
"DS4.ErrorCannotRollUnownedItem": "Rolling for item '{name}' ({id})is not possible because it is not owned.",
"DS4.ErrorRollingForItemTypeNotPossible": "Rolling is not possible for items of type '{type}'.",
"DS4.ErrorWrongItemType": "Expected an item of type '{expectedType}' but item '{name}' ({id}) is of type '{actualType}'.",
@ -255,9 +273,11 @@
"DS4.WarningItemIsNotRollable": "Item '{name}' ({id}) of type '{type}' is not rollable.",
"DS4.WarningMacrosCanOnlyBeCreatedForOwnedItems": "Macros can only be created for owned items.",
"DS4.WarningInvalidCheckDropped": "An invalid check was dropped on the Hotbar.",
"DS4.InfoManuallyEnterSpellBonus": "The correct value of the spell bonus '{spellBonus}' of the spell '{name}' needs to be entered by manually.",
"DS4.InfoManuallyEnterSpellModifier": "The correct value of the spell modifier '{spellModifier}' of the spell '{name}' needs to be entered by manually.",
"DS4.InfoSystemUpdateStart": "Migrating DS4 system from migration version {currentVersion} to {targetVersion}. Please be patient and do not close your game or shut down your server.",
"DS4.InfoSystemUpdateCompleted": "Migration of DS4 system from migration version {currentVersion} to {targetVersion} successful!",
"DS4.InfoCompendiumMigrationStart": "Migrating compendium '{pack}' for DS4 from migration version {currentVersion} to {targetVersion}. Please be patient and do not close your game or shut down your server.",
"DS4.InfoCompendiumMigrationCompleted": "Migration of compendium '{pack}' for DS4 from migration version {currentVersion} to {targetVersion} successful!",
"DS4.UnitRounds": "Rounds",
"DS4.UnitRoundsAbbr": "rnd",
"DS4.UnitMinutes": "Minutes",

View file

@ -1881,10 +1881,7 @@
"value": "",
"unit": "custom"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": null,
"wizard": null,
@ -3584,10 +3581,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": 2,
"wizard": 5,
@ -3629,10 +3623,7 @@
"value": "VE / 2",
"unit": "rounds"
},
"cooldownDuration": {
"value": "24",
"unit": "hours"
},
"cooldownDuration": "1d",
"minimumLevels": {
"healer": null,
"wizard": 12,
@ -3674,10 +3665,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "5",
"unit": "rounds"
},
"cooldownDuration": "5r",
"minimumLevels": {
"healer": 16,
"wizard": 10,
@ -3719,10 +3707,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": 16,
"wizard": 12,
@ -3764,10 +3749,7 @@
"value": "Prb.",
"unit": "rounds"
},
"cooldownDuration": {
"value": "100",
"unit": "rounds"
},
"cooldownDuration": "100r",
"minimumLevels": {
"healer": 4,
"wizard": 8,
@ -3809,10 +3791,7 @@
"value": "Prb.",
"unit": "rounds"
},
"cooldownDuration": {
"value": "100",
"unit": "rounds"
},
"cooldownDuration": "100r",
"minimumLevels": {
"healer": 4,
"wizard": 8,
@ -3854,10 +3833,7 @@
"value": "Konzentration",
"unit": "custom"
},
"cooldownDuration": {
"value": "0",
"unit": "rounds"
},
"cooldownDuration": "0r",
"minimumLevels": {
"healer": null,
"wizard": 6,
@ -3899,10 +3875,7 @@
"value": "Prb.",
"unit": "minutes"
},
"cooldownDuration": {
"value": "24",
"unit": "hours"
},
"cooldownDuration": "1d",
"minimumLevels": {
"healer": 20,
"wizard": 12,
@ -3944,10 +3917,7 @@
"value": "Prb.",
"unit": "rounds"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": 8,
"wizard": 5,
@ -3990,10 +3960,7 @@
"value": "Prb.",
"unit": "rounds"
},
"cooldownDuration": {
"value": "5",
"unit": "rounds"
},
"cooldownDuration": "5r",
"minimumLevels": {
"healer": 1,
"wizard": 5,
@ -16049,10 +16016,7 @@
"value": "Prb.",
"unit": "rounds"
},
"cooldownDuration": {
"value": "100",
"unit": "rounds"
},
"cooldownDuration": "100r",
"minimumLevels": {
"healer": 7,
"wizard": 7,
@ -20114,10 +20078,7 @@
"value": "VE",
"unit": "minutes"
},
"cooldownDuration": {
"value": "24",
"unit": "hours"
},
"cooldownDuration": "1d",
"minimumLevels": {
"healer": 5,
"wizard": 9,
@ -21132,7 +21093,7 @@
"name": "Gedankenzehrerstrahl",
"type": "spell",
"data": {
"description": "<p>Gedankenzehrerstrahl (nicht sichtbar; verursacht mental Schaden und f&uuml;hrt zu <strong>Werteverlust</strong>)</p>",
"description": "<p>Nicht sichtbar; verursacht mental Schaden und f&uuml;hrt zu <strong>Werteverlust</strong></p>",
"equipped": true,
"spellType": "targetedSpellcasting",
"bonus": "",
@ -21149,10 +21110,7 @@
"value": "",
"unit": "custom"
},
"cooldownDuration": {
"value": "",
"unit": "rounds"
},
"cooldownDuration": "0r",
"minimumLevels": {
"healer": null,
"wizard": null,
@ -25446,10 +25404,7 @@
"value": "VE x 2",
"unit": "rounds"
},
"cooldownDuration": {
"value": "24",
"unit": "hours"
},
"cooldownDuration": "1d",
"minimumLevels": {
"healer": null,
"wizard": 10,
@ -25491,10 +25446,7 @@
"value": "VE",
"unit": "minutes"
},
"cooldownDuration": {
"value": "W20",
"unit": "days"
},
"cooldownDuration": "d20d",
"minimumLevels": {
"healer": null,
"wizard": 18,
@ -25536,10 +25488,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": 2,
"wizard": 5,
@ -25581,10 +25530,7 @@
"value": "VE",
"unit": "rounds"
},
"cooldownDuration": {
"value": "24",
"unit": "hours"
},
"cooldownDuration": "1d",
"minimumLevels": {
"healer": null,
"wizard": 15,
@ -25626,10 +25572,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": null,
"wizard": 12,
@ -25671,10 +25614,7 @@
"value": "Prb. x 5",
"unit": "rounds"
},
"cooldownDuration": {
"value": "24",
"unit": "hours"
},
"cooldownDuration": "1d",
"minimumLevels": {
"healer": null,
"wizard": 15,
@ -25716,10 +25656,7 @@
"value": "VE / 2",
"unit": "rounds"
},
"cooldownDuration": {
"value": "24",
"unit": "hours"
},
"cooldownDuration": "1d",
"minimumLevels": {
"healer": null,
"wizard": 12,
@ -25761,10 +25698,7 @@
"value": "Bis erlöst",
"unit": "custom"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": null,
"wizard": 8,
@ -25806,10 +25740,7 @@
"value": "Bis Schloss geöffnet",
"unit": "custom"
},
"cooldownDuration": {
"value": "5",
"unit": "rounds"
},
"cooldownDuration": "5r",
"minimumLevels": {
"healer": 3,
"wizard": 1,
@ -25851,10 +25782,7 @@
"value": "Prb. / 2",
"unit": "rounds"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": 4,
"wizard": 9,
@ -25896,10 +25824,7 @@
"value": "Prb. / 2",
"unit": "rounds"
},
"cooldownDuration": {
"value": "5",
"unit": "rounds"
},
"cooldownDuration": "5r",
"minimumLevels": {
"healer": null,
"wizard": 6,
@ -25941,10 +25866,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "24",
"unit": "hours"
},
"cooldownDuration": "1d",
"minimumLevels": {
"healer": null,
"wizard": null,
@ -25986,10 +25908,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "0",
"unit": "rounds"
},
"cooldownDuration": "0r",
"minimumLevels": {
"healer": null,
"wizard": 15,
@ -26031,10 +25950,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": 5,
"wizard": 2,
@ -26076,10 +25992,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "100",
"unit": "rounds"
},
"cooldownDuration": "100r",
"minimumLevels": {
"healer": null,
"wizard": 4,
@ -26121,10 +26034,7 @@
"value": "VE / 2",
"unit": "hours"
},
"cooldownDuration": {
"value": "100",
"unit": "rounds"
},
"cooldownDuration": "100r",
"minimumLevels": {
"healer": null,
"wizard": 5,
@ -26166,10 +26076,7 @@
"value": "Prb.",
"unit": "minutes"
},
"cooldownDuration": {
"value": "24",
"unit": "hours"
},
"cooldownDuration": "1d",
"minimumLevels": {
"healer": 20,
"wizard": 12,
@ -26211,10 +26118,7 @@
"value": "Prb.",
"unit": "rounds"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": 8,
"wizard": 5,
@ -26256,10 +26160,7 @@
"value": "Prb. / 2",
"unit": "rounds"
},
"cooldownDuration": {
"value": "100",
"unit": "rounds"
},
"cooldownDuration": "100r",
"minimumLevels": {
"healer": null,
"wizard": 6,
@ -26301,10 +26202,7 @@
"value": "Prb. x 2",
"unit": "rounds"
},
"cooldownDuration": {
"value": "100",
"unit": "rounds"
},
"cooldownDuration": "100r",
"minimumLevels": {
"healer": null,
"wizard": null,
@ -26346,10 +26244,7 @@
"value": "Prb.",
"unit": "rounds"
},
"cooldownDuration": {
"value": "W20",
"unit": "days"
},
"cooldownDuration": "d20d",
"minimumLevels": {
"healer": null,
"wizard": 15,
@ -28333,10 +28228,7 @@
"value": "VE / 2",
"unit": "rounds"
},
"cooldownDuration": {
"value": "10",
"unit": "rounds"
},
"cooldownDuration": "10r",
"minimumLevels": {
"healer": null,
"wizard": null,
@ -28882,10 +28774,7 @@
"value": "Sofort",
"unit": "custom"
},
"cooldownDuration": {
"value": "1",
"unit": "rounds"
},
"cooldownDuration": "1r",
"minimumLevels": {
"healer": 10,
"wizard": 7,

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
import { DS4SpellDataSourceData, TemporalUnit, UnitData } from "../../../src/item/item-data-source";
import { CooldownDuration, DS4SpellDataSourceData } from "../../../src/item/item-data-source";
import { calculateSpellPrice } from "../../../src/item/type-specific-helpers/spell";
const defaultData: DS4SpellDataSourceData = {
@ -23,10 +23,7 @@ const defaultData: DS4SpellDataSourceData = {
value: "",
unit: "custom",
},
cooldownDuration: {
value: "",
unit: "rounds",
},
cooldownDuration: "0r",
minimumLevels: {
healer: null,
wizard: null,
@ -167,65 +164,41 @@ function buildCombinedTestCases(): CombinedTestCase[] {
}
describe("calculateSpellPrice", () => {
const cooldownDurations: (UnitData<TemporalUnit> & { factor: number })[] = [
{ value: "", unit: "rounds", factor: 1 },
{ value: "foo", unit: "rounds", factor: 1 },
{ value: "0", unit: "rounds", factor: 1 },
{ value: "1", unit: "rounds", factor: 1 },
{ value: "17279", unit: "rounds", factor: 1 },
{ value: "17280", unit: "rounds", factor: 2 },
{ value: "34559", unit: "rounds", factor: 2 },
{ value: "34560", unit: "rounds", factor: 3 },
{ value: "", unit: "minutes", factor: 1 },
{ value: "foo", unit: "minutes", factor: 1 },
{ value: "0", unit: "minutes", factor: 1 },
{ value: "1", unit: "minutes", factor: 1 },
{ value: "1439", unit: "minutes", factor: 1 },
{ value: "1440", unit: "minutes", factor: 2 },
{ value: "2879", unit: "minutes", factor: 2 },
{ value: "2880", unit: "minutes", factor: 3 },
{ value: "", unit: "hours", factor: 1 },
{ value: "foo", unit: "hours", factor: 1 },
{ value: "0", unit: "hours", factor: 1 },
{ value: "1", unit: "hours", factor: 1 },
{ value: "23", unit: "hours", factor: 1 },
{ value: "24", unit: "hours", factor: 2 },
{ value: "47", unit: "hours", factor: 2 },
{ value: "48", unit: "hours", factor: 3 },
{ value: "", unit: "days", factor: 3 },
{ value: "foo", unit: "days", factor: 3 },
{ value: "0", unit: "days", factor: 1 },
{ value: "1", unit: "days", factor: 2 },
{ value: "2", unit: "days", factor: 3 },
const cooldownDurations: { cooldownDuration: CooldownDuration; factor: number }[] = [
{ cooldownDuration: "0r", factor: 1 },
{ cooldownDuration: "1r", factor: 1 },
{ cooldownDuration: "2r", factor: 1 },
{ cooldownDuration: "5r", factor: 1 },
{ cooldownDuration: "10r", factor: 1 },
{ cooldownDuration: "100r", factor: 1 },
{ cooldownDuration: "1d", factor: 2 },
{ cooldownDuration: "d20d", factor: 3 },
];
describe.each(cooldownDurations)("with cooldown duration set to $value $unit", ({ value, unit, factor }) => {
const dataWithCooldownDuration = {
...defaultData,
cooldownDuration: {
value,
unit,
},
};
describe.each(cooldownDurations)(
"with cooldown duration set to $cooldownDuration",
({ cooldownDuration, factor }) => {
const dataWithCooldownDuration = {
...defaultData,
cooldownDuration,
};
it.each(buildCombinedTestCases())(
`returns ${factor} × $expected if the minimum leves are $minimumLevels`,
({ minimumLevels, expected }) => {
// given
const data: DS4SpellDataSourceData = {
...dataWithCooldownDuration,
minimumLevels,
};
it.each(buildCombinedTestCases())(
`returns ${factor} × $expected if the minimum leves are $minimumLevels`,
({ minimumLevels, expected }) => {
// given
const data: DS4SpellDataSourceData = {
...dataWithCooldownDuration,
minimumLevels,
};
// when
const spellPrice = calculateSpellPrice(data);
// when
const spellPrice = calculateSpellPrice(data);
// then
expect(spellPrice).toBe(expected !== null ? expected * factor : expected);
},
);
});
// then
expect(spellPrice).toBe(expected !== null ? expected * factor : expected);
},
);
},
);
});

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
export const secondsPerRound = 5;
export const secondsPerMinute = 60;
export const minutesPerHour = 60;
export const hoursPerDay = 24;

View file

@ -106,6 +106,17 @@ const i18nKeys = {
unset: "DS4.SpellCategoryUnset",
},
cooldownDurations: {
"0r": "DS4.CooldownDuration0R",
"1r": "DS4.CooldownDuration1R",
"2r": "DS4.CooldownDuration2R",
"5r": "DS4.CooldownDuration5R",
"10r": "DS4.CooldownDuration10R",
"100r": "DS4.CooldownDuration100R",
"1d": "DS4.CooldownDuration1D",
d20d: "DS4.CooldownDurationD20D",
},
/**
* Define the set of actor types
*/
@ -276,16 +287,6 @@ const i18nKeys = {
minutes: "DS4.UnitMinutes",
hours: "DS4.UnitHours",
days: "DS4.UnitDays",
},
/**
* Define translations for available duration units including "custom"
*/
customTemporalUnits: {
rounds: "DS4.UnitRounds",
minutes: "DS4.UnitMinutes",
hours: "DS4.UnitHours",
days: "DS4.UnitDays",
custom: "DS4.UnitCustom",
},
@ -297,16 +298,6 @@ const i18nKeys = {
minutes: "DS4.UnitMinutesAbbr",
hours: "DS4.UnitHoursAbbr",
days: "DS4.UnitDaysAbbr",
},
/**
* Define abbreviations for available duration units including "custom"
*/
customTemporalUnitsAbbr: {
rounds: "DS4.UnitRoundsAbbr",
minutes: "DS4.UnitMinutesAbbr",
hours: "DS4.UnitHoursAbbr",
days: "DS4.UnitDaysAbbr",
custom: "DS4.UnitCustomAbbr",
},

View file

@ -18,7 +18,14 @@ export default function registerForSetupHooks(): void {
* Localizes all objects in {@link DS4.i18n} and sorts them unless they are explicitly excluded.
*/
function localizeAndSortConfigObjects() {
const noSort = ["attributes", "traits", "combatValues", "creatureSizeCategories"];
const noSort = [
"attributes",
"combatValues",
"cooldownDurations",
"creatureSizeCategories",
"spellCategories",
"traits",
];
const localizeObject = <T extends { [s: string]: string }>(obj: T, sort = true): T => {
const localized = Object.entries(obj).map(([key, value]): [string, string] => {

View file

@ -136,14 +136,16 @@ export interface DS4ShieldDataSourceData
DS4ItemDataSourceDataEquipable,
DS4ItemDataSourceDataProtective {}
export type CooldownDuration = keyof typeof DS4.i18n.cooldownDurations;
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>;
duration: UnitData<TemporalUnit>;
cooldownDuration: CooldownDuration;
minimumLevels: {
healer: number | null;
wizard: number | null;
@ -158,9 +160,7 @@ export interface UnitData<UnitType> {
type DistanceUnit = keyof typeof DS4.i18n.distanceUnits;
type CustomTemporalUnit = keyof typeof DS4.i18n.customTemporalUnits;
export type TemporalUnit = keyof typeof DS4.i18n.temporalUnits;
type TemporalUnit = keyof typeof DS4.i18n.temporalUnits;
export interface DS4EquipmentDataSourceData
extends DS4ItemDataSourceDataBase,

View file

@ -148,17 +148,17 @@ export class DS4Item extends Item {
}
const ownerDataData = this.actor.data.data;
const spellBonus = Number.isNumeric(this.data.data.bonus) ? parseInt(this.data.data.bonus) : undefined;
if (spellBonus === undefined) {
const spellModifier = Number.isNumeric(this.data.data.bonus) ? parseInt(this.data.data.bonus) : undefined;
if (spellModifier === undefined) {
notifications.info(
getGame().i18n.format("DS4.InfoManuallyEnterSpellBonus", {
getGame().i18n.format("DS4.InfoManuallyEnterSpellModifier", {
name: this.name,
spellBonus: this.data.data.bonus,
spellModifier: this.data.data.bonus,
}),
);
}
const spellType = this.data.data.spellType;
const checkTargetNumber = ownerDataData.combatValues[spellType].total + (spellBonus ?? 0);
const checkTargetNumber = ownerDataData.combatValues[spellType].total + (spellModifier ?? 0);
const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
await createCheckRoll(checkTargetNumber, {

View file

@ -2,8 +2,7 @@
//
// SPDX-License-Identifier: MIT
import { hoursPerDay, minutesPerHour, secondsPerMinute, secondsPerRound } from "../../common/time-helpers";
import { DS4SpellDataSourceData, TemporalUnit, UnitData } from "../item-data-source";
import { CooldownDuration, DS4SpellDataSourceData } from "../item-data-source";
export function calculateSpellPrice(data: DS4SpellDataSourceData): number | null {
const spellPriceFactor = calculateSpellPriceFactor(data.cooldownDuration);
@ -16,39 +15,18 @@ export function calculateSpellPrice(data: DS4SpellDataSourceData): number | null
return baseSpellPrice === Infinity ? null : baseSpellPrice * spellPriceFactor;
}
function calculateSpellPriceFactor(temporalData: UnitData<TemporalUnit>): number {
let days: number;
if (Number.isNumeric(temporalData.value)) {
const value = Number.fromString(temporalData.value);
switch (temporalData.unit) {
case "days": {
days = value;
break;
}
case "hours": {
days = value / hoursPerDay;
break;
}
case "minutes": {
days = value / (hoursPerDay * minutesPerHour);
break;
}
case "rounds": {
days = (value * secondsPerRound) / (hoursPerDay * minutesPerHour * secondsPerMinute);
break;
}
}
} else {
switch (temporalData.unit) {
case "days": {
days = 2;
break;
}
default: {
days = 0;
break;
}
}
function calculateSpellPriceFactor(cooldownDuration: CooldownDuration): number {
switch (cooldownDuration) {
case "0r":
case "1r":
case "2r":
case "5r":
case "10r":
case "100r":
return 1;
case "1d":
return 2;
case "d20d":
return 3;
}
return Math.clamped(Math.floor(days), 0, 2) + 1;
}

View file

@ -4,10 +4,11 @@
import { getGame } from "./helpers";
import logger from "./logger";
import { migrate as migrate001 } from "./migrations/001";
import { migrate as migrate002 } from "./migrations/002";
import { migrate as migrate003 } from "./migrations/003";
import { migrate as migrate004 } from "./migrations/004";
import { migration as migration001 } from "./migrations/001";
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 notifications from "./ui/notifications";
async function migrate(): Promise<void> {
@ -43,11 +44,11 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
{ permanent: true },
);
for (const [i, migration] of migrationsToExecute.entries()) {
for (const [i, { migrate }] of migrationsToExecute.entries()) {
const currentMigrationVersion = oldMigrationVersion + i + 1;
logger.info("executing migration script ", currentMigrationVersion);
try {
await migration();
await migrate();
getGame().settings.set("ds4", "systemMigrationVersion", currentMigrationVersion);
} catch (err) {
notifications.error(
@ -73,18 +74,76 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
}
}
async function migrateCompendiumFromTo(
pack: CompendiumCollection<CompendiumCollection.Metadata>,
oldMigrationVersion: number,
targetMigrationVersion: number,
): Promise<void> {
if (!getGame().user?.isGM) {
return;
}
const migrationsToExecute = migrations.slice(oldMigrationVersion, targetMigrationVersion);
if (migrationsToExecute.length > 0) {
notifications.info(
getGame().i18n.format("DS4.InfoCompendiumMigrationStart", {
pack: pack.title,
currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion,
}),
{ permanent: true },
);
for (const [i, { migrateCompendium }] of migrationsToExecute.entries()) {
const currentMigrationVersion = oldMigrationVersion + i + 1;
logger.info("executing compendium migration ", currentMigrationVersion);
try {
await migrateCompendium(pack);
} catch (err) {
notifications.error(
getGame().i18n.format("DS4.ErrorDuringCompendiumMigration", {
pack: pack.title,
currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion,
migrationVersion: currentMigrationVersion,
}),
{ permanent: true },
);
logger.error("Failed ds4 compendium migration:", err);
return;
}
}
notifications.info(
getGame().i18n.format("DS4.InfoCompendiumMigrationCompleted", {
pack: pack.title,
currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion,
}),
{ permanent: true },
);
}
}
function getTargetMigrationVersion(): number {
return migrations.length;
}
const migrations: Array<() => Promise<void>> = [migrate001, migrate002, migrate003, migrate004];
interface Migration {
migrate: () => Promise<void>;
migrateCompendium: (pack: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>;
}
const migrations: Migration[] = [migration001, migration002, migration003, migration004, migration005];
function isFirstWorldStart(migrationVersion: number): boolean {
return migrationVersion < 0;
}
export const migration = {
migrate: migrate,
migrateFromTo: migrateFromTo,
getTargetMigrationVersion: getTargetMigrationVersion,
migrate,
migrateFromTo,
getTargetMigrationVersion,
migrateCompendiumFromTo,
};

View file

@ -10,7 +10,7 @@ import {
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> {
async function migrate(): Promise<void> {
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
await migrateCompendiums(migrateCompendium);
@ -39,3 +39,8 @@ function getActorUpdateData(): Record<string, unknown> {
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getActorUpdateData, getSceneUpdateData });
export const migration = {
migrate,
migrateCompendium,
};

View file

@ -12,7 +12,7 @@ import {
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> {
async function migrate(): Promise<void> {
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
@ -32,3 +32,8 @@ const migrateCompendium = getCompendiumMigrator(
{ getItemUpdateData, getActorUpdateData, getSceneUpdateData },
{ migrateToTemplateEarly: false },
);
export const migration = {
migrate,
migrateCompendium,
};

View file

@ -12,7 +12,7 @@ import {
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> {
async function migrate(): Promise<void> {
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
@ -34,3 +34,8 @@ const migrateCompendium = getCompendiumMigrator(
{ getItemUpdateData, getActorUpdateData },
{ migrateToTemplateEarly: false },
);
export const migration = {
migrate,
migrateCompendium,
};

View file

@ -12,7 +12,7 @@ import {
migrateScenes,
} from "./migrationHelpers";
export async function migrate(): Promise<void> {
async function migrate(): Promise<void> {
await migrateItems(getItemUpdateData);
await migrateActors(getActorUpdateData);
await migrateScenes(getSceneUpdateData);
@ -21,6 +21,7 @@ export async function migrate(): Promise<void> {
function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>) {
if (itemData.type !== "spell") return;
// @ts-expect-error the type of cooldownDuration was UnitData<TemporalUnit> at the point for this migration, but it changed later on
const cooldownDurationUnit: string | undefined = itemData.data?.cooldownDuration.unit;
const updateData: Record<string, unknown> = {
@ -38,3 +39,8 @@ function getItemUpdateData(itemData: Partial<foundry.data.ItemData["_source"]>)
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
export const migration = {
migrate,
migrateCompendium,
};

119
src/migrations/005.ts Normal file
View file

@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { CooldownDuration } from "../item/item-data-source";
import {
getActorUpdateDataGetter,
getCompendiumMigrator,
getSceneUpdateDataGetter,
migrateActors,
migrateCompendiums,
migrateItems,
migrateScenes,
} from "./migrationHelpers";
const secondsPerRound = 5;
const secondsPerMinute = 60;
const roundsPerMinute = secondsPerMinute / secondsPerRound;
const minutesPerHour = 60;
const roundsPerHour = minutesPerHour / roundsPerMinute;
const hoursPerDay = 24;
const roundsPerDay = hoursPerDay / roundsPerHour;
const secondsPerDay = secondsPerMinute * minutesPerHour * hoursPerDay;
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 the type of cooldownDuration is changed from UnitData<TemporalUnit> to CooldownDuation with this migration
const cooldownDurationUnit: string | undefined = itemData.data?.cooldownDuration.unit;
// @ts-expect-error the type of cooldownDuration is changed from UnitData<TemporalUnit> to CooldownDuation with this migration
const cooldownDurationValue: string | undefined = itemData.data?.cooldownDuration.value;
const cooldownDuration = migrateCooldownDuration(cooldownDurationValue, cooldownDurationUnit);
const updateData: Record<string, unknown> = {
data: {
cooldownDuration,
},
};
return updateData;
}
function migrateCooldownDuration(cooldownDurationValue?: string, cooldownDurationUnit?: string) {
if (Number.isNumeric(cooldownDurationValue)) {
const value = Number.fromString(cooldownDurationValue!);
const rounds = getRounds(cooldownDurationUnit ?? "", value);
if (rounds * secondsPerRound > secondsPerDay) {
return "d20d";
} else if (rounds > 100) {
return "1d";
} else if (rounds > 10) {
return "100r";
} else if (rounds > 5) {
return "10r";
} else if (rounds > 2) {
return "5r";
} else if (rounds > 1) {
return "2r";
} else if (rounds > 0) {
return "1r";
} else {
return "0r";
}
} else {
// if the value is not numeric, we can only make a best guess
switch (cooldownDurationUnit) {
case "rounds": {
return "10r";
}
case "minutes": {
return "100r";
}
case "hours": {
return "1d";
}
case "days": {
return "d20d";
}
default: {
return "0r";
}
}
}
}
function getRounds(unit: string, value: number): number {
switch (unit) {
case "rounds": {
return value;
}
case "minutes": {
return value * roundsPerMinute;
}
case "hours": {
return value * roundsPerHour;
}
case "days": {
return value * roundsPerDay;
}
default: {
return 0;
}
}
}
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
export const migration = {
migrate,
migrateCompendium,
};

View file

@ -187,10 +187,7 @@
"value": "",
"unit": "custom"
},
"cooldownDuration": {
"value": "",
"unit": "rounds"
},
"cooldownDuration": "0r",
"minimumLevels": {
"healer": null,
"wizard": null,

View file

@ -25,7 +25,7 @@ SPDX-License-Identifier: MIT
{{/inline}}
{{!--
!-- Three templates based on the "unit" template for displaying values with unit.
!-- Two templates based on the "unit" template for displaying values with unit.
!-- Both accept a `config` object holding the unitNames and unitAbbr instead of
!-- directly handing over the latter two.
!-- @param titleKey: The key of the localized title to use.
@ -35,11 +35,6 @@ SPDX-License-Identifier: MIT
titleKey=titleKey}}
{{/inline}}
{{#*inline "customTemporalUnit"}}
{{> unit unitNames=config.i18n.customTemporalUnits unitAbbrs=config.i18n.customTemporalUnitsAbbr unitDatum=unitDatum
titleKey=titleKey}}
{{/inline}}
{{#*inline "distanceUnit"}}
{{> unit unitNames=config.i18n.distanceUnits unitAbbrs=config.i18n.distanceUnitsAbbr unitDatum=unitDatum
titleKey=titleKey}}
@ -60,16 +55,16 @@ titleKey=titleKey}}
{{!-- spell bonus --}}
<div class="ds4-embedded-document-list__clickable sort-items" data-data-path="data.bonus"
title="{{localize 'DS4.SortBySpellBonus'}}">{{localize 'DS4.SpellBonusAbbr'}}</div>
title="{{localize 'DS4.SortBySpellModifier'}}">{{localize 'DS4.SpellModifierAbbr'}}</div>
{{!-- max. distance --}}
<div title="{{localize 'DS4.SpellMaxDistance'}}"><i class="fas fa-ruler"></i></div>
<div title="{{localize 'DS4.SpellDistance'}}"><i class="fas fa-ruler"></i></div>
{{!-- duration --}}
<div title="{{localize 'DS4.SpellDuration'}}"><i class="far fa-clock"></i></div>
{{!-- cooldown duration --}}
<div title="{{localize 'DS4.SpellCooldownDuration'}}"><i class="fas fa-hourglass-half"></i></div>
<div title="{{localize 'DS4.CooldownDuration'}}"><i class="fas fa-hourglass-half"></i></div>
{{/systems/ds4/templates/sheets/actor/components/item-list-header.hbs}}
{{#each itemsByType.spell as |itemData id|}}
@ -77,23 +72,24 @@ titleKey=titleKey}}
hideDescription=true}}
{{!-- spell type --}}
<img class="ds4-embedded-document-list__image"
src="{{lookup ../../config.icons.spellTypes itemData.data.spellType}}"
title="{{lookup ../../config.i18n.spellTypes itemData.data.spellType}}" />
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.SpellBonus'}}" />
data-property="data.bonus" value="{{itemData.data.bonus}}" title="{{localize 'DS4.SpellModifier'}}" />
{{!-- max. distance --}}
{{> distanceUnit titleKey='DS4.SpellMaxDistance' unitDatum=itemData.data.maxDistance
config=../../config}}
{{> distanceUnit titleKey='DS4.SpellDistance' unitDatum=itemData.data.maxDistance
config=@root/config}}
{{!-- duration --}}
{{> customTemporalUnit titleKey='DS4.SpellDuration' unitDatum=itemData.data.duration config=../../config}}
{{> temporalUnit titleKey='DS4.SpellDuration' unitDatum=itemData.data.duration config=@root/config}}
{{!-- cooldown duration --}}
{{> temporalUnit titleKey='DS4.SpellCooldownDuration' unitDatum=itemData.data.cooldownDuration
config=../../config}}
<div title="{{localize 'DS4.CooldownDuration'}}">{{lookup @root/config.i18n.cooldownDurations
itemData.data.cooldownDuration}}</div>
{{/systems/ds4/templates/sheets/actor/components/item-list-entry.hbs}}
{{/each}}
</ol>

View file

@ -7,12 +7,16 @@ 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}}">{{localize "DS4.SpellBonus"}}</label>
<input id="data.bonus-{{data._id}}" data-dtype="String" type="text" name="data.bonus" placeholder="0"
value="{{data.data.bonus}}" />
<label for="data.bonus-{{data._id}}" title="{{localize 'DS4.SpellModifierDescription'}}">{{localize
"DS4.SpellModifier"}}</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}}" />
</div>
</div>
<div class="form-group">
<label for="data.spellType-{{data._id}}">{{localize "DS4.SpellType"}}</label>
<label for="data.spellType-{{data._id}}" title="{{localize 'DS4.SpellTypeDescription'}}">{{localize
"DS4.SpellType"}}</label>
<div class="form-fields">
<select id="data.spellType-{{data._id}}" name="data.spellType" data-dtype="String">
{{#select data.data.spellType}}
@ -24,7 +28,8 @@ SPDX-License-Identifier: MIT
</div>
</div>
<div class="form-group">
<label for="data.spellCategory-{{data._id}}">{{localize "DS4.SpellCategory"}}</label>
<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}}
@ -36,7 +41,7 @@ SPDX-License-Identifier: MIT
</div>
</div>
<div class="form-group slim">
<label>{{localize "DS4.SpellMaxDistance"}}</label>
<label title="{{localize 'DS4.SpellDistanceDescription'}}">{{localize "DS4.SpellDistance"}}</label>
<div class="form-fields">
<input data-dtype="String" type="text" name="data.maxDistance.value"
value="{{data.data.maxDistance.value}}" />
@ -50,7 +55,7 @@ SPDX-License-Identifier: MIT
</div>
</div>
<div class="form-group slim">
<label>{{localize "DS4.SpellEffectRadius"}}</label>
<label title="{{localize 'DS4.SpellEffectRadiusDescription'}}">{{localize "DS4.SpellEffectRadius"}}</label>
<div class="form-fields">
<input data-dtype="String" type="text" name="data.effectRadius.value"
value="{{data.data.effectRadius.value}}" />
@ -64,25 +69,11 @@ SPDX-License-Identifier: MIT
</div>
</div>
<div class="form-group slim">
<label>{{localize "DS4.SpellDuration"}}</label>
<label title="{{localize 'DS4.SpellDurationDescription'}}">{{localize "DS4.SpellDuration"}}</label>
<div class="form-fields">
<input data-dtype="String" type="text" name="data.duration.value" value="{{data.data.duration.value}}" />
<select name="data.duration.unit" data-dtype="String">
{{#select data.data.duration.unit}}
{{#each config.i18n.customTemporalUnits as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
</div>
<div class="form-group slim">
<label>{{localize "DS4.SpellCooldownDuration"}}</label>
<div class="form-fields">
<input data-dtype="String" type="text" name="data.cooldownDuration.value"
value="{{data.data.cooldownDuration.value}}" />
<select name="data.cooldownDuration.unit" data-dtype="String">
{{#select data.data.cooldownDuration.unit}}
{{#each config.i18n.temporalUnits as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
@ -90,8 +81,21 @@ SPDX-License-Identifier: MIT
</select>
</div>
</div>
<div class="form-group">
<label for="data.cooldownDuration-{{data._id}}"
title="{{localize 'DS4.CooldownDurationDescription'}}">{{localize "DS4.CooldownDuration"}}</label>
<div class="form-fields">
<select id="data.cooldownDuration-{{data._id}}" name="data.cooldownDuration" data-dtype="String">
{{#select data.data.cooldownDuration}}
{{#each config.i18n.cooldownDurations as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
</select>
</div>
</div>
<div class="form-group slim">
<label>{{localize "DS4.SpellMinimumLevel"}}</label>
<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"
@ -105,7 +109,10 @@ SPDX-License-Identifier: MIT
</div>
</div>
<div class="form-group">
<label for="data.price-{{data._id}}">{{localize "DS4.SpellPrice"}}</label>
<span id="data.price-{{data._id}}">{{data.data.price}}</span>
<label for="data.price-{{data._id}}" title="{{localize 'DS4.SpellPriceDescription'}}">{{localize
"DS4.SpellPrice"}}</label>
<div class="form-fields">
<span id="data.price-{{data._id}}">{{data.data.price}}</span>
</div>
</div>
</div>