Compare commits

...

2 commits

37 changed files with 578 additions and 40 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -149,6 +149,10 @@
"DS4.CooldownDuration100R": "100 Kampfrunden",
"DS4.CooldownDuration1D": "1 Tag",
"DS4.CooldownDurationD20D": "W20 Tage",
"DS4.ManaCost": "Manakosten",
"DS4.ManaCostDescription": "The Anzahl an Manapunkten, die es kostet, den Zauber zu wirken.",
"DS4.CalculateManaCost": "Manakosten Automatisch Berechnen",
"DS4.CalculateManaCostConfirmationQuestion": "<p>Die Manakosten des Zaubers werden automatisch an Hand der Tabelle auf Seite 19 der <a href=\"http://dungeonslayers.net/download/Slay05.pdf\">5. Ausgabe der Slay!</a> berechnet.</p><p><strong>Achtung:</strong> Dieser Vorgang überschreibt die bestehenden Werte und kann nicht automatisch rückgängig gemacht werden.</p>",
"DS4.SpellMinimumLevel": "Zugangsstufe",
"DS4.SpellMinimumLevelDescription": "Die minimale Stufe, ab der ein Zauberwirker den Zauberspruch erlernen kann.",
"DS4.SpellCasterClassHealer": "Heiler",
@ -189,6 +193,9 @@
"DS4.CombatValuesRangedAttack": "Schießen",
"DS4.CombatValuesSpellcasting": "Zaubern",
"DS4.CombatValuesTargetedSpellcasting": "Zielzaubern",
"DS4.CombatValuesMana": "Mana",
"DS4.CombatValuesManaCurrent": "Aktuelles Mana",
"DS4.CombatValuesManaCurrentAbbr": "MP",
"DS4.CombatValuesHitPointsSheet": "Lebenskraft",
"DS4.CombatValuesDefenseSheet": "Abwehr",
"DS4.CombatValuesInitiativeSheet": "Initiative",
@ -197,6 +204,7 @@
"DS4.CombatValuesRangedAttackSheet": "Schießen",
"DS4.CombatValuesSpellcastingSheet": "Zaubern",
"DS4.CombatValuesTargetedSpellcastingSheet": "Zielzaubern",
"DS4.CombatValuesManaSheet": "Mana",
"DS4.CharacterBaseInfoRace": "Volk",
"DS4.CharacterBaseInfoClass": "Klasse",
"DS4.CharacterBaseInfoHeroClass": "Heldenklasse",
@ -311,9 +319,11 @@
"DS4.TooltipModifier": "Modifikator",
"DS4.TooltipEffects": "Effekte",
"DS4.SettingUseSlayingDiceForAutomatedChecksName": "Slayende Würfel",
"DS4.SettingUseSlayingDiceForAutomatedChecksHint": "Benutze Slayende Würfel bei automatisierten Proben.",
"DS4.SettingUseSlayingDiceForAutomatedChecksHint": "Verwende Slayende Würfel bei automatisierten Proben.",
"DS4.SettingShowSlayerPointsName": "Slayerpunkte",
"DS4.SettingShowSlayerPointsHint": "Zeige Slayerpunkte im Charakterbogen an.",
"DS4.SettingUseManaSystemName": "Manasystem",
"DS4.SettingUseManaSystemHint": "Verwende das Manasystem für Zauber.",
"DS4.Checks": "Proben",
"DS4.ChecksAppraise": "Schätzen",
"DS4.ChecksChangeSpell": "Zauber Wechseln",

View file

@ -149,6 +149,10 @@
"DS4.CooldownDuration100R": "100 Rounds",
"DS4.CooldownDuration1D": "1 Day",
"DS4.CooldownDurationD20D": "D20 Days",
"DS4.ManaCost": "Mana Cost",
"DS4.ManaCostDescription": "The amount of mana points casting the spell costs.",
"DS4.CalculateManaCost": "Automatically Calculate Mana Cost",
"DS4.CalculateManaCostConfirmationQuestion": "<p>The mana cost of the spell is automatically calculated using the table on page 19 of the <a href=\"http://dungeonslayers.net/download/Slay05.pdf\">5th edition of the Slay!</a>.</p><p><strong>Warning:</strong> This process overwrites the existing values and connot be reverted automatically.</p>",
"DS4.SpellMinimumLevel": "Minimum Level",
"DS4.SpellMinimumLevelDescription": "The minimum level at which a spell caster may learn the spell.",
"DS4.SpellCasterClassHealer": "Healer",
@ -189,6 +193,9 @@
"DS4.CombatValuesRangedAttack": "Ranged Attack",
"DS4.CombatValuesSpellcasting": "Spellcasting",
"DS4.CombatValuesTargetedSpellcasting": "Targeted Spellcasting",
"DS4.CombatValuesMana": "Mana",
"DS4.CombatValuesManaCurrent": "Current Mana",
"DS4.CombatValuesManaCurrentAbbr": "MP",
"DS4.CombatValuesHitPointsSheet": "Hit Points",
"DS4.CombatValuesDefenseSheet": "Defense",
"DS4.CombatValuesInitiativeSheet": "Initiative",
@ -197,6 +204,7 @@
"DS4.CombatValuesRangedAttackSheet": "RAT",
"DS4.CombatValuesSpellcastingSheet": "Spellcasting",
"DS4.CombatValuesTargetedSpellcastingSheet": "TSC",
"DS4.CombatValuesManaSheet": "Mana",
"DS4.CharacterBaseInfoRace": "Race",
"DS4.CharacterBaseInfoClass": "Class",
"DS4.CharacterBaseInfoHeroClass": "Hero Class",
@ -314,6 +322,8 @@
"DS4.SettingUseSlayingDiceForAutomatedChecksHint": "Use Slaying Dice for automated checks.",
"DS4.SettingShowSlayerPointsName": "Slayer Points",
"DS4.SettingShowSlayerPointsHint": "Show Slayer Points in the character sheet.",
"DS4.SettingUseManaSystemName": "Mana System",
"DS4.SettingUseManaSystemHint": "Use the Mana System for spells.",
"DS4.Checks": "Checks",
"DS4.ChecksAppraise": "Appraise",
"DS4.ChecksChangeSpell": "Change Spell",

