Fix linting errors and cleanup.
This commit is contained in:
@@ -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).
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } = {},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" }),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {} }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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} */
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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).`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user