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

@ -12,6 +12,8 @@ module.exports = {
"plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
plugins: ["@typescript-eslint"],
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",

View file

@ -3,9 +3,8 @@
An implementation of the Dungeonslayers 4 game system for [Foundry Virtual
Tabletop](http://foundryvtt.com).
This system provides character sheet support for Actors and Items and mechanical
support for dice and rules necessary to
play games of Dungeponslayers 4.
This system provides sheet support for Actors and Items and mechanical support
for dice and rules necessary to play games of Dungeonslayers 4.
## Installation
@ -17,7 +16,7 @@ https://git.f3l.de/dungeonslayers/ds4/-/raw/latest/src/system.json?inline=false
## Development
### Prerequisits
### Prerequisites
In order to build this system, recent versions of `node` and `npm` are required.
We recommend using the latest lts version of `node`, which is `v14.15.4` at the
@ -83,16 +82,18 @@ npm test
## Contributing
Code and content contributions are accepted. Please feel free to submit issues
to the issue tracker or submit merge requests for code changes. To create an issue send a mail to [git+dungeonslayers-ds4-155-issue-@git.f3l.de](mailto:git+dungeonslayers-ds4-155-issue-@git.f3l.de).
to the issue tracker or submit merge requests for code changes. To create an
issue, send a mail to [git+dungeonslayers-ds4-155-issue-@git.f3l.de](mailto:git+dungeonslayers-ds4-155-issue-@git.f3l.de).
## Licensing
[Dungeonslayers](http://dungeonslayers.de/) (© Christian Kennig) is licensed under [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/de/deed.en).
[Dungeonslayers](http://dungeonslayers.de/) (© Christian Kennig) is licensed
under [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/de/deed.en).
The icons in [src/assets/icons/official](src/assets/icons/official) are slightly modifed
versions of original Dungeonslayers icons, which have also been published under
CC BY-NC-SA 3.0. Hence the modified icons are also published under this
license. A copy of this license can be found under
The icons in [src/assets/icons/official](src/assets/icons/official) are slightly
modified versions of original Dungeonslayers icons, which have also been
published under CC BY-NC-SA 3.0. Hence the modified icons are also published
under this license. A copy of this license can be found under
[src/assets/icons/official/LICENSE](src/assets/icons/official/LICENSE).
Similarly, the compendium packs found in [src/packs](src/packs) are based on

3498
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
{
"private": true,
"name": "test",
"name": "dungeonslayers4",
"description": "An implementation of the Dungeonslayers 4 game system for Foundry Virtual Tabletop.",
"version": "0.2.0",
"version": "0.2.1",
"license": "MIT",
"homepage": "https://git.f3l.de/dungeonslayers/ds4",
"repository": {
@ -35,7 +35,7 @@
"build:watch": "gulp watch",
"link": "gulp link",
"clean": "gulp clean && gulp link --clean",
"update": "npm install --save-dev git+https://git.f3l.de/dungeonslayers/foundry-pc-types.git#f3l-fixes",
"update": "npm install --save-dev github:League-of-Foundry-Developers/foundry-vtt-types#foundry-0.7.9",
"updateManifest": "gulp updateManifest",
"lint": "eslint 'src/**/*.ts' --cache",
"lint:fix": "eslint 'src/**/*.ts' --cache --fix",
@ -45,28 +45,28 @@
},
"devDependencies": {
"@types/fs-extra": "^9.0.6",
"@types/jasmine": "^3.6.2",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"@types/jasmine": "^3.6.3",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"archiver": "^5.2.0",
"chalk": "^4.1.0",
"eslint": "^7.18.0",
"eslint-config-prettier": "^7.1.0",
"eslint": "^7.19.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.3.1",
"foundry-pc-types": "git+https://git.f3l.de/dungeonslayers/foundry-pc-types.git#f3l-fixes",
"fs-extra": "^9.0.1",
"foundry-vtt-types": "github:League-of-Foundry-Developers/foundry-vtt-types#foundry-0.7.9",
"fs-extra": "^9.1.0",
"gulp": "^4.0.2",
"gulp-git": "^2.10.1",
"gulp-less": "^4.0.1",
"gulp-sass": "^4.1.0",
"gulp-typescript": "^6.0.0-alpha.1",
"husky": "^4.3.8",
"jasmine": "^3.6.3",
"jasmine": "^3.6.4",
"jasmine-xml-reporter": "^1.2.1",
"json-stringify-pretty-compact": "^2.0.0",
"lint-staged": "^10.5.3",
"lint-staged": "^10.5.4",
"prettier": "^2.2.1",
"sass": "^1.32.4",
"sass": "^1.32.6",
"ts-node": "^9.1.1",
"typescript": "^4.1.3",
"yargs": "^16.2.0"

View file

@ -2,6 +2,9 @@
"DS4.UserInteractionAddItem": "Neu",
"DS4.UserInteractionEditItem": "Bearbeiten",
"DS4.UserInteractionDeleteItem": "Löschen",
"DS4.UserInteractionAddEffect": "Neuer Effekt",
"DS4.UserInteractionEditEffect": "Effekt bearbeiten",
"DS4.UserInteractionDeleteEffect": "Effekt löschen",
"DS4.NotOwned": "Nicht besessen",
"DS4.HeadingBiography": "Biografie",
"DS4.HeadingDetails": "Details",
@ -12,11 +15,11 @@
"DS4.HeadingSpells": "Zaubersprüche",
"DS4.HeadingDescription": "Beschreibung",
"DS4.HeadingSpecialCreatureAbilites": "Besondere Fähigkeiten",
"DS4.AttackType": "Angriffs Typ",
"DS4.AttackType": "Angriffstyp",
"DS4.AttackTypeAbbr": "AT",
"DS4.WeaponBonus": "Waffen Bonus",
"DS4.WeaponBonus": "Waffenbonus",
"DS4.WeaponBonusAbbr": "WB",
"DS4.OpponentDefense": "Gegner Abwehr",
"DS4.OpponentDefense": "Gegnerabwehr",
"DS4.OpponentDefenseAbbr": "GA",
"DS4.AttackTypeMelee": "Schlagen",
"DS4.AttackTypeRanged": "Schießen",
@ -61,9 +64,9 @@
"DS4.ItemTypeSpecialCreatureAbilityPlural": "Besondere Kreaturenfähigkeiten",
"DS4.ArmorType": "Panzerungstyp",
"DS4.ArmorTypeAbbr": "PAT",
"DS4.ArmorMaterialType": "Material Typ",
"DS4.ArmorMaterialType": "Materialtyp",
"DS4.ArmorMaterialTypeAbbr": "Mat.",
"DS4.ArmorValue": "Panzerungs Wert",
"DS4.ArmorValue": "Panzerungswert",
"DS4.ArmorValueAbbr": "PA",
"DS4.ArmorTypeBody": "Körper",
"DS4.ArmorTypeBodyAbbr": "Körper",
@ -125,7 +128,7 @@
"DS4.CombatValuesTargetedSpellcasting": "Zielzaubern",
"DS4.CharacterBaseInfoRace": "Volk",
"DS4.CharacterBaseInfoClass": "Klasse",
"DS4.CharacterBaseInfoHeroClass": "Helden Klasse",
"DS4.CharacterBaseInfoHeroClass": "Heldenklasse",
"DS4.CharacterBaseInfoCulture": "Kultur",
"DS4.CharacterProgressionLevel": "Stufe",
"DS4.CharacterProgressionExperiencePoints": "Erfahrungspunkte",
@ -144,9 +147,9 @@
"DS4.CharacterProfileBirthday": "Geburtstag",
"DS4.CharacterProfileBirthplace": "Geburtsort",
"DS4.CharacterProfileAge": "Alter",
"DS4.CharacterProfileHeight": "Größe",
"DS4.CharacterProfileHeight": "Größe [cm]",
"DS4.CharacterProfileHairColor": "Haarfarbe",
"DS4.CharacterProfileWeight": "Gewicht",
"DS4.CharacterProfileWeight": "Gewicht [kg]",
"DS4.CharacterProfileEyeColor": "Augenfarbe",
"DS4.CharacterProfileSpecialCharacteristics": "Besondere Eigenschaften",
"DS4.CharacterCurrencyGold": "Gold",
@ -193,9 +196,10 @@
"DS4.UnitCustom": "individuell",
"DS4.UnitCustomAbbr": " ",
"DS4.RollDialogDefaultTitle": "Proben-Optionen",
"DS4.RollDialogOkButton": "Ok",
"DS4.RollDialogOkButton": "OK",
"DS4.RollDialogCancelButton": "Abbrechen",
"DS4.ErrorUnexpectedHtmlType": "Typfehler: Erwartet wurde {exType}, tatsächlich erhalten wurde {realType}",
"DS4.ErrorUnexpectedHtmlType": "Typfehler: Erwartet wurde '{exType}', tatsächlich erhalten wurde '{realType}'.",
"DS4.ErrorCouldNotFindForm": "Konnte HTML Element '{htmlElement}' nicht finden.",
"DS4.RollDialogTargetLabel": "Probenwert",
"DS4.RollDialogModifierLabel": "SL-Modifikator",
"DS4.RollDialogCoupLabel": "Immersieg bis",

View file

@ -2,6 +2,9 @@
"DS4.UserInteractionAddItem": "Add item",
"DS4.UserInteractionEditItem": "Edit item",
"DS4.UserInteractionDeleteItem": "Delete item",
"DS4.UserInteractionAddEffect": "Add Effect",
"DS4.UserInteractionEditEffect": "Edit Effect",
"DS4.UserInteractionDeleteEffect": "Delete Effect",
"DS4.NotOwned": "No owner",
"DS4.HeadingBiography": "Biography",
"DS4.HeadingDetails": "Details",
@ -144,9 +147,9 @@
"DS4.CharacterProfileBirthday": "Birthday",
"DS4.CharacterProfileBirthplace": "Birthplace",
"DS4.CharacterProfileAge": "Age",
"DS4.CharacterProfileHeight": "Height",
"DS4.CharacterProfileHeight": "Height [m]",
"DS4.CharacterProfileHairColor": "Hair Color",
"DS4.CharacterProfileWeight": "Weight",
"DS4.CharacterProfileWeight": "Weight [kg]",
"DS4.CharacterProfileEyeColor": "Eye Color",
"DS4.CharacterProfileSpecialCharacteristics": "Special Characteristics",
"DS4.CharacterCurrencyGold": "Gold",
@ -195,7 +198,8 @@
"DS4.RollDialogDefaultTitle": "Roll Options",
"DS4.RollDialogOkButton": "Ok",
"DS4.RollDialogCancelButton": "Cancel",
"DS4.ErrorUnexpectedHtmlType": "Type Error: Expected {exType}, got {realType}",
"DS4.ErrorUnexpectedHtmlType": "Type Error: Expected '{exType}' but got '{realType}'.",
"DS4.ErrorCouldNotFindForm": "Could not find html element '{htmlElement}'.",
"DS4.RollDialogTargetLabel": "Check Target Number",
"DS4.RollDialogModifierLabel": "Game Master Modifier",
"DS4.RollDialogCoupLabel": "Coup to",

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]);
}
}

View file

@ -2,7 +2,7 @@
"name": "ds4",
"title": "Dungeonslayers 4",
"description": "The Dungeonslayers 4 system for FoundryVTT. Dungeonslayers (© Christian Kennig) is licensed under CC BY-NC-SA 3.0 (https://creativecommons.org/licenses/by-nc-sa/3.0/de/deed.en).",
"version": "0.2.0",
"version": "0.2.1",
"minimumCoreVersion": "0.7.9",
"compatibleCoreVersion": "0.7.9",
"templateVersion": 2,
@ -43,7 +43,7 @@
"primaryTokenAttribute": "combatValues.hitPoints",
"url": "https://git.f3l.de/dungeonslayers/ds4",
"manifest": "https://git.f3l.de/dungeonslayers/ds4/-/raw/latest/src/system.json?inline=false",
"download": "https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/0.2.0/download?job=build",
"download": "https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/0.2.1/download?job=build",
"license": "MIT",
"initiative": "@combatValues.initiative.total"
}

View file

@ -13,20 +13,20 @@
<div class="flexrow basic-properties">
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.race">{{config.characterBaseInfo.race}}</label>
for="data.baseInfo.race">{{config.i18n.characterBaseInfo.race}}</label>
<input type="text" name="data.baseInfo.race" id="data.baseInfo.race" value="{{data.baseInfo.race}}"
data-dtype="String" />
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.culture">{{config.characterBaseInfo.culture}}</label>
for="data.baseInfo.culture">{{config.i18n.characterBaseInfo.culture}}</label>
<input id="data.baseInfo.culture" type="text" name="data.baseInfo.culture"
value="{{data.baseInfo.culture}}"
data-dtype="String" />
</div>
<div class="basic-property flex125">
<label class="basic-property-label"
for="data.progression.progressPoints.used">{{config.characterProgression.progressPoints}}</label>
for="data.progression.progressPoints.used">{{config.i18n.characterProgression.progressPoints}}</label>
<div class="flexrow">
<input id="data.progression.progressPoints.used" type="number"
name="data.progression.progressPoints.used"
@ -43,7 +43,7 @@
</div>
<div class="basic-property flex125">
<label class="basic-property-label"
for="data.progression.talentPoints.used">{{config.characterProgression.talentPoints}}</label>
for="data.progression.talentPoints.used">{{config.i18n.characterProgression.talentPoints}}</label>
<div class="flexrow">
<input type="number" name="data.progression.talentPoints.used"
id="data.progression.talentPoints.used"
@ -56,13 +56,13 @@
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.class">{{config.characterBaseInfo.class}}</label>
for="data.baseInfo.class">{{config.i18n.characterBaseInfo.class}}</label>
<input type="text" id="data.baseInfo.class" name="data.baseInfo.class"
value="{{data.baseInfo.class}}" data-dtype="String" />
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.heroClass">{{config.characterBaseInfo.heroClass}}</label>
for="data.baseInfo.heroClass">{{config.i18n.characterBaseInfo.heroClass}}</label>
<input type="text" id="data.baseInfo.heroClass" name="data.baseInfo.heroClass"
value="{{data.baseInfo.heroClass}}"
data-dtype="String" />
@ -84,24 +84,28 @@
<a class="item" data-tab="biography">{{localize 'DS4.HeadingBiography'}}</a>
</nav>
{{!-- Sheet Body --}}
<section class="sheet-body">
{{!-- Items Tab --}}
{{> systems/ds4/templates/actor/partials/character-inventory.hbs}}
<!-- beautify ignore:start -->
<!-- prettier-ignore-start -->
{{!-- Sheet Body (remove indentation to avoid annoying Handlebars auto-indent) --}}
<section class="sheet-body">
{{!-- Items Tab --}}
{{> systems/ds4/templates/actor/partials/character-inventory.hbs}}
{{!-- Spells Tab --}}
{{> systems/ds4/templates/actor/partials/spells-overview.hbs}}
{{!-- Spells Tab --}}
{{> systems/ds4/templates/actor/partials/spells-overview.hbs}}
{{!-- Talents Tab --}}
{{> systems/ds4/templates/actor/partials/talents-abilities-overview.hbs}}
{{!-- Talents Tab --}}
{{> systems/ds4/templates/actor/partials/talents-abilities-overview.hbs}}
{{! Profile Tab --}}
{{> systems/ds4/templates/actor/partials/profile.hbs}}
{{! Profile Tab --}}
{{> systems/ds4/templates/actor/partials/profile.hbs}}
{{!-- Biography Tab --}}
<div class="tab biography" data-group="primary" data-tab="biography">
{{editor content=data.profile.biography target="data.profile.biography" button=true owner=owner
editable=editable}}
</div>
</section>
{{!-- Biography Tab --}}
<div class="tab biography" data-group="primary" data-tab="biography">
{{editor content=data.profile.biography target="data.profile.biography" button=true owner=owner
editable=editable}}
</div>
</section>
<!-- prettier-ignore-end -->
<!-- beautify ignore:end -->
</form>

View file

@ -6,10 +6,10 @@
<h1 class="charname"><input name="name" type="text" value="{{actor.name}}" placeholder="Name" /></h1>
<div class="flexrow basic-properties">
<div class="basic-property">
<label>{{config.creatureBaseInfo.creatureType}}</label>
<label>{{config.i18n.creatureBaseInfo.creatureType}}</label>
<select name="data.baseInfo.creatureType" data-type="String">
{{#select data.baseInfo.creatureType}}
{{#each config.creatureTypes as |value key|}}
{{#each config.i18n.creatureTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
@ -17,20 +17,20 @@
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.loot">{{config.creatureBaseInfo.loot}}</label>
for="data.baseInfo.loot">{{config.i18n.creatureBaseInfo.loot}}</label>
<input type="text" name="data.baseInfo.loot" value="{{data.baseInfo.loot}}" data-dtype="String" />
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.foeFactor">{{config.creatureBaseInfo.foeFactor}}</label>
for="data.baseInfo.foeFactor">{{config.i18n.creatureBaseInfo.foeFactor}}</label>
<input type="text" name="data.baseInfo.foeFactor" value="{{data.baseInfo.foeFactor}}"
data-dtype="Number" />
</div>
<div class="basic-property">
<label>{{config.creatureBaseInfo.sizeCategory}}</label>
<label>{{config.i18n.creatureBaseInfo.sizeCategory}}</label>
<select name="data.baseInfo.sizeCategory" data-type="String">
{{#select data.baseInfo.sizeCategory}}
{{#each config.creatureSizeCategories as |value key|}}
{{#each config.i18n.creatureSizeCategories as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
@ -38,7 +38,7 @@
</div>
<div class="basic-property">
<label class="basic-property-label"
for="data.baseInfo.experiencePoints">{{config.creatureBaseInfo.experiencePoints}}</label>
for="data.baseInfo.experiencePoints">{{config.i18n.creatureBaseInfo.experiencePoints}}</label>
<input type="text" name="data.baseInfo.experiencePoints" value="{{data.baseInfo.experiencePoints}}"
data-dtype="Number" />
</div>
@ -64,7 +64,7 @@
{{> systems/ds4/templates/actor/partials/creature-inventory.hbs}}
{{!-- Special Creature Abilities Tab --}}
{{> systems/ds4/templates/actor/partials/special-creature-abilites-overview.hbs}}
{{> systems/ds4/templates/actor/partials/special-creature-abilities-overview.hbs}}
{{!-- Spells Tab --}}
{{> systems/ds4/templates/actor/partials/spells-overview.hbs}}

View file

@ -42,11 +42,11 @@
{{!-- ======================================================================== --}}
<div class="attributes-traits grid grid-3col">
{{#each config.attributes as |attribute-label attribute-key|}}
{{#each config.i18n.attributes as |attribute-label attribute-key|}}
{{> attribute attribute-label=attribute-label attribute-key=attribute-key attribute-data=(lookup ../data.attributes
attribute-key)}}
{{/each}}
{{#each config.traits as |trait-label trait-key|}}
{{#each config.i18n.traits as |trait-label trait-key|}}
{{> trait trait-label=trait-label trait-key=trait-key trait-data=(lookup ../data.traits trait-key)}}
{{/each}}
</div>

View file

@ -4,13 +4,13 @@
<h4 class="items-list-title">{{localize 'DS4.CharacterCurrency'}}</h4>
<ol class="items-list">
<li class="item flexrow item-header">
<label for="data.currency.gold" class="flex05">{{config.characterCurrency.gold}}</label>
<label for="data.currency.gold" class="flex05">{{config.i18n.characterCurrency.gold}}</label>
<input class="flex3 item-num-val item-change" type="number" min="0" step="1" name="data.currency.gold"
id="data.currency.gold" value="{{data.currency.gold}}" data-dtype="Number" />
<label for="data.currency.silver" class="flex05">{{config.characterCurrency.silver}}</label>
<label for="data.currency.silver" class="flex05">{{config.i18n.characterCurrency.silver}}</label>
<input class="flex3 item-num-val item-change" type="number" min="0" step="1" name="data.currency.silver"
id="data.currency.silver" value="{{data.currency.silver}}" data-dtype="Number" />
<label for="data.currency.copper" class="flex05">{{config.characterCurrency.copper}}</label>
<label for="data.currency.copper" class="flex05">{{config.i18n.characterCurrency.copper}}</label>
<input class="flex3 item-num-val item-change" type="number" min="0" step="1" name="data.currency.copper"
id="data.currency.copper" value="{{data.currency.copper}}" data-dtype="Number" />
</li>

View file

@ -1,6 +1,6 @@
<div class="progression flexrow">
<div class="progression-entry">
<h2 class="progression-label"><label for="data.progression.level">{{config.characterProgression.level}}</label>
<h2 class="progression-label"><label for="data.progression.level">{{config.i18n.characterProgression.level}}</label>
</h2>
<label for="data.progression.level" class="hidden">Progression Level</label>
<input class="progression-value" type="number" name="data.progression.level" id="data.progression.level" value="{{data.progression.level}}"
@ -8,7 +8,7 @@
</div>
<div class="progression-entry">
<h2 class="progression-label"><label
for="data.progression.experiencePoints">{{config.characterProgression.experiencePoints}}</label>
for="data.progression.experiencePoints">{{config.i18n.characterProgression.experiencePoints}}</label>
</h2>
<label for="data.progression.experiencePoints" class="hidden">Experience Points</label>
<input class="progression-value" type="number" name="data.progression.experiencePoints" id="data.progression.experiencePoints"

View file

@ -23,7 +23,7 @@
{{!-- ======================================================================== --}}
<div class="combat-values flexrow flex-between">
{{#each config.combatValues as |combat-value-label combat-value-key|}}
{{#each config.i18n.combatValues as |combat-value-label combat-value-key|}}
{{> combat-value combat-value-key=combat-value-key combat-value-data=(lookup ../data.combatValues
combat-value-key)}}
{{/each}}

View file

@ -15,9 +15,8 @@
{{#*inline "ifHasItemOfType"}}
{{#if (and (ne itemsArray undefined) (gt itemsArray.length 0))}}
{{> @partial-block}}
{{else}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType=dataType }}
{{/if}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType=dataType }}
{{/inline}}
@ -48,8 +47,8 @@
{{> @partial-block }}
{{!-- description --}}
<div class="flex4">{{localize 'DS4.Description'}}</div>
{{!-- add button --}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType=dataType }}
{{!-- control buttons placeholder --}}
<div></div>
</li>
{{/inline}}
@ -110,8 +109,8 @@
{{#each itemsByType.weapon as |item id|}}
{{#> itemListEntry item=item}}
<div class="flex05 item-image">
<img src="{{lookup ../../config.attackTypesIcons item.data.data.attackType}}"
title="{{lookup ../../config.attackTypes item.data.data.attackType}}" width="24" height="24" />
<img src="{{lookup ../../config.icons.attackTypes item.data.data.attackType}}"
title="{{lookup ../../config.i18n.attackTypes item.data.data.attackType}}" width="24" height="24" />
</div>
<div class="flex05 item-num-val">{{ item.data.data.weaponBonus}}</div>
<div class="flex05 item-num-val">{{ item.data.data.opponentDefense}}</div>
@ -135,11 +134,11 @@
{{/itemListHeader}}
{{#each itemsByType.armor as |item id|}}
{{#> itemListEntry item=item }}
<div title="{{lookup ../../config.armorMaterialTypes item.data.data.armorMaterialType}}">
{{lookup ../../config.armorMaterialTypesAbbr item.data.data.armorMaterialType}}
<div title="{{lookup ../../config.i18n.armorMaterialTypes item.data.data.armorMaterialType}}">
{{lookup ../../config.i18n.armorMaterialTypesAbbr item.data.data.armorMaterialType}}
</div>
<div title="{{lookup ../../config.armorTypes item.data.data.armorType}}">
{{lookup ../../config.armorTypesAbbr item.data.data.armorType}}
<div title="{{lookup ../../config.i18n.armorTypes item.data.data.armorType}}">
{{lookup ../../config.i18n.armorTypesAbbr item.data.data.armorType}}
</div>
<div class="flex05 item-num-val">{{ item.data.data.armorValue}}</div>
{{/itemListEntry}}

View file

@ -3,6 +3,6 @@
!-- The current item is defined by the data-item-id HTML property of the parent li element.
--}}
<div class="item-controls">
<a class="item-control item-edit" title="Edit Item"><i class="fas fa-edit"></i></a>
<a class="item-control item-delete" title="Delete Item"><i class="fas fa-trash"></i></a>
<a class="item-control item-edit" title="{{localize 'DS4.UserInteractionEditItem'}}"><i class="fas fa-edit"></i></a>
<a class="item-control item-delete" title="{{localize 'DS4.UserInteractionDeleteItem'}}"><i class="fas fa-trash"></i></a>
</div>

View file

@ -1,15 +1,21 @@
<div class="tab profile" data-group="primary" data-tab="profile">
<div class="grid grid-2col">
{{#each data.profile as |profile-data-value profile-data-key|}}
{{#if (ne profile-data-key 'biography')}}
{{#if (and (ne profile-data-key 'biography') (ne profile-data-key 'specialCharacteristics'))}}
<div class="profile-entry">
<label for="data.profile.{{profile-data-key}}">
{{lookup ../config.characterProfile profile-data-key}}
{{lookup ../config.i18n.characterProfile profile-data-key}}
</label>
<input type="text" name="data.profile.{{profile-data-key}}" value="{{profile-data-value}}"
data-dtype="{{lookup ../config/characterProfileDTypes profile-data-key}}" />
data-dtype="{{lookup ../config.i18n.characterProfileDTypes profile-data-key}}" />
</div>
{{/if}}
{{/each}}
<div>
<label for="data.profile.specialCharacteristics">
{{lookup config.i18n.characterProfile 'specialCharacteristics'}}
</label>
<textarea name="data.profile.specialCharacteristics" data-dtype="String" rows="4">{{data.profile.specialCharacteristics}}</textarea>
</div>
</div>
</div>

View file

@ -42,8 +42,8 @@
<div class="flex1 item-name">{{localize 'DS4.ItemName'}}</div>
{{!-- description --}}
<div class="flex3">{{localize 'DS4.Description'}}</div>
{{!-- add button --}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType=dataType }}
{{!-- control buttons placeholder --}}
<div></div>
</li>
{{/inline}}
@ -58,4 +58,5 @@
{{> baseItemListEntry item=item}}
{{/each}}
</ol>
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType='specialCreatureAbility' }}
</div>

View file

@ -13,7 +13,7 @@
<div class="unit-data-pair item-num-val"
title="{{localize localizationString}} [{{lookup unitNames unitDatum.unit}}]" >
{{#if unitDatum.value }}
{{unitDatum.value}}{{lookup unitAbbrs unitDatum.unit}}
{{unitDatum.value}}&thinsp;{{lookup unitAbbrs unitDatum.unit}}
{{else}}-{{/if}}
</div>
{{/inline}}
@ -23,11 +23,11 @@
!-- directly handing over the latter two.
--}}
{{#*inline "temporalUnit"}}
{{> unit unitNames=config.temporalUnits unitAbbrs=config.temporalUnitsAbbr unitDatum=unitDatum localizationString=localizationString}}
{{> unit unitNames=config.i18n.temporalUnits unitAbbrs=config.i18n.temporalUnitsAbbr unitDatum=unitDatum localizationString=localizationString}}
{{/inline}}
{{#*inline "distanceUnit"}}
{{> unit unitNames=config.distanceUnits unitAbbrs=config.distanceUnitsAbbr unitDatum=unitDatum localizationString=localizationString}}
{{> unit unitNames=config.i18n.distanceUnits unitAbbrs=config.i18n.distanceUnitsAbbr unitDatum=unitDatum localizationString=localizationString}}
{{/inline}}
@ -55,8 +55,8 @@
<div class="item-num-val" title="{{localize 'DS4.SpellCooldownDuration'}}"><i class="fas fa-hourglass-half"></i></div>
{{!-- description --}}
{{!-- <div class="flex3">{{localize 'DS4.Description'}}</div> --}}
{{!-- add button --}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType='spell' }}
{{!-- control buttons placeholder --}}
<div></div>
</li>
{{#each itemsByType.spell as |item id|}}
<li class="item flexrow" data-item-id="{{item._id}}">
@ -71,8 +71,8 @@
data-property="name" title="{{localize 'DS4.ItemName'}}" />
{{!-- spell type --}}
<div class="flex05 item-image">
<img src="{{lookup ../config.spellTypesIcons item.data.data.spellType}}"
title="{{lookup ../config.spellTypes item.data.data.spellType}}" width="24" height="24" />
<img src="{{lookup ../config.icons.spellTypes item.data.data.spellType}}"
title="{{lookup ../config.i18n.spellTypes item.data.data.spellType}}" width="24" height="24" />
</div>
{{!-- spell bonus --}}
<input class="item-num-val item-change" type="text" data-dtype="String"
@ -90,4 +90,6 @@
</li>
{{/each}}
</ol>
{{!-- add button --}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType='spell' }}
</div>

View file

@ -15,9 +15,8 @@
{{#*inline "ifHasItemOfType"}}
{{#if (and (ne itemsArray undefined) (gt itemsArray.length 0))}}
{{> @partial-block}}
{{else}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType=dataType }}
{{/if}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType=dataType }}
{{/inline}}
@ -98,7 +97,7 @@
{{/inline}}
{{!--
!-- Render a list header for a base item list entries from a given item.
!-- Render a list header for a base item list entry from a given item.
!-- The partial assumes a variable dataType to be given in the context.
!--
!-- @param dataType: the string item type for the list
@ -111,8 +110,8 @@
<div class="flex1 item-name">{{localize 'DS4.ItemName'}}</div>
{{!-- description --}}
<div class="flex3">{{localize 'DS4.Description'}}</div>
{{!-- add button --}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType=dataType }}
{{!-- control buttons placeholder --}}
<div></div>
</li>
{{/inline}}
@ -133,8 +132,8 @@
<div class="flex3">{{localize 'DS4.TalentRank'}}</div>
{{!-- description --}}
<div class="flex4">{{localize 'DS4.Description'}}</div>
{{!-- add button --}}
{{> systems/ds4/templates/actor/partials/overview-add-button.hbs dataType='talent' }}
{{!-- control buttons placeholder --}}
<div></div>
</li>
{{#each itemsByType.talent as |item id|}}
{{> talentListEntry item=item}}

View file

@ -5,7 +5,7 @@
<label>{{localize "DS4.ArmorType"}}</label>
<select name="data.armorType" data-type="String">
{{#select data.armorType}}
{{#each config.armorTypes as |value key|}}
{{#each config.i18n.armorTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
@ -15,7 +15,7 @@
<label for="data.armorMaterialType">{{localize "DS4.ArmorMaterialType"}}</label>
<select name="data.armorMaterialType" data-type="String">
{{#select data.armorMaterialType}}
{{#each config.armorMaterialTypes as |value key|}}
{{#each config.i18n.armorMaterialTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}

View file

@ -11,7 +11,7 @@
<label for="data.availability">{{localize "DS4.ItemAvailability"}}</label>
<select name="data.availability" data-type="String">
{{#select data.availability}}
{{#each config.itemAvailabilities as |value key|}}
{{#each config.i18n.itemAvailabilities as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}

View file

@ -5,16 +5,18 @@
<div class="effect-image"></div>
<div class="effect-name">Name</div>
<div class="effect-controls">
<a class="effect-control" data-action="create" title="Create Effect"><i class="fas fa-plus"></i> Add
effect</a>
<a class="effect-control" data-action="create" title="{{localize 'DS4.UserInteractionAddEffect'}}">
<i class="fas fa-plus"></i> {{localize 'DS4.UserInteractionAddEffect'}}</a>
</div>
</li>
{{#each item.effects as |effect id|}}
<li class="effect flexrow" data-effect-id="{{effect._id}}">
<h4 class="effect-name">{{effect.label}}</h4>
<div class="effect-controls">
<a class="effect-control" data-action="edit" title="Edit Effect"><i class="fas fa-edit"></i></a>
<a class="effect-control" data-action="delete" title="Delete Effect"><i class="fas fa-trash"></i></a>
<a class="effect-control" data-action="edit" title="{{localize 'DS4.UserInteractionEditEffect'}}">
<i class="fas fa-edit"></i></a>
<a class="effect-control" data-action="delete" title="{{localize 'DS4.UserInteractionDeleteEffect'}}">
<i class="fas fa-trash"></i></a>
</div>
</li>
{{/each}}

View file

@ -2,7 +2,7 @@
<img class="profile-img" src="{{item.img}}" data-edit="img" title="{{item.name}}" />
<div class="header-fields flexrow">
<h1 class="charname"><input name="name" type="text" value="{{item.name}}" placeholder="Name" /></h1>
<h2 class="item-type">{{localize (lookup config.itemTypes item.type)}}</h2>
<h2 class="item-type">{{lookup config.i18n.itemTypes item.type}}</h2>
{{> @partial-block}}
</div>
</header>

View file

@ -12,9 +12,9 @@
<select name="data.{{property}}.unit" data-type="String">
{{#select (lookup (lookup data property) 'unit')}}
{{#if (eq unitType 'temporal')}}
{{#each (lookup config 'temporalUnitsAbbr') as |value key|}}<option value="{{key}}">{{value}}</option>{{/each}}
{{#each (lookup config.i18n 'temporalUnitsAbbr') as |value key|}}<option value="{{key}}">{{value}}</option>{{/each}}
{{else}}
{{#each (lookup config 'distanceUnitsAbbr') as |value key|}}<option value="{{key}}">{{value}}</option>{{/each}}
{{#each (lookup config.i18n 'distanceUnitsAbbr') as |value key|}}<option value="{{key}}">{{value}}</option>{{/each}}
{{/if}}
{{/select}}
</select>
@ -33,7 +33,7 @@
<label for="data.spellType">{{localize "DS4.SpellType"}}</label>
<select id="data.spellType" name="data.spellType" data-type="String">
{{#select data.spellType}}
{{#each config.spellTypes as |value key|}}
{{#each config.i18n.spellTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}
@ -52,7 +52,7 @@
<label for="data.spellCategory">{{localize "DS4.SpellCategory"}}</label>
<select id="data.spellCategory" name="data.spellCategory" data-type="String">
{{#select data.spellCategory}}
{{#each config.spellCategories as |value key|}}
{{#each config.i18n.spellCategories as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}

View file

@ -5,7 +5,7 @@
<label>{{localize "DS4.AttackType"}}</label>
<select name="data.attackType" data-type="String">
{{#select data.attackType}}
{{#each config.attackTypes as |value key|}}
{{#each config.i18n.attackTypes as |value key|}}
<option value="{{key}}">{{value}}</option>
{{/each}}
{{/select}}

View file

@ -2,15 +2,17 @@
<label for="ctv">{{localize "DS4.RollDialogTargetLabel"}}</label>
<input id="ctv" data-type="Number" type="number" name="ctv" value="{{checkTargetValue}}" />
<label for="gmmod">{{localize "DS4.RollDialogModifierLabel"}}</label>
<input id="gmmod" data-type="Number" type="number" name="gmmod" value="0" />
<input id="gmmod" data-type="Number" type="number" name="gmmod" value="0" autofocus />
<label for="maxcoup">{{localize "DS4.RollDialogCoupLabel"}}</label>
<input id="maxcoup" data-type="Number" type="number" name="maxcoup" value="{{maxCritSuccess}}" />
<label for="minfumble">{{localize "DS4.RollDialogFumbleLabel"}}</label>
<input id="minfumble" data-type="Number" type="number" name="minfumble" value="{{minCritFailure}}" />
<label for="visibility">{{localize "DS4.RollDialogVisibilityLabel"}}</label>
<select id="visibility" data-type="String">
{{#each rollModes as |rollMode|}}
<option value="{{rollMode}}">{{lookup ../config.chatVisibilities rollMode}}</option>
{{#select rollMode}}
{{#each config.i18n.chatVisibilities as |rollModeValue rollModeKey|}}
<option value="{{rollModeKey}}">{{rollModeValue}}</option>
{{/each}}
{{/select}}
</select>
</form>
</form>

View file

@ -1,8 +1,12 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["DOM", "ES6", "ES2017"],
"types": ["foundry-pc-types"],
"esModuleInterop": true
}
"target": "ES2020",
"lib": ["DOM", "ES2020"],
"types": ["foundry-vtt-types"],
"esModuleInterop": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"strict": true
},
"include": ["src"]
}