feat: display opponent defense in attack/spell rolls and make it adjustable via effects
This makes it so that the Talents “Verletzen” and “Verheerer” can sort of be automated. Compendium packs have been updated accordingly.
This commit is contained in:
parent
e55da9a0e6
commit
1e094691ff
20 changed files with 2801 additions and 1670 deletions
|
@ -60,8 +60,8 @@ class CheckFactory {
|
|||
}
|
||||
|
||||
createCheckTargetNumberModifier(): string {
|
||||
const totalCheckTargetNumber = Math.max(this.checkTargetNumber + this.checkModifier, 0);
|
||||
return `v${totalCheckTargetNumber}`;
|
||||
const totalCheckTargetNumber = this.checkTargetNumber + this.checkModifier;
|
||||
return totalCheckTargetNumber >= 0 ? `v(${this.checkTargetNumber} + ${this.checkModifier})` : "v0";
|
||||
}
|
||||
|
||||
createCoupFumbleModifier(): string | null {
|
||||
|
|
|
@ -12,4 +12,5 @@ export interface DS4SpellDataProperties {
|
|||
|
||||
interface DS4SpellDataPropertiesData extends DS4SpellDataSourceData, DS4ItemDataPropertiesDataRollable {
|
||||
price: number | null;
|
||||
opponentDefense?: number;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface DS4SpellDataSourceData extends DS4ItemDataSourceDataBase, DS4It
|
|||
numerical: number;
|
||||
complex: string;
|
||||
};
|
||||
allowsDefense: boolean;
|
||||
spellGroups: Record<keyof typeof DS4.i18n.spellGroups, boolean>;
|
||||
maxDistance: UnitData<DistanceUnit>;
|
||||
effectRadius: UnitData<DistanceUnit>;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { createCheckRoll } from "../../../dice/check-factory";
|
||||
import { createCheckRoll, DS4CheckFactoryOptions } from "../../../dice/check-factory";
|
||||
import { notifications } from "../../../ui/notifications";
|
||||
import { getGame } from "../../../utils/utils";
|
||||
import { DS4Item } from "../item";
|
||||
|
@ -12,6 +12,9 @@ export class DS4Spell extends DS4Item {
|
|||
override prepareDerivedData(): void {
|
||||
this.data.data.rollable = this.data.data.equipped;
|
||||
this.data.data.price = calculateSpellPrice(this.data.data);
|
||||
if (this.data.data.allowsDefense) {
|
||||
this.data.data.opponentDefense = 0;
|
||||
}
|
||||
}
|
||||
|
||||
override async roll(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> {
|
||||
|
@ -42,17 +45,29 @@ export class DS4Spell extends DS4Item {
|
|||
);
|
||||
}
|
||||
const spellType = this.data.data.spellType;
|
||||
const opponentDefense = this.data.data.opponentDefense;
|
||||
const checkTargetNumber =
|
||||
ownerDataData.combatValues[spellType].total +
|
||||
(hasComplexModifier ? 0 : this.data.data.spellModifier.numerical);
|
||||
|
||||
const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
|
||||
const flavor =
|
||||
opponentDefense !== undefined && opponentDefense !== 0
|
||||
? "DS4.ItemSpellCheckFlavorWithOpponentDefense"
|
||||
: "DS4.ItemSpellCheckFlavor";
|
||||
const flavorData: DS4CheckFactoryOptions["flavorData"] = {
|
||||
actor: speaker.alias ?? this.actor.name,
|
||||
spell: this.name,
|
||||
};
|
||||
if (opponentDefense !== undefined && opponentDefense !== 0) {
|
||||
flavorData.opponentDefense = (opponentDefense < 0 ? "" : "+") + opponentDefense;
|
||||
}
|
||||
|
||||
await createCheckRoll(checkTargetNumber, {
|
||||
rollMode: game.settings.get("core", "rollMode"),
|
||||
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
|
||||
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
|
||||
flavor: "DS4.ItemSpellCheckFlavor",
|
||||
flavorData: { actor: speaker.alias ?? this.actor.name, spell: this.name },
|
||||
flavor: flavor,
|
||||
flavorData: flavorData,
|
||||
speaker,
|
||||
});
|
||||
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
import type { DS4ItemDataPropertiesDataRollable } from "../item-data-properties-base";
|
||||
import type { DS4WeaponDataSourceData } from "./weapon-data-source";
|
||||
|
||||
interface DS4WeaponDataPropertiesData extends DS4WeaponDataSourceData, DS4ItemDataPropertiesDataRollable {}
|
||||
interface DS4WeaponDataPropertiesData extends DS4WeaponDataSourceData, DS4ItemDataPropertiesDataRollable {
|
||||
opponentDefenseForAttackType: {
|
||||
melee?: number;
|
||||
ranged?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DS4WeaponDataProperties {
|
||||
type: "weapon";
|
||||
|
|
|
@ -3,16 +3,22 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { DS4 } from "../../../config";
|
||||
import { createCheckRoll } from "../../../dice/check-factory";
|
||||
import { createCheckRoll, DS4CheckFactoryOptions } from "../../../dice/check-factory";
|
||||
import { notifications } from "../../../ui/notifications";
|
||||
import { getGame } from "../../../utils/utils";
|
||||
import { DS4Item } from "../item";
|
||||
|
||||
import type { AttackType } from "./weapon-data-source";
|
||||
|
||||
export class DS4Weapon extends DS4Item {
|
||||
override prepareDerivedData(): void {
|
||||
this.data.data.rollable = this.data.data.equipped;
|
||||
const data = this.data.data;
|
||||
data.rollable = data.equipped;
|
||||
data.opponentDefenseForAttackType = {};
|
||||
if (data.attackType === "melee" || data.attackType === "meleeRanged") {
|
||||
data.opponentDefenseForAttackType.melee = data.opponentDefense;
|
||||
}
|
||||
if (data.attackType === "ranged" || data.attackType === "meleeRanged") {
|
||||
data.opponentDefenseForAttackType.ranged = data.opponentDefense;
|
||||
}
|
||||
}
|
||||
|
||||
override async roll(options: { speaker?: { token?: TokenDocument; alias?: string } } = {}): Promise<void> {
|
||||
|
@ -33,54 +39,67 @@ export class DS4Weapon extends DS4Item {
|
|||
|
||||
const ownerDataData = this.actor.data.data;
|
||||
const weaponBonus = this.data.data.weaponBonus;
|
||||
const combatValue = await this.getCombatValueKeyForAttackType(this.data.data.attackType);
|
||||
const attackType = await this.getPerformedAttackType();
|
||||
const opponentDefense = this.data.data.opponentDefenseForAttackType[attackType];
|
||||
const combatValue = `${attackType}Attack` as const;
|
||||
const checkTargetNumber = ownerDataData.combatValues[combatValue].total + weaponBonus;
|
||||
|
||||
const speaker = ChatMessage.getSpeaker({ actor: this.actor, ...options.speaker });
|
||||
const flavor =
|
||||
opponentDefense !== undefined && opponentDefense !== 0
|
||||
? "DS4.ItemWeaponCheckFlavorWithOpponentDefense"
|
||||
: "DS4.ItemWeaponCheckFlavor";
|
||||
const flavorData: DS4CheckFactoryOptions["flavorData"] = {
|
||||
actor: speaker.alias ?? this.actor.name,
|
||||
weapon: this.name,
|
||||
};
|
||||
if (opponentDefense !== undefined && opponentDefense !== 0) {
|
||||
flavorData.opponentDefense = (opponentDefense < 0 ? "" : "+") + opponentDefense;
|
||||
}
|
||||
|
||||
await createCheckRoll(checkTargetNumber, {
|
||||
rollMode: getGame().settings.get("core", "rollMode"),
|
||||
maximumCoupResult: ownerDataData.rolling.maximumCoupResult,
|
||||
minimumFumbleResult: ownerDataData.rolling.minimumFumbleResult,
|
||||
flavor: "DS4.ItemWeaponCheckFlavor",
|
||||
flavorData: { actor: speaker.alias ?? this.actor.name, weapon: this.name },
|
||||
speaker,
|
||||
flavor,
|
||||
flavorData,
|
||||
});
|
||||
|
||||
Hooks.callAll("ds4.rollItem", this);
|
||||
}
|
||||
|
||||
private async getCombatValueKeyForAttackType(attackType: AttackType): Promise<"meleeAttack" | "rangedAttack"> {
|
||||
if (attackType === "meleeRanged") {
|
||||
const { melee, ranged } = { ...DS4.i18n.attackTypes };
|
||||
const identifier = "attack-type-selection";
|
||||
return Dialog.prompt({
|
||||
title: getGame().i18n.localize("DS4.DialogAttackTypeSelection"),
|
||||
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
|
||||
selects: [
|
||||
{
|
||||
label: getGame().i18n.localize("DS4.AttackType"),
|
||||
identifier,
|
||||
options: { melee, ranged },
|
||||
},
|
||||
],
|
||||
}),
|
||||
label: getGame().i18n.localize("DS4.GenericOkButton"),
|
||||
callback: (html) => {
|
||||
const selectedAttackType = html.find(`#${identifier}`).val();
|
||||
if (selectedAttackType !== "melee" && selectedAttackType !== "ranged") {
|
||||
throw new Error(
|
||||
getGame().i18n.format("DS4.ErrorUnexpectedAttackType", {
|
||||
actualType: selectedAttackType,
|
||||
expectedTypes: "'melee', 'ranged'",
|
||||
}),
|
||||
);
|
||||
}
|
||||
return `${selectedAttackType}Attack` as const;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return `${attackType}Attack` as const;
|
||||
private async getPerformedAttackType(): Promise<"melee" | "ranged"> {
|
||||
if (this.data.data.attackType !== "meleeRanged") {
|
||||
return this.data.data.attackType;
|
||||
}
|
||||
|
||||
const { melee, ranged } = { ...DS4.i18n.attackTypes };
|
||||
const identifier = `attack-type-selection-${foundry.utils.randomID()}`;
|
||||
return Dialog.prompt({
|
||||
title: getGame().i18n.localize("DS4.DialogAttackTypeSelection"),
|
||||
content: await renderTemplate("systems/ds4/templates/dialogs/simple-select-form.hbs", {
|
||||
selects: [
|
||||
{
|
||||
label: getGame().i18n.localize("DS4.AttackType"),
|
||||
identifier,
|
||||
options: { melee, ranged },
|
||||
},
|
||||
],
|
||||
}),
|
||||
label: getGame().i18n.localize("DS4.GenericOkButton"),
|
||||
callback: (html) => {
|
||||
const selectedAttackType = html.find(`#${identifier}`).val();
|
||||
if (selectedAttackType !== "melee" && selectedAttackType !== "ranged") {
|
||||
throw new Error(
|
||||
getGame().i18n.format("DS4.ErrorUnexpectedAttackType", {
|
||||
actualType: selectedAttackType,
|
||||
expectedTypes: "'melee', 'ranged'",
|
||||
}),
|
||||
);
|
||||
}
|
||||
return selectedAttackType;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
39
src/migration/007.ts
Normal file
39
src/migration/007.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
// SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import {
|
||||
getActorUpdateDataGetter,
|
||||
getCompendiumMigrator,
|
||||
getSceneUpdateDataGetter,
|
||||
migrateActors,
|
||||
migrateCompendiums,
|
||||
migrateItems,
|
||||
migrateScenes,
|
||||
} from "./migrationHelpers";
|
||||
|
||||
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;
|
||||
|
||||
return {
|
||||
data: {
|
||||
allowsDefense: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const getActorUpdateData = getActorUpdateDataGetter(getItemUpdateData);
|
||||
const getSceneUpdateData = getSceneUpdateDataGetter(getActorUpdateData);
|
||||
const migrateCompendium = getCompendiumMigrator({ getItemUpdateData, getActorUpdateData, getSceneUpdateData });
|
||||
|
||||
export const migration = {
|
||||
migrate,
|
||||
migrateCompendium,
|
||||
};
|
|
@ -11,13 +11,14 @@ import { migration as migration003 } from "./003";
|
|||
import { migration as migration004 } from "./004";
|
||||
import { migration as migration005 } from "./005";
|
||||
import { migration as migration006 } from "./006";
|
||||
import { migration as migration007 } from "./007";
|
||||
|
||||
async function migrate(): Promise<void> {
|
||||
if (!getGame().user?.isGM) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldMigrationVersion = getGame().settings.get("ds4", "systemMigrationVersion");
|
||||
const oldMigrationVersion = getCurrentMigrationVersion();
|
||||
|
||||
const targetMigrationVersion = migrations.length;
|
||||
|
||||
|
@ -47,7 +48,7 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
|
|||
|
||||
for (const [i, { migrate }] of migrationsToExecute.entries()) {
|
||||
const currentMigrationVersion = oldMigrationVersion + i + 1;
|
||||
logger.info("executing migration script ", currentMigrationVersion);
|
||||
logger.info("executing migration script", currentMigrationVersion);
|
||||
try {
|
||||
await migrate();
|
||||
getGame().settings.set("ds4", "systemMigrationVersion", currentMigrationVersion);
|
||||
|
@ -127,6 +128,10 @@ async function migrateCompendiumFromTo(
|
|||
}
|
||||
}
|
||||
|
||||
function getCurrentMigrationVersion(): number {
|
||||
return getGame().settings.get("ds4", "systemMigrationVersion");
|
||||
}
|
||||
|
||||
function getTargetMigrationVersion(): number {
|
||||
return migrations.length;
|
||||
}
|
||||
|
@ -136,7 +141,15 @@ interface Migration {
|
|||
migrateCompendium: (pack: CompendiumCollection<CompendiumCollection.Metadata>) => Promise<void>;
|
||||
}
|
||||
|
||||
const migrations: Migration[] = [migration001, migration002, migration003, migration004, migration005, migration006];
|
||||
const migrations: Migration[] = [
|
||||
migration001,
|
||||
migration002,
|
||||
migration003,
|
||||
migration004,
|
||||
migration005,
|
||||
migration006,
|
||||
migration007,
|
||||
];
|
||||
|
||||
function isFirstWorldStart(migrationVersion: number): boolean {
|
||||
return migrationVersion < 0;
|
||||
|
@ -145,6 +158,7 @@ function isFirstWorldStart(migrationVersion: number): boolean {
|
|||
export const migration = {
|
||||
migrate,
|
||||
migrateFromTo,
|
||||
getCurrentMigrationVersion,
|
||||
getTargetMigrationVersion,
|
||||
migrateCompendiumFromTo,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue