Merge branch '084-automatically-calculate-scroll-price' into 'master'

Automatically calculate spell price

Closes #84

See merge request dungeonslayers/ds4!106
This commit is contained in:
Johannes Loher 2021-05-13 18:45:52 +00:00
commit 441e907b8a
17 changed files with 279 additions and 57 deletions

View file

@ -119,7 +119,7 @@
"DS4.SpellMinimumLevelsWizardAbbr": "Zugangsstufe Zau", "DS4.SpellMinimumLevelsWizardAbbr": "Zugangsstufe Zau",
"DS4.SpellMinimumLevelsSorcerer": "Zugangsstufe für Schwarzmagier", "DS4.SpellMinimumLevelsSorcerer": "Zugangsstufe für Schwarzmagier",
"DS4.SpellMinimumLevelsSorcererAbbr": "Zugangsstufe Sch", "DS4.SpellMinimumLevelsSorcererAbbr": "Zugangsstufe Sch",
"DS4.SpellScrollPriceGold": "Schriftrollenpreis (Gold)", "DS4.SpellPrice": "Preis (Gold)",
"DS4.ActorTypeCharacter": "Charakter", "DS4.ActorTypeCharacter": "Charakter",
"DS4.ActorTypeCreature": "Kreatur", "DS4.ActorTypeCreature": "Kreatur",
"DS4.AttributeBody": "Körper", "DS4.AttributeBody": "Körper",

View file

@ -119,7 +119,7 @@
"DS4.SpellMinimumLevelsWizardAbbr": "Min lvl WIZ", "DS4.SpellMinimumLevelsWizardAbbr": "Min lvl WIZ",
"DS4.SpellMinimumLevelsSorcerer": "Minimum level for Sorcerers", "DS4.SpellMinimumLevelsSorcerer": "Minimum level for Sorcerers",
"DS4.SpellMinimumLevelsSorcererAbbr": "Min lvl SRC", "DS4.SpellMinimumLevelsSorcererAbbr": "Min lvl SRC",
"DS4.SpellScrollPriceGold": "Scroll Price (Gold)", "DS4.SpellPrice": "Price (Gold)",
"DS4.ActorTypeCharacter": "Character", "DS4.ActorTypeCharacter": "Character",
"DS4.ActorTypeCreature": "Creature", "DS4.ActorTypeCreature": "Creature",
"DS4.AttributeBody": "Body", "DS4.AttributeBody": "Body",

View file

@ -111,6 +111,6 @@ export interface DS4CreatureDataDataBaseInfo {
description: string; description: string;
} }
type CreatureType = "animal" | "construct" | "humanoid" | "magicalEntity" | "plantBeing" | "undead"; type CreatureType = keyof typeof DS4.i18n.creatureTypes;
type SizeCategory = "tiny" | "small" | "normal" | "large" | "huge" | "colossal"; type SizeCategory = keyof typeof DS4.i18n.creatureSizeCategories;

View file

@ -0,0 +1,4 @@
export const secondsPerRound = 5;
export const secondsPerMinute = 60;
export const minutesPerHour = 60;
export const hoursPerDay = 24;

View file

@ -261,24 +261,44 @@ export const DS4 = {
}, },
/** /**
* Define translations for available distance units * Define translations for available duration units
*/ */
temporalUnits: { temporalUnits: {
rounds: "DS4.UnitRounds", rounds: "DS4.UnitRounds",
minutes: "DS4.UnitMinutes", minutes: "DS4.UnitMinutes",
hours: "DS4.UnitHours", hours: "DS4.UnitHours",
days: "DS4.UnitDays", 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", custom: "DS4.UnitCustom",
}, },
/** /**
* Define abbreviations for available units * Define abbreviations for available duration units
*/ */
temporalUnitsAbbr: { temporalUnitsAbbr: {
rounds: "DS4.UnitRoundsAbbr", rounds: "DS4.UnitRoundsAbbr",
minutes: "DS4.UnitMinutesAbbr", minutes: "DS4.UnitMinutesAbbr",
hours: "DS4.UnitHoursAbbr", hours: "DS4.UnitHoursAbbr",
days: "DS4.UnitDaysAbbr", 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", custom: "DS4.UnitCustomAbbr",
}, },

