Skip to content

Commit

Permalink
Add support for damage dice downgrades (foundryvtt#2393)
Browse files Browse the repository at this point in the history
* Add support for damage dice downgrades

Also cleanup unused code and useless test

* Adjust comment for accuracy

Co-authored-by: Carlos Fernandez <[email protected]>

Co-authored-by: Carlos Fernandez <[email protected]>
  • Loading branch information
stwlam and CarlosFdez authored Jun 10, 2022
1 parent ed564ee commit e8e98bc
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 214 deletions.
7 changes: 4 additions & 3 deletions src/module/rules/rule-element/damage-dice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,17 @@ export class DamageDiceRuleElement extends RuleElementPF2e {
const isValidOverride = (override: unknown): override is DamageDiceOverride => {
return (
isObject<DamageDiceOverride>(override) &&
(typeof override.upgrade === "boolean" ||
((typeof override.upgrade === "boolean" && !("downgrade" in override)) ||
(typeof override.downgrade === "boolean" && !("upgrade" in override)) ||
setHasElement(DAMAGE_DIE_FACES, override.dieSize) ||
setHasElement(DAMAGE_TYPES, override.damageType))
);
};

if (!isValidOverride(data.override)) {
this.failValidation(
"The override property must be an object with one property of `upgrade` (boolean), `dieSize` (d6-d12),",
"or `damageType` (recognized damage type)"
"The override property must be an object with one property of `upgrade` (boolean),",
"`downgrade (boolean)`, `dieSize` (d6-d12), or `damageType` (recognized damage type)"
);
return;
}
Expand Down
78 changes: 22 additions & 56 deletions src/module/system/damage/damage.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
import { StrikeSelf, AttackTarget } from "@actor/creature/types";
import { DegreeOfSuccessString } from "@system/degree-of-success";
import { BaseRollContext } from "@system/rolls";
import { combineObjects } from "@util";

/** The possible standard damage die sizes. */
export const DAMAGE_DIE_FACES = new Set(["d4", "d6", "d8", "d10", "d12"] as const);
export type DamageDieSize = SetElement<typeof DAMAGE_DIE_FACES>;

export function nextDamageDieSize(dieSize: DamageDieSize) {
switch (dieSize) {
case "d4":
return "d6";
case "d6":
return "d8";
case "d8":
return "d10";
case "d10":
return "d12";
case "d12":
return "d12";
}
const dieFacesTuple = ["d4", "d6", "d8", "d10", "d12"] as const;
const DAMAGE_DIE_FACES = new Set(dieFacesTuple);
type DamageDieSize = SetElement<typeof DAMAGE_DIE_FACES>;

function nextDamageDieSize(next: { upgrade: DamageDieSize }): DamageDieSize;
function nextDamageDieSize(next: { downgrade: DamageDieSize }): DamageDieSize;
function nextDamageDieSize(next: { upgrade: DamageDieSize } | { downgrade: DamageDieSize }): DamageDieSize {
const [faces, direction] = "upgrade" in next ? [next.upgrade, 1] : [next.downgrade, -1];
return dieFacesTuple[dieFacesTuple.indexOf(faces) + direction] ?? faces;
}

/** Provides constants for typical damage categories, as well as a simple API for adding custom damage types and categories. */
export const DamageCategorization = {
const DamageCategorization = {
/**
* Physical damage; one of bludgeoning, piercing, or slashing, and usually caused by a physical object hitting you.
*/
Expand All @@ -43,58 +35,28 @@ export const DamageCategorization = {
* Map a damage type to it's corresponding damage category. If the type has no category, the type itself will be
* returned.
*/
fromDamageType: (damageType: string) =>
CUSTOM_DAMAGE_TYPES_TO_CATEGORIES[damageType] || BASE_DAMAGE_TYPES_TO_CATEGORIES[damageType] || damageType,

/** Adds a custom damage type -> category mapping. This method can be used to override base damage type/category mappings. */
addCustomDamageType: (category: string, type: string) => {
CUSTOM_DAMAGE_TYPES_TO_CATEGORIES[type] = category;
},

/** Removes the custom mapping for the given type. */
removeCustomDamageType: (type: string) => delete CUSTOM_DAMAGE_TYPES_TO_CATEGORIES[type],
fromDamageType: (damageType: string) => BASE_DAMAGE_TYPES_TO_CATEGORIES[damageType] || damageType,

/** Get a set of all damage categories (both base and custom). */
allCategories: () =>
new Set(
Object.values(BASE_DAMAGE_TYPES_TO_CATEGORIES).concat(Object.values(CUSTOM_DAMAGE_TYPES_TO_CATEGORIES))
),
allCategories: () => new Set(Object.values(BASE_DAMAGE_TYPES_TO_CATEGORIES)),

/** Get a set of all of the base rule damage types. */
baseCategories: () => new Set(Object.values(BASE_DAMAGE_TYPES_TO_CATEGORIES)),

/** Get a set of all custom damage categories (exluding the base damage types). */
customCategories: () => {
const result = new Set(Object.values(CUSTOM_DAMAGE_TYPES_TO_CATEGORIES));
for (const base of DamageCategorization.baseCategories()) result.delete(base);

return result;
},

/** Get the full current map of damage types -> their current damage category (taking custom mappings into account). */
currentTypeMappings: () =>
combineObjects(BASE_DAMAGE_TYPES_TO_CATEGORIES, CUSTOM_DAMAGE_TYPES_TO_CATEGORIES, (_first, second) => second),

/** Map a damage category to the set of damage types in it. */
toDamageTypes: (category: string) => {
// Get all of the types in the current mappings which map to the given category
const types = Object.entries(DamageCategorization.currentTypeMappings())
const types = Object.entries(BASE_DAMAGE_TYPES_TO_CATEGORIES)
.filter(([_key, value]) => value === category)
.map(([key]) => key);

// And return as a set to eliminate duplicates.
return new Set(types);
},

/** Clear all custom damage type mappings. */
clearCustom: () =>
Object.keys(CUSTOM_DAMAGE_TYPES_TO_CATEGORIES).forEach((key) => {
delete CUSTOM_DAMAGE_TYPES_TO_CATEGORIES[key];
}),
} as const;

/** Maps damage types to their damage category; these are the immutable base mappings used if there is no override. */
export const BASE_DAMAGE_TYPES_TO_CATEGORIES: Readonly<Record<string, string>> = {
const BASE_DAMAGE_TYPES_TO_CATEGORIES: Readonly<Record<string, string>> = {
// The three default physical damage types.
bludgeoning: DamageCategorization.PHYSICAL,
piercing: DamageCategorization.PHYSICAL,
Expand All @@ -117,9 +79,6 @@ export const BASE_DAMAGE_TYPES_TO_CATEGORIES: Readonly<Record<string, string>> =
lawful: DamageCategorization.ALIGNMENT,
} as const;

/** Custom damage type mappings; maps damage types to their damage category. */
export const CUSTOM_DAMAGE_TYPES_TO_CATEGORIES: Record<string, string> = {};

interface DamageRollContext extends BaseRollContext {
type: "damage-roll";
outcome?: DegreeOfSuccessString;
Expand All @@ -129,4 +88,11 @@ interface DamageRollContext extends BaseRollContext {
secret?: boolean;
}

export { DamageRollContext };
export {
BASE_DAMAGE_TYPES_TO_CATEGORIES,
DAMAGE_DIE_FACES,
DamageCategorization,
DamageDieSize,
DamageRollContext,
nextDamageDieSize,
};
12 changes: 9 additions & 3 deletions src/module/system/damage/weapon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,10 +565,16 @@ export class WeaponDamagePF2e {
const { base } = damage;
const diceModifiers: DiceModifierPF2e[] = damage.diceModifiers;

// First, increase the damage die. This can only be done once, so we
// First, increase or decrease the damage die. This can only be done once, so we
// only need to find the presence of a rule that does this
if (diceModifiers.some((dm) => dm.enabled && dm.override?.upgrade && (critical || !dm.critical))) {
base.dieSize = nextDamageDieSize(base.dieSize);
const hasUpgrade = diceModifiers.some((dm) => dm.enabled && dm.override?.upgrade && (critical || !dm.critical));
const hasDowngrade = diceModifiers.some(
(dm) => dm.enabled && dm.override?.downgrade && (critical || !dm.critical)
);
if (hasUpgrade && !hasDowngrade) {
base.dieSize = nextDamageDieSize({ upgrade: base.dieSize });
} else if (hasDowngrade && !hasUpgrade) {
base.dieSize = nextDamageDieSize({ downgrade: base.dieSize });
}

// Override next, to ensure the dice stacking works properly
Expand Down
41 changes: 0 additions & 41 deletions src/util/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,40 +35,6 @@ function padArray<T>(array: T[], requiredLength: number, padWith: T): T[] {
return result;
}

/**
* Return a new object that combines all the keys and values from
* both. If both have the same key, assign the value of the merge function.
* Example:
* // returns {a: 3, b: 5, c: 0}
* combineObjects({a: 3, b: 4}, {b: 1, c: 0}, (a, b) => a+b)
* @param first
* @param second
* @param mergeFunction if duplicate keys exist, both values
* are passed into this function to return the result
* @return
*/
function combineObjects<V>(
first: Record<RecordKey, V>,
second: Record<RecordKey, V>,
mergeFunction: (first: V, second: V) => V
): Record<RecordKey, V> {
const combinedKeys = new Set([...Object.keys(first), ...Object.keys(second)]) as Set<RecordKey>;

const combinedObject: Record<RecordKey, V> = {};
for (const name of combinedKeys) {
if (name in first && name in second) {
combinedObject[name] = mergeFunction(first[name], second[name]);
} else if (name in first) {
combinedObject[name] = first[name];
} else if (name in second) {
combinedObject[name] = second[name];
}
}
return combinedObject;
}

type RecordKey = string | number;

type Optional<T> = T | null | undefined;

/**
Expand All @@ -78,11 +44,6 @@ function isBlank(text: Optional<string>): text is null | undefined | "" {
return text === null || text === undefined || text.trim() === "";
}

/** Used as a function reference */
function add(x: number, y: number): number {
return x + y;
}

/**
* Adds a + if positive, nothing if 0 or - if negative
*/
Expand Down Expand Up @@ -353,10 +314,8 @@ export {
ErrorPF2e,
Fraction,
Optional,
add,
addSign,
applyNTimes,
combineObjects,
fontAwesomeIcon,
getActionGlyph,
getActionIcon,
Expand Down
78 changes: 0 additions & 78 deletions tests/module/system/category.test.ts

This file was deleted.

34 changes: 1 addition & 33 deletions tests/module/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,4 @@
import { add, addSign, applyNTimes, combineObjects, padArray, zip } from "@util";

describe("should combine objects", () => {
test("combine two empty objects", () => {
const result = combineObjects({}, {}, add);

expect(result).toEqual({});
});

test("combine one empty object", () => {
const result = combineObjects({ a: 3 }, {}, add);

expect(result).toEqual({ a: 3 });
});

test("should be commutative", () => {
const result = combineObjects({}, { a: 3 }, add);

expect(result).toEqual({ a: 3 });
});

test("should merge different objects", () => {
const result = combineObjects({ b: 5 }, { a: 3 }, add);

expect(result).toEqual({ a: 3, b: 5 });
});

test("should merge intersecting objects", () => {
const result = combineObjects({ a: 1, b: 5 }, { a: 3 }, add);

expect(result).toEqual({ a: 4, b: 5 });
});
});
import { addSign, applyNTimes, padArray, zip } from "@util";

describe("format sign for numbers", () => {
test("0", () => {
Expand Down

0 comments on commit e8e98bc

Please sign in to comment.