View file

@ -61,7 +61,7 @@
"@commitlint/cli": "16.2.1",
"@commitlint/config-conventional": "16.2.1",
"@guanghechen/rollup-plugin-copy": "1.8.6",
"@league-of-foundry-developers/foundry-vtt-types": "9.249.3",
"@league-of-foundry-developers/foundry-vtt-types": "9.249.4",
"@rollup/plugin-typescript": "8.3.0",
"@seald-io/nedb": "2.2.1",
"@types/fs-extra": "9.0.13",

View file

@ -14,7 +14,7 @@
place-items: center;
row-gap: 0.125em;
&__value {
&__total {
$combat-values-icons-path: "#{variables.$official-icons-path}/combat-values";
@include mixins.centered-content;
@ -49,6 +49,9 @@
&--targetedSpellcasting {
background-image: url("#{$combat-values-icons-path}/targeted-spellcasting.png");
}
&--mana {
background-image: url("#{$combat-values-icons-path}/mana.png");
}
}
&__label {

View file

@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2021 Johannes Loher
*
* SPDX-License-Identifier: MIT
*/
.ds4-form-field-icon-button {
&__icon {
margin: 0;
}
}

View file

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2021 Johannes Loher
*
* SPDX-License-Identifier: MIT
*/
.ds4-form-field-input-extra-slim {
&[type="number"],
&[type="text"] {
flex: 0 0 32px !important; // needs to be made more specific to override foundry's style
text-align: center;
}
}

View file

@ -15,6 +15,8 @@
@use "components/shared/control_button_group";
@use "components/shared/editor";
@use "components/shared/embedded_document_list";
@use "components/shared/form_field_icon_button";
@use "components/shared/form_field_input_extra_slim";
@use "components/shared/rollable_image";
@use "components/shared/sheet_body";
@use "components/shared/sheet_form";

View file

@ -25,6 +25,11 @@ const defaultData: DS4SpellDataSourceData = {
unit: "custom",
},
cooldownDuration: "0r",
manaCost: {
healer: null,
wizard: null,
sorcerer: null,
},
minimumLevels: {
healer: null,
wizard: null,

View file

@ -24,7 +24,7 @@ type DS4ActorDataPropertiesDataAttributes = {
type DS4ActorDataPropertiesDataTraits = { [Key in keyof typeof DS4.i18n.traits]: ModifiableDataBaseTotal<number> };
type DS4ActorDataPropertiesDataCombatValues = {
[Key in keyof typeof DS4.i18n.combatValues]: Key extends "hitPoints"
[Key in keyof typeof DS4.i18n.combatValues]: Key extends "hitPoints" | "mana"
? ResourceDataBaseTotalMax<number>
: ModifiableDataBaseTotal<number>;
};

View file

@ -61,12 +61,14 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
});
const enrichedEffects = await Promise.all(enrichedEffectPromises);
const settings = getDS4Settings();
const data = {
...this.addTooltipsToData(await super.getData()),
config: DS4,
itemsByType,
enrichedEffects,
settings: getDS4Settings(),
settings,
};
return data;
}
@ -409,7 +411,7 @@ export class DS4ActorSheet extends ActorSheet<ActorSheet.Options, DS4ActorSheetD
}
}
interface DS4ActorSheetData extends ActorSheet.Data {
export interface DS4ActorSheetData extends ActorSheet.Data {
config: typeof DS4;
itemsByType: Record<string, foundry.data.ItemData[]>;
enrichedEffects: EnrichedActiveEffectDataSource[];

View file

@ -47,12 +47,12 @@ export class DS4Actor extends Actor {
const attributes = data.data.attributes;
Object.values(attributes).forEach(
(attribute: ModifiableDataBaseTotal<number>) => (attribute.total = attribute.base + attribute.mod),
(attribute: ModifiableDataBaseTotal<number>) => (attribute.total = attribute.base + (attribute.mod ?? 0)),
);
const traits = data.data.traits;
Object.values(traits).forEach(
(trait: ModifiableDataBaseTotal<number>) => (trait.total = trait.base + trait.mod),
(trait: ModifiableDataBaseTotal<number>) => (trait.total = trait.base + (trait.mod ?? 0)),
);
}
@ -158,6 +158,8 @@ export class DS4Actor extends Actor {
});
this.data.data.combatValues.hitPoints.max = this.data.data.combatValues.hitPoints.total;
this.data.data.combatValues.mana.max = this.data.data.combatValues.mana.total;
this.data.data.checks.defend = this.data.data.combatValues.defense.total;
}
@ -166,7 +168,7 @@ export class DS4Actor extends Actor {
* given in dot notation.
*/
get finalDerivedDataProperties(): string[] {
return ["data.combatValues.hitPoints.max", "data.checks.defend"];
return ["data.combatValues.hitPoints.max", "data.combatValues.mana.max", "data.checks.defend"];
}
/**
@ -203,9 +205,11 @@ export class DS4Actor extends Actor {
data.attributes.mind.total + data.traits.aura.total - spellMalusOfEquippedItems;
data.combatValues.targetedSpellcasting.base =
data.attributes.mind.total + data.traits.dexterity.total - spellMalusOfEquippedItems;
data.combatValues.mana.base = data.attributes.mind.total + data.traits.aura.total;
Object.values(data.combatValues).forEach(
(combatValue: ModifiableDataBaseTotal<number>) => (combatValue.total = combatValue.base + combatValue.mod),
(combatValue: ModifiableDataBaseTotal<number>) =>
(combatValue.total = combatValue.base + (combatValue.mod ?? 0)),
);
}

View file

@ -2,16 +2,51 @@
//
// SPDX-License-Identifier: MIT
import { DS4ActorSheet } from "../actor-sheet";
import { DS4ActorSheet, DS4ActorSheetData } from "../actor-sheet";
import type { DS4Actor } from "../actor";
import type { Classes, HeroClasses } from "../../apps/class-config";
/**
* The Sheet class for DS4 Character Actors
*/
export class DS4CharacterActorSheet extends DS4ActorSheet {
export class DS4CharacterSheet extends DS4ActorSheet {
/** @override */
static get defaultOptions(): ActorSheet.Options {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sheet", "ds4-actor-sheet", "ds4-character-sheet"],
});
}
/** @override */
async getData(): Promise<DS4CharacterSheetData> {
const data = await super.getData();
const settings = data.settings;
const classes: Classes = {
none: { name: "DS4.ClassNone", hasSpellCasterConfiguration: false },
...settings.classes,
};
const heroClasses: HeroClasses = Object.fromEntries([
["none", { name: "DS4.HeroClassNone", baseClass: "none", hasSpellCasterConfiguration: false }],
...Object.entries(settings.heroClasses).filter(
([, value]) => this.actor.data.data.baseInfo.class === value.baseClass,
),
]);
return {
...data,
classes,
heroClasses,
};
}
}
export interface DS4CharacterSheet {
object: DS4Actor & { data: { type: "character"; _source: { type: "character" } } };
}
interface DS4CharacterSheetData extends DS4ActorSheetData {
classes: Classes;
heroClasses: HeroClasses;
}

