Merge branch '068-enable-strict-mode' into 050-basic-active-effects

This commit is contained in:
Johannes Loher 2021-02-08 03:24:36 +01:00
commit b74ee5ec7c
45 changed files with 3088 additions and 2119 deletions

View file

@ -1,20 +1,31 @@
import { ModifiableData, ResourceData, UsableResource } from "../common/common-data";
import { DS4 } from "../config";
import { DS4ItemData } from "../item/item-data";
export type DS4ActorDataType = DS4ActorDataCharacter | DS4ActorDataCreature;
export type DS4ActorData = DS4CharacterData | DS4CreatureData;
interface DS4ActorDataBase {
attributes: DS4ActorDataAttributes;
traits: DS4ActorDataTraits;
combatValues: DS4ActorDataCombatValues;
type ActorType = keyof typeof DS4.i18n.actorTypes;
interface DS4ActorDataHelper<T, U extends ActorType> extends Actor.Data<T, DS4ItemData> {
type: U;
}
interface DS4ActorDataAttributes {
type DS4CharacterData = DS4ActorDataHelper<DS4CharacterDataData, "character">;
type DS4CreatureData = DS4ActorDataHelper<DS4CreatureDataData, "creature">;
interface DS4ActorDataDataBase {
attributes: DS4ActorDataDataAttributes;
traits: DS4ActorDataDataTraits;
combatValues: DS4ActorDataDataCombatValues;
}
interface DS4ActorDataDataAttributes {
body: ModifiableData<number>;
mobility: ModifiableData<number>;
mind: ModifiableData<number>;
}
interface DS4ActorDataTraits {
interface DS4ActorDataDataTraits {
strength: ModifiableData<number>;
constitution: ModifiableData<number>;
agility: ModifiableData<number>;
@ -23,7 +34,7 @@ interface DS4ActorDataTraits {
aura: ModifiableData<number>;
}
interface DS4ActorDataCombatValues {
interface DS4ActorDataDataCombatValues {
hitPoints: ResourceData<number>;
defense: ModifiableData<number>;
initiative: ModifiableData<number>;
@ -34,34 +45,32 @@ interface DS4ActorDataCombatValues {
targetedSpellcasting: ModifiableData<number>;
}
interface DS4ActorDataCharacter extends DS4ActorDataBase {
baseInfo: DS4ActorDataCharacterBaseInfo;
progression: DS4ActorDataCharacterProgression;
language: DS4ActorDataCharacterLanguage;
profile: DS4ActorDataCharacterProfile;
currency: DS4ActorDataCharacterCurrency;
interface DS4CharacterDataData extends DS4ActorDataDataBase {
baseInfo: DS4CharacterDataDataBaseInfo;
progression: DS4CharacterDataDataProgression;
language: DS4CharacterDataDataLanguage;
profile: DS4CharacterDataDataProfile;
currency: DS4CharacterDataDataCurrency;
}
interface DS4ActorDataCharacterBaseInfo {
interface DS4CharacterDataDataBaseInfo {
race: string;
class: string;
heroClass: string;
culture: string;
}
interface DS4ActorDataCharacterProgression {
interface DS4CharacterDataDataProgression {
level: number;
experiencePoints: number;
talentPoints: UsableResource<number>;
progressPoints: UsableResource<number>;
}
interface DS4ActorDataCharacterLanguage {
interface DS4CharacterDataDataLanguage {
languages: string;
alphabets: string;
}
interface DS4ActorDataCharacterProfile {
interface DS4CharacterDataDataProfile {
biography: string;
gender: string;
birthday: string;
@ -74,21 +83,21 @@ interface DS4ActorDataCharacterProfile {
specialCharacteristics: string;
}
interface DS4ActorDataCharacterCurrency {
interface DS4CharacterDataDataCurrency {
gold: number;
silver: number;
copper: number;
}
interface DS4ActorDataCreature extends DS4ActorDataBase {
baseInfo: DS4ActorDataCreatureBaseInfo;
interface DS4CreatureDataData extends DS4ActorDataDataBase {
baseInfo: DS4CreatureDataDataBaseInfo;
}
type CreatureType = "animal" | "construct" | "humanoid" | "magicalEntity" | "plantBeing" | "undead";
type SizeCategory = "tiny" | "small" | "normal" | "large" | "huge" | "colossal";
interface DS4ActorDataCreatureBaseInfo {
interface DS4CreatureDataDataBaseInfo {
loot: string;
foeFactor: number;
creatureType: CreatureType;

View file

@ -1,15 +1,16 @@
import { ModifiableData } from "../common/common-data";
import { DS4 } from "../config";
import { DS4Item } from "../item/item";
import { DS4Armor, DS4EquippableItemDataType, DS4ItemDataType, DS4Shield, ItemType } from "../item/item-data";
import { DS4ActorDataType } from "./actor-data";
import { DS4ItemData, ItemType } from "../item/item-data";
import { DS4ActorData } from "./actor-data";
type DS4ActiveEffect = ActiveEffect<DS4ActorDataType, DS4ItemDataType, DS4Actor, DS4Item>;
export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item> {
/**
* The Actor class for DS4
*/
export class DS4Actor extends Actor<DS4ActorData, DS4Item> {
/** @override */
prepareData(): void {
this.data = duplicate(this._data);
this.data = duplicate(this._data) as DS4ActorData;
if (!this.data.img) this.data.img = CONST.DEFAULT_TOKEN;
if (!this.data.name) this.data.name = "New " + this.entity;
this.prepareBaseData();
@ -45,20 +46,23 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
* @param predicate The predicate that ActiveEffectChanges need to satisfy in order to be applied
*/
applyActiveEffectsFiltered(predicate: (change: ActiveEffectChange) => boolean): void {
const overrides = {};
const overrides: Record<string, unknown> = {};
// Organize non-disabled effects by their application priority
const changes = this.effects.reduce((changes: Array<ActiveEffectChange & { effect: DS4ActiveEffect }>, e) => {
if (e.data.disabled) return changes;
const changes = this.effects.reduce(
(changes: Array<ActiveEffectChange & { effect: ActiveEffect<DS4Actor> }>, e) => {
if (e.data.disabled) return changes;
return changes.concat(
e.data.changes.filter(predicate).map((c) => {
c = duplicate(c);
c.priority = c.priority ?? c.mode * 10;
return { ...c, effect: e };
}),
);
}, []);
return changes.concat(
e.data.changes.filter(predicate).map((c) => {
const duplicatedChange = duplicate(c) as ActiveEffect.Change;
duplicatedChange.priority = duplicatedChange.priority ?? duplicatedChange.mode * 10;
return { ...duplicatedChange, effect: e };
}),
);
},
[],
);
changes.sort((a, b) => a.priority - b.priority);
// Apply all changes
@ -68,7 +72,7 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
}
// Expand the set of final overrides
this["overrides"] = expandObject({ ...flattenObject(this["overrides"] ?? {}), ...overrides });
this.overrides = expandObject({ ...flattenObject(this.overrides ?? {}), ...overrides });
}
/** @override */
@ -78,7 +82,7 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
/** The list of properties that are derived from others, given in dot notation */
get derivedDataProperties(): Array<string> {
return Object.keys(DS4.combatValues)
return Object.keys(DS4.i18n.combatValues)
.map((combatValue) => `data.combatValues.${combatValue}.total`)
.concat("data.combatValues.hitPoints.max");
}
@ -110,7 +114,7 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
/**
* Checks whether or not the given item type can be owned by the actor.
* @param itemType the item type to check
* @param itemType - The item type to check
*/
canOwnItemType(itemType: ItemType): boolean {
return this.ownableItemTypes.includes(itemType);
@ -149,10 +153,13 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
*/
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)
.map((item) => {
if (item.data.type === "armor" || item.data.type === "shield") {
return item.data.data.equipped ? item.data.data.armorValue : 0;
} else {
return 0;
}
})
.reduce((a, b) => a + b, 0);
}
@ -161,7 +168,7 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
* This only differs from the base implementation by also allowing negative values.
* @override
*/
async modifyTokenAttribute(attribute: string, value: number, isDelta = false, isBar = true): Promise<DS4Actor> {
async modifyTokenAttribute(attribute: string, value: number, isDelta = false, isBar = true): Promise<this> {
const current = getProperty(this.data.data, attribute);
// Determine the updates to make to the actor data
@ -180,59 +187,77 @@ export class DS4Actor extends Actor<DS4ActorDataType, DS4ItemDataType, DS4Item>
}
/** @override */
// TODO(types): Improve typing once it's fixed in upstream (arrays can be passed!)
createEmbeddedEntity(
embeddedName: string,
createData: Record<string, unknown> | Array<Record<string, unknown>>,
embeddedName: "OwnedItem",
data: DeepPartial<DS4ItemData>,
options?: Record<string, unknown>,
): Promise<this> {
): Promise<DS4ItemData>;
createEmbeddedEntity(
embeddedName: "ActiveEffect",
data: DeepPartial<DS4ItemData>,
options?: Record<string, unknown>,
): Promise<ActiveEffect.Data>;
createEmbeddedEntity(
embeddedName: "OwnedItem" | "ActiveEffect",
data: DeepPartial<DS4ItemData> | DeepPartial<ActiveEffect.Data>,
options?: Record<string, unknown>,
): Promise<DS4ItemData> | Promise<ActiveEffect.Data> {
if (embeddedName === "OwnedItem") {
this._preCreateOwnedItem((createData as unknown) as ItemData<DS4ItemDataType>);
this._preCreateOwnedItem(data);
return super.createEmbeddedEntity(embeddedName, data, options);
}
return super.createEmbeddedEntity(embeddedName, createData, options);
return super.createEmbeddedEntity(embeddedName, data, options);
}
/**
* If the item that is going to be created is equippable, set it to be non equipped and disable all ActiveEffects
* If the item that is going to be created is equipable, set it to be non equipped and disable all ActiveEffects
* contained in the item
* @param itemData The data of the item to be created
*/
private _preCreateOwnedItem(itemData: ItemData<DS4ItemDataType>): void {
if ("equipped" in itemData.data) {
itemData.effects = itemData.effects.map((effect) => ({ ...effect, disabled: true }));
const equippableUpdateData = itemData as ItemData<DS4EquippableItemDataType>;
equippableUpdateData.data.equipped = false;
protected _preCreateOwnedItem(itemData: DeepPartial<DS4ItemData>): void {
if (itemData.data && "equipped" in itemData.data) {
itemData.effects = itemData.effects?.map((effect) => ({ ...effect, disabled: true }));
itemData.data.equipped = false;
}
}
/** @override */
// TODO(types): Improve typing once it's fixed in upstream
updateEmbeddedEntity(embeddedName: string, data: unknown[], options?: Entity.UpdateOptions): Promise<unknown[]>;
updateEmbeddedEntity(embeddedName: string, data: unknown, options?: Entity.UpdateOptions): Promise<unknown>;
updateEmbeddedEntity(
embeddedName: string,
updateData: Record<string, unknown> | Array<Record<string, unknown>>,
updateData: unknown | unknown[],
options?: Record<string, unknown>,
): Promise<this> {
): Promise<unknown> {
if (embeddedName === "OwnedItem") {
this._preUpdateOwnedItem(updateData as Partial<ItemData<DS4ItemDataType>>);
this._preUpdateOwnedItem(updateData as DeepPartial<DS4ItemData> | Array<DeepPartial<DS4ItemData>>);
}
return super.updateEmbeddedEntity(embeddedName, updateData, options);
}
/**
* If the equipped flag of an item changed, update all ActiveEffects originating from that item accordingly.
* @param updateData The change that is going to be applied to the owned item
* If the equipped flag of one or more items changed, update all ActiveEffects originating from those items
* accordingly.
* @param updateData The change that is going to be applied to the owned item(s)
*/
private _preUpdateOwnedItem(updateData: Partial<ItemData<DS4ItemDataType>>): void {
if ("equipped" in updateData.data) {
const equippableUpdateData = updateData as Partial<ItemData<DS4EquippableItemDataType>>;
const origin = `Actor.${this.id}.OwnedItem.${updateData._id}`;
const effects = this.effects
.filter((e) => e.data.origin === origin)
.map((e) => {
const data = duplicate(e.data);
data.disabled = !equippableUpdateData.data.equipped;
return data;
});
if (effects.length > 0)
this.updateEmbeddedEntity("ActiveEffect", (effects as unknown) as Record<string, unknown>);
}
private _preUpdateOwnedItem(updateData: DeepPartial<DS4ItemData> | Array<DeepPartial<DS4ItemData>>): void {
const dataArray = updateData instanceof Array ? updateData : [updateData];
dataArray.forEach((data) => {
if (data.data && "equipped" in data.data) {
const equipped = data.data.equipped;
const origin = `Actor.${this.id}.OwnedItem.${data._id}`;
const effects = this.effects
.filter((e) => e.data.origin === origin)
.map((e) => {
const effectData = duplicate(e.data);
effectData.disabled = !(equipped ?? true);
return effectData;
});
if (effects.length > 0)
this.updateEmbeddedEntity("ActiveEffect", (effects as unknown) as Record<string, unknown>);
}
});
}
}

View file

@ -1,20 +1,38 @@
import { DS4 } from "../../config";
import { DS4Item } from "../../item/item";
import { DS4ItemDataType, ItemType } from "../../item/item-data";
import { DS4ItemData } from "../../item/item-data";
import { DS4Actor } from "../actor";
import { DS4ActorDataType } from "../actor-data";
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
* The base Sheet class for all DS4 Actors
*/
export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4ItemDataType> {
export class DS4ActorSheet extends ActorSheet<ActorSheet.Data<DS4Actor>> {
// TODO(types): Improve mergeObject in upstream so that it isn't necessary to provide all parameters (see https://github.com/League-of-Foundry-Developers/foundry-vtt-types/issues/272)
/** @override */
static get defaultOptions(): FormApplicationOptions {
return mergeObject(super.defaultOptions, {
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
classes: ["ds4", "sheet", "actor"],
width: 745,
height: 600,
scrollY: [".sheet-body"],
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,
tabs: superDefaultOptions.tabs,
});
}
@ -24,27 +42,23 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
return `${path}/${this.actor.data.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/**
* This method returns the data for the template of the actor sheet.
* It explicitly adds the items of the object sorted by type in the
* object itemsByType.
* @returns the data fed to the template of the actor sheet
* @returns The data fed to the template of the actor sheet
*/
getData(): ActorSheetData<DS4ActorDataType, DS4Actor> {
async getData(): Promise<ActorSheet.Data<DS4Actor>> {
const data = {
...super.getData(),
...(await super.getData()),
// Add the localization config to the data:
config: CONFIG.DS4,
config: DS4,
// Add the items explicitly sorted by type to the data:
itemsByType: this.actor.itemTypes,
};
return data;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html: JQuery): void {
super.activateListeners(html);
@ -59,7 +73,7 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
html.find(".item-edit").on("click", (ev) => {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.getOwnedItem(li.data("itemId"));
item.sheet.render(true);
item.sheet?.render(true);
});
// Delete Inventory Item
@ -75,20 +89,16 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
html.find(".rollable").click(this._onRoll.bind(this));
}
/* -------------------------------------------- */
/**
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
* @param {JQuery.ClickEvent} event The originating click event
* @private
* @param event - The originating click event
*/
private _onItemCreate(event: JQuery.ClickEvent): Promise<Item> {
protected _onItemCreate(event: JQuery.ClickEvent): Promise<DS4ItemData> {
event.preventDefault();
const header = event.currentTarget;
// Get the type of item to create.
const type = header.dataset.type;
// Grab any data associated with this control.
const data = duplicate(header.dataset);
const { type, ...data } = duplicate(header.dataset);
// Initialize a default name.
const name = `New ${type.capitalize()}`;
// Prepare the item object.
@ -97,8 +107,6 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
type: type,
data: data,
};
// Remove the type from the dataset since it's in the itemData.type prop.
delete itemData.data.type;
// Finally, create the item!
return this.actor.createOwnedItem(itemData);
@ -108,15 +116,14 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
* Handle changes to properties of an Owned Item from within character sheet.
* Can currently properly bind: see getValue().
* Assumes the item property is given as the value of the HTML element property 'data-property'.
* @param {JQuery.ChangeEvent<HTMLFormElement>} ev The originating change event
* @private
* @param ev - The originating change event
*/
private _onItemChange(ev: JQuery.ChangeEvent<HTMLFormElement>): void {
protected _onItemChange(ev: JQuery.ChangeEvent): void {
ev.preventDefault();
console.log("Current target:", $(ev.currentTarget).get(0)["name"]);
const el: HTMLFormElement = $(ev.currentTarget).get(0);
const id = $(ev.currentTarget).parents(".item").data("itemId");
const item = duplicate(this.actor.getOwnedItem(id)); // getOwnedItem is typed incorrectly, it actually returns a ItemData<DS4ItemDataType>, not an Item
const item = duplicate<DS4Item, "lenient">(this.actor.getOwnedItem(id));
const property: string | undefined = $(ev.currentTarget).data("property");
// Early return:
@ -139,7 +146,7 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
* - Checkbox: boolean
* - Text input: string
* - Number: number
* @param el the input element to collect the value of
* @param el - The input element to collect the value of
*/
private getValue(el: HTMLFormElement): boolean | string | number {
// One needs to differentiate between e.g. checkboxes (value="on") and select boxes etc.
@ -190,10 +197,9 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
/**
* Handle clickable rolls.
* @param {JQuery.ClickEvent} event The originating click event
* @private
* @param event - The originating click event
*/
private _onRoll(event: JQuery.ClickEvent): void {
protected _onRoll(event: JQuery.ClickEvent): void {
event.preventDefault();
const element = event.currentTarget;
const dataset = element.dataset;
@ -209,10 +215,13 @@ export class DS4ActorSheet extends ActorSheet<DS4ActorDataType, DS4Actor, DS4Ite
}
/** @override */
async _onDropItem(event: DragEvent, data: Parameters<typeof DS4Item.fromDropData>[0]): Promise<unknown> {
const item = await Item.fromDropData(data);
if (item && !this.actor.canOwnItemType(item.data.type as ItemType)) {
ui.notifications.warn(
protected async _onDropItem(
event: DragEvent,
data: { type: "Item" } & (DeepPartial<ActorSheet.OwnedItemData<DS4Actor>> | { pack: string } | { id: string }),
): Promise<boolean | undefined | ActorSheet.OwnedItemData<DS4Actor>> {
const item = ((await Item.fromDropData(data)) as unknown) as DS4Item;
if (item && !this.actor.canOwnItemType(item.data.type)) {
ui.notifications?.warn(
game.i18n.format("DS4.WarningActorCannotOwnItem", {
actorName: this.actor.name,
actorType: this.actor.data.type,

View file

@ -1,11 +1,34 @@
import { DS4ActorSheet } from "./actor-sheet";
/**
* The Sheet class for DS4 Character Actors
*/
export class DS4CharacterActorSheet extends DS4ActorSheet {
/** @override */
static get defaultOptions(): FormApplicationOptions {
return mergeObject(super.defaultOptions, {
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
classes: ["ds4", "sheet", "actor", "character"],
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "inventory" }],
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,
width: superDefaultOptions.width,
height: superDefaultOptions.height,
scrollY: superDefaultOptions.scrollY,
});
}
}

View file

@ -1,11 +1,34 @@
import { DS4ActorSheet } from "./actor-sheet";
/**
* The Sheet class for DS4 Creature Actors
*/
export class DS4CreatureActorSheet extends DS4ActorSheet {
/** @override */
static get defaultOptions(): FormApplicationOptions {
return mergeObject(super.defaultOptions, {
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
classes: ["ds4", "sheet", "actor", "creature"],
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "inventory" }],
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,
width: superDefaultOptions.width,
height: superDefaultOptions.height,
scrollY: superDefaultOptions.scrollY,
});
}
}

View file

@ -0,0 +1,37 @@
/**
* Partition an array into two, following a predicate.
* @param input - The Array to split.
* @param predicate - The predicate by which to split.
* @returns A tuple of two arrays, the first one containing all elements from `input` that match the predicate, the second one containing those that do not.
*/
export function partition<T>(input: Array<T>, predicate: (v: T) => boolean): [T[], T[]] {
return input.reduce(
(p: [Array<T>, Array<T>], cur: T) => {
if (predicate(cur)) {
p[0].push(cur);
} else {
p[1].push(cur);
}
return p;
},
[[], []],
);
}
/**
* Zips two Arrays to an array of pairs of elements with corresponding indices. Excessive elements are dropped.
* @param a1 - First array to zip.
* @param a2 - Second array to zip.
*
* @typeParam T - Type of elements contained in `a1`.
* @typeParam U - Type of elements contained in `a2`.
*
* @returns The array of pairs that had the same index in their source array.
*/
export function zip<T, U>(a1: Array<T>, a2: Array<U>): Array<[T, U]> {
if (a1.length <= a2.length) {
return a1.map((e1, i) => [e1, a2[i]]);
} else {
return a2.map((e2, i) => [a1[i], e2]);
}
}

View file

@ -9,205 +9,310 @@ export const DS4 = {
=============================================================================================`,
/**
* Define the set of acttack types that can be performed with weapon items
* A dictionary of dictionaries each mapping keys to localized strings
* resp. their localization keys.
* The localization is assumed to take place on each reload.
*/
attackTypes: {
melee: "DS4.AttackTypeMelee",
ranged: "DS4.AttackTypeRanged",
meleeRanged: "DS4.AttackTypeMeleeRanged",
i18n: {
/**
* Define the set of acttack types that can be performed with weapon items
*/
attackTypes: {
melee: "DS4.AttackTypeMelee",
ranged: "DS4.AttackTypeRanged",
meleeRanged: "DS4.AttackTypeMeleeRanged",
},
/**
* Define the set of item availabilties
*/
itemAvailabilities: {
unset: "DS4.ItemAvailabilityUnset",
hamlet: "DS4.ItemAvailabilityHamlet",
village: "DS4.ItemAvailabilityVilage",
city: "DS4.ItemAvailabilityCity",
elves: "DS4.ItemAvailabilityElves",
dwarves: "DS4.ItemAvailabilityDwarves",
nowhere: "DS4.ItemAvailabilityNowhere",
},
/**
* Define the set of item types
*/
itemTypes: {
weapon: "DS4.ItemTypeWeapon",
armor: "DS4.ItemTypeArmor",
shield: "DS4.ItemTypeShield",
spell: "DS4.ItemTypeSpell",
trinket: "DS4.ItemTypeTrinket",
equipment: "DS4.ItemTypeEquipment",
talent: "DS4.ItemTypeTalent",
racialAbility: "DS4.ItemTypeRacialAbility",
language: "DS4.ItemTypeLanguage",
alphabet: "DS4.ItemTypeAlphabet",
specialCreatureAbility: "DS4.ItemTypeSpecialCreatureAbility",
},
/**
* Define the set of armor types, a character may only wear one item of each at any given time
*/
armorTypes: {
body: "DS4.ArmorTypeBody",
helmet: "DS4.ArmorTypeHelmet",
vambrace: "DS4.ArmorTypeVambrace",
greaves: "DS4.ArmorTypeGreaves",
vambraceGreaves: "DS4.ArmorTypeVambraceGreaves",
},
/**
* Define abbreviations for the armor types
*/
armorTypesAbbr: {
body: "DS4.ArmorTypeBodyAbbr",
helmet: "DS4.ArmorTypeHelmetAbbr",
vambrace: "DS4.ArmorTypeVambraceAbbr",
greaves: "DS4.ArmorTypeGreavesAbbr",
vambraceGreaves: "DS4.ArmorTypeVambraceGreavesAbbr",
},
/**
* Define the set of armor materials, used to determine if a character may wear the armor without additional penalties
*/
armorMaterialTypes: {
cloth: "DS4.ArmorMaterialTypeCloth",
leather: "DS4.ArmorMaterialTypeLeather",
chain: "DS4.ArmorMaterialTypeChain",
plate: "DS4.ArmorMaterialTypePlate",
},
/**
* Define the abbreviations of armor materials
*/
armorMaterialTypesAbbr: {
cloth: "DS4.ArmorMaterialTypeClothAbbr",
leather: "DS4.ArmorMaterialTypeLeatherAbbr",
chain: "DS4.ArmorMaterialTypeChainAbbr",
plate: "DS4.ArmorMaterialTypePlateAbbr",
},
spellTypes: {
spellcasting: "DS4.SpellTypeSpellcasting",
targetedSpellcasting: "DS4.SpellTypeTargetedSpellcasting",
},
spellCategories: {
healing: "DS4.SpellCategoryHealing",
fire: "DS4.SpellCategoryFire",
ice: "DS4.SpellCategoryIce",
light: "DS4.SpellCategoryLight",
darkness: "DS4.SpellCategoryDarkness",
mindAffecting: "DS4.SpellCategoryMindAffecting",
electricity: "DS4.SpellCategoryElectricity",
none: "DS4.SpellCategoryNone",
unset: "DS4.SpellCategoryUnset",
},
/**
* Define the set of actor types
*/
actorTypes: {
character: "DS4.ActorTypeCharacter",
creature: "DS4.ActorTypeCreature",
},
/**
* Define the set of attributes an actor has
*/
attributes: {
body: "DS4.AttributeBody",
mobility: "DS4.AttributeMobility",
mind: "DS4.AttributeMind",
},
/**
* Define the set of traits an actor has
*/
traits: {
strength: "DS4.TraitStrength",
agility: "DS4.TraitAgility",
intellect: "DS4.TraitIntellect",
constitution: "DS4.TraitConstitution",
dexterity: "DS4.TraitDexterity",
aura: "DS4.TraitAura",
},
/**
* Define the set of combat values an actor has
*/
combatValues: {
hitPoints: "DS4.CombatValuesHitPoints",
defense: "DS4.CombatValuesDefense",
initiative: "DS4.CombatValuesInitiative",
movement: "DS4.CombatValuesMovement",
meleeAttack: "DS4.CombatValuesMeleeAttack",
rangedAttack: "DS4.CombatValuesRangedAttack",
spellcasting: "DS4.CombatValuesSpellcasting",
targetedSpellcasting: "DS4.CombatValuesTargetedSpellcasting",
},
/**
* Define the base info of a character
*/
characterBaseInfo: {
race: "DS4.CharacterBaseInfoRace",
class: "DS4.CharacterBaseInfoClass",
heroClass: "DS4.CharacterBaseInfoHeroClass",
culture: "DS4.CharacterBaseInfoCulture",
},
/**
* Define the progression info of a character
*/
characterProgression: {
level: "DS4.CharacterProgressionLevel",
experiencePoints: "DS4.CharacterProgressionExperiencePoints",
talentPoints: "DS4.CharacterProgressionTalentPoints",
progressPoints: "DS4.CharacterProgressionProgressPoints",
},
/**
* Define the language info of a character
*/
characterLanguage: {
languages: "DS4.CharacterLanguageLanguages",
alphabets: "DS4.CharacterLanguageAlphabets",
},
/**
* Define the profile info of a character
*/
characterProfile: {
biography: "DS4.CharacterProfileBiography",
gender: "DS4.CharacterProfileGender",
birthday: "DS4.CharacterProfileBirthday",
birthplace: "DS4.CharacterProfileBirthplace",
age: "DS4.CharacterProfileAge",
height: "DS4.CharacterProfileHeight",
hairColor: "DS4.CharacterProfileHairColor",
weight: "DS4.CharacterProfileWeight",
eyeColor: "DS4.CharacterProfileEyeColor",
specialCharacteristics: "DS4.CharacterProfileSpecialCharacteristics",
},
/**
* Define currency elements of a character
*/
characterCurrency: {
gold: "DS4.CharacterCurrencyGold",
silver: "DS4.CharacterCurrencySilver",
copper: "DS4.CharacterCurrencyCopper",
},
/**
* Define the different creature types a creature can be
*/
creatureTypes: {
animal: "DS4.CreatureTypeAnimal",
construct: "DS4.CreatureTypeConstruct",
humanoid: "DS4.CreatureTypeHumanoid",
magicalEntity: "DS4.CreatureTypeMagicalEntity",
plantBeing: "DS4.CreatureTypePlantBeing",
undead: "DS4.CreatureTypeUndead",
},
/**
* Define the different size categories creatures fall into
*/
creatureSizeCategories: {
tiny: "DS4.CreatureSizeCategoryTiny",
small: "DS4.CreatureSizeCategorySmall",
normal: "DS4.CreatureSizeCategoryNormal",
large: "DS4.CreatureSizeCategoryLarge",
huge: "DS4.CreatureSizeCategoryHuge",
colossal: "DS4.CreatureSizeCategoryColossal",
},
/**
* Define the base info of a creature
*/
creatureBaseInfo: {
loot: "DS4.CreatureBaseInfoLoot",
foeFactor: "DS4.CreatureBaseInfoFoeFactor",
creatureType: "DS4.CreatureBaseInfoCreatureType",
sizeCategory: "DS4.CreatureBaseInfoSizeCategory",
experiencePoints: "DS4.CreatureBaseInfoExperiencePoints",
description: "DS4.CreatureBaseInfoDescription",
},
/**
* Define translations for available distance units
*/
distanceUnits: {
meter: "DS4.UnitMeters",
kilometer: "DS4.UnitKilometers",
custom: "DS4.UnitCustom",
},
/**
* Define abbreviations for available distance units
*/
distanceUnitsAbbr: {
meter: "DS4.UnitMetersAbbr",
kilometer: "DS4.UnitKilometersAbbr",
custom: "DS4.UnitCustomAbbr",
},
/**
* Define translations for available distance units
*/
temporalUnits: {
rounds: "DS4.UnitRounds",
minutes: "DS4.UnitMinutes",
hours: "DS4.UnitHours",
days: "DS4.UnitDays",
custom: "DS4.UnitCustom",
},
/**
* Define abbreviations for available units
*/
temporalUnitsAbbr: {
rounds: "DS4.UnitRoundsAbbr",
minutes: "DS4.UnitMinutesAbbr",
hours: "DS4.UnitHoursAbbr",
days: "DS4.UnitDaysAbbr",
custom: "DS4.UnitCustomAbbr",
},
/**
* Define localization strings for Chat Visibility
*/
chatVisibilities: {
roll: "DS4.ChatVisibilityRoll",
gmroll: "DS4.ChatVisibilityGmRoll",
blindroll: "DS4.ChatVisibilityBlindRoll",
selfroll: "DS4.ChatVisibilitySelfRoll",
},
},
/**
* Define the file paths to icon images
* A dictionary of dictionaries mapping keys to icon file paths.
*/
attackTypesIcons: {
melee: "systems/ds4/assets/icons/official/combat-values/melee-attack.png",
meleeRanged: "systems/ds4/assets/icons/official/combat-values/melee-ranged-attack.png",
ranged: "systems/ds4/assets/icons/official/combat-values/ranged-attack.png",
},
icons: {
/**
* Define the file paths to icon images
*/
attackTypes: {
melee: "systems/ds4/assets/icons/official/combat-values/melee-attack.png",
meleeRanged: "systems/ds4/assets/icons/official/combat-values/melee-ranged-attack.png",
ranged: "systems/ds4/assets/icons/official/combat-values/ranged-attack.png",
},
/**
* Define the file paths to icon images
*/
spellTypesIcons: {
spellcasting: "systems/ds4/assets/icons/official/combat-values/spellcasting.png",
targetedSpellcasting: "systems/ds4/assets/icons/official/combat-values/targeted-spellcasting.png",
},
/**
* Define the set of item availabilties
*/
itemAvailabilities: {
unset: "DS4.ItemAvailabilityUnset",
hamlet: "DS4.ItemAvailabilityHamlet",
village: "DS4.ItemAvailabilityVilage",
city: "DS4.ItemAvailabilityCity",
elves: "DS4.ItemAvailabilityElves",
dwarves: "DS4.ItemAvailabilityDwarves",
nowhere: "DS4.ItemAvailabilityNowhere",
},
/**
* Define the set of item types
*/
itemTypes: {
weapon: "DS4.ItemTypeWeapon",
armor: "DS4.ItemTypeArmor",
shield: "DS4.ItemTypeShield",
spell: "DS4.ItemTypeSpell",
trinket: "DS4.ItemTypeTrinket",
equipment: "DS4.ItemTypeEquipment",
talent: "DS4.ItemTypeTalent",
racialAbility: "DS4.ItemTypeRacialAbility",
language: "DS4.ItemTypeLanguage",
alphabet: "DS4.ItemTypeAlphabet",
specialCreatureAbility: "DS4.ItemTypeSpecialCreatureAbility",
},
/**
* Define the set of armor types, a character may only wear one item of each at any given time
*/
armorTypes: {
body: "DS4.ArmorTypeBody",
helmet: "DS4.ArmorTypeHelmet",
vambrace: "DS4.ArmorTypeVambrace",
greaves: "DS4.ArmorTypeGreaves",
vambraceGreaves: "DS4.ArmorTypeVambraceGreaves",
},
/**
* Define abbreviations for the armor types
*/
armorTypesAbbr: {
body: "DS4.ArmorTypeBodyAbbr",
helmet: "DS4.ArmorTypeHelmetAbbr",
vambrace: "DS4.ArmorTypeVambraceAbbr",
greaves: "DS4.ArmorTypeGreavesAbbr",
vambraceGreaves: "DS4.ArmorTypeVambraceGreavesAbbr",
},
/**
* Define the set of armor materials, used to determine if a characer may wear the armor without additional penalties
*/
armorMaterialTypes: {
cloth: "DS4.ArmorMaterialTypeCloth",
leather: "DS4.ArmorMaterialTypeLeather",
chain: "DS4.ArmorMaterialTypeChain",
plate: "DS4.ArmorMaterialTypePlate",
},
/**
* Define the abbreviations of armor materials
*/
armorMaterialTypesAbbr: {
cloth: "DS4.ArmorMaterialTypeClothAbbr",
leather: "DS4.ArmorMaterialTypeLeatherAbbr",
chain: "DS4.ArmorMaterialTypeChainAbbr",
plate: "DS4.ArmorMaterialTypePlateAbbr",
},
spellTypes: {
spellcasting: "DS4.SpellTypeSpellcasting",
targetedSpellcasting: "DS4.SpellTypeTargetedSpellcasting",
},
spellCategories: {
healing: "DS4.SpellCategoryHealing",
fire: "DS4.SpellCategoryFire",
ice: "DS4.SpellCategoryIce",
light: "DS4.SpellCategoryLight",
darkness: "DS4.SpellCategoryDarkness",
mindAffecting: "DS4.SpellCategoryMindAffecting",
electricity: "DS4.SpellCategoryElectricity",
none: "DS4.SpellCategoryNone",
unset: "DS4.SpellCategoryUnset",
},
/**
* Define the set of actor types
*/
actorTypes: {
character: "DS4.ActorTypeCharacter",
creature: "DS4.ActorTypeCreature",
},
/**
* Define the set of attributes an actor has
*/
attributes: {
body: "DS4.AttributeBody",
mobility: "DS4.AttributeMobility",
mind: "DS4.AttributeMind",
},
/**
* Define the set of traits an actor has
*/
traits: {
strength: "DS4.TraitStrength",
agility: "DS4.TraitAgility",
intellect: "DS4.TraitIntellect",
constitution: "DS4.TraitConstitution",
dexterity: "DS4.TraitDexterity",
aura: "DS4.TraitAura",
},
/**
* Define the set of combat values an actor has
*/
combatValues: {
hitPoints: "DS4.CombatValuesHitPoints",
defense: "DS4.CombatValuesDefense",
initiative: "DS4.CombatValuesInitiative",
movement: "DS4.CombatValuesMovement",
meleeAttack: "DS4.CombatValuesMeleeAttack",
rangedAttack: "DS4.CombatValuesRangedAttack",
spellcasting: "DS4.CombatValuesSpellcasting",
targetedSpellcasting: "DS4.CombatValuesTargetedSpellcasting",
},
/**
* Define the base info of a character
*/
characterBaseInfo: {
race: "DS4.CharacterBaseInfoRace",
class: "DS4.CharacterBaseInfoClass",
heroClass: "DS4.CharacterBaseInfoHeroClass",
culture: "DS4.CharacterBaseInfoCulture",
},
/**
* Define the progression info of a character
*/
characterProgression: {
level: "DS4.CharacterProgressionLevel",
experiencePoints: "DS4.CharacterProgressionExperiencePoints",
talentPoints: "DS4.CharacterProgressionTalentPoints",
progressPoints: "DS4.CharacterProgressionProgressPoints",
},
/**
* Define the language info of a character
*/
characterLanguage: {
languages: "DS4.CharacterLanguageLanguages",
alphabets: "DS4.CharacterLanguageAlphabets",
},
/**
* Define the profile info of a character
*/
characterProfile: {
biography: "DS4.CharacterProfileBiography",
gender: "DS4.CharacterProfileGender",
birthday: "DS4.CharacterProfileBirthday",
birthplace: "DS4.CharacterProfileBirthplace",
age: "DS4.CharacterProfileAge",
height: "DS4.CharacterProfileHeight",
hairColor: "DS4.CharacterProfileHairColor",
weight: "DS4.CharacterProfileWeight",
eyeColor: "DS4.CharacterProfileEyeColor",
specialCharacteristics: "DS4.CharacterProfileSpecialCharacteristics",
/**
* Define the file paths to icon images
*/
spellTypes: {
spellcasting: "systems/ds4/assets/icons/official/combat-values/spellcasting.png",
targetedSpellcasting: "systems/ds4/assets/icons/official/combat-values/targeted-spellcasting.png",
},
},
/**
@ -225,98 +330,4 @@ export const DS4 = {
eyeColor: "String",
specialCharacteristics: "String",
},
/**
* Define currency elements of a character
*/
characterCurrency: {
gold: "DS4.CharacterCurrencyGold",
silver: "DS4.CharacterCurrencySilver",
copper: "DS4.CharacterCurrencyCopper",
},
/**
* Define the different creature types a creature can be
*/
creatureTypes: {
animal: "DS4.CreatureTypeAnimal",
construct: "DS4.CreatureTypeConstruct",
humanoid: "DS4.CreatureTypeHumanoid",
magicalEntity: "DS4.CreatureTypeMagicalEntity",
plantBeing: "DS4.CreatureTypePlantBeing",
undead: "DS4.CreatureTypeUndead",
},
/**
* Define the different size categories creatures fall into
*/
creatureSizeCategories: {
tiny: "DS4.CreatureSizeCategoryTiny",
small: "DS4.CreatureSizeCategorySmall",
normal: "DS4.CreatureSizeCategoryNormal",
large: "DS4.CreatureSizeCategoryLarge",
huge: "DS4.CreatureSizeCategoryHuge",
colossal: "DS4.CreatureSizeCategoryColossal",
},
/**
* Define the base info of a creature
*/
creatureBaseInfo: {
loot: "DS4.CreatureBaseInfoLoot",
foeFactor: "DS4.CreatureBaseInfoFoeFactor",
creatureType: "DS4.CreatureBaseInfoCreatureType",
sizeCategory: "DS4.CreatureBaseInfoSizeCategory",
experiencePoints: "DS4.CreatureBaseInfoExperiencePoints",
description: "DS4.CreatureBaseInfoDescription",
},
/**
* Define translations for available distance units
*/
distanceUnits: {
meter: "DS4.UnitMeters",
kilometer: "DS4.UnitKilometers",
custom: "DS4.UnitCustom",
},
/**
* Define abbreviations for available distance units
*/
distanceUnitsAbbr: {
meter: "DS4.UnitMetersAbbr",
kilometer: "DS4.UnitKilometersAbbr",
custom: "DS4.UnitCustomAbbr",
},
/**
* Define translations for available distance units
*/
temporalUnits: {
rounds: "DS4.UnitRounds",
minutes: "DS4.UnitMinutes",
hours: "DS4.UnitHours",
days: "DS4.UnitDays",
custom: "DS4.UnitCustom",
},
/**
* Define abbreviations for available units
*/
temporalUnitsAbbr: {
rounds: "DS4.UnitRoundsAbbr",
minutes: "DS4.UnitMinutesAbbr",
hours: "DS4.UnitHoursAbbr",
days: "DS4.UnitDaysAbbr",
custom: "DS4.UnitCustomAbbr",
},
/**
* Define localization strings for Chat Visibility
*/
chatVisibilities: {
roll: "DS4.ChatVisibilityRoll",
gmroll: "DS4.ChatVisibilityGmRoll",
blindroll: "DS4.ChatVisibilityBlindRoll",
selfroll: "DS4.ChatVisibilitySelfRoll",
},
};

View file

@ -1,4 +1,3 @@
// Import Modules
import { DS4Actor } from "./actor/actor";
import { DS4Item } from "./item/item";
import { DS4ItemSheet } from "./item/item-sheet";
@ -10,7 +9,7 @@ import { createCheckRoll } from "./rolls/check-factory";
import { registerSystemSettings } from "./settings";
import { migration } from "./migrations";
Hooks.once("init", async function () {
Hooks.once("init", async () => {
console.log(`DS4 | Initializing the DS4 Game System\n${DS4.ASCII}`);
game.ds4 = {
@ -21,29 +20,19 @@ Hooks.once("init", async function () {
migration,
};
// Record configuration
CONFIG.DS4 = DS4;
// Define custom Entity classes
CONFIG.Actor.entityClass = DS4Actor as typeof Actor;
CONFIG.Item.entityClass = DS4Item as typeof Item;
CONFIG.Actor.entityClass = DS4Actor;
CONFIG.Item.entityClass = DS4Item;
// Define localized type labels
CONFIG.Actor.typeLabels = DS4.actorTypes;
CONFIG.Item.typeLabels = DS4.itemTypes;
CONFIG.Actor.typeLabels = DS4.i18n.actorTypes;
CONFIG.Item.typeLabels = DS4.i18n.itemTypes;
// Configure Dice
CONFIG.Dice.types = [Die, DS4Check];
CONFIG.Dice.terms = {
c: Coin,
d: Die,
s: DS4Check,
};
CONFIG.Dice.types.push(DS4Check);
CONFIG.Dice.terms.s = DS4Check;
// Register system settings
registerSystemSettings();
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("ds4", DS4CharacterActorSheet, { types: ["character"], makeDefault: true });
Actors.registerSheet("ds4", DS4CreatureActorSheet, { types: ["creature"], makeDefault: true });
@ -69,67 +58,59 @@ async function registerHandlebarsPartials() {
"systems/ds4/templates/actor/partials/combat-values.hbs",
"systems/ds4/templates/actor/partials/profile.hbs",
"systems/ds4/templates/actor/partials/character-progression.hbs",
"systems/ds4/templates/actor/partials/special-creature-abilites-overview.hbs",
"systems/ds4/templates/actor/partials/special-creature-abilities-overview.hbs",
"systems/ds4/templates/actor/partials/character-inventory.hbs",
"systems/ds4/templates/actor/partials/creature-inventory.hbs",
];
return loadTemplates(templatePaths);
}
/* -------------------------------------------- */
/* Foundry VTT Setup */
/* -------------------------------------------- */
/**
* This function runs after game data has been requested and loaded from the servers, so entities exist
*/
Hooks.once("setup", function () {
// Localize CONFIG objects once up-front
const toLocalize = [
"attackTypes",
"itemAvailabilities",
"itemTypes",
"armorTypes",
"armorTypesAbbr",
"armorMaterialTypes",
"armorMaterialTypesAbbr",
"armorMaterialTypes",
"spellTypes",
"spellCategories",
"attributes",
"traits",
"combatValues",
"characterBaseInfo",
"characterProgression",
"characterLanguage",
"characterProfile",
"characterCurrency",
"creatureTypes",
"creatureSizeCategories",
"creatureBaseInfo",
"temporalUnits",
"temporalUnitsAbbr",
"distanceUnits",
"distanceUnitsAbbr",
"chatVisibilities",
];
// Exclude some from sorting where the default order matters
const noSort = ["attributes", "traits", "combatValues", "creatureSizeCategories"];
// Localize and sort CONFIG objects
for (const o of toLocalize) {
const localized = Object.entries(CONFIG.DS4[o]).map((e) => {
return [e[0], game.i18n.localize(e[1] as string)];
});
if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1]));
CONFIG.DS4[o] = localized.reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {});
}
Hooks.once("setup", () => {
localizeAndSortConfigObjects();
});
Hooks.once("ready", function () {
Hooks.once("ready", () => {
migration.migrate();
});
/**
* Select the text of input elements in given sheets via onfocus listener.
* The hook names are of the form "render"+sheet_superclassname and are called within
* the render() method of the foundry Application class.
* Note: The render hooks of all classes in the class hierarchy are called,
* so e.g. for a Dialog, both "renderDialog" and "renderApplication" are called
* (in this order).
*/
["renderApplication", "renderActorSheet", "renderItemSheet"].forEach((hookName: string) => {
Hooks.on(hookName, (app: Dialog, html: JQueryStatic) => {
$(html)
.find("input")
.on("focus", (ev: JQuery.FocusEvent<HTMLInputElement>) => {
ev.currentTarget.select();
});
});
});
/**
* Localizes all objects in {@link DS4.i18n} and sorts them unless they are explicitly excluded.
*/
function localizeAndSortConfigObjects() {
const noSort = ["attributes", "traits", "combatValues", "creatureSizeCategories"];
const localizeObject = <T extends { [s: string]: string }>(obj: T, sort = true): T => {
const localized = Object.entries(obj).map(([key, value]) => {
return [key, game.i18n.localize(value)];
});
if (sort) localized.sort((a, b) => a[1].localeCompare(b[1]));
return Object.fromEntries(localized);
};
DS4.i18n = Object.fromEntries(
Object.entries(DS4.i18n).map(([key, value]) => {
return [key, localizeObject(value, !noSort.includes(key))];
}),
) as typeof DS4.i18n;
}

View file

@ -1,37 +1,53 @@
import { ModifiableData } from "../common/common-data";
import { DS4 } from "../config";
export type ItemType = keyof typeof DS4.itemTypes;
export type ItemType = keyof typeof DS4.i18n.itemTypes;
export type DS4ItemDataType =
| DS4Weapon
| DS4Armor
| DS4Shield
| DS4Spell
| DS4Trinket
| DS4Equipment
| DS4Talent
| DS4RacialAbility
| DS4Language
| DS4Alphabet
| DS4SpecialCreatureAbility;
export type DS4ItemData =
| DS4WeaponData
| DS4ArmorData
| DS4ShieldData
| DS4SpellData
| DS4TrinketData
| DS4EquipmentData
| DS4TalentData
| DS4RacialAbilityData
| DS4LanguageData
| DS4AlphabetData
| DS4SpecialCreatureAbilityData;
export type DS4EquippableItemDataType = DS4Weapon | DS4Armor | DS4Shield | DS4Trinket;
interface DS4ItemDataHelper<T, U extends ItemType> extends Item.Data<T> {
type: U;
}
// types
type DS4WeaponData = DS4ItemDataHelper<DS4WeaponDataData, "weapon">;
type DS4ArmorData = DS4ItemDataHelper<DS4ArmorDataData, "armor">;
type DS4ShieldData = DS4ItemDataHelper<DS4ShieldDataData, "shield">;
type DS4SpellData = DS4ItemDataHelper<DS4SpellDataData, "spell">;
type DS4TrinketData = DS4ItemDataHelper<DS4TrinketDataData, "trinket">;
type DS4EquipmentData = DS4ItemDataHelper<DS4EquipmentDataData, "equipment">;
type DS4TalentData = DS4ItemDataHelper<DS4TalentDataData, "talent">;
type DS4RacialAbilityData = DS4ItemDataHelper<DS4RacialAbilityDataData, "racialAbility">;
type DS4LanguageData = DS4ItemDataHelper<DS4LanguageDataData, "language">;
type DS4AlphabetData = DS4ItemDataHelper<DS4AlphabetDataData, "alphabet">;
type DS4SpecialCreatureAbilityData = DS4ItemDataHelper<DS4SpecialCreatureAbilityDataData, "specialCreatureAbility">;
interface DS4Weapon extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable {
interface DS4WeaponDataData extends DS4ItemDataDataBase, DS4ItemDataDataPhysical, DS4ItemDataDataEquipable {
attackType: "melee" | "ranged" | "meleeRanged";
weaponBonus: number;
opponentDefense: number;
}
export interface DS4Armor extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable, DS4ItemProtective {
interface DS4ArmorDataData
extends DS4ItemDataDataBase,
DS4ItemDataDataPhysical,
DS4ItemDataDataEquipable,
DS4ItemDataDataProtective {
armorMaterialType: "cloth" | "leather" | "chain" | "plate";
armorType: "body" | "helmet" | "vambrace" | "greaves" | "vambraceGreaves";
}
export interface DS4Talent extends DS4ItemBase {
interface DS4TalentDataData extends DS4ItemDataDataBase {
rank: DS4TalentRank;
}
@ -39,7 +55,7 @@ interface DS4TalentRank extends ModifiableData<number> {
max: number;
}
interface DS4Spell extends DS4ItemBase, DS4ItemEquipable {
interface DS4SpellDataData extends DS4ItemDataDataBase, DS4ItemDataDataEquipable {
spellType: "spellcasting" | "targetedSpellcasting";
bonus: string;
spellCategory:
@ -59,37 +75,41 @@ interface DS4Spell extends DS4ItemBase, DS4ItemEquipable {
scrollPrice: number;
}
export interface DS4Shield extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable, DS4ItemProtective {}
interface DS4Trinket extends DS4ItemBase, DS4ItemPhysical, DS4ItemEquipable {}
interface DS4Equipment extends DS4ItemBase, DS4ItemPhysical {}
type DS4RacialAbility = DS4ItemBase;
type DS4Language = DS4ItemBase;
type DS4Alphabet = DS4ItemBase;
interface DS4SpecialCreatureAbility extends DS4ItemBase {
interface DS4ShieldDataData
extends DS4ItemDataDataBase,
DS4ItemDataDataPhysical,
DS4ItemDataDataEquipable,
DS4ItemDataDataProtective {}
interface DS4TrinketDataData extends DS4ItemDataDataBase, DS4ItemDataDataPhysical, DS4ItemDataDataEquipable {}
interface DS4EquipmentDataData extends DS4ItemDataDataBase, DS4ItemDataDataPhysical {}
type DS4RacialAbilityDataData = DS4ItemDataDataBase;
type DS4LanguageDataData = DS4ItemDataDataBase;
type DS4AlphabetDataData = DS4ItemDataDataBase;
interface DS4SpecialCreatureAbilityDataData extends DS4ItemDataDataBase {
experiencePoints: number;
}
// templates
interface DS4ItemBase {
interface DS4ItemDataDataBase {
description: string;
}
interface DS4ItemPhysical {
interface DS4ItemDataDataPhysical {
quantity: number;
price: number;
availability: "hamlet" | "village" | "city" | "elves" | "dwarves" | "nowhere" | "unset";
storageLocation: string;
}
export function isDS4ItemDataTypePhysical(input: DS4ItemDataType): boolean {
export function isDS4ItemDataTypePhysical(input: DS4ItemData["data"]): boolean {
return "quantity" in input && "price" in input && "availability" in input && "storageLocation" in input;
}
interface DS4ItemEquipable {
interface DS4ItemDataDataEquipable {
equipped: boolean;
}
interface DS4ItemProtective {
interface DS4ItemDataDataProtective {
armorValue: number;
}

View file

@ -1,19 +1,36 @@
import { DS4 } from "../config";
import { DS4Item } from "./item";
import { DS4ItemDataType, isDS4ItemDataTypePhysical } from "./item-data";
import { isDS4ItemDataTypePhysical } from "./item-data";
/**
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
* The Sheet class for DS4 Items
*/
export class DS4ItemSheet extends ItemSheet<DS4ItemDataType, DS4Item> {
export class DS4ItemSheet extends ItemSheet<ItemSheet.Data<DS4Item>> {
/** @override */
static get defaultOptions(): FormApplicationOptions {
return mergeObject(super.defaultOptions, {
static get defaultOptions(): BaseEntitySheet.Options {
const superDefaultOptions = super.defaultOptions;
return mergeObject(superDefaultOptions, {
width: 530,
height: 400,
classes: ["ds4", "sheet", "item"],
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }],
scrollY: [".sheet-body"],
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,
});
}
@ -23,13 +40,11 @@ export class DS4ItemSheet extends ItemSheet<DS4ItemDataType, DS4Item> {
return `${path}/${this.item.data.type}-sheet.hbs`;
}
/* -------------------------------------------- */
/** @override */
getData(): ItemSheetData<DS4ItemDataType, DS4Item> {
async getData(): Promise<ItemSheet.Data<DS4Item>> {
const data = {
...super.getData(),
config: CONFIG.DS4,
...(await super.getData()),
config: DS4,
isOwned: this.item.isOwned,
actor: this.item.actor,
isPhysical: isDS4ItemDataTypePhysical(this.item.data.data),
@ -37,10 +52,8 @@ export class DS4ItemSheet extends ItemSheet<DS4ItemDataType, DS4Item> {
return data;
}
/* -------------------------------------------- */
/** @override */
setPosition(options: ApplicationPosition = {}): ApplicationPosition {
setPosition(options: Partial<Application.Position> = {}): Application.Position {
const position = super.setPosition(options);
if ("find" in this.element) {
const sheetBody = this.element.find(".sheet-body");
@ -52,8 +65,6 @@ export class DS4ItemSheet extends ItemSheet<DS4ItemDataType, DS4Item> {
return position;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html: JQuery): void {
super.activateListeners(html);
@ -65,13 +76,13 @@ export class DS4ItemSheet extends ItemSheet<DS4ItemDataType, DS4Item> {
/**
* Handle management of ActiveEffects.
* @param {Event} event The originating click event
* @param event - he originating click event
*/
private async _onManageActiveEffect(event: JQuery.ClickEvent): Promise<unknown> {
protected async _onManageActiveEffect(event: JQuery.ClickEvent): Promise<unknown> {
event.preventDefault();
if (this.item.isOwned) {
return ui.notifications.warn(game.i18n.localize("DS4.WarningManageActiveEffectOnOwnedItem"));
return ui.notifications?.warn(game.i18n.localize("DS4.WarningManageActiveEffectOnOwnedItem"));
}
const a = event.currentTarget;
const li = $(a).parents(".effect");
@ -81,7 +92,7 @@ export class DS4ItemSheet extends ItemSheet<DS4ItemDataType, DS4Item> {
return this._createActiveEffect();
case "edit":
const effect = this.item.effects.get(li.data("effectId"));
return effect.sheet.render(true);
return effect?.sheet.render(true);
case "delete": {
return this.item.deleteEmbeddedEntity("ActiveEffect", li.data("effectId"));
}
@ -91,7 +102,7 @@ export class DS4ItemSheet extends ItemSheet<DS4ItemDataType, DS4Item> {
/**
* Create a new ActiveEffect for the item using default data.
*/
private async _createActiveEffect(): Promise<unknown> {
protected async _createActiveEffect(): Promise<ActiveEffect.Data> {
const label = `New Effect`;
const createData = {
@ -101,7 +112,7 @@ export class DS4ItemSheet extends ItemSheet<DS4ItemDataType, DS4Item> {
transfer: true,
};
const effect = await ActiveEffect.create(createData, this.item);
const effect = ActiveEffect.create(createData, this.item);
return effect.create({});
}
}

View file

@ -1,28 +1,20 @@
import { DS4Actor } from "../actor/actor";
import { DS4ActorDataType } from "../actor/actor-data";
import { DS4ItemDataType, DS4Talent } from "./item-data";
import { DS4ItemData } from "./item-data";
/**
* Extend the basic Item with some very simple modifications.
* @extends {Item}
* The Item class for DS4
*/
export class DS4Item extends Item<DS4ItemDataType, DS4ActorDataType, DS4Actor> {
export class DS4Item extends Item<DS4ItemData> {
/**
* Augment the basic Item data model with additional dynamic data.
* @override
*/
prepareData(): void {
super.prepareData();
this.prepareDerivedData();
// Get the Item's data
// const itemData = this.data;
// const actorData = this.actor ? this.actor.data : {};
// const data = itemData.data;
}
prepareDerivedData(): void {
if (this.type === "talent") {
const data = this.data.data as DS4Talent;
if (this.data.type === "talent") {
const data = this.data.data;
data.rank.total = data.rank.base + data.rank.mod;
}
}

View file

@ -1,7 +1,7 @@
import { migrate as migrate001 } from "./migrations/001";
async function migrate(): Promise<void> {
if (!game.user.isGM) {
if (!game.user?.isGM) {
return;
}
@ -18,14 +18,14 @@ async function migrate(): Promise<void> {
}
async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion: number): Promise<void> {
if (!game.user.isGM) {
if (!game.user?.isGM) {
return;
}
const migrationsToExecute = migrations.slice(oldMigrationVersion, targetMigrationVersion);
if (migrationsToExecute.length > 0) {
ui.notifications.info(
ui.notifications?.info(
game.i18n.format("DS4.InfoSystemUpdateStart", {
currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion,
@ -40,7 +40,7 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
await migration();
game.settings.set("ds4", "systemMigrationVersion", currentMigrationVersion);
} catch (err) {
ui.notifications.error(
ui.notifications?.error(
game.i18n.format("DS4.ErrorDuringMigration", {
currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion,
@ -54,7 +54,7 @@ async function migrateFromTo(oldMigrationVersion: number, targetMigrationVersion
}
}
ui.notifications.info(
ui.notifications?.info(
game.i18n.format("DS4.InfoSystemUpdateCompleted", {
currentVersion: oldMigrationVersion,
targetVersion: targetMigrationVersion,

View file

@ -1,5 +1,5 @@
export async function migrate(): Promise<void> {
for (const a of game.actors.entities) {
for (const a of game.actors?.entities ?? []) {
const updateData = getActorUpdateData();
console.log(`Migrating actor ${a.name}`);
await a.update(updateData, { enforceTypes: false });
@ -18,7 +18,7 @@ function getActorUpdateData(): Record<string, unknown> {
"rangedAttack",
"spellcasting",
"targetedSpellcasting",
].reduce((acc, curr) => {
].reduce((acc: Partial<Record<string, { "-=base": null }>>, curr) => {
acc[curr] = { "-=base": null };
return acc;
}, {}),

View file

@ -4,13 +4,13 @@ import { DS4 } from "../config";
* Provides default values for all arguments the `CheckFactory` expects.
*/
class DefaultCheckOptions implements DS4CheckFactoryOptions {
maxCritSuccess = 1;
minCritFailure = 20;
useSlayingDice = false;
rollMode: DS4RollMode = "roll";
readonly maxCritSuccess = 1;
readonly minCritFailure = 20;
readonly useSlayingDice = false;
readonly rollMode: DS4RollMode = "roll";
mergeWith(other: Partial<DS4CheckFactoryOptions>): DS4CheckFactoryOptions {
return { ...this, ...other } as DS4CheckFactoryOptions;
return { ...this, ...other };
}
}
@ -28,13 +28,13 @@ class CheckFactory {
private gmModifier: number,
passedOptions: Partial<DS4CheckFactoryOptions> = {},
) {
this.checkOptions = new DefaultCheckOptions().mergeWith(passedOptions);
this.checkOptions = defaultCheckOptions.mergeWith(passedOptions);
}
private checkOptions: DS4CheckFactoryOptions;
async execute(): Promise<ChatMessage | unknown> {
const rollCls: typeof Roll = CONFIG.Dice.rolls[0];
const rollCls = CONFIG.Dice.rolls[0];
const formula = [
"ds",
@ -45,7 +45,6 @@ class CheckFactory {
const roll = new rollCls(formula);
const rollModeTemplate = this.checkOptions.rollMode;
console.log(rollModeTemplate);
return roll.toMessage({}, { rollMode: rollModeTemplate, create: true });
}
@ -76,8 +75,8 @@ class CheckFactory {
/**
* Asks the user for all unknown/necessary information and passes them on to perform a roll.
* @param targetValue {number} The Check Target Number ("CTN")
* @param options {Partial<DS4CheckFactoryOptions>} Options changing the behaviour of the roll and message.
* @param targetValue - The Check Target Number ("CTN")
* @param options - Options changing the behavior of the roll and message.
*/
export async function createCheckRoll(
targetValue: number,
@ -86,6 +85,8 @@ export async function createCheckRoll(
// Ask for additional required data;
const gmModifierData = await askGmModifier(targetValue, options);
const newTargetValue = gmModifierData.checkTargetValue ?? targetValue;
const gmModifier = gmModifierData.gmModifier ?? 0;
const newOptions: Partial<DS4CheckFactoryOptions> = {
maxCritSuccess: gmModifierData.maxCritSuccess ?? options.maxCritSuccess ?? undefined,
minCritFailure: gmModifierData.minCritFailure ?? options.minCritFailure ?? undefined,
@ -94,7 +95,7 @@ export async function createCheckRoll(
};
// Create Factory
const cf = new CheckFactory(gmModifierData.checkTargetValue, gmModifierData.gmModifier, newOptions);
const cf = new CheckFactory(newTargetValue, gmModifier, newOptions);
// Possibly additional processing
@ -108,13 +109,13 @@ export async function createCheckRoll(
* @notes
* At the moment, this asks for more data than it will do after some iterations.
*
* @returns {Promise<IntermediateGmModifierData>} The data given by the user.
* @returns The data given by the user.
*/
async function askGmModifier(
targetValue: number,
options: Partial<DS4CheckFactoryOptions> = {},
{ template, title }: { template?: string; title?: string } = {},
): Promise<IntermediateGmModifierData> {
): Promise<Partial<IntermediateGmModifierData>> {
// Render model interface and return value
const usedTemplate = template ?? "systems/ds4/templates/roll/roll-options.hbs";
const usedTitle = title ?? game.i18n.localize("DS4.RollDialogDefaultTitle");
@ -124,7 +125,7 @@ async function askGmModifier(
checkTargetValue: targetValue,
maxCritSuccess: options.maxCritSuccess ?? defaultCheckOptions.maxCritSuccess,
minCritFailure: options.minCritFailure ?? defaultCheckOptions.minCritFailure,
rollModes: rollModes,
rollMode: options.rollMode,
config: DS4,
};
const renderedHtml = await renderTemplate(usedTemplate, templateData);
@ -133,14 +134,12 @@ async function askGmModifier(
new Dialog(
{
title: usedTitle,
close: () => {
// Don't do anything
},
content: renderedHtml,
buttons: {
ok: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize("DS4.RollDialogOkButton"),
callback: (html: HTMLElement | JQuery) => {
callback: (html) => {
if (!("jquery" in html)) {
throw new Error(
game.i18n.format("DS4.ErrorUnexpectedHtmlType", {
@ -150,39 +149,40 @@ async function askGmModifier(
);
} else {
const innerForm = html[0].querySelector("form");
if (!innerForm) {
throw new Error(
game.i18n.format("DS4.ErrorCouldNotFindHtmlElement", { htmlElement: "form" }),
);
}
resolve(innerForm);
}
},
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("DS4.RollDialogCancelButton"),
callback: () => {
// Don't do anything
},
},
},
default: "ok",
},
{},
{ jQuery: true },
).render(true);
});
const dialogForm = await dialogPromise;
return parseDialogFormData(dialogForm, targetValue);
return parseDialogFormData(dialogForm);
}
/**
* Extracts Dialog data from the returned DOM element.
* @param formData {HTMLFormElement} The filed dialog
* @param targetValue {number} The previously known target value (slated for removal once data automation is available)
* @param formData - The filed dialog
*/
function parseDialogFormData(formData: HTMLFormElement, targetValue: number): IntermediateGmModifierData {
function parseDialogFormData(formData: HTMLFormElement): Partial<IntermediateGmModifierData> {
return {
checkTargetValue: parseInt(formData["ctv"]?.value) ?? targetValue,
gmModifier: parseInt(formData["gmmod"]?.value) ?? 0,
maxCritSuccess: parseInt(formData["maxcoup"]?.value) ?? defaultCheckOptions.maxCritSuccess,
minCritFailure: parseInt(formData["minfumble"]?.value) ?? defaultCheckOptions.minCritFailure,
useSlayingDice: false,
rollMode: formData["visibility"]?.value ?? defaultCheckOptions.rollMode,
checkTargetValue: parseInt(formData["ctv"]?.value),
gmModifier: parseInt(formData["gmmod"]?.value),
maxCritSuccess: parseInt(formData["maxcoup"]?.value),
minCritFailure: parseInt(formData["minfumble"]?.value),
rollMode: formData["visibility"]?.value,
};
}

View file

@ -56,8 +56,8 @@ export class DS4Check extends DiceTerm {
}
}
success = null;
failure = null;
success: boolean | null = null;
failure: boolean | null = null;
targetValue = DS4Check.DEFAULT_TARGET_VALUE;
minCritFailure = DS4Check.DEFAULT_MIN_CRIT_FAILURE;
maxCritSuccess = DS4Check.DEFAULT_MAX_CRIT_SUCCESS;
@ -93,16 +93,11 @@ export class DS4Check extends DiceTerm {
}
}
/** Term Modifiers */
noop(): this {
return this;
}
// DS4 only allows recursive explosions
explode(modifier: string): this {
explode(modifier: string): void {
const rgx = /[xX]/;
const match = modifier.match(rgx);
if (!match) return this;
if (!match) return;
this.results = (this.results as Array<RollResult>)
.map((r) => {
@ -110,7 +105,7 @@ export class DS4Check extends DiceTerm {
let checked = 0;
while (checked < intermediateResults.length) {
const r = (intermediateResults as Array<RollResult>)[checked];
const r = intermediateResults[checked];
checked++;
if (!r.active) continue;
@ -135,7 +130,7 @@ export class DS4Check extends DiceTerm {
static DENOMINATION = "s";
static MODIFIERS = {
x: "explode",
c: "noop", // Modifier is consumed in constructor for target value
v: "noop", // Modifier is consumed in constructor for target value
c: (): void => undefined, // Modifier is consumed in constructor for crit
v: (): void => undefined, // Modifier is consumed in constructor for target value
};
}

View file

@ -12,7 +12,7 @@ export class DefaultRollOptions implements RollOptions {
public slayingDiceRepetition = false;
mergeWith(other: Partial<RollOptions>): RollOptions {
return { ...this, ...other } as RollOptions;
return { ...this, ...other };
}
}

View file

@ -4,14 +4,14 @@ import { calculateRollResult, isDiceSwapNecessary, isSlayingDiceRepetition, sepa
/**
* Performs a roll against a check target number, e.g. for usage in battle, but not for herbs.
* @param {number} checkTargetValue the final CTN, including all static modifiers.
* @param {Partial<RollOptions>} rollOptions optional, final option override that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
* @param {Array<number>} dice optional, pass already thrown dice that are used instead of rolling new ones.
* @param checkTargetValue - the final CTN, including all static modifiers.
* @param rollOptions - optional, final option override that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
* @param dice - optional, pass already thrown dice that are used instead of rolling new ones.
*/
export function ds4roll(
checkTargetValue: number,
rollOptions: Partial<RollOptions> = {},
dice: Array<number> = null,
dice: Array<number> = [],
): RollResult {
if (checkTargetValue <= 20) {
return rollCheckSingleDie(checkTargetValue, rollOptions, dice);
@ -27,20 +27,20 @@ export function ds4roll(
* This is not intended for direct usage. Use
* {@link ds4roll | the function that is not bound to an amount of Dice} instead.
*
* @param {number} checkTargetValue - The target value to check against.
* @param {RollOptions} rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
* @param {Array<number>} dice optional, pass already thrown dice that are used instead of rolling new ones.
* @param checkTargetValue - The target value to check against.
* @param rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
* @param dice - optional, pass already thrown dice that are used instead of rolling new ones.
*
* @returns {RollResult} An object containing detailed information on the roll result.
* @returns An object containing detailed information on the roll result.
*/
export function rollCheckSingleDie(
checkTargetValue: number,
rollOptions: Partial<RollOptions>,
dice: Array<number> = null,
dice: Array<number> = [],
): RollResult {
const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
if (dice?.length != 1) {
if (dice.length != 1) {
dice = [new DS4RollProvider().getNextRoll()];
}
const usedDice = dice;
@ -66,22 +66,22 @@ export function rollCheckSingleDie(
* This is not intended for direct usage. Use
* {@link ds4roll | the function that is not bound to an amount of Dice} instead.
*
* @param {number} targetValue- - The target value to check against.
* @param {RollOptions} rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
* @param {Array<number>} dice - Optional array of dice values to consider instead of rolling new ones.
* @param targetValue - The target value to check against.
* @param rollOptions - Options that affect the checks outcome, e.g. different values for crits or whether slaying dice are used.
* @param dice - Optional array of dice values to consider instead of rolling new ones.
*
* @returns {RollResult} An object containing detailed information on the roll result.
* @returns An object containing detailed information on the roll result.
*/
export function rollCheckMultipleDice(
targetValue: number,
rollOptions: Partial<RollOptions>,
dice: Array<number> = null,
dice: Array<number> = [],
): RollResult {
const usedOptions = new DefaultRollOptions().mergeWith(rollOptions);
const remainderTargetValue = targetValue % 20;
const numberOfDice = Math.ceil(targetValue / 20);
if (!dice || dice.length != numberOfDice) {
if (dice.length != numberOfDice) {
dice = new DS4RollProvider().getNextRolls(numberOfDice);
}
const usedDice = dice;

View file

@ -1,3 +1,4 @@
import { partition, zip } from "../common/utils";
import { RollOptions } from "./roll-data";
/**
@ -8,9 +9,9 @@ import { RollOptions } from "./roll-data";
* @private_remarks
* This uses an internal implementation of a `partition` method. Don't let typescript fool you, it will tell you that a partition method is available for Arrays, but that one's imported globally from foundry's declarations and not available during the test stage!
*
* @param {Array<number>} dice - The dice values.
* @param {RollOptions} usedOptions - Options that affect the check's behaviour.
* @returns {[Array<number>, Array<number>]} A tuple containing two arrays of dice values, the first one containing all critical hits, the second one containing all others. Both arrays are sorted descendingby value.
* @param dice - The dice values.
* @param usedOptions - Options that affect the check's behavior.
* @returns A tuple containing two arrays of dice values, the first one containing all critical hits, the second one containing all others. Both arrays are sorted descending by value.
*/
export function separateCriticalHits(dice: Array<number>, usedOptions: RollOptions): CritsAndNonCrits {
const [critSuccesses, otherRolls] = partition(dice, (v: number) => {
@ -25,40 +26,19 @@ export function separateCriticalHits(dice: Array<number>, usedOptions: RollOptio
*/
type CritsAndNonCrits = [Array<number>, Array<number>];
/**
* Partition an array into two, following a predicate.
* @param {Array<T>} input The Array to split.
* @param {(T) => boolean} predicate The predicate by which to split.
* @returns A tuple of two arrays, the first one containing all elements from `input` that matched the predicate, the second one containing those that don't.
*/
// TODO: Move to generic utils method?
function partition<T>(input: Array<T>, predicate: (v: T) => boolean) {
return input.reduce(
(p: [Array<T>, Array<T>], cur: T) => {
if (predicate(cur)) {
p[0].push(cur);
} else {
p[1].push(cur);
}
return p;
},
[[], []],
);
}
/**
* Calculates if a critical success should be moved to the last position in order to maximize the check's result.
*
* @example
* With regular dice rolling rules and a check target number of 31, the two dice 1 and 19 can get to a check result of 30.
* This method would be called as follows:
* ```
* ```ts
* isDiceSwapNecessary([[1], [19]], 11)
* ```
*
* @param {[Array<number>, Array<number>]} critsAndNonCrits the dice values thrown. It is assumed that both critical successes and other rolls are sorted descending.
* @param {number} remainingTargetValue the target value for the last dice, that is the only one that can be less than 20.
* @returns {boolean} Bool indicating whether a critical success has to be used as the last dice.
* @param critsAndNonCrits - The dice values thrown. It is assumed that both critical successes and other rolls are sorted descending.
* @param remainingTargetValue - The target value for the last dice, that is the only one that can be less than 20.
* @returns Bool indicating whether a critical success has to be used as the last dice.
*/
export function isDiceSwapNecessary(
[critSuccesses, otherRolls]: CritsAndNonCrits,
@ -81,7 +61,7 @@ export function isDiceSwapNecessary(
*
* @internal
*
* @param {RollOptions} opts the roll options to check against
* @param opts - The roll options to check against
*/
export function isSlayingDiceRepetition(opts: RollOptions): boolean {
return opts.useSlayingDice && opts.slayingDiceRepetition;
@ -92,9 +72,9 @@ export function isSlayingDiceRepetition(opts: RollOptions): boolean {
*
* @internal
*
* @param assignedRollResults The dice values in the order of usage.
* @param remainderTargetValue Target value for the last dice (the only one differing from `20`).
* @param rollOptions Config object containing options that change the way dice results are handled.
* @param assignedRollResults - The dice values in the order of usage.
* @param remainderTargetValue - Target value for the last dice (the only one differing from `20`).
* @param rollOptions - Config object containing options that change the way dice results are handled.
*
* @returns {number} The total check value.
*/
@ -118,22 +98,3 @@ export function calculateRollResult(
.map(([v]) => v)
.reduce((a, b) => a + b);
}
// TODO: Move to generic utils method?
/**
* Zips two Arrays to an array of pairs of elements with corresponding indices. Excessive elements are dropped.
* @param {Array<T>} a1 First array to zip.
* @param {Array<U>} a2 Second array to zip.
*
* @typeParam T - Type of elements contained in `a1`.
* @typeParam U - Type of elements contained in `a2`.
*
* @returns {Array<[T,U]>} The array of pairs that had the same index in their source array.
*/
function zip<T, U>(a1: Array<T>, a2: Array<U>): Array<[T, U]> {
if (a1.length <= a2.length) {
return a1.map((e1, i) => [e1, a2[i]]);
} else {
return a2.map((e2, i) => [a1[i], e2]);
}
}