Fix linting errors and cleanup.

This commit is contained in:
Aaron Lutomski
2026-06-21 13:44:55 -07:00
parent 54800e234f
commit 5b4881d265
60 changed files with 575 additions and 502 deletions
+29
View File
@@ -0,0 +1,29 @@
# Delta Green system — `module/` layout
Entry point: [`deltagreen.js`](deltagreen.js) (registered in `system.json`).
## Folder responsibilities
| Path | Purpose |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`config/`](config/) | System constants (`DG`, skill list, template paths) |
| [`data/`](data/) | TypeDataModels (`actor/`, `item/`, `derived/`) |
| [`active-effect/`](active-effect/) | Active effect metadata, data model, document class, runtime sync |
| [`profession/`](profession/) | Profession item skill catalog, validation, and character-creation math |
| [`roll/`](roll/) | Dice roll subclasses, roll pipeline, and roll UX. [`roll/roll.js`](roll/roll.js) is the public API. Internal Roll subclasses live in [`roll/classes/`](roll/classes/). |
| [`sheets/`](sheets/) | ApplicationV2 sheets and mixins |
| [`applications/`](applications/) | Standalone dialogs (profession setup, stats, active effect config) |
| [`actor/`](actor/), [`item/`](item/) | Document subclasses |
| [`chat/`](chat/) | Chat cards and inline actions |
| [`integrations/`](integrations/) | Optional module hooks (Dice So Nice) |
| [`utils/`](utils/) | Generic helpers only (Handlebars, migration, skill layout/tooltips) |
## Import conventions
- **Roll types and pipeline:** import `DGRoll`, `DGPercentileRoll`, `createDGRollFromDataset`, `processDGRoll`, etc. from [`roll/roll.js`](roll/roll.js) only — not from [`roll/classes/`](roll/classes/).
- **Roll modifier dialogs:** import from [`roll/roll-dialogs.js`](roll/roll-dialogs.js) only when calling dialogs directly (today only used from roll classes).
- **Skill improvement rolls:** import from [`roll/skill-improvement-roll.js`](roll/skill-improvement-roll.js) (used by agent sheet).
- **Profession logic:** import from [`profession/index.js`](profession/index.js).
- **Active effect runtime:** import exhaustion/stimulant sync from [`active-effect/runtime/`](active-effect/runtime/).
- **Derived actor math:** import from [`data/derived/actor-derived.js`](data/derived/actor-derived.js).
- **Agent skill defaults:** single source in [`data/actor/base/agent-skill-defaults.js`](data/actor/base/agent-skill-defaults.js) (used by human actor schema and profession code).
-29
View File
@@ -1,29 +0,0 @@
# Delta Green system — `module/` layout
Entry point: [`deltagreen.js`](deltagreen.js) (registered in `system.json`).
## Folder responsibilities
| Path | Purpose |
|------|---------|
| [`config/`](config/) | System constants (`DG`, skill list, template paths) |
| [`data/`](data/) | TypeDataModels (`actor/`, `item/`, `derived/`) |
| [`active-effect/`](active-effect/) | Active effect metadata, data model, document class, runtime sync |
| [`profession/`](profession/) | Profession item skill catalog, validation, and character-creation math |
| [`roll/`](roll/) | Dice roll subclasses, roll pipeline, and roll UX. [`roll/roll.js`](roll/roll.js) is the public API. Internal Roll subclasses live in [`roll/classes/`](roll/classes/). |
| [`sheets/`](sheets/) | ApplicationV2 sheets and mixins |
| [`applications/`](applications/) | Standalone dialogs (profession setup, stats, active effect config) |
| [`actor/`](actor/), [`item/`](item/) | Document subclasses |
| [`chat/`](chat/) | Chat cards and inline actions |
| [`integrations/`](integrations/) | Optional module hooks (Dice So Nice) |
| [`utils/`](utils/) | Generic helpers only (Handlebars, migration, skill layout/tooltips) |
## Import conventions
- **Roll types and pipeline:** import `DGRoll`, `DGPercentileRoll`, `createDGRollFromDataset`, `processDGRoll`, etc. from [`roll/roll.js`](roll/roll.js) only — not from [`roll/classes/`](roll/classes/).
- **Roll modifier dialogs:** import from [`roll/roll-dialogs.js`](roll/roll-dialogs.js) only when calling dialogs directly (today only used from roll classes).
- **Skill improvement rolls:** import from [`roll/skill-improvement-roll.js`](roll/skill-improvement-roll.js) (used by agent sheet).
- **Profession logic:** import from [`profession/index.js`](profession/index.js).
- **Active effect runtime:** import exhaustion/stimulant sync from [`active-effect/runtime/`](active-effect/runtime/).
- **Derived actor math:** import from [`data/derived/actor-derived.js`](data/derived/actor-derived.js).
- **Agent skill defaults:** single source in [`data/actor/base/agent-skill-defaults.js`](data/actor/base/agent-skill-defaults.js) (used by human actor schema and profession code).
@@ -41,7 +41,8 @@ export default class DGActiveEffectTypeDataModel extends foundry.data
* @returns {true}
*/
static #validateType(type) {
if (type.length < 3) throw new Error("must be at least three characters long");
if (type.length < 3)
throw new Error("must be at least three characters long");
if (
!/^custom\.-?\d+$/.test(type) &&
!type.split(".").every((s) => /^[a-z0-9]+$/i.test(s))
@@ -6,7 +6,7 @@ function resolveTransferItem(effect) {
const item = effect.parent;
if (item?.documentName !== "Item") return undefined;
const actor = item.actor;
const { actor } = item;
if (actor?.documentName === "Actor") {
return actor.items.get(item.id) ?? item;
}
+1 -1
View File
@@ -46,7 +46,7 @@ export const SUPPORTED_ITEM_TYPES = new Set([
* @returns {Record<string, { label: string, fields: Record<string, string> }>}
*/
export function getEffectFieldGroupsForDocument(effect) {
const parent = effect.parent;
const { parent } = effect;
if (parent?.documentName === "Actor") {
if (parent.type !== "agent") return {};
@@ -18,7 +18,7 @@ function hasActiveStimulantEffect(actor) {
* @param {Actor} actor
* @returns {boolean}
*/
export function getEffectiveSuppressExhaustion(actor) {
export default function getEffectiveSuppressExhaustion(actor) {
const persisted =
actor._source?.system?.physical?.suppressExhaustion ?? false;
return Boolean(persisted) || hasActiveStimulantEffect(actor);
+7 -5
View File
@@ -77,8 +77,8 @@ function attachRollTargetDisplay(entry, modifier) {
export function prepareAgentRollTargetDisplay(actor) {
if (actor.type !== "agent") return;
const system = actor.system;
const rollTarget = system.rollTarget;
const { system } = actor;
const { rollTarget } = system;
if (!rollTarget) return;
const allSkillsMod = Number(rollTarget.allSkills) || 0;
@@ -108,7 +108,7 @@ export function prepareAgentRollTargetDisplay(actor) {
export function prepareAgentActiveEffectDisplay(actor) {
if (actor.type !== "agent") return;
const system = actor.system;
const { system } = actor;
const source = actor._source?.system;
if (!source) return;
@@ -128,7 +128,9 @@ export function prepareAgentActiveEffectDisplay(actor) {
const healthFormulaMax = calculateHealthMax(persistedStats, persistedStats);
const wpFormulaMax = persistedStats.pow?.value ?? 0;
const persistedUnnatural =
source.skills?.unnatural?.proficiency ?? system.skills?.unnatural?.proficiency ?? 0;
source.skills?.unnatural?.proficiency ??
system.skills?.unnatural?.proficiency ??
0;
const sanityFormulaMax = 99 - persistedUnnatural;
const healthBaseMax = healthFormulaMax + (source.health?.maxBonus ?? 0);
@@ -159,7 +161,7 @@ export function prepareAgentActiveEffectDisplay(actor) {
export function refreshDerivedAfterActiveEffects(actor) {
if (actor.type !== "agent") return;
const system = actor.system;
const { system } = actor;
const sourceStatistics = actor._source?.system?.statistics;
prepareStatisticsX5(system.statistics, sourceStatistics);
@@ -1,6 +1,6 @@
import DG from "../../config/index.js";
import { ROLL_TARGET_FIELD_KEYS } from "../effect-fields.js";
import { getEffectiveSuppressExhaustion } from "./agent-condition-sync.js";
import getEffectiveSuppressExhaustion from "./agent-condition-sync.js";
/** @type {WeakMap<Actor, Promise<void>>} */
const syncLocks = new WeakMap();
@@ -10,7 +10,9 @@ const syncLocks = new WeakMap();
* @returns {ActiveEffect[]}
*/
function getExhaustionEffects(actor) {
return actor.effects?.filter((effect) => effect.getFlag(DG.ID, "exhaustion")) ?? [];
return (
actor.effects?.filter((effect) => effect.getFlag(DG.ID, "exhaustion")) ?? []
);
}
/**
@@ -77,9 +79,7 @@ async function syncExhaustionEffectInner(actor) {
const { exhausted, exhaustedPenalty } = actor.system.physical;
const suppressExhaustion = getEffectiveSuppressExhaustion(actor);
const effect = getExhaustionEffect(actor);
const penalty = exhausted
? -1 * Math.abs(Number(exhaustedPenalty) || 0)
: 0;
const penalty = exhausted ? -1 * Math.abs(Number(exhaustedPenalty) || 0) : 0;
if (!exhausted) {
if (effect) await effect.delete();
@@ -95,7 +95,7 @@ async function syncExhaustionEffectInner(actor) {
return;
}
const documentClass = getDocumentClass("ActiveEffect");
const documentClass = foundry.utils.getDocumentClass("ActiveEffect");
await documentClass.create(
{
name: game.i18n.localize("DG.Physical.ExhaustionEffectName"),
@@ -51,7 +51,7 @@ function getStimulantRemainingHours(effect) {
return 0;
}
export { getEffectiveSuppressExhaustion } from "./agent-condition-sync.js";
export { default as getEffectiveSuppressExhaustion } from "./agent-condition-sync.js";
/**
* @param {number} hours
@@ -99,7 +99,7 @@ export async function applyStimulantEffect(actor, newHours) {
return hours;
}
const documentClass = getDocumentClass("ActiveEffect");
const documentClass = foundry.utils.getDocumentClass("ActiveEffect");
await documentClass.create(data, { parent: actor });
return hours;
}
@@ -150,8 +150,9 @@ export async function clearStimulantEffects(actor) {
export async function pruneAllAgentsExpiredStimulants() {
if (!game.user.isActiveGM) return;
for (const actor of game.actors) {
if (actor.type !== "agent") continue;
await pruneExpiredStimulantEffects(actor);
await syncExhaustionEffect(actor);
if (actor.type === "agent") {
await pruneExpiredStimulantEffects(actor);
await syncExhaustionEffect(actor);
}
}
}
+31 -27
View File
@@ -1,23 +1,6 @@
import DG from "../../config/index.js";
import { getStatisticEffectiveValue } from "../../data/derived/actor-derived.js";
/**
* Strip form values that match post–AE prepared numbers so persisted data stays at base values.
* @param {Actor} actor
* @param {object} data Update payload (mutated in place).
* @returns {object}
*/
export function sanitizeActiveEffectBackedUpdateData(actor, data) {
if (!data?.system) return data;
const source = actor._source?.system;
if (!source) return data;
sanitizeStatisticValues(actor, data, source);
return data;
}
/**
* @param {Actor} actor
* @param {object} data
@@ -29,17 +12,38 @@ function sanitizeStatisticValues(actor, data, source) {
data,
`system.statistics.${key}.value`,
);
if (submitted === undefined) continue;
if (submitted !== undefined) {
const sourceStat = source.statistics?.[key];
const preparedStat = actor.system.statistics?.[key];
if (sourceStat && preparedStat) {
const base = sourceStat.value;
const effective = getStatisticEffectiveValue(preparedStat, sourceStat);
const sourceStat = source.statistics?.[key];
const preparedStat = actor.system.statistics?.[key];
if (!sourceStat || !preparedStat) continue;
const base = sourceStat.value;
const effective = getStatisticEffectiveValue(preparedStat, sourceStat);
if (submitted === effective && submitted !== base) {
foundry.utils.setProperty(data, `system.statistics.${key}.value`, base);
if (submitted === effective && submitted !== base) {
foundry.utils.setProperty(
data,
`system.statistics.${key}.value`,
base,
);
}
}
}
}
}
/**
* Strip form values that match post–AE prepared numbers so persisted data stays at base values.
* @param {Actor} actor
* @param {object} data Update payload (mutated in place).
* @returns {object}
*/
export default function sanitizeActiveEffectBackedUpdateData(actor, data) {
if (!data?.system) return data;
const source = actor._source?.system;
if (!source) return data;
sanitizeStatisticValues(actor, data, source);
return data;
}
+1 -1
View File
@@ -5,7 +5,7 @@ import {
updateTouchesExhaustionPhysical,
} from "../active-effect/runtime/exhaustion-effect.js";
import { pruneExpiredStimulantEffects } from "../active-effect/runtime/stimulant-effect.js";
import { sanitizeActiveEffectBackedUpdateData } from "../active-effect/runtime/submit.js";
import sanitizeActiveEffectBackedUpdateData from "../active-effect/runtime/submit.js";
/**
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
+8 -9
View File
@@ -34,7 +34,7 @@ const { renderTemplate } = foundry.applications.handlebars;
* @param {Actor} actor
* @returns {Promise<boolean>}
*/
export async function showPickSkillsDialog(professionItem, actor) {
export default async function showPickSkillsDialog(professionItem, actor) {
// eslint-disable-next-line no-use-before-define -- class defined below in this module
const controller = new AddProfessionDialogController(professionItem, actor);
return controller.run();
@@ -428,11 +428,12 @@ class AddProfessionDialogController {
`[name="optionSkillChecked"][value="${CSS.escape(key)}"]`,
);
if (cb) cb.checked = true;
if (!this.optionSkillMeta[key]?.chooseOne) continue;
const nameInput = this.#getOptionChooseOneInput(key);
if (nameInput) {
nameInput.disabled = false;
nameInput.value = this.chooseOneLabels[key] ?? "";
if (this.optionSkillMeta[key]?.chooseOne) {
const nameInput = this.#getOptionChooseOneInput(key);
if (nameInput) {
nameInput.disabled = false;
nameInput.value = this.chooseOneLabels[key] ?? "";
}
}
}
@@ -519,9 +520,7 @@ class AddProfessionDialogController {
.join("");
return (
toErrorHtml(validationMessages) +
capHtml +
toErrorHtml(wasteMessages)
toErrorHtml(validationMessages) + capHtml + toErrorHtml(wasteMessages)
);
}
+6 -5
View File
@@ -24,10 +24,11 @@ function syncValuesFromDom(dialog, values) {
for (const key of STAT_KEYS) {
const input = root.querySelector(`[data-stat-key="${key}"]`);
if (!input) continue;
let value = Number(input.value);
if (!Number.isFinite(value)) value = STAT_MIN;
values[key] = Math.clamp(Math.trunc(value), STAT_MIN, STAT_MAX);
if (input) {
let value = Number(input.value);
if (!Number.isFinite(value)) value = STAT_MIN;
values[key] = Math.clamp(Math.trunc(value), STAT_MIN, STAT_MAX);
}
}
}
@@ -85,7 +86,7 @@ function bindAssignStatsListeners(dialog, values) {
* @param {Actor} actor
* @returns {Promise<AssignStatsResult>}
*/
export async function showAssignStatsDialog(actor) {
export default async function showAssignStatsDialog(actor) {
/** @type {Record<string, number>} */
const values = getDefaultPointBuyValues();
const { remaining } = validatePointBuyValues(values);
@@ -54,10 +54,13 @@ export default class DGActiveEffectConfig extends ActiveEffectConfig {
Object.assign(
change,
["key", "type", "value", "phase", "priority"].reduce((paths, fieldName) => {
paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
return paths;
}, {}),
["key", "type", "value", "phase", "priority"].reduce(
(paths, fieldName) => {
paths[`${fieldName}Path`] = `system.changes.${index}.${fieldName}`;
return paths;
},
{},
),
);
return renderTemplate(
@@ -8,7 +8,7 @@ const { renderTemplate } = foundry.applications.handlebars;
/**
* @returns {Promise<PickStatisticsChoice | null>}
*/
export async function showPickStatisticsDialog() {
export default async function showPickStatisticsDialog() {
const content = await renderTemplate(
`${BASE_TEMPLATE_PATH}/dialog/pick-statistics.html`,
{},
@@ -25,7 +25,9 @@ export async function showPickStatisticsDialog() {
buttons: [
{
action: "roll",
label: game.i18n.localize("DG.ProfessionSetup.PickStatistics.RollStats"),
label: game.i18n.localize(
"DG.ProfessionSetup.PickStatistics.RollStats",
),
tooltip: game.i18n.localize(
"DG.ProfessionSetup.PickStatistics.RollStatsTooltip",
),
+5 -5
View File
@@ -1,7 +1,7 @@
import { showPickSkillsDialog } from "./add-profession-dialog.js";
import { showAssignStatsDialog } from "./assign-stats-dialog.js";
import { showPickStatisticsDialog } from "./pick-statistics-dialog.js";
import { showRollStatsDialog } from "./roll-stats-dialog.js";
import showPickSkillsDialog from "./add-profession-dialog.js";
import showAssignStatsDialog from "./assign-stats-dialog.js";
import showPickStatisticsDialog from "./pick-statistics-dialog.js";
import showRollStatsDialog from "./roll-stats-dialog.js";
/**
* @param {Actor} actor
@@ -58,7 +58,7 @@ async function runProfessionSetupFlow(
* @param {TokenDocument|null} [options.token]
* @returns {Promise<Item|null>}
*/
export async function assignProfessionToAgent(
export default async function assignProfessionToAgent(
actor,
itemData,
{ token = null } = {},
@@ -7,7 +7,7 @@ const { renderTemplate } = foundry.applications.handlebars;
* @param {Item} professionItem
* @returns {Promise<boolean>} True if the name was updated.
*/
export async function showRenameProfessionDialog(professionItem) {
export default async function showRenameProfessionDialog(professionItem) {
if (professionItem.type !== "profession") return false;
const content = await renderTemplate(
+12 -7
View File
@@ -43,12 +43,14 @@ function syncDomFromAssignments(state, dialog) {
for (const key of STAT_KEYS) {
const rollIndex = state.assignments[key];
if (rollIndex === null) continue;
const slot = root.querySelector(`[data-stat-slot][data-stat-key="${key}"]`);
const token = pool.querySelector(`[data-roll-index="${rollIndex}"]`);
if (slot && token) {
slot.appendChild(token);
if (rollIndex !== null) {
const slot = root.querySelector(
`[data-stat-slot][data-stat-key="${key}"]`,
);
const token = pool.querySelector(`[data-roll-index="${rollIndex}"]`);
if (slot && token) {
slot.appendChild(token);
}
}
}
}
@@ -183,7 +185,10 @@ function bindRollStatsListeners(state, dialog) {
* @param {TokenDocument|null} [options.token]
* @returns {Promise<RollStatsResult>}
*/
export async function showRollStatsDialog(actor, { token = null } = {}) {
export default async function showRollStatsDialog(
actor,
{ token = null } = {},
) {
const rolled = await rollStatisticScores({ actor, token });
const rolls = rolled.map((entry, index) => ({
index,
+1 -1
View File
@@ -11,7 +11,7 @@ import { createDGRollChatMessage } from "./dg-chat-card.js";
* @param {string|null} [params.extraContentKey] Optional i18n key appended on a new line after the main body.
* @returns {Promise<ChatMessage>}
*/
export async function createAgentResourceChatMessage({
export default async function createAgentResourceChatMessage({
actor,
token = null,
contentKey,
+1 -1
View File
@@ -41,7 +41,7 @@ const ITEM_RICH_TEXT_BY_PART = {
* @param {boolean} [options.showNotesInSkills]
* @returns {{ path: string, key: string }[]}
*/
export function getRichTextFieldsForPart(
export default function getRichTextFieldsForPart(
documentName,
partId,
{ actorType, itemType, showNotesInSkills = true } = {},
+3 -7
View File
@@ -1,16 +1,14 @@
import HumanSkillsActorData from "./base/human-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
import {
agentResourceField,
} from "./base/general.js";
import { agentResourceField } from "./base/general.js";
import {
computeEquippedArmorProtection,
initializeSanityIfUnset,
prepareSanityAdaptations,
prepareStatisticsX5,
removeLegacyRitualSkill,
} from "../../data/derived/actor-derived.js";
} from "../derived/actor-derived.js";
const { SchemaField, NumberField, StringField, BooleanField } =
foundry.data.fields;
@@ -96,9 +94,7 @@ export default class AgentData extends CharacterData {
initializeSanityIfUnset(this.sanity, this.statistics);
prepareSanityAdaptations(this.sanity.adaptations);
this.health.protection = computeEquippedArmorProtection(
this.parent.items,
);
this.health.protection = computeEquippedArmorProtection(this.parent.items);
if (this.physical.exhaustedPenalty > 0) {
this.physical.exhaustedPenalty =
@@ -1,5 +1,5 @@
/** Default proficiency for fixed agent skills (single source of truth for schema initials). */
export const AGENT_SKILL_DEFAULTS = /** @type {Record<string, number>} */ ({
const AGENT_SKILL_DEFAULTS = /** @type {Record<string, number>} */ ({
accounting: 10,
alertness: 20,
anthropology: 0,
@@ -37,3 +37,5 @@ export const AGENT_SKILL_DEFAULTS = /** @type {Record<string, number>} */ ({
swim: 20,
unarmed_combat: 40,
});
export default AGENT_SKILL_DEFAULTS;
+7 -2
View File
@@ -1,4 +1,4 @@
import { AGENT_SKILL_DEFAULTS } from "./agent-skill-defaults.js";
import AGENT_SKILL_DEFAULTS from "./agent-skill-defaults.js";
import { skillField } from "./general.js";
const { SchemaField, NumberField, StringField, ArrayField, ObjectField } =
@@ -47,7 +47,12 @@ export default class HumanSkillsActorData extends TypeDataModel {
swim: skillField(d.swim, "Swim"),
unarmed_combat: skillField(d.unarmed_combat, "Unarmed Combat"),
unnatural: new SchemaField({
proficiency: new NumberField({ initial: 0, min: 0, max: 99, integer: true }),
proficiency: new NumberField({
initial: 0,
min: 0,
max: 99,
integer: true,
}),
label: new StringField({ initial: "Unnatural" }),
}),
}),
+1 -1
View File
@@ -11,7 +11,7 @@ import {
prepareStatisticsX5,
removeLegacyRitualSkill,
setSkillTargetProficiencies,
} from "../../data/derived/actor-derived.js";
} from "../derived/actor-derived.js";
const { SchemaField, NumberField, StringField, BooleanField } =
foundry.data.fields;
+1 -1
View File
@@ -8,7 +8,7 @@ import {
prepareStatisticsX5,
removeLegacyRitualSkill,
setSkillTargetProficiencies,
} from "../../data/derived/actor-derived.js";
} from "../derived/actor-derived.js";
const { SchemaField, NumberField, StringField, BooleanField } =
foundry.data.fields;
+1 -1
View File
@@ -1,6 +1,6 @@
import SheetSettingsActorData from "./base/sheet-settings.js";
import DGHTMLField from "../fields/html-content-field.js";
import { computeEquippedArmorProtection } from "../../data/derived/actor-derived.js";
import { computeEquippedArmorProtection } from "../derived/actor-derived.js";
const { SchemaField, NumberField, StringField, ArrayField } =
foundry.data.fields;
+18 -24
View File
@@ -90,6 +90,22 @@ export function cleanDerivedNumber(model, path, raw) {
return field.clean(numeric, { persisted: false });
}
/**
* @param {object} statistics
* @returns {number}
*/
export function calculateHealthMax(statistics, sourceStatistics) {
try {
return Math.ceil(
(getStatisticEffectiveValue(statistics.con, sourceStatistics?.con) +
getStatisticEffectiveValue(statistics.str, sourceStatistics?.str)) /
2,
);
} catch {
return 10;
}
}
/**
* @param {foundry.abstract.TypeDataModel} model Agent system TypeDataModel.
* @returns {void}
@@ -111,9 +127,7 @@ export function applyAgentResourceMaxBonuses(model, sourceStatistics) {
model.sanity.max = cleanDerivedNumber(
model,
"sanity.max",
99 -
model.skills.unnatural.proficiency +
(model.sanity.maxBonus ?? 0),
99 - model.skills.unnatural.proficiency + (model.sanity.maxBonus ?? 0),
);
}
@@ -132,8 +146,7 @@ export function initializeSanityIfUnset(sanity, statistics) {
if (sanity.value >= 100) {
sanity.value = statistics.pow.x5;
sanity.currentBreakingPoint =
sanity.value -
(statistics.pow.effectiveValue ?? statistics.pow.value);
sanity.value - (statistics.pow.effectiveValue ?? statistics.pow.value);
}
}
@@ -198,22 +211,3 @@ export function removeLegacyRitualSkill(system) {
// Legacy field may be absent or non-configurable.
}
}
/**
* @param {object} statistics
* @returns {number}
*/
export function calculateHealthMax(statistics, sourceStatistics) {
try {
return Math.ceil(
(getStatisticEffectiveValue(
statistics.con,
sourceStatistics?.con,
) +
getStatisticEffectiveValue(statistics.str, sourceStatistics?.str)) /
2,
);
} catch {
return 10;
}
}
+3 -1
View File
@@ -19,7 +19,9 @@ export default class ProfessionItemData extends foundry.abstract.TypeDataModel {
bonds: new NumberField({ initial: 1, min: 1, max: 5, integer: true }),
automaticSkills: new ObjectField({ initial: {} }),
automaticSkillMeta: new ObjectField({ initial: {} }),
optionSkills: new ObjectField({ initial: { [PROFESSION_OPTION_PICKS_KEY]: 0 } }),
optionSkills: new ObjectField({
initial: { [PROFESSION_OPTION_PICKS_KEY]: 0 },
}),
optionSkillMeta: new ObjectField({ initial: {} }),
};
}
+1 -1
View File
@@ -23,7 +23,7 @@ import {
rollSkillTestAndDamageForOwnedItem,
} from "./macros/macro-functions.js";
import { handleInlineActions } from "./chat/inline.js";
import { runWorldMigration } from "./utils/world-migration.js";
import runWorldMigration from "./utils/world-migration.js";
import DGNPCSheet from "./sheets/npc-sheet.js";
import DGUnnaturalSheet from "./sheets/unnatural-sheet.js";
import DGVehicleSheet from "./sheets/vehicle-sheet.js";
+1 -1
View File
@@ -1,5 +1,5 @@
import { getDGRollToken } from "../chat/dg-chat-card.js";
import { appendMeleeDamageBonus } from "../roll/melee-damage.js";
import appendMeleeDamageBonus from "../roll/melee-damage.js";
import { DGDamageRoll, DGLethalityRoll } from "../roll/roll.js";
/**
+1 -3
View File
@@ -115,9 +115,7 @@ function GetAttacksFromInput(inputText) {
if (attackLine.indexOf("ARMOR PIERCING") >= 0) {
const apMatch = attackLine.match(/ARMOR\s+PIERCING\s*(\d+)/i);
const armorPiercing = apMatch?.[1]
? parseInt(apMatch[1], 10)
: 5;
const armorPiercing = apMatch?.[1] ? parseInt(apMatch[1]) : 5;
weaponData.armorPiercing = armorPiercing;
weaponData.system.armorPiercing = armorPiercing;
}
+21 -21
View File
@@ -167,16 +167,16 @@ export function collectBonusCapValidationErrors(
const catalogId = bonusCatalogIds?.[i];
if (!catalogId) {
slotTrackKeys.push(null);
continue;
} else {
const ref = catalogIdToSkillRef(catalogId, bonusTypedLabels?.[i] ?? "");
if (!ref) {
slotTrackKeys.push(null);
} else {
const trackKey = getBonusTrackKey(ref);
slotTrackKeys.push(trackKey);
counts[trackKey] = (counts[trackKey] ?? 0) + 1;
}
}
const ref = catalogIdToSkillRef(catalogId, bonusTypedLabels?.[i] ?? "");
if (!ref) {
slotTrackKeys.push(null);
continue;
}
const trackKey = getBonusTrackKey(ref);
slotTrackKeys.push(trackKey);
counts[trackKey] = (counts[trackKey] ?? 0) + 1;
}
/** @type {Set<string>} */
@@ -200,19 +200,19 @@ export function collectBonusCapValidationErrors(
const errors = [];
for (let i = 0; i < BONUS_SKILL_COUNT; i++) {
const trackKey = slotTrackKeys[i];
if (!trackKey || !violatingTrackKeys.has(trackKey)) continue;
if (trackKey && violatingTrackKeys.has(trackKey)) {
const base = getBonusTrackBaseValue(
trackKey,
baseFixed,
baseTyped,
defaults,
);
const count = counts[trackKey] ?? 0;
const final = base + count * BONUS_SKILL_INCREMENT;
const waste = Math.max(0, final - SKILL_CAP);
const base = getBonusTrackBaseValue(
trackKey,
baseFixed,
baseTyped,
defaults,
);
const count = counts[trackKey] ?? 0;
const final = base + count * BONUS_SKILL_INCREMENT;
const waste = Math.max(0, final - SKILL_CAP);
errors.push(`bonusWaste:${i}|${waste}|${trackKey}`);
errors.push(`bonusWaste:${i}|${waste}|${trackKey}`);
}
}
return errors;
+59 -51
View File
@@ -40,7 +40,7 @@ import { validateProfessionFormState } from "./validation.js";
* modifiedTypedKeys: string[],
* }}
*/
export function computeSkillValues(
export default function computeSkillValues(
automaticSkills,
optionSkills,
optionPicks,
@@ -100,39 +100,46 @@ export function computeSkillValues(
for (const [key, rating] of Object.entries(automaticSkills ?? {})) {
const ref = parseProfessionSkillKey(key);
if (!ref) continue;
let labelOverride;
if (ref.kind === "typed" && automaticMeta[key]?.chooseOne) {
labelOverride = formState.chooseOneLabels?.[key]?.trim();
if (!labelOverride) continue;
}
const storageKey = applyOverride(key, rating, { labelOverride });
if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key);
else if (storageKey) {
markTypedModified(ref, labelOverride ?? ref.label);
if (ref) {
let labelOverride;
let skip = false;
if (ref.kind === "typed" && automaticMeta[key]?.chooseOne) {
labelOverride = formState.chooseOneLabels?.[key]?.trim();
if (!labelOverride) skip = true;
}
if (!skip) {
const storageKey = applyOverride(key, rating, { labelOverride });
if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key);
else if (storageKey) {
markTypedModified(ref, labelOverride ?? ref.label);
}
}
}
}
for (const key of formState.checkedOptionKeys ?? []) {
if (!optionSkills[key] && optionSkills[key] !== 0) continue;
const ref = parseProfessionSkillKey(key);
if (!ref) continue;
let labelOverride;
if (ref.kind === "typed" && optionMeta[key]?.chooseOne) {
labelOverride = formState.chooseOneLabels?.[key]?.trim();
if (!labelOverride) continue;
} else if (ref.kind === "typed") {
labelOverride = ref.label?.trim();
if (!labelOverride) continue;
}
const storageKey = applyOverride(key, optionSkills[key], { labelOverride });
if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key);
else if (storageKey) {
markTypedModified(ref, labelOverride ?? ref.label);
if (optionSkills[key] || optionSkills[key] === 0) {
const ref = parseProfessionSkillKey(key);
if (ref) {
let labelOverride;
let skip = false;
if (ref.kind === "typed" && optionMeta[key]?.chooseOne) {
labelOverride = formState.chooseOneLabels?.[key]?.trim();
if (!labelOverride) skip = true;
} else if (ref.kind === "typed") {
labelOverride = ref.label?.trim();
if (!labelOverride) skip = true;
}
if (!skip) {
const storageKey = applyOverride(key, optionSkills[key], {
labelOverride,
});
if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key);
else if (storageKey) {
markTypedModified(ref, labelOverride ?? ref.label);
}
}
}
}
}
@@ -144,21 +151,22 @@ export function computeSkillValues(
for (let i = 0; i < BONUS_SKILL_COUNT; i++) {
const catalogId = bonusIds[i];
if (!catalogId) continue;
const typedLabel = bonusLabels[i] ?? "";
const ref = catalogIdToSkillRef(catalogId, typedLabel);
if (!ref) continue;
if (catalogId) {
const typedLabel = bonusLabels[i] ?? "";
const ref = catalogIdToSkillRef(catalogId, typedLabel);
if (ref) {
if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key);
else modifiedTypedKeys.add(formatProfessionSkillKey(ref));
if (ref.kind === "fixed") modifiedFixedKeys.add(ref.key);
else modifiedTypedKeys.add(formatProfessionSkillKey(ref));
let trackKey;
if (ref.kind === "fixed") {
trackKey = `fixed:${ref.key}`;
} else {
trackKey = formatProfessionSkillKey(ref);
let trackKey;
if (ref.kind === "fixed") {
trackKey = `fixed:${ref.key}`;
} else {
trackKey = formatProfessionSkillKey(ref);
}
bonusCounts[trackKey] = (bonusCounts[trackKey] ?? 0) + 1;
}
}
bonusCounts[trackKey] = (bonusCounts[trackKey] ?? 0) + 1;
}
/** Pre-bonus base for cap warnings */
@@ -172,14 +180,14 @@ export function computeSkillValues(
fixedValues[key] = (fixedValues[key] ?? defaults[key] ?? 0) + bonus;
} else {
const ref = parseProfessionSkillKey(trackKey);
if (ref?.kind !== "typed") continue;
const base = typedValues[trackKey]?.value ?? 0;
typedValues[trackKey] = {
group: ref.group,
label: ref.label,
value: base + bonus,
};
if (ref?.kind === "typed") {
const base = typedValues[trackKey]?.value ?? 0;
typedValues[trackKey] = {
group: ref.group,
label: ref.label,
value: base + bonus,
};
}
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ export {
catalogIdToSkillRef,
collectBonusCapValidationErrors,
} from "./catalog.js";
export { computeSkillValues } from "./compute.js";
export { default as computeSkillValues } from "./compute.js";
export {
validateProfessionFormState,
formatProfessionValidationMessages,
+74 -71
View File
@@ -1,5 +1,5 @@
import DG from "../config/index.js";
import { AGENT_SKILL_DEFAULTS } from "../data/actor/base/agent-skill-defaults.js";
import AGENT_SKILL_DEFAULTS from "../data/actor/base/agent-skill-defaults.js";
import { PROFESSION_OPTION_PICKS_KEY } from "../data/item/profession.js";
import {
INTERNAL_TYPED_KEY_PATTERN,
@@ -48,6 +48,64 @@ export function isChooseOneProfessionSkillKey(
);
}
/**
* @returns {Record<string, number>}
*/
export function getAgentSkillDefaults() {
return { ...AGENT_SKILL_DEFAULTS };
}
/**
* @param {string} group
* @returns {string}
*/
export function normalizeTypedGroup(group) {
if (group == null) return "";
const trimmed = String(group).trim();
const map = {
"Foreign Language": "ForeignLanguage",
"Military Science": "MilitaryScience",
};
return map[trimmed] ?? trimmed.replace(/\s+/g, "");
}
/**
* @param {string} key
* @returns {ProfessionSkillRef | null}
*/
export function parseProfessionSkillKey(key) {
if (!key || key === PROFESSION_OPTION_PICKS_KEY) return null;
if (DG.skills.includes(key)) return { kind: "fixed", key };
const internalMatch = key.match(INTERNAL_TYPED_KEY_PATTERN);
if (internalMatch) {
let label = "";
if (internalMatch[3] !== undefined) {
try {
label = decodeURIComponent(internalMatch[3]);
} catch {
const [, , , fallbackLabel] = internalMatch;
label = fallbackLabel;
}
}
return {
kind: "typed",
group: internalMatch[1],
label,
};
}
const match = key.match(TYPED_KEY_PATTERN);
if (match) {
return {
kind: "typed",
group: normalizeTypedGroup(match[1]),
label: match[2].trim(),
};
}
return null;
}
/**
* @param {object} params
* @param {string} params.group
@@ -85,44 +143,24 @@ export function findTypedSkillNameConflict({
for (const [skillMap, metaMap] of maps) {
for (const mapKey of Object.keys(skillMap ?? {})) {
if (mapKey === excludeKey || mapKey === PROFESSION_OPTION_PICKS_KEY)
continue;
const ref = parseProfessionSkillKey(mapKey);
if (!ref || ref.kind !== "typed") continue;
if (normalizeTypedGroup(ref.group) !== normalizedGroup) continue;
// Unresolved choose-one slots have no fixed name yet.
if (metaMap[mapKey]?.chooseOne) continue;
if (normalizeTypedSkillName(ref.label) === normalizedNew) return mapKey;
if (mapKey !== excludeKey && mapKey !== PROFESSION_OPTION_PICKS_KEY) {
const ref = parseProfessionSkillKey(mapKey);
if (
ref &&
ref.kind === "typed" &&
normalizeTypedGroup(ref.group) === normalizedGroup &&
!metaMap[mapKey]?.chooseOne &&
normalizeTypedSkillName(ref.label) === normalizedNew
) {
return mapKey;
}
}
}
}
return null;
}
/**
* @returns {Record<string, number>}
*/
export function getAgentSkillDefaults() {
return { ...AGENT_SKILL_DEFAULTS };
}
/**
* @param {string} group
* @returns {string}
*/
export function normalizeTypedGroup(group) {
if (group == null) return "";
const trimmed = String(group).trim();
const map = {
"Foreign Language": "ForeignLanguage",
"Military Science": "MilitaryScience",
};
return map[trimmed] ?? trimmed.replace(/\s+/g, "");
}
/**
* @param {string} group
* @returns {string}
@@ -169,42 +207,6 @@ export function allocateProfessionSkillStorageKey(
});
}
/**
* @param {string} key
* @returns {ProfessionSkillRef | null}
*/
export function parseProfessionSkillKey(key) {
if (!key || key === PROFESSION_OPTION_PICKS_KEY) return null;
if (DG.skills.includes(key)) return { kind: "fixed", key };
const internalMatch = key.match(INTERNAL_TYPED_KEY_PATTERN);
if (internalMatch) {
let label = "";
if (internalMatch[3] !== undefined) {
try {
label = decodeURIComponent(internalMatch[3]);
} catch {
label = internalMatch[3];
}
}
return {
kind: "typed",
group: internalMatch[1],
label,
};
}
const match = key.match(TYPED_KEY_PATTERN);
if (match) {
return {
kind: "typed",
group: normalizeTypedGroup(match[1]),
label: match[2].trim(),
};
}
return null;
}
/**
* @param {ProfessionSkillRef} ref
* @returns {string}
@@ -262,8 +264,9 @@ export function splitProfessionSkillMap(obj) {
/** @type {Record<string, number>} */
const skills = {};
for (const [key, value] of Object.entries(source)) {
if (key === PROFESSION_OPTION_PICKS_KEY) continue;
skills[key] = Number(value);
if (key !== PROFESSION_OPTION_PICKS_KEY) {
skills[key] = Number(value);
}
}
return { optionPicks, skills };
}
+52 -61
View File
@@ -28,7 +28,6 @@ export function validateProfessionFormState(
const picks = Number(optionPicks) || 0;
const {
automaticSkills = {},
optionSkills = {},
automaticMeta = {},
optionMeta = {},
bondCount = 0,
@@ -49,18 +48,19 @@ export function validateProfessionFormState(
const catalogId = formState.bonusCatalogIds?.[i];
if (!catalogId) {
errors.push(`bonus${i}`);
continue;
}
const ref = catalogIdToSkillRef(
catalogId,
formState.bonusTypedLabels?.[i] ?? "",
);
if (!ref) {
errors.push(`bonus${i}`);
continue;
}
if (ref.kind === "typed" && !formState.bonusTypedLabels?.[i]?.trim()) {
errors.push(`bonusType${i}`);
} else {
const ref = catalogIdToSkillRef(
catalogId,
formState.bonusTypedLabels?.[i] ?? "",
);
if (!ref) {
errors.push(`bonus${i}`);
} else if (
ref.kind === "typed" &&
!formState.bonusTypedLabels?.[i]?.trim()
) {
errors.push(`bonusType${i}`);
}
}
}
@@ -86,21 +86,23 @@ export function validateProfessionFormState(
for (const [key] of Object.entries(automaticSkills)) {
const ref = parseProfessionSkillKey(key);
if (!ref || ref.kind !== "typed") continue;
if (automaticMeta[key]?.chooseOne) {
registerResolvedName(ref, formState.chooseOneLabels?.[key]);
} else {
registerResolvedName(ref, ref.label);
if (ref && ref.kind === "typed") {
if (automaticMeta[key]?.chooseOne) {
registerResolvedName(ref, formState.chooseOneLabels?.[key]);
} else {
registerResolvedName(ref, ref.label);
}
}
}
for (const key of checked) {
const ref = parseProfessionSkillKey(key);
if (!ref || ref.kind !== "typed") continue;
if (optionMeta[key]?.chooseOne) {
registerResolvedName(ref, formState.chooseOneLabels?.[key]);
} else {
registerResolvedName(ref, ref.label);
if (ref && ref.kind === "typed") {
if (optionMeta[key]?.chooseOne) {
registerResolvedName(ref, formState.chooseOneLabels?.[key]);
} else {
registerResolvedName(ref, ref.label);
}
}
}
@@ -140,46 +142,35 @@ export function formatProfessionValidationMessages(
for (const code of errors) {
if (code === "optionPicks") {
needsOptionPicks = true;
continue;
}
if (code.startsWith("bondName")) {
} else if (code.startsWith("bondName")) {
needsBondName = true;
continue;
}
if (code.startsWith("bondRelationship")) {
} else if (code.startsWith("bondRelationship")) {
needsBondRelationship = true;
continue;
}
const bonusWaste = code.match(/^bonusWaste:\d+\|(\d+)\|(.+)$/);
if (bonusWaste) {
bonusWasteTrackKeys.push(bonusWaste[2]);
continue;
}
if (code.startsWith("bonusType")) {
needsBonusSkillType = true;
continue;
}
if (/^bonus\d+$/.test(code)) {
needsBonusSkill = true;
continue;
}
const typedNameRequired = code.match(/^typedNameRequired:(.+)$/);
if (typedNameRequired) {
typedNameRequiredGroups.add(typedNameRequired[1]);
continue;
}
const conflict = code.match(/^typedNameConflict:([^:]+):(.+)$/);
if (conflict) {
const [, group, name] = conflict;
push(
game.i18n.format("DG.Profession.Dialog.DuplicateTypedName", {
type: getTypedGroupDisplayName(group),
name,
}),
);
} else {
const bonusWaste = code.match(/^bonusWaste:\d+\|(\d+)\|(.+)$/);
if (bonusWaste) {
bonusWasteTrackKeys.push(bonusWaste[2]);
} else if (code.startsWith("bonusType")) {
needsBonusSkillType = true;
} else if (/^bonus\d+$/.test(code)) {
needsBonusSkill = true;
} else {
const typedNameRequired = code.match(/^typedNameRequired:(.+)$/);
if (typedNameRequired) {
typedNameRequiredGroups.add(typedNameRequired[1]);
} else {
const conflict = code.match(/^typedNameConflict:([^:]+):(.+)$/);
if (conflict) {
const [, group, name] = conflict;
push(
game.i18n.format("DG.Profession.Dialog.DuplicateTypedName", {
type: getTypedGroupDisplayName(group),
name,
}),
);
}
}
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
* @param {string|null|undefined} skill Weapon skill key from item system
* @returns {string}
*/
export function appendMeleeDamageBonus(diceFormula, actor, skill) {
export default function appendMeleeDamageBonus(diceFormula, actor, skill) {
if (
!actor ||
(actor.type !== "agent" && actor.type !== "npc") ||
+1 -1
View File
@@ -3,7 +3,7 @@ import { DGPercentileRoll } from "./classes/dg-percentile-roll.js";
import { DGLethalityRoll } from "./classes/dg-lethality-roll.js";
import { DGDamageRoll } from "./classes/dg-damage-roll.js";
import { DGSanityDamageRoll } from "./classes/dg-sanity-damage-roll.js";
import { appendMeleeDamageBonus } from "./melee-damage.js";
import appendMeleeDamageBonus from "./melee-damage.js";
export {
DGRoll,
+4 -1
View File
@@ -86,7 +86,10 @@ export async function createSkillImprovementChatMessage({
};
const failedSkillNames = localizeFailedSkills(failedSkills, "fixed");
const failedTypedSkillNames = localizeFailedSkills(failedTypedSkills, "typed");
const failedTypedSkillNames = localizeFailedSkills(
failedTypedSkills,
"typed",
);
const content = [...failedSkillNames, ...failedTypedSkillNames].join(", ");
const label = game.i18n.format(
+2 -2
View File
@@ -88,7 +88,7 @@ const SettingForm = class extends HandlebarsApplicationMixin(ApplicationV2) {
}
// Once all promises resolve, show a notification.
Promise.allSettled(settingsPromises).then((values) => {
Promise.allSettled(settingsPromises).then(() => {
ui.notifications.info(game.i18n.localize("DG.Settings.Saved"));
});
}
@@ -298,7 +298,7 @@ export default function registerSystemSettings() {
program: game.i18n.localize("DG.Settings.charactersheet.program"),
},
default: "program", // The default value for the setting
onChange: (value) => {
onChange: () => {
// A callback function which triggers when the setting is changed
// console.log(value)
},
+7 -8
View File
@@ -2,7 +2,7 @@ import DG, { BASE_TEMPLATE_PATH } from "../config/index.js";
import { prepareAgentSkillColumns } from "../utils/skill-layout.js";
import { formatProfessionSkillLabel } from "../profession/index.js";
import { buildSkillTooltip } from "../utils/skill-tooltip.js";
import { createAgentResourceChatMessage } from "../chat/resource-chat.js";
import createAgentResourceChatMessage from "../chat/resource-chat.js";
import {
createSkillImprovementChatMessage,
evaluateSkillImprovementRolls,
@@ -17,9 +17,9 @@ import {
getEffectiveSuppressExhaustion,
hasActiveStimulantEffect,
} from "../active-effect/runtime/stimulant-effect.js";
import { assignProfessionToAgent } from "../applications/profession-setup-flow.js";
import assignProfessionToAgent from "../applications/profession-setup-flow.js";
import { showDgDialog } from "../applications/dg-dialog.js";
import { showRenameProfessionDialog } from "../applications/rename-profession-dialog.js";
import showRenameProfessionDialog from "../applications/rename-profession-dialog.js";
import { PROFESSION_OPTION_PICKS_KEY } from "../data/item/profession.js";
const { renderTemplate } = foundry.applications.handlebars;
@@ -304,8 +304,9 @@ export default class DGAgentSheet extends AgentSheetBase {
static _clearBondDamage() {
for (const bond of this.actor.itemTypes.bond) {
if (!bond.system.hasBeenDamagedSinceLastHomeScene) continue;
bond.update({ "system.hasBeenDamagedSinceLastHomeScene": false });
if (bond.system.hasBeenDamagedSinceLastHomeScene) {
bond.update({ "system.hasBeenDamagedSinceLastHomeScene": false });
}
}
}
@@ -558,9 +559,7 @@ export default class DGAgentSheet extends AgentSheetBase {
*/
static _collectFailedSkills(skillMap) {
return Object.entries(skillMap)
.filter(
([, skill]) => skill.failure && !skill.cannotBeImprovedByFailure,
)
.filter(([, skill]) => skill.failure && !skill.cannotBeImprovedByFailure)
.map(([key, skill]) => ({ ...skill, key }));
}
+16 -8
View File
@@ -39,10 +39,7 @@ function activeEffectItemLayout({ header, description, descriptionLabel }) {
return {
tabs: {
initial: "description",
tabs: [
{ id: "description", label: descriptionLabel },
EFFECTS_TAB,
],
tabs: [{ id: "description", label: descriptionLabel }, EFFECTS_TAB],
},
parts: {
header: { template: header },
@@ -195,8 +192,13 @@ export default class DGItemSheet extends ItemSheetBase {
.filter((tab) => !tab.gmOnly || game.user.isGM)
.map((tab) => tab.id);
for (const partId of Object.keys(parts)) {
if (partId === "tabs" || partId === "header") continue;
if (!tabIds.includes(partId)) delete parts[partId];
if (
partId !== "tabs" &&
partId !== "header" &&
!tabIds.includes(partId)
) {
delete parts[partId];
}
}
}
Object.values(parts).forEach((p) => {
@@ -212,13 +214,19 @@ export default class DGItemSheet extends ItemSheetBase {
return super._getTabsConfig(group);
}
const tabs = layout.tabs.tabs.filter((tab) => !tab.gmOnly || game.user.isGM);
const tabs = layout.tabs.tabs.filter(
(tab) => !tab.gmOnly || game.user.isGM,
);
return { ...layout.tabs, tabs };
}
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
const partContext = await super._preparePartContext(
partId,
context,
options,
);
if (partId === "tabs") partContext.tabClasses = "sheet-tabs";
return partContext;
}
+2 -2
View File
@@ -1,5 +1,5 @@
import DG, { BASE_TEMPLATE_PATH } from "../config/index.js";
import { getRichTextFieldsForPart } from "../config/rich-text-fields.js";
import getRichTextFieldsForPart from "../config/rich-text-fields.js";
import { prepareRichTextContext } from "../utils/rich-text.js";
const HbsAppMixin = foundry.applications.api.HandlebarsApplicationMixin;
@@ -40,7 +40,7 @@ const DGSheetMixin = (Base) => {
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
const documentName = this.document.documentName;
const { documentName } = this.document;
const fieldSpecs = getRichTextFieldsForPart(documentName, partId, {
actorType: this.document.type,
itemType: this.document.type,
+21 -21
View File
@@ -27,34 +27,34 @@ export default function AeInputMixin(Base) {
const input = host.querySelector("input");
const display = host.querySelector(".ae-input-display");
if (!input || !display) continue;
if (input && display) {
const modifier = Number(display.dataset.aeModifier) || 0;
const modifier = Number(display.dataset.aeModifier) || 0;
const syncTypography = () => syncAeDisplayTypography(input, display);
const syncTypography = () => syncAeDisplayTypography(input, display);
const syncDisplay = () => {
if (host.matches(":focus-within")) return;
const base = Number(input.value);
if (Number.isFinite(base)) {
const effective = Math.max(0, Math.round(base + modifier));
display.textContent = String(effective);
}
};
const syncDisplay = () => {
if (host.matches(":focus-within")) return;
const base = Number(input.value);
if (Number.isFinite(base)) {
const effective = Math.max(0, Math.round(base + modifier));
display.textContent = String(effective);
}
};
input.addEventListener("focus", () => input.select());
input.addEventListener("blur", () => {
syncTypography();
syncDisplay();
});
input.addEventListener("focus", () => input.select());
input.addEventListener("blur", () => {
syncTypography();
requestAnimationFrame(syncTypography);
syncDisplay();
});
syncTypography();
requestAnimationFrame(syncTypography);
syncDisplay();
const observer = new ResizeObserver(syncTypography);
observer.observe(input);
hostObservers.set(host, observer);
const observer = new ResizeObserver(syncTypography);
observer.observe(input);
hostObservers.set(host, observer);
}
}
}
};
+2 -2
View File
@@ -96,10 +96,10 @@ export default function EffectsTabMixin(Base) {
* @param {PointerEvent} _event
* @param {HTMLElement} target
*/
static async createEffect(_event, target) {
static async createEffect() {
const sheet = /** @type {EffectsTabHost} */ (this);
const parent = sheet._effectsParent;
const documentClass = getDocumentClass("ActiveEffect");
const documentClass = foundry.utils.getDocumentClass("ActiveEffect");
const docData = {
name: game.i18n.localize("DG.ActiveEffects.DefaultName"),
+4 -2
View File
@@ -4,7 +4,7 @@ import { showDgDialog } from "../../applications/dg-dialog.js";
export default function ItemListMixin(Base) {
return class extends Base {
/** @param {object} [context] */
_prepareCharacterItems(context) {
_prepareCharacterItems() {
const { actor } = this;
const armor = [];
@@ -190,7 +190,9 @@ export default function ItemListMixin(Base) {
static _browsePack(event, target) {
const { packType } = target.dataset;
if (packType === "weapon") {
void this._browseWeaponPack();
this._browseWeaponPack().catch((error) => {
console.error(error);
});
return;
}
+27 -12
View File
@@ -21,7 +21,11 @@ export default function ProfessionItemMixin(Base) {
return class extends Base {
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
const partContext = await super._preparePartContext(
partId,
context,
options,
);
if (this.item.type !== "profession" || partId !== "skills") {
return partContext;
}
@@ -83,7 +87,9 @@ export default function ProfessionItemMixin(Base) {
onRender: (dialog) => {
const root = getDialogContentRoot(dialog);
const select = root?.querySelector('[name="skillCatalogId"]');
const typedGroup = root?.querySelector(".profession-typed-label-group");
const typedGroup = root?.querySelector(
".profession-typed-label-group",
);
const typeInput = root?.querySelector('[name="typedSkillLabel"]');
const chooseOneInput = root?.querySelector('[name="chooseOne"]');
@@ -135,14 +141,20 @@ export default function ProfessionItemMixin(Base) {
const isTyped = catalogId.startsWith("typed:");
if (isTyped && !chooseOne && !typedLabel) {
ui.notifications.warn(
game.i18n.localize("DG.ItemWindow.Profession.TypedSkillRequired"),
game.i18n.localize(
"DG.ItemWindow.Profession.TypedSkillRequired",
),
);
return false;
}
const ref = catalogIdToSkillRef(catalogId, chooseOne ? "" : typedLabel, {
allowEmptyTypedLabel: chooseOne,
});
const ref = catalogIdToSkillRef(
catalogId,
chooseOne ? "" : typedLabel,
{
allowEmptyTypedLabel: chooseOne,
},
);
if (!ref) return false;
const skillKey = allocateProfessionSkillStorageKey(ref, {
@@ -156,7 +168,9 @@ export default function ProfessionItemMixin(Base) {
const automaticMeta = {
...(this.item.system.automaticSkillMeta ?? {}),
};
const optionMeta = { ...(this.item.system.optionSkillMeta ?? {}) };
const optionMeta = {
...(this.item.system.optionSkillMeta ?? {}),
};
const conflict =
ref.kind === "typed"
@@ -262,8 +276,9 @@ export default function ProfessionItemMixin(Base) {
*/
static removeAutomaticSkill(event, target) {
const skillKey = this.#getProfessionSkillKeyFromTarget(target);
if (!skillKey) return;
return this.#removeProfessionSkill("automaticSkills", skillKey);
if (skillKey) {
this.#removeProfessionSkill("automaticSkills", skillKey);
}
}
/**
@@ -272,9 +287,9 @@ export default function ProfessionItemMixin(Base) {
*/
static removeOptionSkill(event, target) {
const skillKey = this.#getProfessionSkillKeyFromTarget(target);
if (!skillKey) return;
if (skillKey === PROFESSION_OPTION_PICKS_KEY) return;
return this.#removeProfessionSkill("optionSkills", skillKey);
if (skillKey && skillKey !== PROFESSION_OPTION_PICKS_KEY) {
this.#removeProfessionSkill("optionSkills", skillKey);
}
}
};
}
+3 -5
View File
@@ -1,8 +1,5 @@
import { getDGRollToken } from "../../chat/dg-chat-card.js";
import {
createDGRollFromDataset,
processDGRoll,
} from "../../roll/roll.js";
import { createDGRollFromDataset, processDGRoll } from "../../roll/roll.js";
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
export default function RollSheetMixin(Base) {
@@ -39,7 +36,8 @@ export default function RollSheetMixin(Base) {
* @private
*/
static async _onRoll(event, target) {
if (target.classList.contains("not-rollable") || event.which === 2) return;
if (target.classList.contains("not-rollable") || event.which === 2)
return;
const item = this.actor.items.get(target.dataset.iid);
const roll = createDGRollFromDataset(target.dataset, {
+4 -2
View File
@@ -76,7 +76,9 @@ export default function SkillPrepMixin(Base) {
const specialTrainings = this._prepareSpecialTrainings();
const sortedCustomSkills = [];
for (const [key, skill] of Object.entries(this.actor.system.typedSkills)) {
for (const [key, skill] of Object.entries(
this.actor.system.typedSkills,
)) {
skill.type = "typeSkill";
skill.key = key;
skill.sortLabel = `${skill.group}.${skill.label}`;
@@ -180,7 +182,7 @@ export default function SkillPrepMixin(Base) {
simplifiedTraining.attributeTooltip = attributeTooltip;
if (this.actor.type === "agent") {
const rollTarget = this.actor.system.rollTarget;
const { rollTarget } = this.actor.system;
let modifier = 0;
if (rollTarget) {
@@ -23,7 +23,9 @@ export default function SpecialTrainingMixin(Base) {
}
break;
default:
void this._showSpecialTrainingDialog(actionType, id);
this._showSpecialTrainingDialog(actionType, id).catch((error) => {
console.error(error);
});
break;
}
}
+7 -3
View File
@@ -26,12 +26,16 @@ export default function TypedSkillMixin(Base) {
const { actionType, typedskill } = target.dataset;
switch (actionType) {
case "create":
void this._showTypedSkillDialog({ mode: "create" });
this._showTypedSkillDialog({ mode: "create" }).catch((error) => {
console.error(error);
});
break;
case "edit":
void this._showTypedSkillDialog({
this._showTypedSkillDialog({
mode: "edit",
targetSkill: typedskill,
}).catch((error) => {
console.error(error);
});
break;
case "delete":
@@ -47,7 +51,7 @@ export default function TypedSkillMixin(Base) {
*/
async _showTypedSkillDialog({ mode, targetSkill }) {
const isEdit = mode === "edit";
const typedSkills = this.actor.system.typedSkills;
const { typedSkills } = this.actor.system;
const currentLabel = isEdit ? typedSkills[targetSkill].label : "";
const currentGroup = isEdit
? typedSkills[targetSkill].group
+1 -1
View File
@@ -1,4 +1,4 @@
import { flagUnnaturalResourceMaxUpdates } from "../utils/actor-submit.js";
import flagUnnaturalResourceMaxUpdates from "../utils/actor-submit.js";
import DGNPCSheet from "./npc-sheet.js";
/** @extends {DGNPCSheet} */
+1 -1
View File
@@ -3,7 +3,7 @@
* @param {Actor} actor
* @returns {void}
*/
export function flagUnnaturalResourceMaxUpdates(submittedData, actor) {
export default function flagUnnaturalResourceMaxUpdates(submittedData, actor) {
const sourceStats = actor._source?.system?.statistics;
submittedData.system.wp.maxNeedsUpdate =
+3 -3
View File
@@ -44,9 +44,9 @@ export default function registerHandlebarsHelpers() {
Handlebars.registerHelper("getAvailableRollModes", () => {
try {
return Object.fromEntries(
ROLL_MESSAGE_MODE_KEYS.filter((key) => key in CONFIG.ChatMessage.modes).map(
(key) => [key, CONFIG.ChatMessage.modes[key]],
),
ROLL_MESSAGE_MODE_KEYS.filter(
(key) => key in CONFIG.ChatMessage.modes,
).map((key) => [key, CONFIG.ChatMessage.modes[key]]),
);
} catch (error) {
return console.log(error);
+5 -1
View File
@@ -7,7 +7,11 @@ const { HTMLField } = foundry.data.fields;
* @param {object} [options] Additional options passed to DataField#toInput
* @returns {Promise<string>}
*/
export async function prepareProseMirrorInput(document, fieldPath, options = {}) {
export async function prepareProseMirrorInput(
document,
fieldPath,
options = {},
) {
const field = document.system.schema.getField(fieldPath);
if (!(field instanceof HTMLField)) {
throw new Error(
+27 -20
View File
@@ -27,7 +27,7 @@ function getRollTargetEffectLabel(effect) {
if (effect.name?.trim()) return effect.name;
const parent = effect.parent;
const { parent } = effect;
if (parent?.documentName === "Item") return parent.name ?? "";
return effect.name ?? "";
@@ -39,27 +39,30 @@ function getRollTargetEffectLabel(effect) {
* @returns {Array<{ name: string, modifier: number }>}
*/
export function collectRollTargetContributions(actor, rollTargetFieldKey) {
if (actor.type !== "agent" || !ROLL_TARGET_FIELD_KEYS.includes(rollTargetFieldKey)) {
if (
actor.type !== "agent" ||
!ROLL_TARGET_FIELD_KEYS.includes(rollTargetFieldKey)
) {
return [];
}
const contributions = [];
for (const effect of actor.appliedEffects ?? []) {
if (!isRollTargetEffectActive(effect)) continue;
const change = (effect.changes ?? []).find(
(entry) => entry.key === rollTargetFieldKey && entry.type === "add",
);
if (!change) continue;
const modifier = Number(change.value);
if (!Number.isFinite(modifier) || modifier === 0) continue;
contributions.push({
name: getRollTargetEffectLabel(effect),
modifier,
});
if (isRollTargetEffectActive(effect)) {
const change = (effect.changes ?? []).find(
(entry) => entry.key === rollTargetFieldKey && entry.type === "add",
);
if (change) {
const modifier = Number(change.value);
if (Number.isFinite(modifier) && modifier !== 0) {
contributions.push({
name: getRollTargetEffectLabel(effect),
modifier,
});
}
}
}
}
return contributions;
@@ -119,7 +122,9 @@ function buildRollTargetBreakdownTable(
const body = rows
.map(
([label, value]) =>
`<tr><td>${foundry.utils.escapeHTML(label)}</td><td>${foundry.utils.escapeHTML(value)}</td></tr>`,
`<tr><td>${foundry.utils.escapeHTML(
label,
)}</td><td>${foundry.utils.escapeHTML(value)}</td></tr>`,
)
.join("");
@@ -181,7 +186,7 @@ export function appendRollTargetTooltipSection(
export function prepareAgentStatSanityTooltips(actor) {
if (actor.type !== "agent") return;
const system = actor.system;
const { system } = actor;
for (const [key, stat] of Object.entries(system.statistics ?? {})) {
const base = Number(stat.x5) || 0;
@@ -195,9 +200,11 @@ export function prepareAgentStatSanityTooltips(actor) {
);
}
const sanity = system.sanity;
const { sanity } = system;
if (sanity) {
const existing = `${game.i18n.localize("DG.Tooltip.CurrentSanityPartOne")}${sanity.currentBreakingPoint}${game.i18n.localize("DG.Tooltip.CurrentSanityPartTwo")}`;
const existing = `${game.i18n.localize("DG.Tooltip.CurrentSanityPartOne")}${
sanity.currentBreakingPoint
}${game.i18n.localize("DG.Tooltip.CurrentSanityPartTwo")}`;
const base = Number(sanity.value) || 0;
sanity.tooltip = appendRollTargetTooltipSection(
existing,
+37 -37
View File
@@ -7,43 +7,6 @@ const OBSOLETE_WORLD_SETTINGS = [
"characterSheetBackgroundImageSetting",
];
/**
* Run one-time world migrations for the Delta Green system.
*
* @returns {Promise<void>}
*/
export async function runWorldMigration() {
if (!game.user.isGM) return;
const currentVersion =
game.settings.get(DG.ID, "schemaMigrationVersion") ?? 0;
if (currentVersion >= MIGRATION_VERSION) return;
let migratedActors = 0;
const actors = game.actors.filter((actor) =>
ACTOR_TYPES_WITH_SKILLS.includes(actor.type),
);
for (const actor of actors) {
if (!actor.system.skills?.ritual) continue;
await actor.update({
"system.skills.-=ritual": null,
"system.schemaVersion": MIGRATION_VERSION,
});
migratedActors += 1;
}
removeObsoleteWorldSettings();
await game.settings.set(DG.ID, "schemaMigrationVersion", MIGRATION_VERSION);
console.log(
`Delta Green | World migration v${MIGRATION_VERSION} complete. Removed legacy ritual skill from ${migratedActors} actor(s).`,
);
}
/**
* @returns {void}
*/
@@ -56,3 +19,40 @@ function removeObsoleteWorldSettings() {
delete dgConfig[key];
}
}
/**
* Run one-time world migrations for the Delta Green system.
*
* @returns {Promise<void>}
*/
export default async function runWorldMigration() {
if (!game.user.isGM) return;
const currentVersion =
game.settings.get(DG.ID, "schemaMigrationVersion") ?? 0;
if (currentVersion >= MIGRATION_VERSION) return;
let migratedActors = 0;
const actors = game.actors.filter((actor) =>
ACTOR_TYPES_WITH_SKILLS.includes(actor.type),
);
for (const actor of actors) {
if (actor.system.skills?.ritual) {
await actor.update({
"system.skills.-=ritual": null,
"system.schemaVersion": MIGRATION_VERSION,
});
migratedActors += 1;
}
}
removeObsoleteWorldSettings();
await game.settings.set(DG.ID, "schemaMigrationVersion", MIGRATION_VERSION);
console.log(
`Delta Green | World migration v${MIGRATION_VERSION} complete. Removed legacy ritual skill from ${migratedActors} actor(s).`,
);
}
+11 -6
View File
@@ -793,7 +793,10 @@
.tab.combat .items-list .item-header,
.tab.combat .items-list .item:not(.item-header) {
display: grid;
grid-template-columns: 24px minmax(0, 1fr) minmax(4rem, auto) minmax(3rem, auto) 86px;
grid-template-columns: 24px minmax(0, 1fr) minmax(4rem, auto) minmax(
3rem,
auto
) 86px;
align-items: center;
column-gap: 5px;
}
@@ -807,7 +810,10 @@
}
.tab.combat .items-list .item-header .item-image,
.tab.combat .items-list .item-header > span:not(.centered-item-property):not(.item-controls) {
.tab.combat
.items-list
.item-header
> span:not(.centered-item-property):not(.item-controls) {
min-width: 0;
}
@@ -858,10 +864,9 @@
}
.tab.personal .bio-grid-5col {
grid-template-columns: minmax(4rem, max-content) minmax(0, 1fr) 20px minmax(
4rem,
max-content
) minmax(0, 1fr);
grid-template-columns:
minmax(4rem, max-content) minmax(0, 1fr) 20px minmax(4rem, max-content)
minmax(0, 1fr);
}
.tab.personal .notes-wrapper {
+4 -1
View File
@@ -14,7 +14,10 @@
padding: 0 0.5rem 0.5rem;
}
.deltagreen .tab.profession-skills .profession-skills-section + .profession-skills-section {
.deltagreen
.tab.profession-skills
.profession-skills-section
+ .profession-skills-section {
margin-top: 1rem;
padding-top: 0.75rem;
border-top: 1px solid var(--color-border-light-2, rgba(0, 0, 0, 0.15));
+5 -1
View File
@@ -28,7 +28,11 @@
}
},
"esmodules": ["module/deltagreen.js"],
"styles": ["css/deltagreen.css", "css/dg-dialog.css", "css/damage-modify.css"],
"styles": [
"css/deltagreen.css",
"css/dg-dialog.css",
"css/damage-modify.css"
],
"languages": [
{
"lang": "en",