View file

@ -40,7 +40,7 @@ interface DS4ItemDataDataBase {
interface DS4ItemDataDataPhysical { interface DS4ItemDataDataPhysical {
quantity: number; quantity: number;
price: number; price: number;
availability: "hamlet" | "village" | "city" | "elves" | "dwarves" | "nowhere" | "unset"; availability: keyof typeof DS4.i18n.itemAvailabilities;
storageLocation: string; storageLocation: string;
} }
@ -56,12 +56,13 @@ interface DS4ItemDataDataProtective {
armorValue: number; armorValue: number;
} }
interface UnitData<UnitType> { export interface UnitData<UnitType> {
value: string; value: string;
unit: UnitType; unit: UnitType;
} }
type TemporalUnit = "rounds" | "minutes" | "hours" | "days" | "custom"; export type TemporalUnit = keyof typeof DS4.i18n.temporalUnits;
type DistanceUnit = "meter" | "kilometer" | "custom"; type CustomTemporalUnit = keyof typeof DS4.i18n.customTemporalUnits;
type DistanceUnit = keyof typeof DS4.i18n.distanceUnits;
// types // types
@ -71,15 +72,15 @@ export interface DS4WeaponDataData extends DS4ItemDataDataBase, DS4ItemDataDataP
opponentDefense: number; opponentDefense: number;
} }
export type AttackType = keyof typeof DS4["i18n"]["attackTypes"]; export type AttackType = keyof typeof DS4.i18n.attackTypes;
export interface DS4ArmorDataData export interface DS4ArmorDataData
extends DS4ItemDataDataBase, extends DS4ItemDataDataBase,
DS4ItemDataDataPhysical, DS4ItemDataDataPhysical,
DS4ItemDataDataEquipable, DS4ItemDataDataEquipable,
DS4ItemDataDataProtective { DS4ItemDataDataProtective {
armorMaterialType: "cloth" | "leather" | "chain" | "plate"; armorMaterialType: keyof typeof DS4.i18n.armorMaterialTypes;
armorType: "body" | "helmet" | "vambrace" | "greaves" | "vambraceGreaves"; armorType: keyof typeof DS4.i18n.armorTypes;
} }
export interface DS4TalentDataData extends DS4ItemDataDataBase { export interface DS4TalentDataData extends DS4ItemDataDataBase {
@ -91,28 +92,18 @@ export interface DS4TalentRank extends ModifiableDataBase<number> {
} }
export interface DS4SpellDataData extends DS4ItemDataDataBase, DS4ItemDataDataEquipable { export interface DS4SpellDataData extends DS4ItemDataDataBase, DS4ItemDataDataEquipable {
spellType: "spellcasting" | "targetedSpellcasting"; spellType: keyof typeof DS4.i18n.spellTypes;
bonus: string; bonus: string;
spellCategory: spellCategory: keyof typeof DS4.i18n.spellCategories;
| "healing"
| "fire"
| "ice"
| "light"
| "darkness"
| "mindAffecting"
| "electricity"
| "none"
| "unset";
maxDistance: UnitData<DistanceUnit>; maxDistance: UnitData<DistanceUnit>;
effectRadius: UnitData<DistanceUnit>; effectRadius: UnitData<DistanceUnit>;
duration: UnitData<TemporalUnit>; duration: UnitData<CustomTemporalUnit>;
cooldownDuration: UnitData<TemporalUnit>; cooldownDuration: UnitData<TemporalUnit>;
minimumLevels: { minimumLevels: {
healer: number | null; healer: number | null;
wizard: number | null; wizard: number | null;
sorcerer: number | null; sorcerer: number | null;
}; };
scrollPrice: number;
} }
export interface DS4ShieldDataData export interface DS4ShieldDataData

View file

@ -57,7 +57,9 @@ interface DS4ArmorPreparedDataData extends DS4ArmorDataData, DS4ItemPreparedData
interface DS4ShieldPreparedDataData extends DS4ShieldDataData, DS4ItemPreparedDataDataRollable {} interface DS4ShieldPreparedDataData extends DS4ShieldDataData, DS4ItemPreparedDataDataRollable {}
interface DS4SpellPreparedDataData extends DS4SpellDataData, DS4ItemPreparedDataDataRollable {} interface DS4SpellPreparedDataData extends DS4SpellDataData, DS4ItemPreparedDataDataRollable {
price: number | null;
}
interface DS4EquipmentPreparedDataData extends DS4EquipmentDataData, DS4ItemPreparedDataDataRollable {} interface DS4EquipmentPreparedDataData extends DS4EquipmentDataData, DS4ItemPreparedDataDataRollable {}

View file

@ -11,27 +11,12 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
static get defaultOptions(): BaseEntitySheet.Options { static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions; const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, { return mergeObject(superDefaultOptions, {
...superDefaultOptions,
width: 540, width: 540,
height: 400, height: 400,
classes: ["ds4", "sheet", "item"], classes: ["ds4", "sheet", "item"],
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }], tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }],
scrollY: [".sheet-body"], scrollY: [".tab.description", ".tab.effects", ".tab.details"],
template: superDefaultOptions.template,
viewPermission: superDefaultOptions.viewPermission,
closeOnSubmit: superDefaultOptions.closeOnSubmit,
submitOnChange: superDefaultOptions.submitOnChange,
submitOnClose: superDefaultOptions.submitOnClose,
editable: superDefaultOptions.editable,
baseApplication: superDefaultOptions.baseApplication,
top: superDefaultOptions.top,
left: superDefaultOptions.left,
popOut: superDefaultOptions.popOut,
minimizable: superDefaultOptions.minimizable,
resizable: superDefaultOptions.resizable,
id: superDefaultOptions.id,
dragDrop: superDefaultOptions.dragDrop,
filters: superDefaultOptions.filters,
title: superDefaultOptions.title,
}); });
} }

