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:
parent
73e2d44c55
commit
da1f6999eb
20 changed files with 558 additions and 876 deletions
|
@ -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;
|
|
@ -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",
|
||||
},
|
||||
|
||||
|
|
|
@ -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] => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
119
src/migrations/005.ts
Normal 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,
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue