diff --git a/src/lang/de.json b/src/lang/de.json index 59146324..f81a5f29 100644 --- a/src/lang/de.json +++ b/src/lang/de.json @@ -175,6 +175,9 @@ "DS4.WarningActorCannotOwnItem": "Der Aktor '{actorName}' vom Typ '{actorType}' kann das Item '{itemName}' vom Typ '{itemType}' nicht besitzen.", "DS4.ErrorDiceCritOverlap": "Es gibt eine Überlappung zwischen Patzern und Immersiegen.", "DS4.ErrorExplodingRecursionLimitExceeded": "Die maximale Rekursionstiefe für slayende Würfelwürfe wurde überschritten.", + "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.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.UnitRounds": "Runden", "DS4.UnitRoundsAbbr": "Rnd", "DS4.UnitMinutes": "Minuten", diff --git a/src/lang/en.json b/src/lang/en.json index c28b165f..ee1927ee 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -175,6 +175,9 @@ "DS4.WarningActorCannotOwnItem": "The actor '{actorName}' of type '{actorType}' cannot own the item '{itemName}' of type '{itemType}'.", "DS4.ErrorDiceCritOverlap": "There's an overlap between Fumbles and Coups", "DS4.ErrorExplodingRecursionLimitExceeded": "Maximum recursion depth for exploding dice roll exceeded", + "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.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.UnitRounds": "Rounds", "DS4.UnitRoundsAbbr": "rnd", "DS4.UnitMinutes": "Minutes", diff --git a/src/module/actor/actor.ts b/src/module/actor/actor.ts index df3ad6d7..0ad31fb8 100644 --- a/src/module/actor/actor.ts +++ b/src/module/actor/actor.ts @@ -1,6 +1,6 @@ import { ModifiableData } from "../common/common-data"; import { DS4Item } from "../item/item"; -import { DS4ItemDataType, ItemType } from "../item/item-data"; +import { DS4Armor, DS4ItemDataType, DS4Shield, ItemType } from "../item/item-data"; import { DS4ActorDataType } from "./actor-data"; export class DS4Actor extends Actor { @@ -15,12 +15,7 @@ export class DS4Actor extends Actor const traits = data.data.traits; Object.values(traits).forEach((trait: ModifiableData) => (trait.total = trait.base + trait.mod)); - const combatValues = data.data.combatValues; - Object.values(combatValues).forEach( - (combatValue: ModifiableData) => (combatValue.total = combatValue.base + combatValue.mod), - ); - - combatValues.hitPoints.max = combatValues.hitPoints.total; + this._prepareCombatValues(); } /** @@ -55,4 +50,44 @@ export class DS4Actor extends Actor canOwnItemType(itemType: ItemType): boolean { return this.ownableItemTypes.includes(itemType); } + + /** + * Prepares the combat values of the actor. + */ + private _prepareCombatValues(): void { + const data = this.data.data; + const armorValueOfEquippedItems = this._calculateArmorValueOfEquippedItems(); + data.combatValues.hitPoints.base = + (data.attributes.body.total ?? 0) + (data.traits.constitution.total ?? 0) + 10; + + data.combatValues.defense.base = + (data.attributes.body.total ?? 0) + (data.traits.constitution.total ?? 0) + armorValueOfEquippedItems; + data.combatValues.initiative.base = (data.attributes.mobility.total ?? 0) + (data.traits.agility.total ?? 0); + data.combatValues.movement.base = (data.attributes.mobility.total ?? 0) / 2 + 1; + data.combatValues.meleeAttack.base = (data.attributes.body.total ?? 0) + (data.traits.strength.total ?? 0); + data.combatValues.rangedAttack.base = + (data.attributes.mobility.total ?? 0) + (data.traits.dexterity.total ?? 0); + data.combatValues.spellcasting.base = + (data.attributes.mind.total ?? 0) + (data.traits.aura.total ?? 0) - armorValueOfEquippedItems; + data.combatValues.targetedSpellcasting.base = + (data.attributes.mind.total ?? 0) + (data.traits.dexterity.total ?? 0) - armorValueOfEquippedItems; + + Object.values(data.combatValues).forEach( + (combatValue: ModifiableData) => (combatValue.total = combatValue.base + combatValue.mod), + ); + + data.combatValues.hitPoints.max = data.combatValues.hitPoints.total; + } + + /** + * Calculates the total armor value of all equipped items. + */ + private _calculateArmorValueOfEquippedItems(): number { + return this.items + .filter((item) => ["armor", "shield"].includes(item.type)) + .map((item) => item.data.data as DS4Armor | DS4Shield) + .filter((itemData) => itemData.equipped) + .map((itemData) => itemData.armorValue) + .reduce((a, b) => a + b, 0); + } } diff --git a/src/module/actor/sheets/actor-sheet.ts b/src/module/actor/sheets/actor-sheet.ts index 26f6bd8b..3dfda80e 100644 --- a/src/module/actor/sheets/actor-sheet.ts +++ b/src/module/actor/sheets/actor-sheet.ts @@ -14,6 +14,7 @@ export class DS4ActorSheet extends ActorSheet { total?: T; } -export interface ResourceData extends ModifiableData { +export interface ModifiableMaybeData { + base?: T; + mod: T; + total?: T; +} + +export interface ResourceData extends ModifiableMaybeData { value: T; max?: T; } diff --git a/src/module/ds4.ts b/src/module/ds4.ts index 131e6675..81115a4c 100644 --- a/src/module/ds4.ts +++ b/src/module/ds4.ts @@ -7,6 +7,8 @@ import { DS4Check } from "./rolls/check"; import { DS4CharacterActorSheet } from "./actor/sheets/character-sheet"; import { DS4CreatureActorSheet } from "./actor/sheets/creature-sheet"; import { createCheckRoll } from "./rolls/check-factory"; +import { registerSystemSettings } from "./settings"; +import { migration } from "./migrations"; Hooks.once("init", async function () { console.log(`DS4 | Initializing the DS4 Game System\n${DS4.ASCII}`); @@ -16,6 +18,7 @@ Hooks.once("init", async function () { DS4Item, DS4, createCheckRoll, + migration, }; // Record configuration @@ -37,6 +40,9 @@ Hooks.once("init", async function () { s: DS4Check, }; + // Register system settings + registerSystemSettings(); + // Register sheet application classes Actors.unregisterSheet("core", ActorSheet); Actors.registerSheet("ds4", DS4CharacterActorSheet, { types: ["character"], makeDefault: true }); @@ -123,3 +129,7 @@ Hooks.once("setup", function () { }, {}); } }); + +Hooks.once("ready", function () { + migration.migrate(); +}); diff --git a/src/module/item/item-data.ts b/src/module/item/item-data.ts index 64ee17f2..d7467f8a 100644 --- a/src/module/item/item-data.ts +++ b/src/module/item/item-data.ts @@ -24,7 +24,7 @@ interface DS4Weapon extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable { opponentDefense: number; } -interface DS4Armor extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable, DS4ItemProtective { +export interface DS4Armor extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable, DS4ItemProtective { armorMaterialType: "cloth" | "leather" | "chain" | "plate"; armorType: "body" | "helmet" | "vambrace" | "greaves" | "vambraceGreaves"; } @@ -57,7 +57,7 @@ interface DS4Spell extends DS4ItemBase, DS4ItemEquipable { scrollPrice: number; } -interface DS4Shield extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable, DS4ItemProtective {} +export interface DS4Shield extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable, DS4ItemProtective {} interface DS4Trinket extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable {} interface DS4Equipment extends DS4ItemBase, DS4ItemPhysical {} type DS4RacialAbility = DS4ItemBase; diff --git a/src/module/item/item-sheet.ts b/src/module/item/item-sheet.ts index 8d8df70f..6464defc 100644 --- a/src/module/item/item-sheet.ts +++ b/src/module/item/item-sheet.ts @@ -13,6 +13,7 @@ export class DS4ItemSheet extends ItemSheet { height: 400, classes: ["ds4", "sheet", "item"], tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }], + scrollY: [".sheet-body"], }); } diff --git a/src/module/migrations.ts b/src/module/migrations.ts new file mode 100644 index 00000000..ec48294a --- /dev/null +++ b/src/module/migrations.ts @@ -0,0 +1,81 @@ +import { migrate as migrate001 } from "./migrations/001"; + +async function migrate(): Promise { + if (!game.user.isGM) { + return; + } + + const oldMigrationVersion: number = game.settings.get("ds4", "systemMigrationVersion"); + + const targetMigrationVersion = migrations.length; + + if (isFirstWorldStart(oldMigrationVersion)) { + game.settings.set("ds4", "systemMigrationVersion", targetMigrationVersion); + return; + } + + return migrateFromTo(oldMigrationVersion, targetMigrationVersion); +} + +async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion: number): Promise { + if (!game.user.isGM) { + return; + } + + const migrationsToExecute = migrations.slice(oldMigrationVersion, targetMigrationVersion); + + if (migrationsToExecute.length > 0) { + ui.notifications.info( + game.i18n.format("DS4.InfoSystemUpdateStart", { + currentVersion: oldMigrationVersion, + targetVersion: targetMigrationVersion, + }), + { permanent: true }, + ); + + for (const [i, migration] of migrationsToExecute.entries()) { + const currentMigrationVersion = oldMigrationVersion + i + 1; + console.log("executing migration script ", currentMigrationVersion); + try { + await migration(); + game.settings.set("ds4", "systemMigrationVersion", currentMigrationVersion); + } catch (err) { + ui.notifications.error( + game.i18n.format("DS4.ErrorDuringMigration", { + currentVersion: oldMigrationVersion, + targetVersion: targetMigrationVersion, + migrationVersion: currentMigrationVersion, + }), + { permanent: true }, + ); + err.message = `Failed ds4 system migration: ${err.message}`; + console.error(err); + return; + } + } + + ui.notifications.info( + game.i18n.format("DS4.InfoSystemUpdateCompleted", { + currentVersion: oldMigrationVersion, + targetVersion: targetMigrationVersion, + }), + { permanent: true }, + ); + } +} + +function getTargetMigrationVersion(): number { + return migrations.length; +} + +const migrations: Array<() => Promise> = [migrate001]; + +function isFirstWorldStart(migrationVersion: number): boolean { + return migrationVersion < 0; +} + +export const migration = { + migrate: migrate, + migrateFromTo: migrateFromTo, + getTargetMigrationVersion: getTargetMigrationVersion, +}; diff --git a/src/module/migrations/001.ts b/src/module/migrations/001.ts new file mode 100644 index 00000000..efc3637e --- /dev/null +++ b/src/module/migrations/001.ts @@ -0,0 +1,28 @@ +export async function migrate(): Promise { + for (const a of game.actors.entities) { + const updateData = getActorUpdateData(); + console.log(`Migrating actor ${a.name}`); + await a.update(updateData, { enforceTypes: false }); + } +} + +function getActorUpdateData(): Record { + const updateData = { + data: { + combatValues: [ + "hitPoints", + "defense", + "initiative", + "movement", + "meleeAttack", + "rangedAttack", + "spellcasting", + "targetedSpellcasting", + ].reduce((acc, curr) => { + acc[curr] = { "-=base": null }; + return acc; + }, {}), + }, + }; + return updateData; +} diff --git a/src/module/rolls/check-factory.ts b/src/module/rolls/check-factory.ts index 066202d6..c972120c 100644 --- a/src/module/rolls/check-factory.ts +++ b/src/module/rolls/check-factory.ts @@ -33,7 +33,7 @@ class CheckFactory { private checkOptions: DS4CheckFactoryOptions; - async execute(): Promise { + async execute(): Promise { const rollCls: typeof Roll = CONFIG.Dice.rolls[0]; const formula = [ @@ -82,7 +82,7 @@ class CheckFactory { export async function createCheckRoll( targetValue: number, options: Partial = {}, -): Promise { +): Promise { // Ask for additional required data; const gmModifierData = await askGmModifier(targetValue, options); @@ -99,7 +99,7 @@ export async function createCheckRoll( // Possibly additional processing // Execute roll - await cf.execute(); + return cf.execute(); } /** diff --git a/src/module/settings.ts b/src/module/settings.ts new file mode 100644 index 00000000..644033f0 --- /dev/null +++ b/src/module/settings.ts @@ -0,0 +1,12 @@ +export function registerSystemSettings(): void { + /** + * Track the migrations version of the latest migration that has been applied + */ + game.settings.register("ds4", "systemMigrationVersion", { + name: "System Migration Version", + scope: "world", + config: false, + type: Number, + default: -1, + }); +} diff --git a/src/scss/components/_combat_values.scss b/src/scss/components/_combat_values.scss index 5b7ad3d2..14e412ee 100644 --- a/src/scss/components/_combat_values.scss +++ b/src/scss/components/_combat_values.scss @@ -40,8 +40,9 @@ .combat-value-formula { width: $size; - input { - text-align: center; + text-align: center; + span { + line-height: $default-input-height; } } } diff --git a/src/template.json b/src/template.json index 726d0a56..34b9b253 100644 --- a/src/template.json +++ b/src/template.json @@ -45,36 +45,28 @@ }, "combatValues": { "hitPoints": { - "base": 0, "mod": 0, "value": 0 }, "defense": { - "base": 0, "mod": 0 }, "initiative": { - "base": 0, "mod": 0 }, "movement": { - "base": 0, "mod": 0 }, "meleeAttack": { - "base": 0, "mod": 0 }, "rangedAttack": { - "base": 0, "mod": 0 }, "spellcasting": { - "base": 0, "mod": 0 }, "targetedSpellcasting": { - "base": 0, "mod": 0 } } diff --git a/src/templates/actor/partials/combat-values.hbs b/src/templates/actor/partials/combat-values.hbs index 49bfd024..d207245a 100644 --- a/src/templates/actor/partials/combat-values.hbs +++ b/src/templates/actor/partials/combat-values.hbs @@ -13,9 +13,9 @@
{{combat-value-data.total}}
-
+
{{combat-value-data.base}}+
{{/inline}}