View file

@ -4,6 +4,7 @@ import { createCheckRoll } from "../rolls/check-factory";
import notifications from "../ui/notifications"; import notifications from "../ui/notifications";
import { AttackType, DS4ItemData, ItemType } from "./item-data"; import { AttackType, DS4ItemData, ItemType } from "./item-data";
import { DS4ItemPreparedData } from "./item-prepared-data"; import { DS4ItemPreparedData } from "./item-prepared-data";
import { calculateSpellPrice } from "./type-specific-helpers/spell";
/** /**
* The Item class for DS4 * The Item class for DS4
@ -27,6 +28,9 @@ export class DS4Item extends Item<DS4ItemData, DS4ItemPreparedData> {
} else { } else {
this.data.data.rollable = false; this.data.data.rollable = false;
} }
if (this.data.type === "spell") {
this.data.data.price = calculateSpellPrice(this.data.data);
}
} }
isNonEquippedEuipable(): boolean { isNonEquippedEuipable(): boolean {

View file

@ -0,0 +1,49 @@
import { hoursPerDay, minutesPerHour, secondsPerMinute, secondsPerRound } from "../../common/time-helpers";
import { DS4SpellDataData, TemporalUnit, UnitData } from "../item-data";
export function calculateSpellPrice(data: DS4SpellDataData): number | null {
const spellPriceFactor = calculateSpellPriceFactor(data.cooldownDuration);
const baseSpellPrices = [
data.minimumLevels.healer !== null ? 10 + (data.minimumLevels.healer - 1) * 35 : null,
data.minimumLevels.wizard !== null ? 10 + (data.minimumLevels.wizard - 1) * 50 : null,
data.minimumLevels.sorcerer !== null ? 10 + (data.minimumLevels.sorcerer - 1) * 65 : null,
].filter((baseSpellPrice: number | null): baseSpellPrice is number => baseSpellPrice !== null);
const baseSpellPrice = Math.min(...baseSpellPrices);
return baseSpellPrice === Infinity ? null : baseSpellPrice * spellPriceFactor;
}
function calculateSpellPriceFactor(temporalData: UnitData<TemporalUnit>): number {
let days: number;
if (Number.isNumeric(temporalData.value)) {
switch (temporalData.unit) {
case "days": {
days = temporalData.value;
break;
}
case "hours": {
days = temporalData.value / hoursPerDay;
break;
}
case "minutes": {
days = temporalData.value / (hoursPerDay * minutesPerHour);
break;
}
case "rounds": {
days = (temporalData.value * secondsPerRound) / (hoursPerDay * minutesPerHour * secondsPerMinute);
break;
}
}
} else {
switch (temporalData.unit) {
case "days": {
days = 2;
break;
}
default: {
days = 0;
break;
}
}
}
return Math.clamped(Math.floor(days), 0, 2) + 1;
}

View file

@ -1,6 +1,7 @@
import { migrate as migrate001 } from "./migrations/001"; import { migrate as migrate001 } from "./migrations/001";
import { migrate as migrate002 } from "./migrations/002"; import { migrate as migrate002 } from "./migrations/002";
import { migrate as migrate003 } from "./migrations/003"; import { migrate as migrate003 } from "./migrations/003";
import { migrate as migrate004 } from "./migrations/004";
import notifications from "./ui/notifications"; import notifications from "./ui/notifications";
@ -72,7 +73,7 @@ function getTargetMigrationVersion(): number {
return migrations.length; return migrations.length;
} }
const migrations: Array<() => Promise<void>> = [migrate001, migrate002, migrate003]; const migrations: Array<() => Promise<void>> = [migrate001, migrate002, migrate003, migrate004];
function isFirstWorldStart(migrationVersion: number): boolean { function isFirstWorldStart(migrationVersion: number): boolean {
return migrationVersion < 0; return migrationVersion < 0;

View file

@ -0,0 +1,153 @@
import { DS4SpellDataData } from "../item/item-data";
export async function migrate(): Promise<void> {
await migrateItems();
await migrateActors();
await migrateScenes();
await migrateCompendiums();
}
async function migrateItems() {
for (const item of game.items?.entities ?? []) {
try {
const updateData = getItemUpdateData(item._data);
if (updateData) {
console.log(`Migrating Item entity ${item.name} (${item.id})`);
await item.update(updateData), { enforceTypes: false };
}
} catch (err) {
err.message = `Error during migration of Item entity ${item.name} (${item.id}), continuing anyways.`;
console.error(err);
}
}
}
function getItemUpdateData(itemData: DeepPartial<Item.Data>) {
if (!["spell"].includes(itemData.type ?? "")) return undefined;
const updateData: Record<string, unknown> = {
"-=data.scrollPrice": null,
"data.minimumLevels": { healer: null, wizard: null, sorcerer: null },
};
if (((itemData.data as DS4SpellDataData).cooldownDuration.unit as string) === "custom") {
updateData["data.cooldownDuration.unit"] = "rounds";
}
return updateData;
}
async function migrateActors() {
for (const actor of game.actors?.entities ?? []) {
try {
const updateData = getActorUpdateData(actor._data);
if (updateData) {
console.log(`Migrating Actor entity ${actor.name} (${actor.id})`);
await actor.update(updateData, { enforceTypes: false });
}
} catch (err) {
err.message = `Error during migration of Actor entity ${actor.name} (${actor.id}), continuing anyways.`;
console.error(err);
}
}
}
function getActorUpdateData(actorData: DeepPartial<Actor.Data>) {
let hasItemUpdates = false;
const items = actorData.items?.map((itemData) => {
const update = itemData ? getItemUpdateData(itemData) : undefined;
if (update) {
hasItemUpdates = true;
return mergeObject(itemData, update, { enforceTypes: false, inplace: false });
} else {
return itemData;
}
});
const updateData: Record<string, unknown> = {};
if (actorData.type === "character") {
updateData["data.slayerPoints"] = { value: 0 };
}
if (hasItemUpdates) {
updateData["items"] = items;
}
return updateData;
}
async function migrateScenes() {
for (const scene of game.scenes?.entities ?? []) {
try {
const updateData = getSceneUpdateData(scene._data);
if (updateData) {
console.log(`Migrating Scene entity ${scene.name} (${scene.id})`);
await scene.update(updateData, { enforceTypes: false });
}
} catch (err) {
err.message = `Error during migration of Scene entity ${scene.name} (${scene.id}), continuing anyways.`;
console.error(err);
}
}
}
function getSceneUpdateData(sceneData: Scene.Data) {
let hasTokenUpdates = false;
const tokens = sceneData.tokens.map((tokenData) => {
if (!tokenData.actorId || tokenData.actorLink || tokenData.actorData.data) {
tokenData.actorData = {};
hasTokenUpdates = true;
return tokenData;
}
const token = new Token(tokenData);
if (!token.actor) {
tokenData.actorId = (null as unknown) as string;
tokenData.actorData = {};
hasTokenUpdates = true;
} else if (!tokenData.actorLink) {
const actorUpdateData = getActorUpdateData(token.data.actorData);
tokenData.actorData = mergeObject(token.data.actorData, actorUpdateData);
hasTokenUpdates = true;
}
return tokenData;
});
if (!hasTokenUpdates) return undefined;
return hasTokenUpdates ? { tokens } : undefined;
}
async function migrateCompendiums() {
for (const compendium of game.packs ?? []) {
if (compendium.metadata.package !== "world") continue;
if (!["Actor", "Item", "Scene"].includes(compendium.metadata.entity)) continue;
await migrateCompendium(compendium);
}
}
async function migrateCompendium(compendium: Compendium) {
const entityName = compendium.metadata.entity;
if (!["Actor", "Item", "Scene"].includes(entityName)) return;
const wasLocked = compendium.locked;
await compendium.configure({ locked: false });
await compendium.migrate({});
const content = await compendium.getContent();
for (const entity of content) {
try {
const getUpdateData = (entity: Entity) => {
switch (entityName) {
case "Item":
return getItemUpdateData(entity._data);
case "Actor":
return getActorUpdateData(entity._data);
case "Scene":
return getSceneUpdateData(entity._data as Scene.Data);
}
};
const updateData = getUpdateData(entity);
if (updateData) {
console.log(`Migrating entity ${entity.name} (${entity.id}) in compendium ${compendium.collection}`);
await compendium.updateEntity({ ...updateData, _id: entity._id });
}
} catch (err) {
err.message = `Error during migration of entity ${entity.name} (${entity.id}) in compendium ${compendium.collection}, continuing anyways.`;
console.error(err);
}
}
await compendium.configure({ locked: wasLocked });
}

View file

@ -29,6 +29,10 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
span {
line-height: variables.$default-input-height;
padding: 0 4px;
}
@include mixins.mark-invalid-or-disabled-input; @include mixins.mark-invalid-or-disabled-input;

View file

@ -5,7 +5,7 @@
"version": "0.6.0", "version": "0.6.0",
"minimumCoreVersion": "0.7.9", "minimumCoreVersion": "0.7.9",
"compatibleCoreVersion": "0.7.9", "compatibleCoreVersion": "0.7.9",
"templateVersion": 5, "templateVersion": 6,
"author": "Johannes Loher, Gesina Schwalbe, Oliver Rümpelein, Siegfried Krug", "author": "Johannes Loher, Gesina Schwalbe, Oliver Rümpelein, Siegfried Krug",
"authors": [ "authors": [
{ {

View file

@ -212,14 +212,13 @@
}, },
"cooldownDuration": { "cooldownDuration": {
"value": "", "value": "",
"unit": "custom" "unit": "rounds"
}, },
"minimumLevels": { "minimumLevels": {
"healer": null, "healer": null,
"wizard": null, "wizard": null,
"sorcerer": null "sorcerer": null
}, }
"scrollPrice": 0
}, },
"specialCreatureAbility": { "specialCreatureAbility": {
"templates": ["base"], "templates": ["base"],

View file

@ -18,7 +18,7 @@
{{/inline}} {{/inline}}
{{!-- {{!--
!-- Two templates based on the "unit" template for displaying values with unit. !-- Three templates based on the "unit" template for displaying values with unit.
!-- Both accept a `config` object holding the unitNames and unitAbbr instead of !-- Both accept a `config` object holding the unitNames and unitAbbr instead of
!-- directly handing over the latter two. !-- directly handing over the latter two.
!-- @param titleKey: The key of the localized title to use. !-- @param titleKey: The key of the localized title to use.
@ -28,6 +28,11 @@
titleKey=titleKey}} titleKey=titleKey}}
{{/inline}} {{/inline}}
{{#*inline "customTemporalUnit"}}
{{> unit unitNames=config.i18n.customTemporalUnits unitAbbrs=config.i18n.customTemporalUnitsAbbr unitDatum=unitDatum
titleKey=titleKey}}
{{/inline}}
{{#*inline "distanceUnit"}} {{#*inline "distanceUnit"}}
{{> unit unitNames=config.i18n.distanceUnits unitAbbrs=config.i18n.distanceUnitsAbbr unitDatum=unitDatum {{> unit unitNames=config.i18n.distanceUnits unitAbbrs=config.i18n.distanceUnitsAbbr unitDatum=unitDatum
titleKey=titleKey}} titleKey=titleKey}}
@ -72,7 +77,7 @@ titleKey=titleKey}}
config=../../config}} config=../../config}}
{{!-- duration --}} {{!-- duration --}}
{{> temporalUnit titleKey='DS4.SpellDuration' unitDatum=itemData.data.duration config=../../config}} {{> customTemporalUnit titleKey='DS4.SpellDuration' unitDatum=itemData.data.duration config=../../config}}
{{!-- cooldown duration --}} {{!-- cooldown duration --}}
{{> temporalUnit titleKey='DS4.SpellCooldownDuration' unitDatum=itemData.data.cooldownDuration {{> temporalUnit titleKey='DS4.SpellCooldownDuration' unitDatum=itemData.data.cooldownDuration

View file

@ -12,10 +12,16 @@
<select name="data.{{property}}.unit" data-type="String"> <select name="data.{{property}}.unit" data-type="String">
{{#select (lookup (lookup data property) 'unit')}} {{#select (lookup (lookup data property) 'unit')}}
{{#if (eq unitType 'temporal')}} {{#if (eq unitType 'temporal')}}
{{#each (lookup config.i18n 'temporalUnitsAbbr') as |value key|}}<option value="{{key}}">{{value}}</option> {{#each (lookup config.i18n 'temporalUnitsAbbr') as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{else if (eq unitType 'customTemporal')}}
{{#each (lookup config.i18n 'customTemporalUnitsAbbr') as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}} {{/each}}
{{else}} {{else}}
{{#each (lookup config.i18n 'distanceUnitsAbbr') as |value key|}}<option value="{{key}}">{{value}}</option> {{#each (lookup config.i18n 'distanceUnitsAbbr') as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}} {{/each}}
{{/if}} {{/if}}
{{/select}} {{/select}}
@ -62,7 +68,7 @@
</div> </div>
{{> unitDatum data=data property='maxDistance' localizeString='DS4.SpellMaxDistance' unitType='distance' }} {{> unitDatum data=data property='maxDistance' localizeString='DS4.SpellMaxDistance' unitType='distance' }}
{{> unitDatum data=data property='effectRadius' localizeString='DS4.SpellEffectRadius' unitType='distance' }} {{> unitDatum data=data property='effectRadius' localizeString='DS4.SpellEffectRadius' unitType='distance' }}
{{> unitDatum data=data property='duration' localizeString='DS4.SpellDuration' unitType='temporal' }} {{> unitDatum data=data property='duration' localizeString='DS4.SpellDuration' unitType='customTemporal' }}
{{> unitDatum data=data property='cooldownDuration' localizeString='DS4.SpellCooldownDuration' unitType='temporal' {{> unitDatum data=data property='cooldownDuration' localizeString='DS4.SpellCooldownDuration' unitType='temporal'
}} }}
<div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsHealer'}}"> <div class="side-property" title="{{localize 'DS4.SpellMinimumLevelsHealer'}}">
@ -81,9 +87,8 @@
id="data.minimumLevels.sorcerer" value="{{data.minimumLevels.sorcerer}}" /> id="data.minimumLevels.sorcerer" value="{{data.minimumLevels.sorcerer}}" />
</div> </div>
<div class="side-property"> <div class="side-property">
<label for="data.scrollPrice">{{localize "DS4.SpellScrollPriceGold"}}</label> <label for="data.price">{{localize "DS4.SpellPrice"}}</label>
<input type="number" min="0" max="9999" step="0.01" data-dtype="Number" name="data.scrollPrice" <span name="data.price" id="data.price">{{data.price}}</span>
id="data.scrollPrice" value="{{data.scrollPrice}}" />
</div> </div>
{{/systems/ds4/templates/sheets/item/components/body.hbs}} {{/systems/ds4/templates/sheets/item/components/body.hbs}}