View file

@ -22,6 +22,13 @@ export class DS4Character extends DS4Actor {
get ownableItemTypes(): Array<ItemType> {
return [...super.ownableItemTypes, "talent", "racialAbility", "language", "alphabet"];
}
/** @override */
protected prepareCombatValues(): void {
super.prepareCombatValues();
this.data.data.combatValues.mana.base += this.data.data.progression.level;
this.data.data.combatValues.mana.total += this.data.data.progression.level;
}
}
export interface DS4Character {

View file

@ -7,7 +7,7 @@ import { DS4ActorSheet } from "../actor-sheet";
/**
* The Sheet class for DS4 Creature Actors
*/
export class DS4CreatureActorSheet extends DS4ActorSheet {
export class DS4CreatureSheet extends DS4ActorSheet {
/** @override */
static get defaultOptions(): ActorSheet.Options {
return foundry.utils.mergeObject(super.defaultOptions, {

View file

@ -11,6 +11,13 @@ export class DS4Creature extends DS4Actor {
get ownableItemTypes(): Array<ItemType> {
return [...super.ownableItemTypes, "specialCreatureAbility"];
}
/** @override */
protected prepareCombatValues(): void {
super.prepareCombatValues();
this.data.data.combatValues.mana.base += this.data.data.baseInfo.foeFactor;
this.data.data.combatValues.mana.total += this.data.data.baseInfo.foeFactor;
}
}
export interface DS4Creature {

67
src/apps/class-config.ts Normal file
View file

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { DS4 } from "../config";
import { getGame } from "../helpers";
export class ClassConfig extends FormApplication<FormApplicationOptions, ClassConfigData, undefined> {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
template: "systems/ds4/templates/apps/class-config.hbs",
});
}
static get defaultClasses(): Classes {
return Object.fromEntries(
Object.entries(DS4.defaultClasses).map(([key, { i18nKey: name, hasSpellCasterConfiguration }]) => [
key,
{ name, hasSpellCasterConfiguration },
]),
);
}
static get defaultHeroClasses(): HeroClasses {
return Object.fromEntries(
Object.entries(DS4.defaultHeroClasses).map(
([key, { i18nKey: name, baseClass, hasSpellCasterConfiguration }]) => [
key,
{ name, baseClass, hasSpellCasterConfiguration },
],
),
);
}
/** @override */
getData(): ClassConfigData {
const game = getGame();
return {
classes: game.settings.get("ds4", "classes"),
heroClasses: game.settings.get("ds4", "heroClasses"),
};
}
protected _updateObject(event: Event, formData?: object): Promise<unknown> {
throw new Error("Method not implemented.");
}
}
interface Class {
name: string;
hasSpellCasterConfiguration: boolean;
}
export type Classes = Record<string, Class>;
interface HeroClass {
name: string;
baseClass: string;
hasSpellCasterConfiguration: boolean;
}
export type HeroClasses = Record<string, HeroClass>;
interface ClassConfigData {
classes: Classes;
heroClasses: HeroClasses;
}

View file

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
export interface ModifiableData<T> {
mod: T;
mod?: T | null | undefined;
}
export interface HasBase<T> {

View file

@ -158,10 +158,11 @@ const i18nKeys = {
rangedAttack: "DS4.CombatValuesRangedAttack",
spellcasting: "DS4.CombatValuesSpellcasting",
targetedSpellcasting: "DS4.CombatValuesTargetedSpellcasting",
mana: "DS4.CombatValuesMana",
},
/**
* The what do display in the actor sheets for the combat value text (in some languages, abbreviations are necessary)
* The what to display in the actor sheets for the combat value text (in some languages, abbreviations are necessary)
*/
combatValuesSheet: {
hitPoints: "DS4.CombatValuesHitPointsSheet",
@ -172,6 +173,7 @@ const i18nKeys = {
rangedAttack: "DS4.CombatValuesRangedAttackSheet",
spellcasting: "DS4.CombatValuesSpellcastingSheet",
targetedSpellcasting: "DS4.CombatValuesTargetedSpellcastingSheet",
mana: "DS4.CombatValuesManaSheet",
},
/**
@ -426,4 +428,105 @@ export const DS4 = {
eyeColor: "String",
specialCharacteristics: "String",
},
defaultClasses: {
fighter: {
i18nKey: "DS4.ClassFighter",
hasSpellCasterConfiguration: false,
},
scout: {
i18nKey: "DS4.ClassScout",
hasSpellCasterConfiguration: false,
},
healer: {
i18nKey: "DS4.ClassHealer",
hasSpellCasterConfiguration: true,
},
wizard: {
i18nKey: "DS4.ClassWizard",
hasSpellCasterConfiguration: true,
},
sorcerer: {
i18nKey: "DS4.ClassSorcerer",
hasSpellCasterConfiguration: true,
},
},
defaultHeroClasses: {
berserker: {
i18nKey: "DS4.HeroClassBerserker",
baseClass: "fighter",
hasSpellCasterConfiguration: false,
},
weaponMaster: {
i18nKey: "DS4.HeroClassWeaponMaster",
baseClass: "fighter",
hasSpellCasterConfiguration: false,
},
paladin: {
i18nKey: "DS4.HeroClassPaladin",
baseClass: "fighter",
hasSpellCasterConfiguration: true,
},
assassin: {
i18nKey: "DS4.HeroClassAssassin",
baseClass: "scout",
hasSpellCasterConfiguration: false,
},
ranger: {
i18nKey: "DS4.HeroClassRanger",
baseClass: "scout",
hasSpellCasterConfiguration: false,
},
rogue: {
i18nKey: "DS4.HeroClassRogue",
baseClass: "scout",
hasSpellCasterConfiguration: false,
},
cleric: {
i18nKey: "DS4.HeroClassCleric",
baseClass: "healer",
hasSpellCasterConfiguration: false,
},
druid: {
i18nKey: "DS4.HeroClassDruid",
baseClass: "healer",
hasSpellCasterConfiguration: false,
},
monk: {
i18nKey: "DS4.HeroClassMonk",
baseClass: "healer",
hasSpellCasterConfiguration: false,
},
archmage: {
i18nKey: "DS4.HeroClassArchmage",
baseClass: "wizard",
hasSpellCasterConfiguration: false,
},
elementalist: {
i18nKey: "DS4.HeroClassElementalist",
baseClass: "wizard",
hasSpellCasterConfiguration: false,
},
battleMage: {
i18nKey: "DS4.HeroClassBattleMage",
baseClass: "wizard",
hasSpellCasterConfiguration: false,
},
bloodMage: {
i18nKey: "DS4.HeroClassBloodMage",
baseClass: "sorcerer",
hasSpellCasterConfiguration: false,
},
demonologist: {
i18nKey: "DS4.HeroClassDemonologist",
baseClass: "sorcerer",
hasSpellCasterConfiguration: false,
},
necromancer: {
i18nKey: "DS4.HeroClassNecromancer",
baseClass: "sorcerer",
hasSpellCasterConfiguration: false,
},
},
};

5
src/global.d.ts vendored
View file

@ -2,12 +2,17 @@
//
// SPDX-License-Identifier: MIT
import type { Classes, HeroClasses } from "./apps/class-config";
declare global {
namespace ClientSettings {
interface Values {
"ds4.systemMigrationVersion": number;
"ds4.useSlayingDiceForAutomatedChecks": boolean;
"ds4.showSlayerPoints": boolean;
"ds4.classes": Classes;
"ds4.heroClasses": HeroClasses;
"ds4.useManaSystem": boolean;
}
}

View file

@ -50,6 +50,7 @@ export default async function registerHandlebarsPartials(): Promise<void> {
"systems/ds4/templates/sheets/item/tabs/properties.hbs",
"systems/ds4/templates/sheets/shared/components/add-button.hbs",
"systems/ds4/templates/sheets/shared/components/control-button-group.hbs",
"systems/ds4/templates/sheets/shared/components/form-field-icon-button.hbs",
"systems/ds4/templates/sheets/shared/components/rollable-image.hbs",
];
await loadTemplates(templatePaths);

View file

@ -5,8 +5,8 @@
// SPDX-License-Identifier: MIT
import { DS4ActiveEffect } from "../active-effect";
import { DS4CharacterActorSheet } from "../actor/character/character-sheet";
import { DS4CreatureActorSheet } from "../actor/creature/creature-sheet";
import { DS4CharacterSheet } from "../actor/character/character-sheet";
import { DS4CreatureSheet } from "../actor/creature/creature-sheet";
import { DS4ActorProxy } from "../actor/proxy";
import { DS4ChatMessage } from "../chat-message";
import { DS4 } from "../config";
@ -16,6 +16,7 @@ import registerHandlebarsPartials from "../handlebars/handlebars-partials";
import { getGame } from "../helpers";
import { DS4ItemSheet } from "../item/item-sheet";
import { DS4ItemProxy } from "../item/proxy";
import { DS4SpellSheet } from "../item/spell/spell-sheet";
import logger from "../logger";
import { macros } from "../macros/macros";
import { migration } from "../migrations";
@ -66,10 +67,28 @@ async function init() {
registerSystemSettings();
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("ds4", DS4CharacterActorSheet, { types: ["character"], makeDefault: true });
Actors.registerSheet("ds4", DS4CreatureActorSheet, { types: ["creature"], makeDefault: true });
Actors.registerSheet("ds4", DS4CharacterSheet, { types: ["character"], makeDefault: true });
Actors.registerSheet("ds4", DS4CreatureSheet, { types: ["creature"], makeDefault: true });
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("ds4", DS4ItemSheet, { makeDefault: true });
Items.registerSheet("ds4", DS4ItemSheet, {
types: [
"weapon",
"armor",
"shield",
"equipment",
"loot",
"talent",
"racialAbility",
"language",
"alphabet",
"specialCreatureAbility",
],
makeDefault: true,
});
Items.registerSheet("ds4", DS4SpellSheet, {
types: ["spell"],
makeDefault: true,
});
preloadFonts();
await registerHandlebarsPartials();

View file

@ -7,6 +7,7 @@
import { DS4ActiveEffect } from "../active-effect";
import { DS4 } from "../config";
import { getGame } from "../helpers";
import { DS4Settings, getDS4Settings } from "../settings";
import notifications from "../ui/notifications";
import { enforce } from "../utils";
import { isDS4ItemDataTypePhysical } from "./item-data-source-base";
@ -40,6 +41,7 @@ export class DS4ItemSheet extends ItemSheet<ItemSheet.Options, DS4ItemSheetData>
isOwned: this.item.isOwned,
actor: this.item.actor,
isPhysical: isDS4ItemDataTypePhysical(this.item.data.data),
settings: getDS4Settings(),
};
return data;
}
@ -128,6 +130,7 @@ interface DS4ItemSheetData extends ItemSheet.Data<ItemSheet.Options> {
isOwned: boolean;
actor: DS4ItemSheet["item"]["actor"];
isPhysical: boolean;
settings: DS4Settings;
}
/**

View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import type { CooldownDuration, DS4SpellDataSourceData } from "./spell-data-source";
type Level = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20;
export function calculateManaCost({
minimumLevels,
cooldownDuration,
}: DS4SpellDataSourceData): DS4SpellDataSourceData["manaCost"] {
// prettier-ignore
const magicalManaTable: Record<Level, Record<CooldownDuration, number>> = {
1: { "0r": 1, "1r": 2, "2r": 2, "5r": 2, "10r": 2, "100r": 3, "1d": 4, "d20d": 5 },
2: { "0r": 1, "1r": 2, "2r": 2, "5r": 2, "10r": 3, "100r": 3, "1d": 4, "d20d": 5 },
3: { "0r": 2, "1r": 2, "2r": 2, "5r": 3, "10r": 3, "100r": 4, "1d": 5, "d20d": 6 },
4: { "0r": 3, "1r": 3, "2r": 3, "5r": 4, "10r": 4, "100r": 5, "1d": 6, "d20d": 7 },
5: { "0r": 4, "1r": 4, "2r": 4, "5r": 5, "10r": 5, "100r": 6, "1d": 7, "d20d": 8 },
6: { "0r": 4, "1r": 5, "2r": 5, "5r": 6, "10r": 6, "100r": 7, "1d": 7, "d20d": 8 },
7: { "0r": 5, "1r": 6, "2r": 6, "5r": 7, "10r": 7, "100r": 7, "1d": 8, "d20d": 9 },
8: { "0r": 6, "1r": 6, "2r": 7, "5r": 7, "10r": 7, "100r": 8, "1d": 8, "d20d": 9 },
9: { "0r": 7, "1r": 7, "2r": 7, "5r": 8, "10r": 8, "100r": 8, "1d": 9, "d20d": 10 },
10: { "0r": 7, "1r": 8, "2r": 8, "5r": 8, "10r": 9, "100r": 8, "1d": 10, "d20d": 11 },
11: { "0r": 8, "1r": 8, "2r": 9, "5r": 9, "10r": 9, "100r": 10, "1d": 10, "d20d": 11 },
12: { "0r": 9, "1r": 9, "2r": 9, "5r": 9, "10r": 10, "100r": 11, "1d": 11, "d20d": 12 },
13: { "0r": 10, "1r": 10, "2r": 10, "5r": 10, "10r": 11, "100r": 12, "1d": 12, "d20d": 13 },
14: { "0r": 10, "1r": 11, "2r": 11, "5r": 12, "10r": 12, "100r": 13, "1d": 13, "d20d": 14 },
15: { "0r": 11, "1r": 12, "2r": 12, "5r": 12, "10r": 12, "100r": 13, "1d": 14, "d20d": 15 },
16: { "0r": 12, "1r": 12, "2r": 12, "5r": 12, "10r": 13, "100r": 14, "1d": 15, "d20d": 15 },
17: { "0r": 12, "1r": 13, "2r": 13, "5r": 13, "10r": 14, "100r": 15, "1d": 16, "d20d": 16 },
18: { "0r": 13, "1r": 14, "2r": 14, "5r": 14, "10r": 14, "100r": 15, "1d": 16, "d20d": 17 },
19: { "0r": 14, "1r": 15, "2r": 15, "5r": 15, "10r": 15, "100r": 15, "1d": 16, "d20d": 17 },
20: { "0r": 14, "1r": 15, "2r": 15, "5r": 16, "10r": 16, "100r": 16, "1d": 17, "d20d": 18 },
};
const getManaCostForSpellcasterClass = (spellcasterClass: keyof DS4SpellDataSourceData["manaCost"]) => {
const minimumLevel = minimumLevels[spellcasterClass];
return minimumLevel !== null
? magicalManaTable[Math.clamped(minimumLevel, 1, 20) as Level][cooldownDuration]
: null;
};
return {
healer: getManaCostForSpellcasterClass("healer"),
wizard: getManaCostForSpellcasterClass("wizard"),
sorcerer: getManaCostForSpellcasterClass("sorcerer"),
};
}

View file

@ -18,6 +18,11 @@ export interface DS4SpellDataSourceData extends DS4ItemDataSourceDataBase, DS4It
effectRadius: UnitData<DistanceUnit>;
duration: UnitData<TemporalUnit>;
cooldownDuration: CooldownDuration;
manaCost: {
healer: number | null;
wizard: number | null;
sorcerer: number | null;
};
minimumLevels: {
healer: number | null;
wizard: number | null;

View file

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { getGame } from "../../helpers";
import { DS4ItemSheet } from "../item-sheet";
import { calculateManaCost } from "./calculate-mana-const";
import type { DS4Item } from "../item";
export class DS4SpellSheet extends DS4ItemSheet {
/** @override */
activateListeners(html: JQuery): void {
super.activateListeners(html);
if (!this.options.editable) return;
html.find(".ds4-calculate-mana-cost").on("click", this.onCalculateManaCost.bind(this));
}
/** Calculates the mana cost of the spell automatically. */
protected onCalculateManaCost(): void {
const game = getGame();
const manaCost = calculateManaCost(this.item.data.data);
Dialog.confirm({
title: game.i18n.localize("DS4.CalculateManaCost"),
content: game.i18n.localize("DS4.CalculateManaCostConfirmationQuestion"),
yes: () => this.item.update({ data: { manaCost } }),
});
}
}
export interface DS4SpellSheet {
object: DS4Item & { data: { type: "spell"; _source: { type: "spell" } } };
}

View file

@ -9,7 +9,7 @@ export class DS4Talent extends DS4Item {
prepareDerivedData(): void {
super.prepareDerivedData();
const data = this.data.data;
data.rank.total = data.rank.base + data.rank.mod;
data.rank.total = data.rank.base + (data.rank.mod ?? 0);
}
/** @override */

View file

@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: MIT
import { ClassConfig, Classes, HeroClasses } from "./apps/class-config";
import { getGame } from "./helpers";
export function registerSystemSettings(): void {
@ -11,7 +12,6 @@ export function registerSystemSettings(): void {
* Track the migrations version of the latest migration that has been applied
*/
game.settings.register("ds4", "systemMigrationVersion", {
name: "System Migration Version",
scope: "world",
config: false,
type: Number,
@ -35,12 +35,48 @@ export function registerSystemSettings(): void {
type: Boolean,
default: false,
});
game.settings.register("ds4", "classes", {
scope: "world",
config: false,
default: ClassConfig.defaultClasses,
onChange: renderActorAndSpellSheets,
});
game.settings.register("ds4", "heroClasses", {
scope: "world",
config: false,
default: ClassConfig.defaultHeroClasses,
onChange: renderActorAndSpellSheets,
});
game.settings.registerMenu("ds4", "classes", {
name: "DS4.MenuClassesName",
hint: "DS4.MenuClassesHint",
label: "DS4.MenuClassesLabel",
icon: "fas fa-chess",
restricted: true,
type: ClassConfig,
});
game.settings.register("ds4", "useManaSystem", {
name: "DS4.SettingUseManaSystemName",
hint: "DS4.SettingUseManaSystemHint",
scope: "world",
config: true,
type: Boolean,
default: false,
onChange: renderActorAndSpellSheets,
});
}
export interface DS4Settings {
systemMigrationVersion: number;
useSlayingDiceForAutomatedChecks: boolean;
showSlayerPoints: boolean;
classes: Classes;
heroClasses: HeroClasses;
useManaSystem: boolean;
}
export function getDS4Settings(): DS4Settings {
@ -49,5 +85,19 @@ export function getDS4Settings(): DS4Settings {
systemMigrationVersion: game.settings.get("ds4", "systemMigrationVersion"),
useSlayingDiceForAutomatedChecks: game.settings.get("ds4", "useSlayingDiceForAutomatedChecks"),
showSlayerPoints: game.settings.get("ds4", "showSlayerPoints"),
classes: game.settings.get("ds4", "classes"),
heroClasses: game.settings.get("ds4", "heroClasses"),
useManaSystem: game.settings.get("ds4", "useManaSystem"),
};
}
function renderActorAndSpellSheets() {
for (const application of Object.values(ui.windows)) {
if (
application instanceof ActorSheet ||
(application instanceof ItemSheet && application.item.type === "spell")
) {
application.render(false);
}
}
}

View file

@ -68,6 +68,10 @@
},
"targetedSpellcasting": {
"mod": 0
},
"mana": {
"mod": 0,
"value": 0
}
}
}
@ -87,8 +91,8 @@
"templates": ["base"],
"baseInfo": {
"race": "",
"class": "",
"heroClass": "",
"class": "none",
"heroClass": "none",
"culture": ""
},
"progression": {
@ -188,6 +192,11 @@
"unit": "custom"
},
"cooldownDuration": "0r",
"manaCost": {
"healer": null,
"wizard": null,
"sorcerer": null
},
"minimumLevels": {
"healer": null,
"wizard": null,

View file

@ -0,0 +1,19 @@
{{!--
SPDX-FileCopyrightText: 2022 Johannes Loher
SPDX-License-Identifier: MIT
--}}
<form>
<ul>
{{#each classes as | value key |}}
<li>{{localize value.name}}</li>
{{/each}}
</ul>
<hr />
<ul>
{{#each heroClasses as | value key |}}
<li>{{localize value.name}}</li>
{{/each}}
</ul>
</form>

View file

@ -16,6 +16,18 @@ SPDX-License-Identifier: MIT
id="data.combatValues.hitPoints.value-{{data._id}}" value="{{data.data.combatValues.hitPoints.value}}"
data-dtype="Number" />
</div>
{{!-- TODO: Find a better place for this --}}
{{#if settings.useManaSystem}}
<div class="ds4-actor-progression__entry">
<h2 class="ds4-actor-progression__label"><label for="data.mana.value-{{data._id}}"
title="{{localize 'DS4.CombatValuesManaCurrent'}}">{{localize
"DS4.CombatValuesManaCurrentAbbr"}}</label>
</h2>
<input class="ds4-actor-progression__input" type="number" name="data.combatValues.mana.value"
id="data.combatValues.mana.value-{{data._id}}" value="{{data.data.combatValues.mana.value}}"
data-dtype="Number" />
</div>
{{/if}}
{{#if (eq data.type "character")}}
{{#if settings.showSlayerPoints}}
<div class="ds4-actor-progression__entry">

View file

@ -51,13 +51,23 @@ SPDX-License-Identifier: MIT
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.class-{{data._id}}">{{config.i18n.characterBaseInfo.class}}</label>
<input type="text" id="data.baseInfo.class-{{data._id}}" name="data.baseInfo.class"
value="{{data.data.baseInfo.class}}" data-dtype="String" />
<select id="data.baseInfo.class-{{data._id}}" name="data.baseInfo.class" data-dtype="String">
{{#select data.data.baseInfo.class}}
{{#each classes as |value key|}}
<option value="{{key}}">{{localize value.name}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="ds4-actor-properties__property">
<label class="ds4-actor-properties__property-label"
for="data.baseInfo.heroClass-{{data._id}}">{{config.i18n.characterBaseInfo.heroClass}}</label>
<input type="text" id="data.baseInfo.heroClass-{{data._id}}" name="data.baseInfo.heroClass"
value="{{data.data.baseInfo.heroClass}}" data-dtype="String" />
<select id="data.baseInfo.heroClass-{{data._id}}" name="data.baseInfo.heroClass" data-dtype="String">
{{#select data.data.baseInfo.heroClass}}
{{#each heroClasses as |value key|}}
<option value="{{key}}">{{localize value.name}}</option>
{{/each}}
{{/select}}
</select>
</div>
</div>

View file

@ -15,7 +15,7 @@ SPDX-License-Identifier: MIT
--}}
<div class="ds4-combat-value">
<div class="ds4-combat-value__value ds4-combat-value__value--{{combat-value-key}}"
<div class="ds4-combat-value__total ds4-combat-value__total--{{combat-value-key}}"
title="{{combat-value-title}}: {{combat-value-data.tooltip}}">
{{combat-value-data.total}}
</div>

View file

@ -7,9 +7,11 @@ SPDX-License-Identifier: MIT
<div class="ds4-combat-values">
{{#each config.i18n.combatValues as |combat-value-title combat-value-key|}}
{{#unless (and (eq combat-value-key "mana") (not ../settings.useManaSystem)) }}
{{> systems/ds4/templates/sheets/actor/components/combat-value.hbs combat-value-key=combat-value-key
combat-value-data=(lookup ../data.data.combatValues combat-value-key) combat-value-label=(lookup
../config.i18n.combatValuesSheet combat-value-key) combat-value-title=combat-value-title
actor-id=../data._id}}
{{/unless}}
{{/each}}
</div>

View file

@ -94,18 +94,42 @@ SPDX-License-Identifier: MIT
</select>
</div>
</div>
{{#if settings.useManaSystem}}
<div class="form-group slim">
<label title="{{localize 'DS4.ManaCostDescription'}}">{{localize "DS4.ManaCost"}}</label>
<div class="form-fields">
{{> systems/ds4/templates/sheets/shared/components/form-field-icon-button.hbs
class="ds4-calculate-mana-cost" title="DS4.CalculateManaCost" faClasses="fas fa-magic"}}
<label for="data.manaCost.healer-{{data._id}}">{{localize "DS4.SpellCasterClassHealer"}}</label>
<input id="data.manaCost.healer-{{data._id}}" class="ds4-form-field-input-extra-slim" data-dtype="Number"
type="number" min="0" step="1" name="data.manaCost.healer" placeholder=""
value="{{data.data.manaCost.healer}}" />
<label for="data.manaCost.sorcerer-{{data._id}}">{{localize "DS4.SpellCasterClassSorcerer"}}</label>
<input id="data.manaCost.sorcerer-{{data._id}}" class="ds4-form-field-input-extra-slim" data-dtype="Number"
type="number" min="0" step="1" name="data.manaCost.sorcerer" placeholder=""
value="{{data.data.manaCost.sorcerer}}" />
<label for="data.manaCost.wizard-{{data._id}}">{{localize "DS4.SpellCasterClassWizard"}}</label>
<input id="data.manaCost.wizard-{{data._id}}" class="ds4-form-field-input-extra-slim" data-dtype="Number"
type="number" min="0" step="1" name="data.manaCost.wizard" placeholder=""
value="{{data.data.manaCost.wizard}}" />
</div>
</div>
{{/if}}
<div class="form-group slim">
<label title="{{localize 'DS4.SpellMinimumLevelDescription'}}">{{localize "DS4.SpellMinimumLevel"}}</label>
<div class="form-fields">
<label for="data.minimumLevels.healer-{{data._id}}">{{localize "DS4.SpellCasterClassHealer"}}</label>
<input id="data.minimumLevels.healer-{{data._id}}" data-dtype="Number" type="number" min="0" step="1"
name="data.minimumLevels.healer" placeholder="" value="{{data.data.minimumLevels.healer}}" />
<input id="data.minimumLevels.healer-{{data._id}}" class="ds4-form-field-input-extra-slim"
data-dtype="Number" type="number" min="0" step="1" name="data.minimumLevels.healer" placeholder=""
value="{{data.data.minimumLevels.healer}}" />
<label for="data.minimumLevels.sorcerer-{{data._id}}">{{localize "DS4.SpellCasterClassSorcerer"}}</label>
<input id="data.minimumLevels.sorcerer-{{data._id}}" data-dtype="Number" type="number" min="0" step="1"
name="data.minimumLevels.sorcerer" placeholder="" value="{{data.data.minimumLevels.sorcerer}}" />
<input id="data.minimumLevels.sorcerer-{{data._id}}" class="ds4-form-field-input-extra-slim"
data-dtype="Number" type="number" min="0" step="1" name="data.minimumLevels.sorcerer" placeholder=""
value="{{data.data.minimumLevels.sorcerer}}" />
<label for="data.minimumLevels.wizard-{{data._id}}">{{localize "DS4.SpellCasterClassWizard"}}</label>
<input id="data.minimumLevels.wizard-{{data._id}}" data-dtype="Number" type="number" min="0" step="1"
name="data.minimumLevels.wizard" placeholder="" value="{{data.data.minimumLevels.wizard}}" />
<input id="data.minimumLevels.wizard-{{data._id}}" class="ds4-form-field-input-extra-slim"
data-dtype="Number" type="number" min="0" step="1" name="data.minimumLevels.wizard" placeholder=""
value="{{data.data.minimumLevels.wizard}}" />
</div>
</div>
<div class="form-group">

View file

@ -0,0 +1,16 @@
{{!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-License-Identifier: MIT
--}}
{{!
!-- Render a form-field icon button.
!-- @param class: A class to identify the button by
!-- @param title: The title to use for the button (will be localized)
!-- @param faClasses: The Fontawesom classes to use for the icon
}}
<button class="ds4-form-field-icon-button {{class}}" type="button" title="{{localize title}}">
<i class="{{faClasses}} ds4-form-field-icon-button__icon"></i>
</button>

View file

@ -892,9 +892,9 @@ __metadata:
languageName: node
linkType: hard
"@league-of-foundry-developers/foundry-vtt-types@npm:9.249.3":
version: 9.249.3
resolution: "@league-of-foundry-developers/foundry-vtt-types@npm:9.249.3"
"@league-of-foundry-developers/foundry-vtt-types@npm:9.249.4":
version: 9.249.4
resolution: "@league-of-foundry-developers/foundry-vtt-types@npm:9.249.4"
dependencies:
"@types/jquery": ~3.5.9
"@types/simple-peer": ~9.11.1
@ -903,7 +903,7 @@ __metadata:
pixi.js: 5.3.11
socket.io-client: 4.3.2
tinymce: 5.10.1
checksum: 13bbcafd57637a59f44ee136e5edd78b1fedb3f0327027bd2bae8971f2eed6ad5ad84755cebc32e4b08f788e0bf5c08dd06cbe4709ae89746198658ebb511451
checksum: 612721d98bb6e8496d8000e86619615b2ae968ecb643273545c1a73c7d2faeca611f6d3adbc84b99cdf47b706a604226582731e0be560701a801a6a23bb75cae
languageName: node
linkType: hard
@ -3262,7 +3262,7 @@ __metadata:
"@commitlint/cli": 16.2.1
"@commitlint/config-conventional": 16.2.1
"@guanghechen/rollup-plugin-copy": 1.8.6
"@league-of-foundry-developers/foundry-vtt-types": 9.249.3
"@league-of-foundry-developers/foundry-vtt-types": 9.249.4
"@rollup/plugin-typescript": 8.3.0
"@seald-io/nedb": 2.2.1
"@types/fs-extra": 9.0.13