Merge branch 'selectable-cooldown-duration' into 'master'

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

See merge request dungeonslayers/ds4!175
This commit is contained in:
Johannes Loher 2022-02-16 01:02:16 +00:00
commit f40e1436f8
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>