Refactor and unify some item styling.
Restyle profession item. Restyle skills tab.
This commit is contained in:
+5
-19
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"DG.SystemName": "Delta Green",
|
||||
"DG.AgentNameFieldTooltip": "LAST NAME, FIRST NAME (AND ALIAS IF APPLICABLE)",
|
||||
"DG.RollLuck": "Roll Luck",
|
||||
"DG.Luck": "Luck",
|
||||
@@ -42,6 +41,8 @@
|
||||
"DG.Skills.unarmed_combat": "Unarmed Combat",
|
||||
"DG.Skills.unnatural": "Unnatural",
|
||||
"DG.Skills.flight": "Flight",
|
||||
"DG.Skills.Tooltip.flight": "Fly or maneuver in air using wings or unnatural means.",
|
||||
"DG.Skills.Tooltip.ritual": "Perform occult rituals; each ritual is a separate skill.",
|
||||
"DG.Skills.Tooltip.accounting": "Spot financial red flags like hidden assets, shell companies, or embezzlement.",
|
||||
"DG.Skills.Tooltip.alertness": "Notice hidden dangers, subtle movements, or quiet sounds before they become threats.",
|
||||
"DG.Skills.Tooltip.anthropology": "Understand the beliefs, behaviors, and taboos of living cultures and societies.",
|
||||
@@ -81,6 +82,7 @@
|
||||
"DG.Skills.Tooltip.unnatural": "Recognize, research, or recall truths about entities and forces beyond human understanding.",
|
||||
"DG.Skills.AddTypedOrCustomSkill": "Add Typed/Custom Skill",
|
||||
"DG.Skills.ToggleEditDeleteSkills": "Edit/Delete Skills",
|
||||
"DG.Skills.EditDeleteTypedSkills": "Edit/Delete Typed Skills",
|
||||
"DG.Skills.AddSkill": "Add Skill",
|
||||
"DG.Skills.Apply": "Apply",
|
||||
"DG.Skills.ApplySkillImprovements.Title": "Apply Skill Improvements",
|
||||
@@ -155,6 +157,7 @@
|
||||
"DG.About.OnlineHelpTooltip": "Launch browser with online GitHub help.",
|
||||
"DG.About.OnlineBugTracker": "Questions, Bug Reports or Feature Requests (ONLINE)",
|
||||
"DG.About.OnlineBugTrackerTooltip": "Launch browser with GitHub issue tracker.",
|
||||
"DG.About.ChangelogTooltip": "Launch browser with the online changelog.",
|
||||
"DG.Roll.Target": "Target: ",
|
||||
"DG.Roll.Success": "Success",
|
||||
"DG.Roll.Failure": "Failure",
|
||||
@@ -164,7 +167,6 @@
|
||||
"DG.Roll.Lethal": "Lethal",
|
||||
"DG.Roll.For": "For",
|
||||
"DG.Roll.Damage": "Damage",
|
||||
"DG.Roll.Or": "Or",
|
||||
"DG.Roll.Roll": "Roll",
|
||||
"DG.Physical.Wounds": "Wounds",
|
||||
"DG.Physical.FirstAidLabel": "First Aid Has Already Been Attempted: ",
|
||||
@@ -175,7 +177,6 @@
|
||||
"DG.Physical.SuppressExhaustion": "Exhaustion Suppressed",
|
||||
"DG.Physical.ExhaustionEffectName": "Exhaustion",
|
||||
"DG.Physical.StimulantsEffectName": "Stimulants",
|
||||
"DG.Physical.RestBlockedWhileSuppressed": "{name} cannot rest while exhaustion is suppressed (stimulants or manual suppression).",
|
||||
"DG.Physical.ExhaustDialogTitle": "Exhaust Agent",
|
||||
"DG.Physical.RollWillpowerLoss": "Roll Willpower Loss",
|
||||
"DG.Physical.StimulantsDialogTitle": "Take Stimulants",
|
||||
@@ -219,7 +220,6 @@
|
||||
"DG.Mental.BreakingPoint": "Breaking Point: ",
|
||||
"DG.Mental.Tooltip.BreakingPoint": "When SAN falls to or below this number, Agent gains a long-term mental disorder; set a new Breaking Point to current SAN - POW. Disorders remain even if SAN later rises above a previous Breaking Point.",
|
||||
"DG.Mental.Reset": "Reset",
|
||||
"DG.Mental.SanityLossIncidentsLabel": "Incidents of SAN Loss Without Going Insane",
|
||||
"DG.Mental.Violence": "Violence",
|
||||
"DG.Mental.Tooltip.Violence": "Adapting to Violence: After losing SAN to violence three times without going insane, you become hardened to it. You automatically succeed SAN rolls against violence, but permanently lose 1D6 CHA and that much from each Bond. Agents Handbook Page 73.",
|
||||
"DG.Mental.Tooltip.Checkbox.Violence": "When you lose SAN due to violence and without going temporarily insane or hitting your breaking point, increase until adapted. Agents Handbook Page 73.",
|
||||
@@ -250,10 +250,8 @@
|
||||
"DG.CV.NationalityPlaceholder": "Nationality",
|
||||
"DG.CV.Education": "Education",
|
||||
"DG.CV.EducationPlaceholder": "Education or Training",
|
||||
"DG.Bonds.Bond": "Bond",
|
||||
"DG.Bonds.Name": "Name",
|
||||
"DG.Bonds.Score": "Score",
|
||||
"DG.Bonds.NewBond": "New Bond",
|
||||
"DG.Tooltip.MaximumHP": "Maximum health is the average of STR + CON, rounded UP.",
|
||||
"DG.Tooltip.MaximumWP": "Maximum WP is equal to POW.",
|
||||
"DG.Tooltip.CurrentSanityPartOne": "Current sanity, current breaking point is ",
|
||||
@@ -280,7 +278,6 @@
|
||||
"DG.Gear.Expense.Extreme": "Extreme",
|
||||
"DG.ItemWindow.Bonds.Description": "Description/Developments",
|
||||
"DG.ItemWindow.Bonds.ScoreTooltip": "A higher score represents a stronger relationship between the agent and the bond.",
|
||||
"DG.ItemWindow.Motivations.Attributes": "Attributes",
|
||||
"DG.ItemWindow.Motivations.Motivation": "Motivation",
|
||||
"DG.ItemWindow.Motivations.Description": "Description/Notes",
|
||||
"DG.ItemWindow.Motivations.Disorder": "Disorder",
|
||||
@@ -292,7 +289,6 @@
|
||||
"DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip": "A critical success on a therapy luck roll can 'cure' a disorder. It may return the next time a breaking point is hit however. See Pages 78-79 of the Agent's Handbook - Therapy.",
|
||||
"DG.ItemWindow.Motivations.AcuteEpisode": "Acute Episode",
|
||||
"DG.ItemWindow.Motivations.AcuteEpisodeTooltip": "While checked, transferred active effects on this motivation apply to the agent.",
|
||||
"DG.ItemWindow.Gear.Attributes": "Attributes",
|
||||
"DG.ItemWindow.Gear.Description": "Description",
|
||||
"DG.ItemWindow.Gear.NamePlaceholder": "Name",
|
||||
"DG.ItemWindow.Gear.Expense": "Expense",
|
||||
@@ -333,18 +329,14 @@
|
||||
"DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Use this field to add a flat modifier such as '+1' or another dice term such as '+1d4'.",
|
||||
"DG.Roll.Inhuman": "Inhuman",
|
||||
"DG.Tooltip.BreakPoint": "The current breaking point, reset when current sanity falls to or below this value. The starting breaking point is SAN - POW.",
|
||||
"DG.Tooltip.BreakPointReset": "Reset breaking point to be current SAN - WP.",
|
||||
"DG.Skills.ritual": "Ritual",
|
||||
"DG.RollMode": "Roll Mode",
|
||||
"DG.Bonds.Relationship": "Relationship",
|
||||
"DG.TypeSkills.Subskills.Painting": "Painting",
|
||||
"DG.Sheet.BlockHeaders.Attributes": "ATTRIBUTES",
|
||||
"DG.Sheet.BlockHeaders.Statistics": "STATISTICS",
|
||||
"DG.Sheet.BlockHeaders.PhysicalDescriptionNotes": "PHYSICAL DESCRIPTION / NOTES",
|
||||
"DG.Sheet.BlockHeaders.SkillsAndTraining": "SKILLS AND TRAINING",
|
||||
"DG.Sheet.BlockHeaders.SpecialTraining": "SPECIAL TRAINING",
|
||||
"DG.Sheet.BlockHeaders.Injuries": "INJURIES",
|
||||
"DG.Sheet.BlockHeaders.BreakingPointAndAdaptations": "BREAKING POINT AND ADAPTATIONS",
|
||||
"DG.Sheet.BlockHeaders.Adaptations": "Adaptations",
|
||||
"DG.Sheet.BlockHeaders.MotivationsAndDisorders": "MOTIVATIONS AND DISORDERS",
|
||||
"DG.Sheet.BlockHeaders.PersonalInfoCV": "PERSONAL INFORMATION / CV",
|
||||
@@ -353,11 +345,8 @@
|
||||
"DG.AgentSheet.ProfessionPlaceholder": "PROFESSION (RANK IF APPLICABLE)",
|
||||
"DG.AgentSheet.OpenProfessionToEdit": "Click to edit profession",
|
||||
"DG.UnnaturalSheet.HideUntrainedSkillsLabel": "Hide Untrained",
|
||||
"DG.UnnaturalSheet.NotesLabel": "Notes",
|
||||
"DG.UnnaturalSheet.ShortDescriptionPlaceholder": "Description of creature or creature type",
|
||||
"DG.UnnaturalSheet.ShowUntrainedSkillsLabel": "Show Untrained",
|
||||
"DG.UnnaturalSheet.Tabs.NotesTabLabel": "Skills & Notes",
|
||||
"DG.UnnaturalSheet.Tabs.ItemsTabLabel": "Attacks & Abilities",
|
||||
"DG.NPCSheet.BreakPointAbbreviation": "BP",
|
||||
"DG.ImpossibleLanscapes.IHaveSeenThe": "I HAVE SEEN THE",
|
||||
"DG.ImpossibleLanscapes.YellowSign": "YELLOW SIGN",
|
||||
@@ -365,9 +354,6 @@
|
||||
"DG.ImpossibleLanscapes.CorruptionSecondHalf": "tion",
|
||||
"DG.ImpossibleLanscapes.Gift": "Gift",
|
||||
"DG.ImpossibleLanscapes.Insight": "Insight",
|
||||
"DG.VehicleSheet.NotesTabName": "Notes",
|
||||
"DG.VehicleSheet.GearTabName": "Gear",
|
||||
"DG.VehicleSheet.NotesLabel": "NOTES",
|
||||
"DG.VehicleSheet.DescriptionPlaceHolder": "Vehicle Description",
|
||||
"DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene": "Check a damaged Bond's box until the next Home Scene ends.",
|
||||
"DG.Generic.ClearAll": "Clear Checks",
|
||||
@@ -393,7 +379,6 @@
|
||||
"TYPES.Item.ritual": "Ritual",
|
||||
"TYPES.Item.profession": "Profession",
|
||||
"DG.Profession.AlreadyAssigned": "This agent already has a profession. Another cannot be assigned.",
|
||||
"DG.Profession.AssignedHeader": "ASSIGNED PROFESSION ITEM",
|
||||
"DG.Profession.Dialog.Title": "Character Creation",
|
||||
"DG.Profession.Dialog.Tab.SkillOptions": "Skill Options",
|
||||
"DG.Profession.Dialog.Tab.BonusSkills": "Bonus Skills",
|
||||
@@ -457,6 +442,7 @@
|
||||
"DG.Tome.Name": "Name",
|
||||
"DG.Tome.Language": "Language",
|
||||
"DG.Tome.StudyTime": "Study Time",
|
||||
"DG.ItemWindow.Tome.NamePlaceholder": "Name",
|
||||
"DG.ItemWindow.Tome.Description": "Description",
|
||||
"DG.ItemWindow.Tome.LanguagePlaceholder": "Language",
|
||||
"DG.ItemWindow.Tome.StudyTime": "Study Time",
|
||||
|
||||
@@ -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).
|
||||
@@ -0,0 +1,25 @@
|
||||
import DG from "../../config/index.js";
|
||||
|
||||
/**
|
||||
* @param {Actor} actor
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasActiveStimulantEffect(actor) {
|
||||
return (
|
||||
actor.effects?.some(
|
||||
(effect) => effect.getFlag(DG.ID, "stimulant") && !effect.isSuppressed,
|
||||
) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persisted manual suppress or an active stimulant AE.
|
||||
*
|
||||
* @param {Actor} actor
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getEffectiveSuppressExhaustion(actor) {
|
||||
const persisted =
|
||||
actor._source?.system?.physical?.suppressExhaustion ?? false;
|
||||
return Boolean(persisted) || hasActiveStimulantEffect(actor);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
prepareAgentSkillFlags,
|
||||
prepareBreakingPointHit,
|
||||
prepareStatisticsX5,
|
||||
} from "./derived-actor-data.js";
|
||||
} from "../../data/derived/actor-derived.js";
|
||||
|
||||
/**
|
||||
* @param {number} current
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import DG from "../config.js";
|
||||
import { ROLL_TARGET_FIELD_KEYS } from "../active-effect/effect-fields.js";
|
||||
import { getEffectiveSuppressExhaustion } from "./stimulant-effect.js";
|
||||
import DG from "../../config/index.js";
|
||||
import { ROLL_TARGET_FIELD_KEYS } from "../effect-fields.js";
|
||||
import { getEffectiveSuppressExhaustion } from "./agent-condition-sync.js";
|
||||
|
||||
/** @type {WeakMap<Actor, Promise<void>>} */
|
||||
const syncLocks = new WeakMap();
|
||||
+2
-11
@@ -1,4 +1,4 @@
|
||||
import DG from "../config.js";
|
||||
import DG from "../../config/index.js";
|
||||
import { syncExhaustionEffect } from "./exhaustion-effect.js";
|
||||
|
||||
/**
|
||||
@@ -51,16 +51,7 @@ function getStimulantRemainingHours(effect) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persisted manual suppress or an active stimulant AE.
|
||||
* @param {Actor} actor
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getEffectiveSuppressExhaustion(actor) {
|
||||
const persisted =
|
||||
actor._source?.system?.physical?.suppressExhaustion ?? false;
|
||||
return Boolean(persisted) || hasActiveStimulantEffect(actor);
|
||||
}
|
||||
export { getEffectiveSuppressExhaustion } from "./agent-condition-sync.js";
|
||||
|
||||
/**
|
||||
* @param {number} hours
|
||||
@@ -1,5 +1,5 @@
|
||||
import DG from "../config.js";
|
||||
import { getStatisticEffectiveValue } from "./derived-actor-data.js";
|
||||
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.
|
||||
@@ -1,11 +1,11 @@
|
||||
import { refreshDerivedAfterActiveEffects } from "../utils/active-effect-derived.js";
|
||||
import { refreshDerivedAfterActiveEffects } from "../active-effect/runtime/derived.js";
|
||||
import { prepareAgentStatSanityTooltips } from "../utils/roll-target-tooltip.js";
|
||||
import {
|
||||
syncExhaustionEffect,
|
||||
updateTouchesExhaustionPhysical,
|
||||
} from "../utils/exhaustion-effect.js";
|
||||
import { pruneExpiredStimulantEffects } from "../utils/stimulant-effect.js";
|
||||
import { sanitizeActiveEffectBackedUpdateData } from "../utils/active-effect-submit.js";
|
||||
} from "../active-effect/runtime/exhaustion-effect.js";
|
||||
import { pruneExpiredStimulantEffects } from "../active-effect/runtime/stimulant-effect.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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import { BASE_TEMPLATE_PATH } from "../config/index.js";
|
||||
import {
|
||||
bindDialogTabs,
|
||||
getDialogContentRoot,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
parseProfessionSkillKey,
|
||||
prepareProfessionSkillRows,
|
||||
splitProfessionSkillMap,
|
||||
} from "../utils/profession-skills.js";
|
||||
} from "../profession/index.js";
|
||||
import {
|
||||
reorderForColumnSorting,
|
||||
splitIntoColumns,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import { BASE_TEMPLATE_PATH } from "../config/index.js";
|
||||
import {
|
||||
STAT_KEYS,
|
||||
STAT_MAX,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
buildStatisticRows,
|
||||
getDefaultPointBuyValues,
|
||||
validatePointBuyValues,
|
||||
} from "../utils/profession-stat-setup.js";
|
||||
} from "../profession/stat-setup.js";
|
||||
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
|
||||
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DG from "../config.js";
|
||||
import DG from "../config/index.js";
|
||||
import {
|
||||
buildEffectFieldSelectOptions,
|
||||
getEffectFieldGroupsForDocument,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DG from "../config.js";
|
||||
import DG from "../config/index.js";
|
||||
|
||||
const { DialogV2 } = foundry.applications.api;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import DG, { BASE_TEMPLATE_PATH } from "../config/index.js";
|
||||
|
||||
export default class ActorEditStatForm extends foundry.applications.api.HandlebarsApplicationMixin(
|
||||
foundry.applications.api.ApplicationV2,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import { BASE_TEMPLATE_PATH } from "../config/index.js";
|
||||
import { showDgDialog } from "./dg-dialog.js";
|
||||
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
@@ -24,7 +24,7 @@ function createProfessionStub(actor, itemData) {
|
||||
* @param {TokenDocument|null} [options.token]
|
||||
* @returns {Promise<boolean>} True if Character Creation was submitted successfully.
|
||||
*/
|
||||
export async function runProfessionSetupFlow(
|
||||
async function runProfessionSetupFlow(
|
||||
professionItem,
|
||||
actor,
|
||||
{ token = null } = {},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import { BASE_TEMPLATE_PATH } from "../config/index.js";
|
||||
import { createDGChatMessage } from "../chat/dg-chat-card.js";
|
||||
import {
|
||||
STAT_KEYS,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
buildRollStatsChatContent,
|
||||
buildStatisticRows,
|
||||
rollStatisticScores,
|
||||
} from "../utils/profession-stat-setup.js";
|
||||
} from "../profession/stat-setup.js";
|
||||
import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js";
|
||||
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* global TokenDocument */
|
||||
|
||||
import DG from "../config.js";
|
||||
import DG from "../config/index.js";
|
||||
|
||||
const CHAT_CARD_TEMPLATE = "systems/deltagreen/templates/chat/dg-chat-card.hbs";
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
createDGChatMessage,
|
||||
createDGRollChatMessage,
|
||||
} from "../chat/dg-chat-card.js";
|
||||
import { createDGRollChatMessage } from "./dg-chat-card.js";
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
@@ -10,8 +7,8 @@ import {
|
||||
* @param {string} params.contentKey i18n key for message body.
|
||||
* @param {string} params.labelKey i18n key for card subtitle.
|
||||
* @param {Record<string, string|number>} params.i18nData
|
||||
* @param {Roll} params.roll Roll attached to the chat message for display and Dice So Nice.
|
||||
* @param {string|null} [params.extraContentKey] Optional i18n key appended on a new line after the main body.
|
||||
* @param {Roll} [params.roll] When set, attaches the roll to the chat message for display and Dice So Nice.
|
||||
* @returns {Promise<ChatMessage>}
|
||||
*/
|
||||
export async function createAgentResourceChatMessage({
|
||||
@@ -20,8 +17,8 @@ export async function createAgentResourceChatMessage({
|
||||
contentKey,
|
||||
labelKey,
|
||||
i18nData,
|
||||
roll,
|
||||
extraContentKey = null,
|
||||
roll = null,
|
||||
}) {
|
||||
let content = game.i18n.format(contentKey, i18nData);
|
||||
if (extraContentKey) {
|
||||
@@ -30,20 +27,10 @@ export async function createAgentResourceChatMessage({
|
||||
const label = game.i18n.localize(labelKey);
|
||||
const messageMode = game.settings.get("core", "messageMode");
|
||||
|
||||
if (roll) {
|
||||
return createDGRollChatMessage({
|
||||
actor,
|
||||
token,
|
||||
roll,
|
||||
label,
|
||||
content,
|
||||
messageMode,
|
||||
});
|
||||
}
|
||||
|
||||
return createDGChatMessage({
|
||||
return createDGRollChatMessage({
|
||||
actor,
|
||||
token,
|
||||
roll,
|
||||
label,
|
||||
content,
|
||||
messageMode,
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
prepareSanityAdaptations,
|
||||
prepareStatisticsX5,
|
||||
removeLegacyRitualSkill,
|
||||
} from "../../utils/derived-actor-data.js";
|
||||
} from "../../data/derived/actor-derived.js";
|
||||
|
||||
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
|
||||
const { SchemaField, NumberField, StringField, BooleanField } =
|
||||
foundry.data.fields;
|
||||
|
||||
export default class AgentData extends CharacterData {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/** Default proficiency for fixed agent skills (single source of truth for schema initials). */
|
||||
export const AGENT_SKILL_DEFAULTS = /** @type {Record<string, number>} */ ({
|
||||
accounting: 10,
|
||||
alertness: 20,
|
||||
anthropology: 0,
|
||||
archeology: 0,
|
||||
artillery: 0,
|
||||
athletics: 30,
|
||||
bureaucracy: 10,
|
||||
computer_science: 0,
|
||||
criminology: 10,
|
||||
demolitions: 0,
|
||||
disguise: 10,
|
||||
dodge: 30,
|
||||
drive: 20,
|
||||
firearms: 20,
|
||||
first_aid: 10,
|
||||
forensics: 0,
|
||||
heavy_machiner: 10,
|
||||
heavy_weapons: 0,
|
||||
history: 10,
|
||||
humint: 10,
|
||||
law: 0,
|
||||
medicine: 0,
|
||||
melee_weapons: 30,
|
||||
navigate: 10,
|
||||
occult: 10,
|
||||
persuade: 20,
|
||||
pharmacy: 0,
|
||||
psychotherapy: 10,
|
||||
ride: 10,
|
||||
search: 20,
|
||||
sigint: 0,
|
||||
stealth: 10,
|
||||
surgery: 0,
|
||||
survival: 10,
|
||||
swim: 20,
|
||||
unarmed_combat: 40,
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AGENT_SKILL_DEFAULTS } from "./agent-skill-defaults.js";
|
||||
import { skillField } from "./general.js";
|
||||
|
||||
const { SchemaField, NumberField, StringField, ArrayField, ObjectField } =
|
||||
@@ -6,44 +7,45 @@ const { TypeDataModel } = foundry.abstract;
|
||||
|
||||
export default class HumanSkillsActorData extends TypeDataModel {
|
||||
static defineSchema() {
|
||||
const d = AGENT_SKILL_DEFAULTS;
|
||||
return {
|
||||
skills: new SchemaField({
|
||||
accounting: skillField(10, "Accounting"),
|
||||
alertness: skillField(20, "Alertness"),
|
||||
anthropology: skillField(0, "Anthropology"),
|
||||
archeology: skillField(0, "Archeology"),
|
||||
artillery: skillField(0, "Artillery"),
|
||||
athletics: skillField(30, "Athletics"),
|
||||
bureaucracy: skillField(10, "Bureaucracy"),
|
||||
computer_science: skillField(0, "Computer Science"),
|
||||
criminology: skillField(10, "Criminology"),
|
||||
demolitions: skillField(0, "Demolitions"),
|
||||
disguise: skillField(10, "Disguise"),
|
||||
dodge: skillField(30, "Dodge"),
|
||||
drive: skillField(20, "Drive"),
|
||||
firearms: skillField(20, "Firearms"),
|
||||
first_aid: skillField(10, "First Aid"),
|
||||
forensics: skillField(0, "Forensics"),
|
||||
heavy_machiner: skillField(10, "Heavy Machinery"),
|
||||
heavy_weapons: skillField(0, "Heavy Weapons"),
|
||||
history: skillField(10, "History"),
|
||||
humint: skillField(10, "HUMINT"),
|
||||
law: skillField(0, "Law"),
|
||||
medicine: skillField(0, "Medicine"),
|
||||
melee_weapons: skillField(30, "Melee Weapons"),
|
||||
navigate: skillField(10, "Navigate"),
|
||||
occult: skillField(10, "Occult"),
|
||||
persuade: skillField(20, "Persuade"),
|
||||
pharmacy: skillField(0, "Pharmacy"),
|
||||
psychotherapy: skillField(10, "Psychotherapy"),
|
||||
ride: skillField(10, "Ride"),
|
||||
search: skillField(20, "Search"),
|
||||
sigint: skillField(0, "SIGINT"),
|
||||
stealth: skillField(10, "Stealth"),
|
||||
surgery: skillField(0, "Surgery"),
|
||||
survival: skillField(10, "Survival"),
|
||||
swim: skillField(20, "Swim"),
|
||||
unarmed_combat: skillField(40, "Unarmed Combat"),
|
||||
accounting: skillField(d.accounting, "Accounting"),
|
||||
alertness: skillField(d.alertness, "Alertness"),
|
||||
anthropology: skillField(d.anthropology, "Anthropology"),
|
||||
archeology: skillField(d.archeology, "Archeology"),
|
||||
artillery: skillField(d.artillery, "Artillery"),
|
||||
athletics: skillField(d.athletics, "Athletics"),
|
||||
bureaucracy: skillField(d.bureaucracy, "Bureaucracy"),
|
||||
computer_science: skillField(d.computer_science, "Computer Science"),
|
||||
criminology: skillField(d.criminology, "Criminology"),
|
||||
demolitions: skillField(d.demolitions, "Demolitions"),
|
||||
disguise: skillField(d.disguise, "Disguise"),
|
||||
dodge: skillField(d.dodge, "Dodge"),
|
||||
drive: skillField(d.drive, "Drive"),
|
||||
firearms: skillField(d.firearms, "Firearms"),
|
||||
first_aid: skillField(d.first_aid, "First Aid"),
|
||||
forensics: skillField(d.forensics, "Forensics"),
|
||||
heavy_machiner: skillField(d.heavy_machiner, "Heavy Machinery"),
|
||||
heavy_weapons: skillField(d.heavy_weapons, "Heavy Weapons"),
|
||||
history: skillField(d.history, "History"),
|
||||
humint: skillField(d.humint, "HUMINT"),
|
||||
law: skillField(d.law, "Law"),
|
||||
medicine: skillField(d.medicine, "Medicine"),
|
||||
melee_weapons: skillField(d.melee_weapons, "Melee Weapons"),
|
||||
navigate: skillField(d.navigate, "Navigate"),
|
||||
occult: skillField(d.occult, "Occult"),
|
||||
persuade: skillField(d.persuade, "Persuade"),
|
||||
pharmacy: skillField(d.pharmacy, "Pharmacy"),
|
||||
psychotherapy: skillField(d.psychotherapy, "Psychotherapy"),
|
||||
ride: skillField(d.ride, "Ride"),
|
||||
search: skillField(d.search, "Search"),
|
||||
sigint: skillField(d.sigint, "SIGINT"),
|
||||
stealth: skillField(d.stealth, "Stealth"),
|
||||
surgery: skillField(d.surgery, "Surgery"),
|
||||
survival: skillField(d.survival, "Survival"),
|
||||
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 }),
|
||||
label: new StringField({ initial: "Unnatural" }),
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
prepareStatisticsX5,
|
||||
removeLegacyRitualSkill,
|
||||
setSkillTargetProficiencies,
|
||||
} from "../../utils/derived-actor-data.js";
|
||||
} from "../../data/derived/actor-derived.js";
|
||||
|
||||
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
|
||||
const { SchemaField, NumberField, StringField, BooleanField } =
|
||||
foundry.data.fields;
|
||||
|
||||
export default class NPCData extends CharacterData {
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
prepareStatisticsX5,
|
||||
removeLegacyRitualSkill,
|
||||
setSkillTargetProficiencies,
|
||||
} from "../../utils/derived-actor-data.js";
|
||||
} from "../../data/derived/actor-derived.js";
|
||||
|
||||
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
|
||||
const { SchemaField, NumberField, StringField, BooleanField } =
|
||||
foundry.data.fields;
|
||||
|
||||
export default class UnnaturalData extends CharacterData {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import SheetSettingsActorData from "./base/sheet-settings.js";
|
||||
import DGHTMLField from "../fields/html-content-field.js";
|
||||
import { computeEquippedArmorProtection } from "../../utils/derived-actor-data.js";
|
||||
import { computeEquippedArmorProtection } from "../../data/derived/actor-derived.js";
|
||||
|
||||
const { SchemaField, NumberField, StringField, ArrayField, HTMLField } =
|
||||
const { SchemaField, NumberField, StringField, ArrayField } =
|
||||
foundry.data.fields;
|
||||
|
||||
export default class VehicleData extends foundry.abstract.TypeDataModel {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import defineBaseItemSystemFields from "./base-fields.js";
|
||||
import DGHTMLField from "../fields/html-content-field.js";
|
||||
|
||||
const { NumberField, StringField, BooleanField, SchemaField, HTMLField } =
|
||||
const { NumberField, StringField, BooleanField, SchemaField } =
|
||||
foundry.data.fields;
|
||||
|
||||
export default class RitualItemData extends foundry.abstract.TypeDataModel {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import defineBaseItemSystemFields from "./base-fields.js";
|
||||
import DGHTMLField from "../fields/html-content-field.js";
|
||||
|
||||
const { NumberField, StringField, BooleanField, SchemaField, HTMLField } =
|
||||
const { NumberField, StringField, BooleanField, SchemaField } =
|
||||
foundry.data.fields;
|
||||
|
||||
export default class TomeItemData extends foundry.abstract.TypeDataModel {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Import Modules
|
||||
import DG from "./config.js";
|
||||
import DG from "./config/index.js";
|
||||
import DeltaGreenActor from "./actor/actor.js";
|
||||
import DGAgentSheet from "./sheets/agent-sheet.js";
|
||||
import DeltaGreenItem from "./item/item.js";
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
DGLethalityRoll,
|
||||
DGDamageRoll,
|
||||
DGSanityDamageRoll,
|
||||
} from "./roll/roll-classes.js";
|
||||
} from "./roll/roll.js";
|
||||
import registerSystemSettings from "./settings.js";
|
||||
import preloadHandlebarsTemplates from "./templates.js";
|
||||
import registerHandlebarsHelpers from "./utils/register-helpers.js";
|
||||
@@ -39,14 +39,14 @@ import GearItemData from "./data/item/gear.js";
|
||||
import TomeItemData from "./data/item/tome.js";
|
||||
import RitualItemData from "./data/item/ritual.js";
|
||||
import ProfessionItemData from "./data/item/profession.js";
|
||||
import DGActiveEffect from "./documents/dg-active-effect.js";
|
||||
import DGActiveEffect from "./active-effect/documents/dg-active-effect.js";
|
||||
import DGActiveEffectConfig from "./applications/dg-active-effect-config.js";
|
||||
import DGActiveEffectTypeDataModel from "./data/active-effect/dg-active-effect-data.js";
|
||||
import { syncExhaustionEffect } from "./utils/exhaustion-effect.js";
|
||||
import DGActiveEffectTypeDataModel from "./active-effect/data/dg-active-effect-data.js";
|
||||
import { syncExhaustionEffect } from "./active-effect/runtime/exhaustion-effect.js";
|
||||
import {
|
||||
pruneAllAgentsExpiredStimulants,
|
||||
pruneExpiredStimulantEffects,
|
||||
} from "./utils/stimulant-effect.js";
|
||||
} from "./active-effect/runtime/stimulant-effect.js";
|
||||
|
||||
const { Actors, Items } = foundry.documents.collections;
|
||||
const { DocumentSheetConfig } = foundry.applications.apps;
|
||||
|
||||
+6
-15
@@ -1,4 +1,5 @@
|
||||
import { getDGRollToken } from "../chat/dg-chat-card.js";
|
||||
import { appendMeleeDamageBonus } from "../roll/melee-damage.js";
|
||||
import { DGDamageRoll, DGLethalityRoll } from "../roll/roll.js";
|
||||
|
||||
/**
|
||||
@@ -6,13 +7,6 @@ import { DGDamageRoll, DGLethalityRoll } from "../roll/roll.js";
|
||||
* @extends {Item}
|
||||
*/
|
||||
export default class DeltaGreenItem extends Item {
|
||||
/**
|
||||
* Augment the basic Item data model with additional dynamic data.
|
||||
*/
|
||||
prepareData() {
|
||||
super.prepareData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clickable rolls.
|
||||
* @param {Event} event The originating click event
|
||||
@@ -22,8 +16,6 @@ export default class DeltaGreenItem extends Item {
|
||||
// Basic template rendering data
|
||||
const item = this;
|
||||
const { actor } = this;
|
||||
const actorSystemData = this.actor.system || {};
|
||||
|
||||
let roll;
|
||||
if (item.system.isLethal) {
|
||||
roll = new DGLethalityRoll(
|
||||
@@ -38,12 +30,11 @@ export default class DeltaGreenItem extends Item {
|
||||
);
|
||||
} else {
|
||||
// regular damage roll
|
||||
let diceFormula = item.system.damage;
|
||||
const skillType = item.system.skill;
|
||||
|
||||
if (skillType === "unarmed_combat" || skillType === "melee_weapons") {
|
||||
diceFormula += actorSystemData.statistics.str.meleeDamageBonusFormula;
|
||||
}
|
||||
let diceFormula = appendMeleeDamageBonus(
|
||||
item.system.damage,
|
||||
actor,
|
||||
item.system.skill,
|
||||
);
|
||||
|
||||
if (isCrit) {
|
||||
diceFormula = `2*(${diceFormula})`;
|
||||
|
||||
@@ -111,9 +111,13 @@ function GetAttacksFromInput(inputText) {
|
||||
}
|
||||
}
|
||||
|
||||
// look for armor piercing
|
||||
if (attackLine.indexOf("ARMOR PIERCING") >= 0) {
|
||||
// TODO: This should do something.
|
||||
const apMatch = attackLine.match(/ARMOR\s+PIERCING\s*(\d+)/i);
|
||||
const armorPiercing = apMatch?.[1]
|
||||
? parseInt(apMatch[1], 10)
|
||||
: 5;
|
||||
weaponData.armorPiercing = armorPiercing;
|
||||
weaponData.system.armorPiercing = armorPiercing;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import DG from "../config/index.js";
|
||||
import {
|
||||
BONUS_SKILL_CATALOG_EXCLUDED_FIXED,
|
||||
BONUS_SKILL_COUNT,
|
||||
BONUS_SKILL_INCREMENT,
|
||||
MAX_ALLOWED_BONUS_WASTE,
|
||||
SKILL_CAP,
|
||||
TYPED_SKILL_TEMPLATE_GROUPS,
|
||||
} from "./constants.js";
|
||||
import {
|
||||
formatProfessionSkillKey,
|
||||
formatProfessionSkillLabel,
|
||||
getAgentSkillDefaults,
|
||||
getTypedGroupDisplayName,
|
||||
parseProfessionSkillKey,
|
||||
} from "./keys.js";
|
||||
|
||||
/**
|
||||
* @typedef {object} SkillCatalogEntry
|
||||
* @property {string} id - Select value (`fixed:accounting` or `typed:Craft`)
|
||||
* @property {ProfessionSkillRef} ref
|
||||
* @property {string} label
|
||||
* @property {string} sortLabel
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {SkillCatalogEntry[]}
|
||||
*/
|
||||
export function buildSkillCatalog() {
|
||||
/** @type {SkillCatalogEntry[]} */
|
||||
const entries = [];
|
||||
|
||||
for (const key of DG.skills) {
|
||||
const ref = /** @type {FixedSkillRef} */ ({ kind: "fixed", key });
|
||||
const label = formatProfessionSkillLabel(ref);
|
||||
entries.push({
|
||||
id: `fixed:${key}`,
|
||||
ref,
|
||||
label,
|
||||
sortLabel: label,
|
||||
});
|
||||
}
|
||||
|
||||
for (const group of TYPED_SKILL_TEMPLATE_GROUPS) {
|
||||
const ref = /** @type {TypedSkillRef} */ ({
|
||||
kind: "typed",
|
||||
group,
|
||||
label: "",
|
||||
});
|
||||
const label = getTypedGroupDisplayName(group);
|
||||
entries.push({
|
||||
id: `typed:${group}`,
|
||||
ref,
|
||||
label,
|
||||
sortLabel: label,
|
||||
});
|
||||
}
|
||||
|
||||
entries.sort((a, b) =>
|
||||
a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang),
|
||||
);
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skill catalog for character-creation bonus picks (excludes Unnatural).
|
||||
*
|
||||
* @returns {SkillCatalogEntry[]}
|
||||
*/
|
||||
export function buildBonusSkillCatalog() {
|
||||
return buildSkillCatalog().filter(
|
||||
(entry) =>
|
||||
entry.ref.kind !== "fixed" ||
|
||||
!BONUS_SKILL_CATALOG_EXCLUDED_FIXED.has(entry.ref.key),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} catalogId
|
||||
* @param {string} [typedLabel]
|
||||
* @param {{ allowEmptyTypedLabel?: boolean }} [options]
|
||||
* @returns {ProfessionSkillRef | null}
|
||||
*/
|
||||
export function catalogIdToSkillRef(
|
||||
catalogId,
|
||||
typedLabel = "",
|
||||
{ allowEmptyTypedLabel = false } = {},
|
||||
) {
|
||||
if (!catalogId) return null;
|
||||
if (catalogId.startsWith("fixed:")) {
|
||||
const key = catalogId.slice(6);
|
||||
if (!DG.skills.includes(key)) return null;
|
||||
return { kind: "fixed", key };
|
||||
}
|
||||
if (catalogId.startsWith("typed:")) {
|
||||
const group = catalogId.slice(6);
|
||||
if (!TYPED_SKILL_TEMPLATE_GROUPS.includes(group)) return null;
|
||||
const label = typedLabel.trim();
|
||||
if (!label && !allowEmptyTypedLabel) return null;
|
||||
return { kind: "typed", group, label };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ProfessionSkillRef} ref
|
||||
* @returns {string}
|
||||
*/
|
||||
function getBonusTrackKey(ref) {
|
||||
if (ref.kind === "fixed") return `fixed:${ref.key}`;
|
||||
return formatProfessionSkillKey(ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} trackKey
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBonusTrackLabel(trackKey) {
|
||||
if (trackKey.startsWith("fixed:")) {
|
||||
const key = trackKey.slice(6);
|
||||
return formatProfessionSkillLabel(
|
||||
/** @type {FixedSkillRef} */ ({ kind: "fixed", key }),
|
||||
);
|
||||
}
|
||||
const ref = parseProfessionSkillKey(trackKey);
|
||||
if (ref) return formatProfessionSkillLabel(ref);
|
||||
return trackKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} trackKey
|
||||
* @param {Record<string, number>} baseFixed
|
||||
* @param {Record<string, { group: string, label: string, value: number }>} baseTyped
|
||||
* @param {Record<string, number>} defaults
|
||||
* @returns {number}
|
||||
*/
|
||||
function getBonusTrackBaseValue(trackKey, baseFixed, baseTyped, defaults) {
|
||||
if (trackKey.startsWith("fixed:")) {
|
||||
const key = trackKey.slice(6);
|
||||
return baseFixed[key] ?? defaults[key] ?? 0;
|
||||
}
|
||||
return baseTyped[trackKey]?.value ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bonus skill slots that would waste 20+ points or raise a skill already at the creation cap.
|
||||
*
|
||||
* @param {Record<string, number>} baseFixed
|
||||
* @param {Record<string, { group: string, label: string, value: number }>} baseTyped
|
||||
* @param {string[]} bonusCatalogIds
|
||||
* @param {string[]} bonusTypedLabels
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function collectBonusCapValidationErrors(
|
||||
baseFixed,
|
||||
baseTyped,
|
||||
bonusCatalogIds,
|
||||
bonusTypedLabels,
|
||||
) {
|
||||
const defaults = getAgentSkillDefaults();
|
||||
/** @type {Record<string, number>} */
|
||||
const counts = {};
|
||||
/** @type {(string | null)[]} */
|
||||
const slotTrackKeys = [];
|
||||
|
||||
for (let i = 0; i < BONUS_SKILL_COUNT; i++) {
|
||||
const catalogId = bonusCatalogIds?.[i];
|
||||
if (!catalogId) {
|
||||
slotTrackKeys.push(null);
|
||||
continue;
|
||||
}
|
||||
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>} */
|
||||
const violatingTrackKeys = new Set();
|
||||
|
||||
for (const [trackKey, count] of Object.entries(counts)) {
|
||||
const base = getBonusTrackBaseValue(
|
||||
trackKey,
|
||||
baseFixed,
|
||||
baseTyped,
|
||||
defaults,
|
||||
);
|
||||
const final = base + count * BONUS_SKILL_INCREMENT;
|
||||
const waste = Math.max(0, final - SKILL_CAP);
|
||||
if (base >= SKILL_CAP || waste >= MAX_ALLOWED_BONUS_WASTE) {
|
||||
violatingTrackKeys.add(trackKey);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const errors = [];
|
||||
for (let i = 0; i < BONUS_SKILL_COUNT; i++) {
|
||||
const trackKey = slotTrackKeys[i];
|
||||
if (!trackKey || !violatingTrackKeys.has(trackKey)) continue;
|
||||
|
||||
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);
|
||||
|
||||
if (base >= SKILL_CAP) {
|
||||
errors.push(`bonusAtCap:${i}|${trackKey}`);
|
||||
} else {
|
||||
errors.push(`bonusWaste:${i}|${waste}|${trackKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import {
|
||||
BONUS_SKILL_COUNT,
|
||||
BONUS_SKILL_INCREMENT,
|
||||
SKILL_CAP,
|
||||
} from "./constants.js";
|
||||
import {
|
||||
catalogIdToSkillRef,
|
||||
collectBonusCapValidationErrors,
|
||||
} from "./catalog.js";
|
||||
import {
|
||||
formatProfessionSkillKey,
|
||||
formatProfessionSkillLabel,
|
||||
getAgentSkillDefaults,
|
||||
parseProfessionSkillKey,
|
||||
} from "./keys.js";
|
||||
import { validateProfessionFormState } from "./validation.js";
|
||||
|
||||
/**
|
||||
* @param {Record<string, number>} automaticSkills
|
||||
* @param {Record<string, number>} optionSkills
|
||||
* @param {number} optionPicks
|
||||
* @param {object} formState
|
||||
* @param {Set<string>} formState.checkedOptionKeys
|
||||
* @param {Record<string, string>} formState.chooseOneLabels - keyed by profession map key
|
||||
* @param {string[]} formState.bonusCatalogIds - length 8
|
||||
* @param {string[]} formState.bonusTypedLabels - length 8
|
||||
* @param {string[]} [formState.bondNames]
|
||||
* @param {string[]} [formState.bondRelationships]
|
||||
* @param {object} [context]
|
||||
* @param {Record<string, ProfessionSkillMeta>} [context.automaticMeta]
|
||||
* @param {Record<string, ProfessionSkillMeta>} [context.optionMeta]
|
||||
* @param {number} [context.bondCount]
|
||||
* @returns {{
|
||||
* fixedValues: Record<string, number>,
|
||||
* typedValues: Record<string, { group: string, label: string, value: number }>,
|
||||
* capWarnings: { label: string, attempted: number, waste: number }[],
|
||||
* isValid: boolean,
|
||||
* validationErrors: string[],
|
||||
* modifiedFixedKeys: string[],
|
||||
* modifiedTypedKeys: string[],
|
||||
* }}
|
||||
*/
|
||||
export function computeSkillValues(
|
||||
automaticSkills,
|
||||
optionSkills,
|
||||
optionPicks,
|
||||
formState,
|
||||
{ automaticMeta = {}, optionMeta = {}, bondCount = 0 } = {},
|
||||
) {
|
||||
const defaults = getAgentSkillDefaults();
|
||||
/** @type {Record<string, number>} */
|
||||
const fixedValues = { ...defaults };
|
||||
/** @type {Record<string, { group: string, label: string, value: number }>} */
|
||||
const typedValues = {};
|
||||
/** @type {Set<string>} */
|
||||
const modifiedFixedKeys = new Set();
|
||||
/** @type {Set<string>} */
|
||||
const modifiedTypedKeys = new Set();
|
||||
|
||||
/**
|
||||
* @param {TypedSkillRef} ref
|
||||
* @param {string} label
|
||||
*/
|
||||
const markTypedModified = (ref, label) => {
|
||||
modifiedTypedKeys.add(
|
||||
formatProfessionSkillKey({ kind: "typed", group: ref.group, label }),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} mapKey
|
||||
* @param {number} rating
|
||||
* @param {{ labelOverride?: string }} [options]
|
||||
* @returns {string | null} Merged typed storage key, if applied.
|
||||
*/
|
||||
const applyOverride = (mapKey, rating, options = {}) => {
|
||||
const ref = parseProfessionSkillKey(mapKey);
|
||||
if (!ref) return null;
|
||||
const value = Number(rating);
|
||||
if (ref.kind === "fixed") {
|
||||
fixedValues[ref.key] = value;
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = options.labelOverride?.trim() ?? ref.label?.trim() ?? "";
|
||||
if (!label) return null;
|
||||
|
||||
const storageKey = formatProfessionSkillKey({
|
||||
kind: "typed",
|
||||
group: ref.group,
|
||||
label,
|
||||
});
|
||||
typedValues[storageKey] = {
|
||||
group: ref.group,
|
||||
label,
|
||||
value,
|
||||
};
|
||||
return storageKey;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Record<string, number>} */
|
||||
const bonusCounts = {};
|
||||
|
||||
const bonusIds = formState.bonusCatalogIds ?? [];
|
||||
const bonusLabels = formState.bonusTypedLabels ?? [];
|
||||
|
||||
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 (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);
|
||||
}
|
||||
bonusCounts[trackKey] = (bonusCounts[trackKey] ?? 0) + 1;
|
||||
}
|
||||
|
||||
/** Pre-bonus base for cap warnings */
|
||||
const baseFixed = { ...fixedValues };
|
||||
const baseTyped = foundry.utils.deepClone(typedValues);
|
||||
|
||||
for (const [trackKey, count] of Object.entries(bonusCounts)) {
|
||||
const bonus = count * BONUS_SKILL_INCREMENT;
|
||||
if (trackKey.startsWith("fixed:")) {
|
||||
const key = trackKey.slice(6);
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {{ label: string, attempted: number, waste: number }[]} */
|
||||
const capWarnings = [];
|
||||
|
||||
const checkCap = (label, base, final) => {
|
||||
if (final > SKILL_CAP) {
|
||||
capWarnings.push({
|
||||
label,
|
||||
attempted: final,
|
||||
waste: final - SKILL_CAP,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for (const [key, final] of Object.entries(fixedValues)) {
|
||||
const base = baseFixed[key] ?? defaults[key] ?? 0;
|
||||
const ref = /** @type {FixedSkillRef} */ ({ kind: "fixed", key });
|
||||
checkCap(formatProfessionSkillLabel(ref), base, final);
|
||||
}
|
||||
|
||||
for (const [storageKey, data] of Object.entries(typedValues)) {
|
||||
const base = baseTyped[storageKey]?.value ?? 0;
|
||||
const ref = /** @type {TypedSkillRef} */ ({
|
||||
kind: "typed",
|
||||
group: data.group,
|
||||
label: data.label,
|
||||
});
|
||||
checkCap(formatProfessionSkillLabel(ref), base, data.value);
|
||||
}
|
||||
|
||||
/** Apply cap for display (values shown capped at 80) */
|
||||
for (const key of Object.keys(fixedValues)) {
|
||||
if (fixedValues[key] > SKILL_CAP) fixedValues[key] = SKILL_CAP;
|
||||
}
|
||||
for (const data of Object.values(typedValues)) {
|
||||
if (data.value > SKILL_CAP) data.value = SKILL_CAP;
|
||||
}
|
||||
|
||||
const validationErrors = validateProfessionFormState(optionPicks, formState, {
|
||||
automaticSkills,
|
||||
optionSkills,
|
||||
automaticMeta,
|
||||
optionMeta,
|
||||
bondCount,
|
||||
});
|
||||
|
||||
validationErrors.push(
|
||||
...collectBonusCapValidationErrors(
|
||||
baseFixed,
|
||||
baseTyped,
|
||||
bonusIds,
|
||||
bonusLabels,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
fixedValues,
|
||||
typedValues,
|
||||
capWarnings,
|
||||
isValid: validationErrors.length === 0,
|
||||
validationErrors,
|
||||
modifiedFixedKeys: [...modifiedFixedKeys],
|
||||
modifiedTypedKeys: [...modifiedTypedKeys],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/** @typedef {"fixed" | "typed"} ProfessionSkillKind */
|
||||
/** @typedef {{ kind: "fixed", key: string }} FixedSkillRef */
|
||||
/** @typedef {{ kind: "typed", group: string, label: string }} TypedSkillRef */
|
||||
/** @typedef {FixedSkillRef | TypedSkillRef} ProfessionSkillRef */
|
||||
/** @typedef {{ chooseOne?: boolean }} ProfessionSkillMeta */
|
||||
|
||||
/** Typed skill template groups (camelCase storage keys). */
|
||||
export const TYPED_SKILL_TEMPLATE_GROUPS = /** @type {const} */ ([
|
||||
"Art",
|
||||
"Craft",
|
||||
"ForeignLanguage",
|
||||
"MilitaryScience",
|
||||
"Pilot",
|
||||
"Science",
|
||||
"Other",
|
||||
]);
|
||||
|
||||
export const TYPED_GROUP_I18N = {
|
||||
Art: "DG.TypeSkills.Art",
|
||||
Craft: "DG.TypeSkills.Craft",
|
||||
ForeignLanguage: "DG.TypeSkills.ForeignLanguage",
|
||||
MilitaryScience: "DG.TypeSkills.MilitaryScience",
|
||||
Pilot: "DG.TypeSkills.Pilot",
|
||||
Science: "DG.TypeSkills.Science",
|
||||
Other: "DG.TypeSkills.Other",
|
||||
};
|
||||
|
||||
export const BONUS_SKILL_COUNT = 8;
|
||||
export const BONUS_SKILL_INCREMENT = 20;
|
||||
|
||||
/** @type {ReadonlySet<string>} */
|
||||
export const BONUS_SKILL_CATALOG_EXCLUDED_FIXED = new Set(["unnatural"]);
|
||||
export const SKILL_CAP = 80;
|
||||
/** Bonus picks that would waste this many points or more are rejected (e.g. 80 + 20% at cap). */
|
||||
export const MAX_ALLOWED_BONUS_WASTE = 20;
|
||||
|
||||
export const TYPED_KEY_PATTERN =
|
||||
/^(Art|Craft|Foreign Language|ForeignLanguage|Military Science|MilitaryScience|Pilot|Science|Other)\s*\(([^)]+)\)\s*$/i;
|
||||
|
||||
/** Legacy internal keys from earlier builds (`typed:ForeignLanguage:abc12345`). */
|
||||
export const INTERNAL_TYPED_KEY_PATTERN =
|
||||
/^typed:([A-Za-z]+):([A-Za-z0-9]{8,16})(?::(.+))?$/;
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
formatProfessionSkillKeyLabel,
|
||||
formatProfessionSkillLabel,
|
||||
} from "./keys.js";
|
||||
|
||||
/**
|
||||
* Sorted fixed skill rows for display.
|
||||
* @param {Record<string, number>} fixedValues
|
||||
* @returns {{ key: string, label: string, value: number }[]}
|
||||
*/
|
||||
export function buildSortedFixedSkillRows(fixedValues) {
|
||||
const rows = Object.entries(fixedValues).map(([key, value]) => {
|
||||
const ref = /** @type {FixedSkillRef} */ ({ kind: "fixed", key });
|
||||
return {
|
||||
key,
|
||||
label: formatProfessionSkillLabel(ref),
|
||||
value,
|
||||
sortLabel: formatProfessionSkillLabel(ref),
|
||||
};
|
||||
});
|
||||
rows.sort((a, b) => a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang));
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorted typed skill rows for display.
|
||||
* @param {Record<string, { group: string, label: string, value: number }>} typedValues
|
||||
* @returns {{ storageKey: string, label: string, value: number, sortLabel: string }[]}
|
||||
*/
|
||||
export function buildSortedTypedSkillRows(typedValues) {
|
||||
const rows = Object.entries(typedValues).map(([storageKey, data]) => {
|
||||
const ref = {
|
||||
kind: "typed",
|
||||
group: data.group,
|
||||
label: data.label,
|
||||
};
|
||||
const label = formatProfessionSkillLabel(ref);
|
||||
return {
|
||||
storageKey,
|
||||
label,
|
||||
value: data.value,
|
||||
sortLabel: label,
|
||||
};
|
||||
});
|
||||
rows.sort((a, b) => a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang));
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare sorted skill rows from profession item maps.
|
||||
* @param {Record<string, number>} skillMap
|
||||
* @param {Record<string, ProfessionSkillMeta>} [skillMeta]
|
||||
* @returns {{ key: string, label: string, rating: number }[]}
|
||||
*/
|
||||
export function prepareProfessionSkillRows(skillMap, skillMeta = {}) {
|
||||
const rows = Object.entries(skillMap ?? {}).map(([key, rating]) => {
|
||||
const label = formatProfessionSkillKeyLabel(key, {
|
||||
automaticMeta: skillMeta,
|
||||
optionMeta: skillMeta,
|
||||
});
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
rating: Number(rating),
|
||||
sortLabel: label,
|
||||
};
|
||||
});
|
||||
rows.sort((a, b) => a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang));
|
||||
return rows;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export { TYPED_SKILL_TEMPLATE_GROUPS, BONUS_SKILL_COUNT } from "./constants.js";
|
||||
export {
|
||||
getChooseOnePlaceholderLabel,
|
||||
normalizeTypedSkillName,
|
||||
isChooseOnePlaceholderLabel,
|
||||
isChooseOneProfessionSkillKey,
|
||||
findTypedSkillNameConflict,
|
||||
getAgentSkillDefaults,
|
||||
normalizeTypedGroup,
|
||||
getTypedGroupDisplayName,
|
||||
formatProfessionSkillKey,
|
||||
allocateProfessionSkillStorageKey,
|
||||
parseProfessionSkillKey,
|
||||
formatProfessionSkillLabel,
|
||||
formatProfessionSkillKeyLabel,
|
||||
splitProfessionSkillMap,
|
||||
} from "./keys.js";
|
||||
export {
|
||||
buildSkillCatalog,
|
||||
buildBonusSkillCatalog,
|
||||
catalogIdToSkillRef,
|
||||
collectBonusCapValidationErrors,
|
||||
} from "./catalog.js";
|
||||
export { computeSkillValues } from "./compute.js";
|
||||
export {
|
||||
validateProfessionFormState,
|
||||
formatProfessionValidationMessages,
|
||||
} from "./validation.js";
|
||||
export {
|
||||
buildSortedFixedSkillRows,
|
||||
buildSortedTypedSkillRows,
|
||||
prepareProfessionSkillRows,
|
||||
} from "./display.js";
|
||||
@@ -0,0 +1,269 @@
|
||||
import DG from "../config/index.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,
|
||||
TYPED_KEY_PATTERN,
|
||||
TYPED_GROUP_I18N,
|
||||
} from "./constants.js";
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getChooseOnePlaceholderLabel() {
|
||||
return game.i18n.localize("DG.Profession.ChooseOnePlaceholder");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} label
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeTypedSkillName(label) {
|
||||
return label.trim().toLocaleLowerCase(game.i18n.lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} label
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isChooseOnePlaceholderLabel(label) {
|
||||
return (
|
||||
normalizeTypedSkillName(label) ===
|
||||
normalizeTypedSkillName(getChooseOnePlaceholderLabel())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} mapKey
|
||||
* @param {Record<string, ProfessionSkillMeta>} automaticMeta
|
||||
* @param {Record<string, ProfessionSkillMeta>} optionMeta
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isChooseOneProfessionSkillKey(
|
||||
mapKey,
|
||||
automaticMeta = {},
|
||||
optionMeta = {},
|
||||
) {
|
||||
return Boolean(
|
||||
automaticMeta[mapKey]?.chooseOne ?? optionMeta[mapKey]?.chooseOne,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {string} params.group
|
||||
* @param {string} params.label
|
||||
* @param {boolean} params.chooseOne
|
||||
* @param {Record<string, number>} params.automaticSkills
|
||||
* @param {Record<string, number>} params.optionSkills
|
||||
* @param {Record<string, ProfessionSkillMeta>} [params.automaticMeta]
|
||||
* @param {Record<string, ProfessionSkillMeta>} [params.optionMeta]
|
||||
* @param {string} [params.excludeKey]
|
||||
* @returns {string | null} Conflicting map key, if any.
|
||||
*/
|
||||
export function findTypedSkillNameConflict({
|
||||
group,
|
||||
label,
|
||||
chooseOne,
|
||||
automaticSkills,
|
||||
optionSkills,
|
||||
automaticMeta = {},
|
||||
optionMeta = {},
|
||||
excludeKey = null,
|
||||
}) {
|
||||
// Multiple choose-one slots per category are allowed on the profession item.
|
||||
if (chooseOne) return null;
|
||||
if (!group) return null;
|
||||
|
||||
const normalizedGroup = normalizeTypedGroup(group);
|
||||
const normalizedNew = normalizeTypedSkillName(label);
|
||||
if (!normalizedNew) return "__empty__";
|
||||
|
||||
const maps = [
|
||||
[automaticSkills, automaticMeta],
|
||||
[optionSkills, optionMeta],
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
*/
|
||||
export function getTypedGroupDisplayName(group) {
|
||||
const key = normalizeTypedGroup(group);
|
||||
const i18nKey = TYPED_GROUP_I18N[key];
|
||||
if (i18nKey) return game.i18n.localize(i18nKey);
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ProfessionSkillRef} ref
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatProfessionSkillKey(ref) {
|
||||
if (ref.kind === "fixed") return ref.key;
|
||||
const groupLabel = getTypedGroupDisplayName(ref.group);
|
||||
return `${groupLabel} (${ref.label.trim()})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage key for a profession skill map entry.
|
||||
*
|
||||
* @param {ProfessionSkillRef} ref
|
||||
* @param {{ chooseOne?: boolean }} [options]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function allocateProfessionSkillStorageKey(
|
||||
ref,
|
||||
{ chooseOne = false } = {},
|
||||
) {
|
||||
if (ref.kind === "fixed") return ref.key;
|
||||
if (chooseOne) {
|
||||
return `typed:${normalizeTypedGroup(ref.group)}:${foundry.utils.randomID(
|
||||
8,
|
||||
)}`;
|
||||
}
|
||||
const label = ref.label?.trim() ?? "";
|
||||
return formatProfessionSkillKey({
|
||||
kind: "typed",
|
||||
group: ref.group,
|
||||
label,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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}
|
||||
*/
|
||||
export function formatProfessionSkillLabel(ref) {
|
||||
if (ref.kind === "fixed") {
|
||||
if (game.i18n.lang === "ja") {
|
||||
const ja = game.i18n.localize(`DG.Skills.ruby.${ref.key}`);
|
||||
if (ja && ja !== `DG.Skills.ruby.${ref.key}`) return ja;
|
||||
}
|
||||
const label = game.i18n.localize(`DG.Skills.${ref.key}`);
|
||||
if (label && label !== `DG.Skills.${ref.key}`) return label;
|
||||
return ref.key;
|
||||
}
|
||||
if (!ref.label?.trim()) return getTypedGroupDisplayName(ref.group);
|
||||
return formatProfessionSkillKey(ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {object} [metaContext]
|
||||
* @param {Record<string, ProfessionSkillMeta>} [metaContext.automaticMeta]
|
||||
* @param {Record<string, ProfessionSkillMeta>} [metaContext.optionMeta]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatProfessionSkillKeyLabel(
|
||||
key,
|
||||
{ automaticMeta = {}, optionMeta = {} } = {},
|
||||
) {
|
||||
const ref = parseProfessionSkillKey(key);
|
||||
if (!ref) return key;
|
||||
if (
|
||||
ref.kind === "typed" &&
|
||||
isChooseOneProfessionSkillKey(key, automaticMeta, optionMeta)
|
||||
) {
|
||||
return formatProfessionSkillKey({
|
||||
kind: "typed",
|
||||
group: ref.group,
|
||||
label: getChooseOnePlaceholderLabel(),
|
||||
});
|
||||
}
|
||||
if (ref.kind === "typed" && !ref.label?.trim()) {
|
||||
return getTypedGroupDisplayName(ref.group);
|
||||
}
|
||||
return formatProfessionSkillLabel(ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, number>} obj
|
||||
* @returns {{ optionPicks: number, skills: Record<string, number> }}
|
||||
*/
|
||||
export function splitProfessionSkillMap(obj) {
|
||||
const source = obj ?? {};
|
||||
const optionPicks = Number(source[PROFESSION_OPTION_PICKS_KEY]) || 0;
|
||||
/** @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);
|
||||
}
|
||||
return { optionPicks, skills };
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import DG from "../config.js";
|
||||
import { showDiceSoNicePooledRolls } from "./dice-so-nice.js";
|
||||
import DG from "../config/index.js";
|
||||
import { showDiceSoNicePooledRolls } from "../integrations/dice-so-nice.js";
|
||||
|
||||
export const STAT_KEYS = DG.statistics;
|
||||
export const POINT_BUY_TOTAL = 72;
|
||||
@@ -46,11 +46,7 @@ export function validatePointBuyValues(values) {
|
||||
let sum = 0;
|
||||
for (const key of STAT_KEYS) {
|
||||
const value = Number(values[key]);
|
||||
if (
|
||||
!Number.isInteger(value) ||
|
||||
value < STAT_MIN ||
|
||||
value > STAT_MAX
|
||||
) {
|
||||
if (!Number.isInteger(value) || value < STAT_MIN || value > STAT_MAX) {
|
||||
return { isValid: false, remaining: POINT_BUY_TOTAL - sum };
|
||||
}
|
||||
sum += value;
|
||||
@@ -95,13 +91,17 @@ export async function rollStatisticScores({
|
||||
export function buildRollStatsChatContent(rolls) {
|
||||
const cells = rolls.map(
|
||||
(entry, index) =>
|
||||
`<td class="roll-stats-chat-value roll-stats-chat-value--${index % 2 === 0 ? "a" : "b"}">${foundry.utils.escapeHTML(String(entry.total))}</td>`,
|
||||
`<td class="roll-stats-chat-value roll-stats-chat-value--${
|
||||
index % 2 === 0 ? "a" : "b"
|
||||
}">${foundry.utils.escapeHTML(String(entry.total))}</td>`,
|
||||
);
|
||||
const rows = [];
|
||||
for (let i = 0; i < cells.length; i += 3) {
|
||||
rows.push(`<tr>${cells.slice(i, i + 3).join("")}</tr>`);
|
||||
}
|
||||
return `<table class="roll-stats-chat-table"><tbody>${rows.join("")}</tbody></table>`;
|
||||
return `<table class="roll-stats-chat-table"><tbody>${rows.join(
|
||||
"",
|
||||
)}</tbody></table>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,238 @@
|
||||
import { BONUS_SKILL_COUNT, MAX_ALLOWED_BONUS_WASTE } from "./constants.js";
|
||||
import { catalogIdToSkillRef, getBonusTrackLabel } from "./catalog.js";
|
||||
import {
|
||||
getTypedGroupDisplayName,
|
||||
normalizeTypedGroup,
|
||||
normalizeTypedSkillName,
|
||||
parseProfessionSkillKey,
|
||||
} from "./keys.js";
|
||||
|
||||
/**
|
||||
* @param {number} optionPicks
|
||||
* @param {object} formState
|
||||
* @param {object} [context]
|
||||
* @param {Record<string, number>} [context.automaticSkills]
|
||||
* @param {Record<string, number>} [context.optionSkills]
|
||||
* @param {Record<string, ProfessionSkillMeta>} [context.automaticMeta]
|
||||
* @param {Record<string, ProfessionSkillMeta>} [context.optionMeta]
|
||||
* @param {number} [context.bondCount]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function validateProfessionFormState(
|
||||
optionPicks,
|
||||
formState,
|
||||
context = {},
|
||||
) {
|
||||
const errors = [];
|
||||
const checked = formState.checkedOptionKeys ?? new Set();
|
||||
const picks = Number(optionPicks) || 0;
|
||||
const {
|
||||
automaticSkills = {},
|
||||
optionSkills = {},
|
||||
automaticMeta = {},
|
||||
optionMeta = {},
|
||||
bondCount = 0,
|
||||
} = context;
|
||||
|
||||
if (checked.size !== picks) {
|
||||
errors.push("optionPicks");
|
||||
}
|
||||
|
||||
for (let i = 0; i < bondCount; i++) {
|
||||
if (!formState.bondNames?.[i]?.trim()) errors.push(`bondName${i}`);
|
||||
if (!formState.bondRelationships?.[i]?.trim()) {
|
||||
errors.push(`bondRelationship${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < BONUS_SKILL_COUNT; i++) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
const resolvedByGroup = new Map();
|
||||
|
||||
const registerResolvedName = (ref, rawLabel) => {
|
||||
const label = rawLabel?.trim();
|
||||
if (!label) {
|
||||
errors.push(`typedNameRequired:${ref.group}`);
|
||||
return;
|
||||
}
|
||||
const group = normalizeTypedGroup(ref.group);
|
||||
const normalized = normalizeTypedSkillName(label);
|
||||
if (!resolvedByGroup.has(group)) resolvedByGroup.set(group, new Set());
|
||||
const names = resolvedByGroup.get(group);
|
||||
if (names.has(normalized)) {
|
||||
errors.push(`typedNameConflict:${group}:${label}`);
|
||||
return;
|
||||
}
|
||||
names.add(normalized);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* User-facing messages for Add Profession validation error codes.
|
||||
* @param {string[]} errors
|
||||
* @param {object} [context]
|
||||
* @param {number} [context.optionPicks]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function formatProfessionValidationMessages(
|
||||
errors,
|
||||
{ optionPicks = 0 } = {},
|
||||
) {
|
||||
const messages = [];
|
||||
const seen = new Set();
|
||||
|
||||
const push = (msg) => {
|
||||
if (seen.has(msg)) return;
|
||||
seen.add(msg);
|
||||
messages.push(msg);
|
||||
};
|
||||
|
||||
let needsOptionPicks = false;
|
||||
let needsBondName = false;
|
||||
let needsBondRelationship = false;
|
||||
let needsBonusSkill = false;
|
||||
let needsBonusSkillType = false;
|
||||
/** @type {Set<string>} */
|
||||
const typedNameRequiredGroups = new Set();
|
||||
/** @type {string[]} */
|
||||
const bonusAtCapTrackKeys = [];
|
||||
|
||||
for (const code of errors) {
|
||||
if (code === "optionPicks") {
|
||||
needsOptionPicks = true;
|
||||
continue;
|
||||
}
|
||||
if (code.startsWith("bondName")) {
|
||||
needsBondName = true;
|
||||
continue;
|
||||
}
|
||||
if (code.startsWith("bondRelationship")) {
|
||||
needsBondRelationship = true;
|
||||
continue;
|
||||
}
|
||||
const bonusAtCap = code.match(/^bonusAtCap:\d+\|(.+)$/);
|
||||
if (bonusAtCap) {
|
||||
bonusAtCapTrackKeys.push(bonusAtCap[1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bonusWaste = code.match(/^bonusWaste:\d+\|(\d+)\|(.+)$/);
|
||||
if (bonusWaste) {
|
||||
push(
|
||||
game.i18n.format("DG.Profession.Dialog.BonusSkillWasteTooHigh", {
|
||||
skill: getBonusTrackLabel(bonusWaste[2]),
|
||||
waste: Number(bonusWaste[1]),
|
||||
maxWaste: MAX_ALLOWED_BONUS_WASTE - 1,
|
||||
}),
|
||||
);
|
||||
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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (needsOptionPicks) {
|
||||
push(
|
||||
game.i18n.format("DG.Profession.Dialog.OptionPicksRequired", {
|
||||
picks: optionPicks,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (needsBonusSkill) {
|
||||
push(
|
||||
game.i18n.format("DG.Profession.Dialog.BonusSkillRequired", {
|
||||
count: BONUS_SKILL_COUNT,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (needsBonusSkillType) {
|
||||
push(game.i18n.localize("DG.Profession.Dialog.BonusSkillTypeRequired"));
|
||||
}
|
||||
for (const group of typedNameRequiredGroups) {
|
||||
push(
|
||||
game.i18n.format("DG.Profession.Dialog.TypedNameRequired", {
|
||||
type: getTypedGroupDisplayName(group),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (needsBondName) {
|
||||
push(game.i18n.localize("DG.Profession.Dialog.BondNameRequired"));
|
||||
}
|
||||
if (needsBondRelationship) {
|
||||
push(game.i18n.localize("DG.Profession.Dialog.BondRelationshipRequired"));
|
||||
}
|
||||
|
||||
for (const trackKey of bonusAtCapTrackKeys) {
|
||||
push(
|
||||
game.i18n.format("DG.Profession.Dialog.BonusSkillAlreadyAtCap", {
|
||||
skill: getBonusTrackLabel(trackKey),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/** @internal Import roll subclasses only from ../roll.js. */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { showDamageRollModifyDialog } from "../roll-dialogs.js";
|
||||
import { DGRoll } from "./dg-roll.js";
|
||||
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
export class DGDamageRoll extends DGRoll {
|
||||
/**
|
||||
* Prepares data for a chat message and then passes that data
|
||||
* to a method that actually creates a ChatMessage.
|
||||
*
|
||||
* @returns {Promise<ChatMessage>} - the created chat message.
|
||||
* @override
|
||||
*/
|
||||
async toChat() {
|
||||
let label = this.formula;
|
||||
try {
|
||||
label = `<b>${game.i18n
|
||||
.localize("DG.Roll.Damage")
|
||||
.toUpperCase()}</b> ${game.i18n.localize("DG.Roll.For")} ${
|
||||
this.item.name
|
||||
} (<b>${
|
||||
this.item.system.armorPiercing
|
||||
} </b><img class="armor-piercing-chat-card-img" src="systems/deltagreen/assets/icons/supersonic-bullet.svg" alt="armor penetration"/>)`;
|
||||
} catch (ex) {
|
||||
// console.log(ex);
|
||||
label = `<b>DAMAGE</b> for <b>${label.toUpperCase()}</b>`;
|
||||
}
|
||||
const html = await renderTemplate(
|
||||
"systems/deltagreen/templates/roll/damage-roll.hbs",
|
||||
{
|
||||
formula: this.formula,
|
||||
total: this.total,
|
||||
},
|
||||
);
|
||||
|
||||
return this.toMessage({ content: html, label });
|
||||
}
|
||||
|
||||
async showDialog() {
|
||||
return showDamageRollModifyDialog({
|
||||
itemName: this.item?.name,
|
||||
formula: this.formula,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/** @internal Import roll subclasses only from ../roll.js. */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import DGUtils from "../../utils/utility-functions.js";
|
||||
import { DGPercentileRoll } from "./dg-percentile-roll.js";
|
||||
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
export class DGLethalityRoll extends DGPercentileRoll {
|
||||
/**
|
||||
* See constructor for DGPercentileRoll. This theoretically could be done in the parent class'
|
||||
* constructor, but since Lethality rolls needs its own class for custom methods anyway,
|
||||
* we will set the target and localized key here.
|
||||
*
|
||||
* @param {String} formula
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(formula, data, options) {
|
||||
super(formula, data, options);
|
||||
this.target = options.item.system.lethality;
|
||||
this.localizedKey = game.i18n.localize("DG.ItemWindow.Weapons.Lethality");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares data for a chat message and then passes that data
|
||||
* to a method that actually creates a ChatMessage.
|
||||
*
|
||||
* Lays out and styles message based on outcome of the roll.
|
||||
*
|
||||
* Overrides `DGPercentileRoll.toChat()`
|
||||
*
|
||||
* @returns {Promise<ChatMessage>} - the created chat message.
|
||||
* @override
|
||||
*/
|
||||
async toChat() {
|
||||
let resultString = "";
|
||||
let styleOverride = "";
|
||||
if (this.total <= this.target) {
|
||||
resultString = `${game.i18n.localize("DG.Roll.Lethal").toUpperCase()}`;
|
||||
styleOverride = "color: red";
|
||||
} else {
|
||||
resultString = `${game.i18n.localize("DG.Roll.Failure")}`;
|
||||
}
|
||||
|
||||
const { nonLethalDamage } = this;
|
||||
let label = `<b>${game.i18n
|
||||
.localize("DG.Roll.Lethality")
|
||||
.toUpperCase()}</b> ${game.i18n.localize(
|
||||
"DG.Roll.For",
|
||||
)} <b>${this.item.name.toUpperCase()}</b> ${game.i18n.localize(
|
||||
"DG.Roll.Target",
|
||||
)} ${this.target + this.modifier}`;
|
||||
if (this.modifier) {
|
||||
label += ` (${DGUtils.formatStringWithLeadingPlus(this.modifier)}%)`;
|
||||
}
|
||||
|
||||
const html = await renderTemplate(
|
||||
"systems/deltagreen/templates/roll/lethality-roll.hbs",
|
||||
{
|
||||
styleOverride,
|
||||
resultString,
|
||||
total: this.total,
|
||||
die1: nonLethalDamage.die1,
|
||||
die2: nonLethalDamage.die2,
|
||||
nonLethalTotal: nonLethalDamage.total,
|
||||
damageLabel: game.i18n.localize("DG.Roll.Damage"),
|
||||
},
|
||||
);
|
||||
|
||||
return this.toMessage({ content: html, label });
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the damage for when a lethality roll fails.
|
||||
* If roll has not been evaluated, return null.
|
||||
*
|
||||
* See full rules on page 57 of agent's handbook.
|
||||
*
|
||||
* Note, this getter does not actually care if the roll has failed.
|
||||
*
|
||||
* @returns {null|Object} - return data about the non-lethal damage.
|
||||
*/
|
||||
get nonLethalDamage() {
|
||||
if (!this.total) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to determine what the d100 result would be as if it was two d10's being rolled.
|
||||
const totalString = this.total.toString();
|
||||
const digits = totalString.length;
|
||||
let die1;
|
||||
let die2;
|
||||
switch (digits) {
|
||||
case 1:
|
||||
// If one digit in the result, one die is a 10, and the other is the result.
|
||||
[die1, die2] = [10, this.total];
|
||||
break;
|
||||
case 2:
|
||||
// If two digits in the result, each die is the value of one of the digits. If one of those digits is 0, make it 10.
|
||||
[die1, die2] = totalString
|
||||
.split("")
|
||||
.map((digit) => parseInt(digit))
|
||||
.map((digit) => digit || 10);
|
||||
break;
|
||||
case 3:
|
||||
// If three digits in the result (aka result === 100), each die is a 10.
|
||||
[die1, die2] = [10, 10];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const total = die1 + die2;
|
||||
return { die1, die2, total };
|
||||
}
|
||||
}
|
||||
@@ -1,102 +1,20 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import {
|
||||
createDGRollChatMessage,
|
||||
prepareDGRollChatMessageData,
|
||||
} from "../chat/dg-chat-card.js";
|
||||
import DGUtils from "../utils/utility-functions.js";
|
||||
import DG from "../config.js";
|
||||
import {
|
||||
showDamageRollModifyDialog,
|
||||
showPercentileRollModifyDialog,
|
||||
} from "./roll-dialogs.js";
|
||||
/** @internal Import roll subclasses only from ../roll.js. */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import DGUtils from "../../utils/utility-functions.js";
|
||||
import DG from "../../config/index.js";
|
||||
import { showPercentileRollModifyDialog } from "../roll-dialogs.js";
|
||||
import {
|
||||
isDiceSoNiceAvailable,
|
||||
waitForDiceSoNiceMessageAnimation,
|
||||
} from "../utils/dice-so-nice.js";
|
||||
} from "../../integrations/dice-so-nice.js";
|
||||
import {
|
||||
clampPercentileRollTarget,
|
||||
getRollTargetDisplayClassFromModifier,
|
||||
} from "../utils/active-effect-derived.js";
|
||||
} from "../../active-effect/runtime/derived.js";
|
||||
import { DGRoll } from "./dg-roll.js";
|
||||
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
export class DGRoll extends Roll {
|
||||
/**
|
||||
* NOTE: This class will rarely be called on its own. It should generally be extended. Look to DGPercentileRoll as an example.
|
||||
*
|
||||
* Customize our roll with some useful information, passed in the `options` Object.
|
||||
*
|
||||
* @param {string} formula Unused - The string formula to parse (from Foundry)
|
||||
* @param {Object} data Unused - The data object against which to parse attributes within the formula
|
||||
* @param {Object} [options] Additional data which is preserved in the database
|
||||
* @param {Number} [options.rollType] The type of roll (stat, skill, sanity, damage, etc).
|
||||
* @param {String} [options.key] The key of the skill, stat, etc. to use as a basis for this roll.
|
||||
* @param {DeltaGreenActor} [options.actor] The actor that this roll originates from.
|
||||
* @param {DeltaGreenItem} [options.item] Optional - The item from which the roll originates.
|
||||
*/
|
||||
constructor(formula, data = {}, options = {}) {
|
||||
super(formula, data, options);
|
||||
const { rollType, key, actor, item } = options;
|
||||
this.type = rollType;
|
||||
this.key = key;
|
||||
this.actor = actor;
|
||||
this.item = item;
|
||||
this.modifier = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts a roll to chat with the Delta Green card shell and an explicit `rolls`
|
||||
* array for Dice So Nice (listen-path compatible).
|
||||
*
|
||||
* @override
|
||||
* The following `@param` descriptions comes from the Foundry VTT code.
|
||||
* @param {object} messageData The data object to use when creating the message
|
||||
* @param {options} [options] Additional options which modify the created message.
|
||||
* @param {string} [options.messageMode] A key of CONFIG.ChatMessage.modes
|
||||
* @param {boolean} [options.create=true] Whether to automatically create the chat message, or only return the
|
||||
* prepared chatData object.
|
||||
* @returns {Promise<ChatMessage|object>} A promise which resolves to the created ChatMessage document if create is
|
||||
* true, or the Object of prepared chatData otherwise.
|
||||
*/
|
||||
async toMessage(messageData = {}, { messageMode, create = true } = {}) {
|
||||
const label = messageData.label ?? messageData.flavor;
|
||||
delete messageData.label;
|
||||
|
||||
const mode =
|
||||
messageMode ??
|
||||
this.options.messageMode ??
|
||||
this.options.rollMode;
|
||||
|
||||
if (create) {
|
||||
return createDGRollChatMessage({
|
||||
roll: this,
|
||||
actor: this.actor,
|
||||
token: this.options.token,
|
||||
label,
|
||||
content: messageData.content,
|
||||
messageMode: mode,
|
||||
flags: messageData.flags ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
const { messageData: prepared, mappedMode } =
|
||||
await prepareDGRollChatMessageData({
|
||||
roll: this,
|
||||
actor: this.actor,
|
||||
token: this.options.token,
|
||||
label,
|
||||
content: messageData.content,
|
||||
messageMode: mode,
|
||||
flags: messageData.flags ?? {},
|
||||
});
|
||||
|
||||
const cls = foundry.utils.getDocumentClass("ChatMessage");
|
||||
const msg = new cls(prepared);
|
||||
msg.applyMode(mappedMode);
|
||||
return msg.toObject();
|
||||
}
|
||||
}
|
||||
|
||||
export class DGPercentileRoll extends DGRoll {
|
||||
/**
|
||||
* Creates D100 rolls, the base die of the system.
|
||||
@@ -123,10 +41,7 @@ export class DGPercentileRoll extends DGRoll {
|
||||
super("1D100", {}, options);
|
||||
|
||||
// Set roll info for Skill, Stat, Typed Skill, and non-custom Weapon Percentile rolls.
|
||||
const { target, localizedKey, skillPath } = this.getRollInfoFromKey(
|
||||
this.key,
|
||||
this.actor.system,
|
||||
);
|
||||
const { target, localizedKey, skillPath } = this.getRollInfoFromKey();
|
||||
this.target = target;
|
||||
this.localizedKey = localizedKey;
|
||||
this.skillPath = skillPath;
|
||||
@@ -191,7 +106,7 @@ export class DGPercentileRoll extends DGRoll {
|
||||
}
|
||||
}
|
||||
|
||||
const rollTargetModifier = this.rollTargetModifier;
|
||||
const { rollTargetModifier } = this;
|
||||
const targetDisplayClass =
|
||||
getRollTargetDisplayClassFromModifier(rollTargetModifier);
|
||||
|
||||
@@ -300,13 +215,11 @@ export class DGPercentileRoll extends DGRoll {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, called in the DGPercentileRoll constructor.
|
||||
* If this roll key corresponds to a stat, skill,
|
||||
* or typedSkill, get pertinent info.
|
||||
* Resolve roll target, localized label, and skill path from `this.key` and `this.actor`.
|
||||
*
|
||||
* This is used for Stat, Skill, Typed Skill, Weapon, and Special Training Rolls.
|
||||
* Used for Stat, Skill, Typed Skill, Weapon, and Special Training Rolls.
|
||||
*
|
||||
* @returns {Object} - Contains the roll target and localized version of the key.
|
||||
* @returns {{ target: number|null, localizedKey: string|null, skillPath: string|null }}
|
||||
*/
|
||||
getRollInfoFromKey() {
|
||||
const actorData = this.actor.system;
|
||||
@@ -351,7 +264,7 @@ export class DGPercentileRoll extends DGRoll {
|
||||
*/
|
||||
createLabel() {
|
||||
const startOfLabel = `<b>${this.localizedKey}`;
|
||||
const rollTargetModifier = this.rollTargetModifier;
|
||||
const { rollTargetModifier } = this;
|
||||
const endOfLabel = `${game.i18n.localize("DG.Roll.Target")} ${
|
||||
this.effectiveTarget
|
||||
}`;
|
||||
@@ -388,7 +301,7 @@ export class DGPercentileRoll extends DGRoll {
|
||||
if (this.type === "luck") return 0;
|
||||
|
||||
try {
|
||||
const rollTarget = this.actor.system.rollTarget;
|
||||
const { rollTarget } = this.actor.system;
|
||||
if (!rollTarget) return 0;
|
||||
|
||||
if (this.type === "sanity") return Number(rollTarget.sanity) || 0;
|
||||
@@ -399,21 +312,6 @@ export class DGPercentileRoll extends DGRoll {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Target after roll-target Active Effects, before dialog modifier.
|
||||
* @returns {number|null}
|
||||
*/
|
||||
get rollTargetEffectiveTarget() {
|
||||
if (!this.target || Number.isNaN(this.target)) return null;
|
||||
|
||||
const rollTargetModifier = this.rollTargetModifier;
|
||||
if (!rollTargetModifier) return parseInt(this.target);
|
||||
|
||||
return clampPercentileRollTarget(this.target, rollTargetModifier, {
|
||||
allowOver99: this.target > 99 && this.type === "stat",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* "Inhuman" stat being rolled, logic is different per page 188 of the Handler's Guide.
|
||||
* Note - originally implemented by Uriele, but my attempt at merging conficts went poorly, so re-implementing.
|
||||
@@ -492,7 +390,7 @@ export class DGPercentileRoll extends DGRoll {
|
||||
get effectiveTarget() {
|
||||
let target = 1;
|
||||
|
||||
const rollTargetModifier = this.rollTargetModifier;
|
||||
const { rollTargetModifier } = this;
|
||||
|
||||
if (!this.target || Number.isNaN(this.target)) {
|
||||
return null;
|
||||
@@ -523,204 +421,3 @@ export class DGPercentileRoll extends DGRoll {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
export class DGLethalityRoll extends DGPercentileRoll {
|
||||
/**
|
||||
* See constructor for DGPercentileRoll. This theoretically could be done in the parent class'
|
||||
* constructor, but since Lethality rolls needs its own class for custom methods anyway,
|
||||
* we will set the target and localized key here.
|
||||
*
|
||||
* @param {String} formula
|
||||
* @param {Object} data
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(formula, data, options) {
|
||||
super(formula, data, options);
|
||||
this.target = options.item.system.lethality;
|
||||
this.localizedKey = game.i18n.localize("DG.ItemWindow.Weapons.Lethality");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares data for a chat message and then passes that data
|
||||
* to a method that actually creates a ChatMessage.
|
||||
*
|
||||
* Lays out and styles message based on outcome of the roll.
|
||||
*
|
||||
* Overrides `DGPercentileRoll.toChat()`
|
||||
*
|
||||
* @returns {Promise<ChatMessage>} - the created chat message.
|
||||
* @override
|
||||
*/
|
||||
async toChat() {
|
||||
let resultString = "";
|
||||
let styleOverride = "";
|
||||
if (this.total <= this.target) {
|
||||
resultString = `${game.i18n.localize("DG.Roll.Lethal").toUpperCase()}`;
|
||||
styleOverride = "color: red";
|
||||
} else {
|
||||
resultString = `${game.i18n.localize("DG.Roll.Failure")}`;
|
||||
}
|
||||
|
||||
const { nonLethalDamage } = this;
|
||||
let label = `<b>${game.i18n
|
||||
.localize("DG.Roll.Lethality")
|
||||
.toUpperCase()}</b> ${game.i18n.localize(
|
||||
"DG.Roll.For",
|
||||
)} <b>${this.item.name.toUpperCase()}</b> ${game.i18n.localize(
|
||||
"DG.Roll.Target",
|
||||
)} ${this.target + this.modifier}`;
|
||||
if (this.modifier) {
|
||||
label += ` (${DGUtils.formatStringWithLeadingPlus(this.modifier)}%)`;
|
||||
}
|
||||
|
||||
const html = await renderTemplate(
|
||||
"systems/deltagreen/templates/roll/lethality-roll.hbs",
|
||||
{
|
||||
styleOverride,
|
||||
resultString,
|
||||
total: this.total,
|
||||
die1: nonLethalDamage.die1,
|
||||
die2: nonLethalDamage.die2,
|
||||
nonLethalTotal: nonLethalDamage.total,
|
||||
damageLabel: game.i18n.localize("DG.Roll.Damage"),
|
||||
},
|
||||
);
|
||||
|
||||
return this.toMessage({ content: html, label });
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the damage for when a lethality roll fails.
|
||||
* If roll has not been evaluated, return null.
|
||||
*
|
||||
* See full rules on page 57 of agent's handbook.
|
||||
*
|
||||
* Note, this getter does not actually care if the roll has failed.
|
||||
*
|
||||
* @returns {null|Object} - return data about the non-lethal damage.
|
||||
*/
|
||||
get nonLethalDamage() {
|
||||
if (!this.total) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to determine what the d100 result would be as if it was two d10's being rolled.
|
||||
const totalString = this.total.toString();
|
||||
const digits = totalString.length;
|
||||
let die1;
|
||||
let die2;
|
||||
switch (digits) {
|
||||
case 1:
|
||||
// If one digit in the result, one die is a 10, and the other is the result.
|
||||
[die1, die2] = [10, this.total];
|
||||
break;
|
||||
case 2:
|
||||
// If two digits in the result, each die is the value of one of the digits. If one of those digits is 0, make it 10.
|
||||
[die1, die2] = totalString
|
||||
.split("")
|
||||
.map((digit) => parseInt(digit))
|
||||
.map((digit) => digit || 10);
|
||||
break;
|
||||
case 3:
|
||||
// If three digits in the result (aka result === 100), each die is a 10.
|
||||
[die1, die2] = [10, 10];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const total = die1 + die2;
|
||||
return { die1, die2, total };
|
||||
}
|
||||
}
|
||||
|
||||
export class DGDamageRoll extends DGRoll {
|
||||
/**
|
||||
* Prepares data for a chat message and then passes that data
|
||||
* to a method that actually creates a ChatMessage.
|
||||
*
|
||||
* @returns {Promise<ChatMessage>} - the created chat message.
|
||||
* @override
|
||||
*/
|
||||
async toChat() {
|
||||
let label = this.formula;
|
||||
try {
|
||||
label = `<b>${game.i18n
|
||||
.localize("DG.Roll.Damage")
|
||||
.toUpperCase()}</b> ${game.i18n.localize("DG.Roll.For")} ${
|
||||
this.item.name
|
||||
} (<b>${
|
||||
this.item.system.armorPiercing
|
||||
} </b><img class="armor-piercing-chat-card-img" src="systems/deltagreen/assets/icons/supersonic-bullet.svg" alt="armor penetration"/>)`;
|
||||
} catch (ex) {
|
||||
// console.log(ex);
|
||||
label = `<b>DAMAGE</b> for <b>${label.toUpperCase()}</b>`;
|
||||
}
|
||||
const html = await renderTemplate(
|
||||
"systems/deltagreen/templates/roll/damage-roll.hbs",
|
||||
{
|
||||
formula: this.formula,
|
||||
total: this.total,
|
||||
},
|
||||
);
|
||||
|
||||
return this.toMessage({ content: html, label });
|
||||
}
|
||||
|
||||
async showDialog() {
|
||||
return showDamageRollModifyDialog({
|
||||
itemName: this.item?.name,
|
||||
formula: this.formula,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DGSanityDamageRoll extends DGRoll {
|
||||
/**
|
||||
* Prepares data for a chat message and then passes that data
|
||||
* to a method that actually creates a ChatMessage.
|
||||
*
|
||||
* @returns {Promise<ChatMessage>} - the created chat message.
|
||||
* @override
|
||||
*/
|
||||
async toChat() {
|
||||
const [lowDie, highDie] = this.terms[0].terms.map((formula) => {
|
||||
return Roll.parse(formula)[0] || { faces: parseInt(formula), number: 1 };
|
||||
});
|
||||
|
||||
const [lowResult, highResult] = this.damageResults;
|
||||
|
||||
const label = `<b>${DGUtils.localizeWithFallback(
|
||||
"DG.Generic.SanDamage",
|
||||
"SAN DAMAGE",
|
||||
)}</b> For <b>${lowDie.formula} / ${highDie.formula}</b>`;
|
||||
|
||||
const html = await renderTemplate(
|
||||
"systems/deltagreen/templates/roll/sanity-damage-roll.hbs",
|
||||
{
|
||||
lowFormula: lowDie.formula,
|
||||
highFormula: highDie.formula,
|
||||
lowFaces: lowDie.faces,
|
||||
highFaces: highDie.faces,
|
||||
lowResult,
|
||||
highResult,
|
||||
},
|
||||
);
|
||||
|
||||
return this.toMessage({ content: html, label });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the two results for a sanity damage roll.
|
||||
*
|
||||
* Returns null if the roll has not been evaluated.
|
||||
*
|
||||
* @returns {null|Array<Number>} - Array of result numbers
|
||||
*/
|
||||
get damageResults() {
|
||||
if (!this.total) return null;
|
||||
|
||||
const [lowResult, highResult] = this.terms[0].results;
|
||||
return [lowResult?.result, highResult?.result];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/** @internal Import roll subclasses only from ../roll.js. */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import {
|
||||
createDGRollChatMessage,
|
||||
prepareDGRollChatMessageData,
|
||||
} from "../../chat/dg-chat-card.js";
|
||||
|
||||
export class DGRoll extends Roll {
|
||||
/**
|
||||
* NOTE: This class will rarely be called on its own. It should generally be extended. Look to DGPercentileRoll as an example.
|
||||
*
|
||||
* Customize our roll with some useful information, passed in the `options` Object.
|
||||
*
|
||||
* @param {string} formula Unused - The string formula to parse (from Foundry)
|
||||
* @param {Object} data Unused - The data object against which to parse attributes within the formula
|
||||
* @param {Object} [options] Additional data which is preserved in the database
|
||||
* @param {Number} [options.rollType] The type of roll (stat, skill, sanity, damage, etc).
|
||||
* @param {String} [options.key] The key of the skill, stat, etc. to use as a basis for this roll.
|
||||
* @param {DeltaGreenActor} [options.actor] The actor that this roll originates from.
|
||||
* @param {DeltaGreenItem} [options.item] Optional - The item from which the roll originates.
|
||||
*/
|
||||
constructor(formula, data = {}, options = {}) {
|
||||
super(formula, data, options);
|
||||
const { rollType, key, actor, item } = options;
|
||||
this.type = rollType;
|
||||
this.key = key;
|
||||
this.actor = actor;
|
||||
this.item = item;
|
||||
this.modifier = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts a roll to chat with the Delta Green card shell and an explicit `rolls`
|
||||
* array for Dice So Nice (listen-path compatible).
|
||||
*
|
||||
* @override
|
||||
* The following `@param` descriptions comes from the Foundry VTT code.
|
||||
* @param {object} messageData The data object to use when creating the message
|
||||
* @param {options} [options] Additional options which modify the created message.
|
||||
* @param {string} [options.messageMode] A key of CONFIG.ChatMessage.modes
|
||||
* @param {boolean} [options.create=true] Whether to automatically create the chat message, or only return the
|
||||
* prepared chatData object.
|
||||
* @returns {Promise<ChatMessage|object>} A promise which resolves to the created ChatMessage document if create is
|
||||
* true, or the Object of prepared chatData otherwise.
|
||||
*/
|
||||
async toMessage(messageData = {}, { messageMode, create = true } = {}) {
|
||||
const label = messageData.label ?? messageData.flavor;
|
||||
delete messageData.label;
|
||||
|
||||
const mode =
|
||||
messageMode ?? this.options.messageMode ?? this.options.rollMode;
|
||||
|
||||
if (create) {
|
||||
return createDGRollChatMessage({
|
||||
roll: this,
|
||||
actor: this.actor,
|
||||
token: this.options.token,
|
||||
label,
|
||||
content: messageData.content,
|
||||
messageMode: mode,
|
||||
flags: messageData.flags ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
const { messageData: prepared, mappedMode } =
|
||||
await prepareDGRollChatMessageData({
|
||||
roll: this,
|
||||
actor: this.actor,
|
||||
token: this.options.token,
|
||||
label,
|
||||
content: messageData.content,
|
||||
messageMode: mode,
|
||||
flags: messageData.flags ?? {},
|
||||
});
|
||||
|
||||
const cls = foundry.utils.getDocumentClass("ChatMessage");
|
||||
// eslint-disable-next-line new-cap -- Foundry document class resolved at runtime
|
||||
const msg = new cls(prepared);
|
||||
msg.applyMode(mappedMode);
|
||||
return msg.toObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/** @internal Import roll subclasses only from ../roll.js. */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import DGUtils from "../../utils/utility-functions.js";
|
||||
import { DGRoll } from "./dg-roll.js";
|
||||
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
export class DGSanityDamageRoll extends DGRoll {
|
||||
/**
|
||||
* Prepares data for a chat message and then passes that data
|
||||
* to a method that actually creates a ChatMessage.
|
||||
*
|
||||
* @returns {Promise<ChatMessage>} - the created chat message.
|
||||
* @override
|
||||
*/
|
||||
async toChat() {
|
||||
const [lowDie, highDie] = this.terms[0].terms.map((formula) => {
|
||||
return Roll.parse(formula)[0] || { faces: parseInt(formula), number: 1 };
|
||||
});
|
||||
|
||||
const [lowResult, highResult] = this.damageResults;
|
||||
|
||||
const label = `<b>${DGUtils.localizeWithFallback(
|
||||
"DG.Generic.SanDamage",
|
||||
"SAN DAMAGE",
|
||||
)}</b> For <b>${lowDie.formula} / ${highDie.formula}</b>`;
|
||||
|
||||
const html = await renderTemplate(
|
||||
"systems/deltagreen/templates/roll/sanity-damage-roll.hbs",
|
||||
{
|
||||
lowFormula: lowDie.formula,
|
||||
highFormula: highDie.formula,
|
||||
lowFaces: lowDie.faces,
|
||||
highFaces: highDie.faces,
|
||||
lowResult,
|
||||
highResult,
|
||||
},
|
||||
);
|
||||
|
||||
return this.toMessage({ content: html, label });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the two results for a sanity damage roll.
|
||||
*
|
||||
* Returns null if the roll has not been evaluated.
|
||||
*
|
||||
* @returns {null|Array<Number>} - Array of result numbers
|
||||
*/
|
||||
get damageResults() {
|
||||
if (!this.total) return null;
|
||||
|
||||
const [lowResult, highResult] = this.terms[0].results;
|
||||
return [lowResult?.result, highResult?.result];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Append STR melee damage bonus to a weapon damage formula when applicable.
|
||||
*
|
||||
* @param {string} diceFormula
|
||||
* @param {Actor|null} actor
|
||||
* @param {string|null|undefined} skill Weapon skill key from item system
|
||||
* @returns {string}
|
||||
*/
|
||||
export function appendMeleeDamageBonus(diceFormula, actor, skill) {
|
||||
if (
|
||||
!actor ||
|
||||
(actor.type !== "agent" && actor.type !== "npc") ||
|
||||
(skill !== "unarmed_combat" && skill !== "melee_weapons")
|
||||
) {
|
||||
return diceFormula;
|
||||
}
|
||||
|
||||
const bonus = actor.system?.statistics?.str?.meleeDamageBonusFormula;
|
||||
if (!bonus) return diceFormula;
|
||||
return `${diceFormula}${bonus}`;
|
||||
}
|
||||
+11
-16
@@ -1,10 +1,9 @@
|
||||
import {
|
||||
DGRoll,
|
||||
DGPercentileRoll,
|
||||
DGLethalityRoll,
|
||||
DGDamageRoll,
|
||||
DGSanityDamageRoll,
|
||||
} from "./roll-classes.js";
|
||||
import { DGRoll } from "./classes/dg-roll.js";
|
||||
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";
|
||||
|
||||
export {
|
||||
DGRoll,
|
||||
@@ -58,15 +57,11 @@ export function createDGRollFromDataset(
|
||||
roll = new DGLethalityRoll("1D100", {}, rollOptions);
|
||||
break;
|
||||
case "damage": {
|
||||
let diceFormula = item.system.damage;
|
||||
const { skill } = item.system;
|
||||
if (
|
||||
actor &&
|
||||
(actor.type === "agent" || actor.type === "npc") &&
|
||||
(skill === "unarmed_combat" || skill === "melee_weapons")
|
||||
) {
|
||||
diceFormula += actor.system.statistics.str.meleeDamageBonusFormula;
|
||||
}
|
||||
const diceFormula = appendMeleeDamageBonus(
|
||||
item.system.damage,
|
||||
actor,
|
||||
item.system.skill,
|
||||
);
|
||||
roll = new DGDamageRoll(diceFormula, {}, rollOptions);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createDGChatMessage } from "../chat/dg-chat-card.js";
|
||||
import DG from "../config.js";
|
||||
import DG from "../config/index.js";
|
||||
|
||||
/**
|
||||
* @param {"1"|"d3"|"d4"|"d4-1"} baseFormula
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import DG, { BASE_TEMPLATE_PATH } from "./config.js";
|
||||
import DG, { BASE_TEMPLATE_PATH } from "./config/index.js";
|
||||
import DGActorSheet from "./sheets/base-actor-sheet.js";
|
||||
import DGItemSheet from "./sheets/base-item-sheet.js";
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import { packSkillGroupsIntoColumns } from "../utils/skill-layout.js";
|
||||
import { createAgentResourceChatMessage } from "../utils/resource-chat.js";
|
||||
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 {
|
||||
createSkillImprovementChatMessage,
|
||||
evaluateSkillImprovementRolls,
|
||||
@@ -14,10 +16,11 @@ import {
|
||||
clearStimulantEffects,
|
||||
getEffectiveSuppressExhaustion,
|
||||
hasActiveStimulantEffect,
|
||||
} from "../utils/stimulant-effect.js";
|
||||
} from "../active-effect/runtime/stimulant-effect.js";
|
||||
import { assignProfessionToAgent } from "../applications/profession-setup-flow.js";
|
||||
import { showDgDialog } from "../applications/dg-dialog.js";
|
||||
import { PROFESSION_OPTION_PICKS_KEY } from "../data/item/profession.js";
|
||||
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
|
||||
const AgentSheetBase = EffectsTabMixin(DGActorSheet);
|
||||
@@ -103,6 +106,8 @@ export default class DGAgentSheet extends AgentSheetBase {
|
||||
`${this.TEMPLATE_PATH}/parts/personal-tab-agent.html`,
|
||||
`${this.TEMPLATE_PATH}/parts/effects-tab.html`,
|
||||
`${this.TEMPLATE_PATH}/parts/about-tab.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/agent-skill-row-partial.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/agent-special-training-row-partial.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/custom-skills-partial-agent.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/bonds-section-partial.html`,
|
||||
`${this.TEMPLATE_PATH}/partials/weapons-section-partial.html`,
|
||||
@@ -149,7 +154,18 @@ export default class DGAgentSheet extends AgentSheetBase {
|
||||
|
||||
if (this.actor.type !== "agent") return context;
|
||||
|
||||
context.typedSkillColumns = this._prepareTypedSkillColumns();
|
||||
const sortSkillsByColumn = game.settings.get("deltagreen", "sortSkills");
|
||||
context.sortSkillsByColumn = sortSkillsByColumn;
|
||||
context.skillColumns = prepareAgentSkillColumns(
|
||||
this.actor.system.sortedSkills,
|
||||
sortSkillsByColumn,
|
||||
);
|
||||
context.specialTrainingRows = this._prepareSpecialTrainingBlocks();
|
||||
context.showSpecialTrainingSection = context.specialTrainingRows.length > 0;
|
||||
context.specialTrainingColumns = prepareAgentSkillColumns(
|
||||
context.specialTrainingRows,
|
||||
sortSkillsByColumn,
|
||||
);
|
||||
context.customSkillsEditMode = this._customSkillsEditMode;
|
||||
context.physicalUi = this._preparePhysicalUi();
|
||||
context.professionItem = this.actor.items.find(
|
||||
@@ -175,8 +191,71 @@ export default class DGAgentSheet extends AgentSheetBase {
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_sortCustomSkills() {
|
||||
// Agent sheet uses typedSkillColumns instead of sortedCustomSkills.
|
||||
_sortSkills() {
|
||||
const sortedSkills = [];
|
||||
|
||||
for (const [key, skill] of Object.entries(this.actor.system.skills)) {
|
||||
let sortLabel;
|
||||
if (game.i18n.lang === "ja") {
|
||||
sortLabel = game.i18n.localize(`DG.Skills.ruby.${key}`);
|
||||
} else {
|
||||
sortLabel = game.i18n.localize(`DG.Skills.${key}`);
|
||||
}
|
||||
|
||||
if (sortLabel === "" || sortLabel === `DG.Skills.${key}`) {
|
||||
sortLabel = key;
|
||||
}
|
||||
|
||||
sortedSkills.push({
|
||||
...skill,
|
||||
skillKind: "fixed",
|
||||
key,
|
||||
sortLabel,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [key, skill] of Object.entries(this.actor.system.typedSkills)) {
|
||||
const displayLabel = formatProfessionSkillLabel({
|
||||
kind: "typed",
|
||||
group: skill.group,
|
||||
label: skill.label,
|
||||
});
|
||||
sortedSkills.push({
|
||||
...skill,
|
||||
skillKind: "typed",
|
||||
key,
|
||||
displayLabel,
|
||||
sortLabel: displayLabel,
|
||||
actorType: this.actor.type,
|
||||
});
|
||||
}
|
||||
|
||||
sortedSkills.sort((a, b) =>
|
||||
a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang),
|
||||
);
|
||||
|
||||
this.actor.system.sortedSkills = sortedSkills;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_prepareSkillTooltips() {
|
||||
for (const skill of this.actor.system.sortedSkills) {
|
||||
if (skill.skillKind === "typed") {
|
||||
skill.tooltip = buildSkillTooltip(
|
||||
"sheet",
|
||||
this.actor,
|
||||
{ kind: "typed", group: skill.group, label: skill.label },
|
||||
{ proficiency: Number(skill.proficiency) || 0 },
|
||||
);
|
||||
} else {
|
||||
skill.tooltip = buildSkillTooltip(
|
||||
"sheet",
|
||||
this.actor,
|
||||
{ kind: "fixed", key: skill.key },
|
||||
{ proficiency: Number(skill.proficiency) || 0 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
@@ -262,61 +341,7 @@ export default class DGAgentSheet extends AgentSheetBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds grouped typed-skill columns for the skills tab.
|
||||
*
|
||||
* @returns {object[][]}
|
||||
*/
|
||||
_prepareTypedSkillColumns() {
|
||||
const groupsMap = new Map();
|
||||
|
||||
for (const [key, skill] of Object.entries(this.actor.system.typedSkills)) {
|
||||
const groupKey = skill.group;
|
||||
if (!groupsMap.has(groupKey)) {
|
||||
groupsMap.set(groupKey, {
|
||||
group: groupKey,
|
||||
label: game.i18n.localize(`DG.TypeSkills.${groupKey}`),
|
||||
skills: [],
|
||||
rowCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
groupsMap.get(groupKey).skills.push({
|
||||
...skill,
|
||||
key,
|
||||
actorType: this.actor.type,
|
||||
});
|
||||
}
|
||||
|
||||
const groups = [...groupsMap.values()].map((group) => {
|
||||
group.skills.sort((a, b) =>
|
||||
a.label.localeCompare(b.label, game.i18n.lang),
|
||||
);
|
||||
group.rowCount = 1 + group.skills.length;
|
||||
return group;
|
||||
});
|
||||
|
||||
groups.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
|
||||
|
||||
const trainings = this._prepareSpecialTrainingBlocks();
|
||||
if (trainings.length > 0) {
|
||||
groups.push({
|
||||
group: "specialTraining",
|
||||
isSpecialTraining: true,
|
||||
label: game.i18n.localize("DG.Sheet.BlockHeaders.SpecialTraining"),
|
||||
skills: trainings,
|
||||
rowCount: 1 + trainings.length,
|
||||
});
|
||||
}
|
||||
|
||||
return packSkillGroupsIntoColumns(
|
||||
groups,
|
||||
3,
|
||||
game.settings.get("deltagreen", "sortSkills"),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorted special-training blocks for the skills tab (single-column list).
|
||||
* Sorted special-training blocks for the skills tab.
|
||||
*
|
||||
* @returns {object[]}
|
||||
*/
|
||||
@@ -416,15 +441,6 @@ export default class DGAgentSheet extends AgentSheetBase {
|
||||
static async _restAgent() {
|
||||
const { actor } = this;
|
||||
|
||||
if (getEffectiveSuppressExhaustion(actor)) {
|
||||
ui.notifications.warn(
|
||||
game.i18n.format("DG.Physical.RestBlockedWhileSuppressed", {
|
||||
name: actor.name,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const wasExhausted = DGAgentSheet._isActorExhausted(actor);
|
||||
const wpRoll = await new Roll("1d6").evaluate();
|
||||
const gain = wpRoll.total;
|
||||
@@ -524,6 +540,7 @@ export default class DGAgentSheet extends AgentSheetBase {
|
||||
await createAgentResourceChatMessage({
|
||||
actor,
|
||||
token: this.token,
|
||||
roll: hoursRoll,
|
||||
contentKey,
|
||||
labelKey,
|
||||
i18nData: {
|
||||
|
||||
@@ -85,7 +85,9 @@ export default class DGActorSheet extends ComposedActorSheetBase {
|
||||
if (this.actor.type === "vehicle") return context;
|
||||
|
||||
this._sortSkills();
|
||||
this._sortCustomSkills();
|
||||
if (this.actor.type !== "agent") {
|
||||
this._sortCustomSkills();
|
||||
}
|
||||
|
||||
context.skillTooltipDisplay = game.settings.get(
|
||||
"deltagreen",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import { BASE_TEMPLATE_PATH } from "../config/index.js";
|
||||
import DGSheetMixin from "./base-sheet.js";
|
||||
import EffectsTabMixin from "./mixins/effects-tab-mixin.js";
|
||||
import ProfessionItemMixin from "./mixins/profession-item-mixin.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
|
||||
import DG, { BASE_TEMPLATE_PATH } from "../config/index.js";
|
||||
import { getRichTextFieldsForPart } from "../config/rich-text-fields.js";
|
||||
import { prepareRichTextContext } from "../utils/rich-text.js";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SUPPORTED_ITEM_TYPES } from "../../active-effect/effect-fields.js";
|
||||
import { getTransferSuppressionReasonKey } from "../../documents/dg-active-effect.js";
|
||||
import { getTransferSuppressionReasonKey } from "../../active-effect/documents/dg-active-effect.js";
|
||||
|
||||
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
|
||||
export default function EffectsTabMixin(Base) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BASE_TEMPLATE_PATH } from "../../config.js";
|
||||
import { BASE_TEMPLATE_PATH } from "../../config/index.js";
|
||||
import { PROFESSION_OPTION_PICKS_KEY } from "../../data/item/profession.js";
|
||||
import {
|
||||
allocateProfessionSkillStorageKey,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
findTypedSkillNameConflict,
|
||||
prepareProfessionSkillRows,
|
||||
splitProfessionSkillMap,
|
||||
} from "../../utils/profession-skills.js";
|
||||
} from "../../profession/index.js";
|
||||
import {
|
||||
getDialogContentRoot,
|
||||
showDgDialog,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import DG from "../../config.js";
|
||||
import DG from "../../config/index.js";
|
||||
import { reorderForColumnSorting } from "../../utils/skill-layout.js";
|
||||
import { getRollTargetDisplayClassFromModifier } from "../../utils/active-effect-derived.js";
|
||||
import { getRollTargetDisplayClassFromModifier } from "../../active-effect/runtime/derived.js";
|
||||
import {
|
||||
applySkillTooltipDisplayMode,
|
||||
buildAgentSpecialTrainingTooltip,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import DG from "../config.js";
|
||||
import DG from "../config/index.js";
|
||||
import { ROLL_TARGET_FIELD_KEYS } from "../active-effect/effect-fields.js";
|
||||
import { clampPercentileRollTarget } from "./active-effect-derived.js";
|
||||
import { clampPercentileRollTarget } from "../active-effect/runtime/derived.js";
|
||||
import DGUtils from "./utility-functions.js";
|
||||
|
||||
/** @typedef {"system.rollTarget.allSkills"|"system.rollTarget.sanity"|"system.rollTarget.statistics"} RollTargetFieldKey */
|
||||
|
||||
+104
-99
@@ -1,20 +1,4 @@
|
||||
/**
|
||||
* Splits a flat list into column arrays for vertical stacking (top-to-bottom per column).
|
||||
*
|
||||
* @template T
|
||||
* @param {T[]} arr
|
||||
* @param {number} numCols
|
||||
* @returns {T[][]}
|
||||
*/
|
||||
export function splitIntoColumns(arr, numCols) {
|
||||
if (numCols <= 1) return [arr];
|
||||
const columns = Array.from({ length: numCols }, () => /** @type {T[]} */ ([]));
|
||||
const perCol = Math.ceil(arr.length / numCols);
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
columns[col] = arr.slice(col * perCol, (col + 1) * perCol);
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
const AGENT_SKILL_COLUMNS = 3;
|
||||
|
||||
/**
|
||||
* Reorders a flat array into column-major order for grid display.
|
||||
@@ -24,7 +8,6 @@ export function splitIntoColumns(arr, numCols) {
|
||||
* @returns {object[]}
|
||||
*/
|
||||
export function reorderForColumnSorting(arr, numCols) {
|
||||
const numRows = Math.ceil(arr.length / numCols);
|
||||
const reordered = new Array(arr.length);
|
||||
|
||||
const baseRowCount = Math.floor(arr.length / numCols);
|
||||
@@ -54,91 +37,113 @@ export function reorderForColumnSorting(arr, numCols) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} column
|
||||
* @param {object} group
|
||||
* @returns {void}
|
||||
*/
|
||||
function appendSkillGroupToColumn(column, group) {
|
||||
column.blocks.push({
|
||||
type: "groupHeader",
|
||||
group: group.group,
|
||||
label: group.label,
|
||||
...(group.isSpecialTraining ? { isSpecialTraining: true } : {}),
|
||||
});
|
||||
|
||||
const skillType = group.isSpecialTraining ? "training" : "typedSkill";
|
||||
for (const skill of group.skills) {
|
||||
column.blocks.push({ ...skill, type: skillType });
|
||||
}
|
||||
|
||||
column.height += group.rowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object[]} groups
|
||||
* @param {number} numCols
|
||||
* @returns {object[][]}
|
||||
*/
|
||||
function packSkillGroupsByColumn(groups, numCols) {
|
||||
const columns = Array.from({ length: numCols }, () => ({
|
||||
blocks: [],
|
||||
height: 0,
|
||||
}));
|
||||
|
||||
if (groups.length === 0) {
|
||||
return columns.map((col) => col.blocks);
|
||||
}
|
||||
|
||||
const totalHeight = groups.reduce((sum, group) => sum + group.rowCount, 0);
|
||||
const targetHeight = totalHeight / numCols;
|
||||
let colIndex = 0;
|
||||
|
||||
for (const group of groups) {
|
||||
const col = columns[colIndex];
|
||||
|
||||
if (
|
||||
colIndex < numCols - 1 &&
|
||||
col.blocks.length > 0 &&
|
||||
col.height + group.rowCount > targetHeight
|
||||
) {
|
||||
colIndex += 1;
|
||||
}
|
||||
|
||||
appendSkillGroupToColumn(columns[colIndex], group);
|
||||
}
|
||||
|
||||
return columns.map((col) => col.blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object[]} groups
|
||||
* @param {number} numCols
|
||||
* @returns {object[][]}
|
||||
*/
|
||||
function packSkillGroupsByRow(groups, numCols) {
|
||||
const columns = Array.from({ length: numCols }, () => ({
|
||||
blocks: [],
|
||||
height: 0,
|
||||
}));
|
||||
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
appendSkillGroupToColumn(columns[i % numCols], groups[i]);
|
||||
}
|
||||
|
||||
return columns.map((col) => col.blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs skill groups into columns without splitting a group across columns.
|
||||
* Column-major packing (matches legacy reorderForColumnSorting + 3-column grid).
|
||||
*
|
||||
* @param {object[]} groups
|
||||
* @template T
|
||||
* @param {T[]} items
|
||||
* @param {number} numCols
|
||||
* @returns {T[][]}
|
||||
*/
|
||||
function packItemsColumnMajor(items, numCols) {
|
||||
const reordered = reorderForColumnSorting([...items], numCols);
|
||||
const columns = Array.from(
|
||||
{ length: numCols },
|
||||
() => /** @type {T[]} */ ([]),
|
||||
);
|
||||
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
for (let row = 0; row * numCols + col < reordered.length; row++) {
|
||||
const item = reordered[row * numCols + col];
|
||||
if (item !== undefined) {
|
||||
columns[col].push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Row-major packing: alphabetical left-to-right, top-to-bottom across columns.
|
||||
*
|
||||
* @template T
|
||||
* @param {T[]} items
|
||||
* @param {number} numCols
|
||||
* @returns {T[][]}
|
||||
*/
|
||||
function packItemsRowMajor(items, numCols) {
|
||||
const columns = Array.from(
|
||||
{ length: numCols },
|
||||
() => /** @type {T[]} */ ([]),
|
||||
);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
columns[i % numCols].push(items[i]);
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs items into column arrays for vertical stacking (top-to-bottom per column).
|
||||
*
|
||||
* @template T
|
||||
* @param {T[]} items
|
||||
* @param {number} numCols
|
||||
* @param {{ sortByColumn?: boolean }} [options]
|
||||
* @returns {T[][]}
|
||||
*/
|
||||
export function packItemsIntoWeightedColumns(
|
||||
items,
|
||||
numCols,
|
||||
{ sortByColumn = false } = {},
|
||||
) {
|
||||
if (numCols <= 1) return [items];
|
||||
|
||||
if (items.length === 0) {
|
||||
return Array.from({ length: numCols }, () => /** @type {T[]} */ ([]));
|
||||
}
|
||||
|
||||
if (sortByColumn) {
|
||||
return packItemsColumnMajor(items, numCols);
|
||||
}
|
||||
|
||||
return packItemsRowMajor(items, numCols);
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs agent skills into sheet columns.
|
||||
*
|
||||
* @param {object[]} sortedSkills
|
||||
* @param {boolean} sortByColumn
|
||||
* @param {number} [numCols]
|
||||
* @returns {object[][]}
|
||||
*/
|
||||
export function packSkillGroupsIntoColumns(groups, numCols, sortByColumn) {
|
||||
if (sortByColumn) {
|
||||
return packSkillGroupsByColumn(groups, numCols);
|
||||
export function prepareAgentSkillColumns(
|
||||
sortedSkills,
|
||||
sortByColumn,
|
||||
numCols = AGENT_SKILL_COLUMNS,
|
||||
) {
|
||||
return packItemsIntoWeightedColumns(sortedSkills, numCols, { sortByColumn });
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a flat list into column arrays for vertical stacking (top-to-bottom per column).
|
||||
*
|
||||
* @template T
|
||||
* @param {T[]} arr
|
||||
* @param {number} numCols
|
||||
* @returns {T[][]}
|
||||
*/
|
||||
export function splitIntoColumns(arr, numCols) {
|
||||
if (numCols <= 1) return [arr];
|
||||
const columns = Array.from(
|
||||
{ length: numCols },
|
||||
() => /** @type {T[]} */ ([]),
|
||||
);
|
||||
const perCol = Math.ceil(arr.length / numCols);
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
columns[col] = arr.slice(col * perCol, (col + 1) * perCol);
|
||||
}
|
||||
return packSkillGroupsByRow(groups, numCols);
|
||||
return columns;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** @typedef {import("./profession-skills.js").ProfessionSkillRef} ProfessionSkillRef */
|
||||
/** @typedef {import("../profession/index.js").ProfessionSkillRef} ProfessionSkillRef */
|
||||
/** @typedef {"dialog" | "sheet"} SkillTooltipContext */
|
||||
|
||||
import { appendRollTargetTooltipSection } from "./roll-target-tooltip.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DG from "../config.js";
|
||||
import DG from "../config/index.js";
|
||||
|
||||
const MIGRATION_VERSION = 2;
|
||||
const ACTOR_TYPES_WITH_SKILLS = ["agent", "npc", "unnatural"];
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "deltagreen",
|
||||
"version": "1.3.1",
|
||||
"description": "This is a fan made but now officially supported rules implementation (system) for [Foundry Virtual Tabletop](https://foundryvtt.com/) system for Delta Green: The RPG, based on the _Agent's Handbook_ rule set from Arc Dream Publishing (_**not** the Call of Cthulu rules_).",
|
||||
"main": "deltagreen.js",
|
||||
"main": "module/deltagreen.js",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format:check": "prettier --check .",
|
||||
|
||||
@@ -26,6 +26,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Parchment/light sheet bodies: override Foundry v14 .application light text and heading typography. */
|
||||
@mixin dg-parchment-content-theme {
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.resource-label,
|
||||
.standard-form .form-group > label,
|
||||
.form-group > label {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dg-parchment-form-fields {
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
select {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgb(122, 121, 113);
|
||||
border-radius: 3px;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin item-sheet-grid-2col {
|
||||
display: grid;
|
||||
grid-template-columns: 25% 75%;
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin-left: -14px;
|
||||
margin-left: -5px;
|
||||
padding-inline: 3px;
|
||||
font-size: 11px;
|
||||
overflow: visible;
|
||||
@@ -141,7 +141,7 @@
|
||||
.tabs [data-action="tab"][data-tab="about"] {
|
||||
flex: 0 0 auto;
|
||||
min-width: unset;
|
||||
margin-left: -14px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.window-content > [data-application-part="rightBar"],
|
||||
@@ -177,6 +177,32 @@
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.right-bar > .tab:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-bar > .tab.skills.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.skills-tab-scroll {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.skills-tab-footer {
|
||||
flex: 0 0 auto;
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.skills-tab-footer .skill-util-buttons {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs [data-action="tab"].active {
|
||||
z-index: 10;
|
||||
margin-bottom: -2px;
|
||||
@@ -294,34 +320,40 @@
|
||||
|
||||
.skill-util-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.action-pill {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
.skill-util-buttons-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
.action-pill {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.typed-skill-add {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.typed-skill-add {
|
||||
margin-right: 0;
|
||||
.skill-util-buttons-row--full .action-pill {
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a[data-action="applySkillImprovements"] {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.custom-skills-edit-toggle {
|
||||
flex: 0 0 auto;
|
||||
min-width: 2rem;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-skill-group-label {
|
||||
@@ -350,9 +382,72 @@
|
||||
color: var(--dg-on-dark);
|
||||
}
|
||||
|
||||
.typed-skill-row {
|
||||
min-height: 27px;
|
||||
.agent-skills-columns {
|
||||
align-items: start;
|
||||
--agent-skill-row-gap: 3px;
|
||||
}
|
||||
|
||||
.agent-skills-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--agent-skill-row-gap);
|
||||
min-width: 0;
|
||||
|
||||
/* Prevent flex column from shrinking skill rows below min-height */
|
||||
> .skill-row,
|
||||
> .item.skill-row {
|
||||
flex: 0 0 auto;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tab.skills .skill-row,
|
||||
.tab.skills .skill-row > .flexrow {
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Fixed skills: one flexrow row — stop Foundry .flexrow > * { flex: 1 } from widening inputs */
|
||||
.tab.skills .flexrow.skill-row > .skill-label {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tab.skills .flexrow.skill-row > .percentile-skill-input {
|
||||
flex: 0 0 40px;
|
||||
width: 40px;
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
.tab.skills .flexrow.skill-row > .checkbox-skill-input {
|
||||
flex: 0 0 20px;
|
||||
width: 20px;
|
||||
max-width: 20px;
|
||||
}
|
||||
|
||||
/* Typed skills: label grows, controls stay fixed width */
|
||||
.tab.skills .typed-skill-row > .flexrow > .skill-label {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tab.skills .typed-skill-row > .flexrow > .custom-skill-controls-slot {
|
||||
flex: 0 0 70px;
|
||||
width: 70px;
|
||||
max-width: 70px;
|
||||
}
|
||||
|
||||
.custom-skill-controls-slot {
|
||||
@@ -400,15 +495,8 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.typed-skills-columns {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.typed-skills-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
.special-training-columns {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.skill-and-training-controls {
|
||||
@@ -528,10 +616,53 @@
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.left-bar-section-divider {
|
||||
margin: 8px 0;
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.15);
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.left-bar-section-heading {
|
||||
margin: 0 0 6px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.left-bar-tab-panels .tab:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.left-bar-rest-sanity-panels {
|
||||
display: grid;
|
||||
|
||||
> .tab {
|
||||
grid-area: 1 / 1;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
> .tab:not(.active) {
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .tab.active {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.left-bar-action-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -292,3 +292,28 @@ div.horizontal-bar-section-divider.skills-header {
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Agent sheet: only the title — center in a single column (not grid column 2 of 3). */
|
||||
div.horizontal-bar-section-divider.skills-header:not(:has(.toggle-untrained)) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.horizontal-bar-section-divider .skills-header-title {
|
||||
grid-column: 2;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
div.horizontal-bar-section-divider.skills-header:not(:has(.toggle-untrained))
|
||||
.skills-header-title {
|
||||
grid-column: 1;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.horizontal-bar-section-divider .toggle-untrained {
|
||||
grid-column: 3;
|
||||
padding-right: 0.5rem;
|
||||
justify-self: end;
|
||||
align-self: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -107,3 +107,34 @@
|
||||
.strike {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.roll-stats-chat-table {
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Override Foundry row striping; alternate per cell in reading order (checkerboard in 3 cols) */
|
||||
.dg-chat-card__body .roll-stats-chat-table tbody tr {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dg-chat-card__body .roll-stats-chat-table td {
|
||||
padding: 0.35rem 1rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-16, 1rem);
|
||||
border: 1px solid var(--color-border-light-2, rgba(255, 255, 255, 0.2));
|
||||
}
|
||||
|
||||
.dg-chat-card__body .roll-stats-chat-table td.roll-stats-chat-value--a {
|
||||
background-color: var(--table-row-color-odd, transparent);
|
||||
}
|
||||
|
||||
.dg-chat-card__body .roll-stats-chat-table td.roll-stats-chat-value--b {
|
||||
background-color: var(--table-row-color-even, rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
.dg-chat-card__body .roll-stats-chat-table .roll-stats-chat-value {
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
.deltagreen section.window-content {
|
||||
@include dg-window-content;
|
||||
@include dg-window-form-inputs;
|
||||
@include dg-parchment-content-theme;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
@use "../mixins" as *;
|
||||
|
||||
/*
|
||||
Item sheets (ApplicationV2): Foundry .application uses light --color-text-primary,
|
||||
but DG window-content uses light parchment backgrounds. Reset ink, headings, labels,
|
||||
and form fields here so new item tabs stay readable without per-type patches.
|
||||
*/
|
||||
.deltagreen.item .window-content {
|
||||
color: var(--dg-ink);
|
||||
scrollbar-color: #215112 #111 !important;
|
||||
|
||||
@include dg-window-form-inputs-with-select;
|
||||
@include dg-parchment-content-theme;
|
||||
@include dg-parchment-form-fields;
|
||||
}
|
||||
|
||||
/*
|
||||
Item Sheet Common CSS
|
||||
*/
|
||||
|
||||
@@ -1,76 +1,96 @@
|
||||
@use "../mixins" as *;
|
||||
|
||||
/* Profession item sheet */
|
||||
.deltagreen .profession-sheet-grid-2col {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.75rem;
|
||||
align-items: start;
|
||||
@include item-sheet-grid-2col;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.deltagreen .profession-sheet-name {
|
||||
.deltagreen .profession-sheet-horizontal-grid-2col {
|
||||
@include item-sheet-horizontal-grid(150px);
|
||||
}
|
||||
|
||||
.deltagreen.item .tab.profession-skills .profession-skills-scroll {
|
||||
padding: 0 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.35rem;
|
||||
border-bottom: 1px solid var(--color-border-light-2, rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-section-title {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-14, 0.875rem);
|
||||
font-weight: bold;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.deltagreen .tab.profession-skills .profession-section-add {
|
||||
@include dg-dark-button;
|
||||
flex: 0 0 auto;
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-size: var(--font-size-12, 0.75rem);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-section-config {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.resource-label {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skill-list-columns-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: var(--font-size-12, 0.75rem);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skill-col-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1.5rem;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills {
|
||||
--profession-toolbar-label-width: 11rem;
|
||||
--profession-toolbar-secondary-label-width: 7rem;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skills-section {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skills-section:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-section-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
var(--profession-toolbar-label-width)
|
||||
auto
|
||||
var(--profession-toolbar-secondary-label-width)
|
||||
4rem;
|
||||
align-items: center;
|
||||
column-gap: 0.5rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skills-toolbar-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skills-toolbar-control {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-bonds-input,
|
||||
.deltagreen .tab.profession-skills .profession-option-picks-input {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skill-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.25rem 1rem;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.2rem 0.75rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skill-row {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1.5rem;
|
||||
gap: 0.25rem 0.5rem;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.15rem 0;
|
||||
min-height: 27px;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skill-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -78,38 +98,27 @@
|
||||
}
|
||||
|
||||
.deltagreen .tab.profession-skills .profession-skill-rating {
|
||||
flex-shrink: 0;
|
||||
min-width: 2.5rem;
|
||||
text-align: right;
|
||||
min-width: 2.5rem;
|
||||
}
|
||||
|
||||
.roll-stats-chat-table {
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.deltagreen .tab.profession-skills .profession-skill-remove {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
opacity: 0.65;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
|
||||
/* Override Foundry row striping; alternate per cell in reading order (checkerboard in 3 cols) */
|
||||
.dg-chat-card__body .roll-stats-chat-table tbody tr {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dg-chat-card__body .roll-stats-chat-table td {
|
||||
padding: 0.35rem 1rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-16, 1rem);
|
||||
border: 1px solid var(--color-border-light-2, rgba(255, 255, 255, 0.2));
|
||||
}
|
||||
|
||||
.dg-chat-card__body .roll-stats-chat-table td.roll-stats-chat-value--a {
|
||||
background-color: var(--table-row-color-odd, transparent);
|
||||
}
|
||||
|
||||
.dg-chat-card__body .roll-stats-chat-table td.roll-stats-chat-value--b {
|
||||
background-color: var(--table-row-color-even, rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
.dg-chat-card__body .roll-stats-chat-table .roll-stats-chat-value {
|
||||
min-width: 2rem;
|
||||
&:hover:not(:disabled) {
|
||||
@include dg-dark-button;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,31 +6,16 @@
|
||||
section.window-content {
|
||||
@include dg-window-content;
|
||||
@include dg-window-form-inputs-with-select;
|
||||
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
color: inherit;
|
||||
}
|
||||
@include dg-parchment-content-theme;
|
||||
@include dg-parchment-form-fields;
|
||||
}
|
||||
|
||||
/* Foundry sets explicit light-theme tokens on headings/labels; inherit parchment body color. */
|
||||
.standard-form .form-group > label,
|
||||
.form-group > label,
|
||||
.dg-dialog__field-label,
|
||||
.dg-dialog .dg-dialog__heading,
|
||||
.dg-dialog__intro {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.dg-dialog input[type="text"],
|
||||
.dg-dialog input[type="number"],
|
||||
.dg-dialog input[type="password"],
|
||||
.dg-dialog select {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgb(122, 121, 113);
|
||||
border-radius: 3px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.dg-dialog__field-label,
|
||||
.dg-dialog .dg-dialog__heading {
|
||||
font-weight: bold;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
},
|
||||
"esmodules": ["module/deltagreen.js"],
|
||||
"styles": ["css/deltagreen.css", "css/dg-dialog.css", "css/dialogue.css"],
|
||||
"styles": ["css/deltagreen.css", "css/dg-dialog.css", "css/damage-modify.css"],
|
||||
"languages": [
|
||||
{
|
||||
"lang": "en",
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{{#if (eq skill.skillKind "typed") }}
|
||||
<div class="item flexrow flex-group-left flex-thin-border skill-row typed-skill-row">
|
||||
<div class="flexrow">
|
||||
<label class="{{if_gt skill.proficiency 0 'rollable' 'not-rollable'}} skill-label"
|
||||
data-action="roll"
|
||||
data-key="{{skill.key}}"
|
||||
data-rolltype="skill"
|
||||
data-roll="d100"
|
||||
data-target="{{skill.proficiency}}"
|
||||
data-label="{{skill.label}}"
|
||||
{{#unless (eq @root.skillTooltipDisplay "never") }}
|
||||
data-tooltip="{{skill.tooltip}}"
|
||||
{{/unless }}>
|
||||
{{skill.displayLabel}}
|
||||
</label>
|
||||
<div class="custom-skill-controls-slot">
|
||||
<div class="custom-skill-play-controls">
|
||||
<input class="percentile-skill-input {{skill.rollTargetDisplayClass}}"
|
||||
type="text"
|
||||
name="system.typedSkills.{{skill.key}}.proficiency"
|
||||
value="{{skill.proficiency}}"
|
||||
data-dtype="Number" />
|
||||
<input class="checkbox-skill-input"
|
||||
type="checkbox"
|
||||
name="system.typedSkills.{{skill.key}}.failure"
|
||||
{{checked skill.failure}}
|
||||
data-dtype="Boolean"
|
||||
{{#if skill.cannotBeImprovedByFailure }}
|
||||
disabled
|
||||
{{/if }} />
|
||||
</div>
|
||||
<div class="item-controls skill-and-training-controls custom-skill-edit-controls">
|
||||
<a class="item-control"
|
||||
data-action="typedSkillAction"
|
||||
data-action-type="edit"
|
||||
data-typedskill="{{skill.key}}"
|
||||
{{#unless (eq @root.skillTooltipDisplay "never") }}
|
||||
title="Edit Skill"
|
||||
{{/unless }}>
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a class="item-control"
|
||||
data-action="typedSkillAction"
|
||||
data-action-type="delete"
|
||||
data-typedskill="{{skill.key}}"
|
||||
{{#unless (eq @root.skillTooltipDisplay "never") }}
|
||||
title="Delete Skill"
|
||||
{{/unless }}>
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="flexrow flex-group-left flex-thin-border skill-row">
|
||||
<label class="{{if_gt skill.proficiency 0 'rollable' 'not-rollable'}} skill-label"
|
||||
data-action="roll"
|
||||
data-key="{{skill.key}}"
|
||||
data-rolltype="skill"
|
||||
for="system.skills.{{skill.key}}.proficiency"
|
||||
{{#unless (eq @root.skillTooltipDisplay "never") }}
|
||||
data-tooltip="{{ skill.tooltip }}"
|
||||
{{/unless }}>
|
||||
{{localizeWithFallback (concat "DG.Skills." skill.key) skill.label }}
|
||||
</label>
|
||||
|
||||
<input class="percentile-skill-input {{skill.rollTargetDisplayClass}}"
|
||||
type="text"
|
||||
name="system.skills.{{skill.key}}.proficiency"
|
||||
value="{{skill.proficiency}}"
|
||||
data-dtype="Number"
|
||||
{{disabled skill.isCalculatedValue}} />
|
||||
<input class="checkbox-skill-input"
|
||||
type="checkbox"
|
||||
name="system.skills.{{skill.key}}.failure"
|
||||
data-tooltip="{{localize 'DG.Tooltip.SkillFailCheckbox' }}"
|
||||
{{checked skill.failure}}
|
||||
{{disabled skill.cannotBeImprovedByFailure }} />
|
||||
</div>
|
||||
{{/if}}
|
||||
@@ -0,0 +1,32 @@
|
||||
<div class="item flexrow flex-group-left flex-thin-border special-training-box skill-row">
|
||||
<div class="flexrow">
|
||||
<label class="{{if_gt block.targetNumber 0 'rollable' 'not-rollable'}} skill-label {{block.rollTargetDisplayClass}}"
|
||||
data-action="roll"
|
||||
data-key="{{block.key}}"
|
||||
data-name="{{block.name}}"
|
||||
data-rolltype="special-training"
|
||||
{{#if block.tooltip }}
|
||||
{{#unless (eq @root.skillTooltipDisplay "never") }}
|
||||
data-tooltip="{{block.tooltip}}"
|
||||
{{/unless }}
|
||||
{{/if }}>
|
||||
{{block.displayLabel}}
|
||||
</label>
|
||||
<div class="item-controls skill-and-training-controls custom-skill-edit-controls">
|
||||
<a class="item-control"
|
||||
data-action="specialTrainingAction"
|
||||
data-action-type="edit"
|
||||
data-id="{{block.id}}"
|
||||
{{#unless (eq @root.skillTooltipDisplay "never") }}
|
||||
title="Edit Training"
|
||||
{{/unless }}><i class="fas fa-edit"></i></a>
|
||||
<a class="item-control"
|
||||
data-action="specialTrainingAction"
|
||||
data-action-type="delete"
|
||||
{{#unless (eq @root.skillTooltipDisplay "never") }}
|
||||
title="Delete Training"
|
||||
{{/unless }}
|
||||
data-id="{{block.id}}"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,140 +1,15 @@
|
||||
<div class="item-controls skill-util-buttons">
|
||||
<a class="item-control typed-skill-add action-pill"
|
||||
data-action="typedSkillAction"
|
||||
data-action-type="create">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{localize 'DG.Skills.AddTypedOrCustomSkill'}}
|
||||
</a>
|
||||
{{#if showSpecialTrainingSection }}
|
||||
<div class="horizontal-bar-section-divider custom-skill-group-label">
|
||||
<span>{{localize "DG.Sheet.BlockHeaders.SpecialTraining"}}</span>
|
||||
</div>
|
||||
|
||||
<a class="item-control action-pill"
|
||||
data-action="specialTrainingAction"
|
||||
data-action-type="create">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{localize 'DG.SpecialTraining.Dialog.CreateSpecialTraining'}}
|
||||
</a>
|
||||
|
||||
{{#if_eq actor.type "agent" }}
|
||||
<a class="item-control action-pill" data-action="applySkillImprovements">
|
||||
{{localize 'DG.Skills.ApplySkillImprovements.Title'}}
|
||||
<i class="fas fa-dice"></i>
|
||||
</a>
|
||||
{{/if_eq}}
|
||||
|
||||
<a class="item-control action-pill custom-skills-edit-toggle"
|
||||
data-action="toggleCustomSkillsEdit"
|
||||
data-tooltip="{{localize 'DG.Skills.ToggleEditDeleteSkills'}}">
|
||||
<i class="fas fa-edit" inert></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="typed-skills-columns grid grid-3col">
|
||||
{{#each typedSkillColumns as |column| }}
|
||||
<div class="typed-skills-column">
|
||||
{{#each column as |block| }}
|
||||
{{#if_eq block.type "groupHeader" }}
|
||||
<div class="horizontal-bar-section-divider custom-skill-group-label"
|
||||
{{#unless block.isSpecialTraining }}
|
||||
{{#unless (eq ../../skillTooltipDisplay "never") }}
|
||||
data-tooltip="{{localize (concat 'DG.TypeSkills.Tooltip.' block.group)}}"
|
||||
{{/unless }}
|
||||
{{/unless }}>
|
||||
<span>{{block.label}}</span>
|
||||
</div>
|
||||
{{/if_eq}}
|
||||
|
||||
{{#if_eq block.type "typedSkill" }}
|
||||
<div class="item flexrow flex-group-left flex-thin-border typed-skill-row">
|
||||
<div class="flexrow">
|
||||
<label class="{{if_gt block.proficiency 0 'rollable' 'not-rollable'}} skill-label"
|
||||
data-action="roll"
|
||||
data-key="{{block.key}}"
|
||||
data-rolltype="skill"
|
||||
data-roll="d100"
|
||||
data-target="{{block.proficiency}}"
|
||||
data-label="{{block.label}}"
|
||||
{{#unless (eq ../../skillTooltipDisplay "never") }}
|
||||
data-tooltip="{{block.tooltip}}"
|
||||
{{/unless }}>
|
||||
{{block.label}}
|
||||
</label>
|
||||
<div class="custom-skill-controls-slot">
|
||||
<div class="custom-skill-play-controls">
|
||||
<input class="percentile-skill-input {{block.rollTargetDisplayClass}}"
|
||||
type="text"
|
||||
name="system.typedSkills.{{block.key}}.proficiency"
|
||||
value="{{block.proficiency}}"
|
||||
data-dtype="Number" />
|
||||
{{#if_eq block.actorType "agent" }}
|
||||
<input class="checkbox-skill-input"
|
||||
type="checkbox"
|
||||
name="system.typedSkills.{{block.key}}.failure"
|
||||
{{checked block.failure}}
|
||||
data-dtype="Boolean"
|
||||
{{#if block.cannotBeImprovedByFailure }}
|
||||
disabled
|
||||
{{/if }} />
|
||||
{{/if_eq}}
|
||||
</div>
|
||||
<div class="item-controls skill-and-training-controls custom-skill-edit-controls">
|
||||
<a class="item-control"
|
||||
data-action="typedSkillAction"
|
||||
data-action-type="edit"
|
||||
data-typedskill="{{block.key}}"
|
||||
{{#unless (eq ../../skillTooltipDisplay "never") }}
|
||||
title="Edit Skill"
|
||||
{{/unless }}>
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a class="item-control"
|
||||
data-action="typedSkillAction"
|
||||
data-action-type="delete"
|
||||
data-typedskill="{{block.key}}"
|
||||
{{#unless (eq ../../skillTooltipDisplay "never") }}
|
||||
title="Delete Skill"
|
||||
{{/unless }}>
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if_eq}}
|
||||
|
||||
{{#if_eq block.type "training" }}
|
||||
<div class="item flexrow flex-group-left flex-thin-border special-training-box">
|
||||
<div class="flexrow">
|
||||
<label class="{{if_gt block.targetNumber 0 'rollable' 'not-rollable'}} skill-label {{block.rollTargetDisplayClass}}"
|
||||
data-action="roll"
|
||||
data-key="{{block.key}}"
|
||||
data-name="{{block.name}}"
|
||||
data-rolltype="special-training"
|
||||
{{#if block.tooltip }}
|
||||
{{#unless (eq ../../skillTooltipDisplay "never") }}
|
||||
data-tooltip="{{block.tooltip}}"
|
||||
{{/unless }}
|
||||
{{/if }}>
|
||||
{{block.displayLabel}}
|
||||
</label>
|
||||
<div class="item-controls skill-and-training-controls custom-skill-edit-controls">
|
||||
<a class="item-control"
|
||||
data-action="specialTrainingAction"
|
||||
data-action-type="edit"
|
||||
data-id="{{block.id}}"
|
||||
{{#unless (eq ../../skillTooltipDisplay "never") }}
|
||||
title="Edit Training"
|
||||
{{/unless }}><i class="fas fa-edit"></i></a>
|
||||
<a class="item-control"
|
||||
data-action="specialTrainingAction"
|
||||
data-action-type="delete"
|
||||
{{#unless (eq ../../skillTooltipDisplay "never") }}
|
||||
title="Delete Training"
|
||||
{{/unless }}
|
||||
data-id="{{block.id}}"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if_eq}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="agent-skills-columns grid grid-3col special-training-columns">
|
||||
{{#each specialTrainingColumns as |column| }}
|
||||
<div class="agent-skills-column">
|
||||
{{#each column as |block| }}
|
||||
{{> "systems/deltagreen/templates/actor/partials/agent-special-training-row-partial.html" block=block }}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="tab {{leftBarRestSanityTabs.sanity.cssClass}}"
|
||||
data-group="leftBarRestSanity"
|
||||
data-tab="sanity">
|
||||
<div class="breaking-point-inline mb-8">
|
||||
<div class="breaking-point-inline">
|
||||
<label class="bolded-label breaking-point-label"
|
||||
title="{{localize 'DG.Mental.Tooltip.BreakingPoint'}}">
|
||||
{{localize 'DG.Mental.BreakingPoint'}}
|
||||
@@ -17,9 +17,9 @@
|
||||
title="{{localize 'DG.Mental.Reset'}}"></a>
|
||||
</div>
|
||||
|
||||
<div class="horizontal-bar-section-divider">
|
||||
<span>{{localize 'DG.Sheet.BlockHeaders.Adaptations'}}</span>
|
||||
</div>
|
||||
<div class="left-bar-section-divider" role="separator"></div>
|
||||
|
||||
<div class="left-bar-section-heading">{{localize 'DG.Sheet.BlockHeaders.Adaptations'}}</div>
|
||||
|
||||
<div class="adaptation-grid-4col">
|
||||
<label data-tooltip="{{localize 'DG.Mental.Tooltip.Violence'}}">
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<a href="https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/blob/master/CHANGELOG.md"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-tooltip="{{localize 'DG.About.Changelogtip'}}">
|
||||
data-tooltip="{{localize 'DG.About.ChangelogTooltip'}}">
|
||||
<i class="fas fa-plus-circle" inert></i>
|
||||
<span>{{localize 'DG.About.Changelog'}}</span>
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="tab skills {{tabs.skills.cssClass}}"
|
||||
data-group="primary"
|
||||
data-tab="skills">
|
||||
<div class="skills-tab-scroll">
|
||||
<div class="horizontal-bar-section-divider skills-header">
|
||||
<span class="skills-header-title">{{localize "DG.Sheet.BlockHeaders.SkillsAndTraining"}}</span>
|
||||
{{#if (ne actor.type "agent") }}
|
||||
@@ -15,37 +16,52 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3col">
|
||||
{{#each actor.system.sortedSkills as |skill key| }}
|
||||
<div class="flexrow flex-group-left flex-thin-border">
|
||||
<label class="{{if_gt skill.proficiency 0 'rollable' 'not-rollable'}} skill-label"
|
||||
data-action="roll"
|
||||
data-key="{{skill.key}}"
|
||||
data-rolltype="skill"
|
||||
for="system.skills.{{skill.key}}.proficiency"
|
||||
data-tooltip="{{ skill.tooltip }}">
|
||||
{{localizeWithFallback (concat "DG.Skills." skill.key) skill.label }}
|
||||
</label>
|
||||
|
||||
<input class="percentile-skill-input {{skill.rollTargetDisplayClass}}"
|
||||
type="text"
|
||||
name="system.skills.{{skill.key}}.proficiency"
|
||||
value="{{skill.proficiency}}"
|
||||
data-dtype="Number"
|
||||
{{disabled skill.isCalculatedValue}} />
|
||||
{{#if (eq ../actor.type "agent") }}
|
||||
<input class="checkbox-skill-input"
|
||||
type="checkbox"
|
||||
name="system.skills.{{skill.key}}.failure"
|
||||
data-tooltip="{{localize 'DG.Tooltip.SkillFailCheckbox' }}"
|
||||
{{checked skill.failure}}
|
||||
{{disabled skill.cannotBeImprovedByFailure }} />
|
||||
{{/if}}
|
||||
<div class="agent-skills-columns grid grid-3col">
|
||||
{{#each skillColumns as |column| }}
|
||||
<div class="agent-skills-column">
|
||||
{{#each column as |skill| }}
|
||||
{{> "systems/deltagreen/templates/actor/partials/agent-skill-row-partial.html" skill=skill }}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div>{{> "systems/deltagreen/templates/actor/partials/custom-skills-partial-agent.html" }}</div>
|
||||
{{> "systems/deltagreen/templates/actor/partials/custom-skills-partial-agent.html" }}
|
||||
</div>
|
||||
|
||||
<div class="skills-tab-footer">
|
||||
<div class="item-controls skill-util-buttons">
|
||||
<div class="skill-util-buttons-row">
|
||||
<a class="item-control typed-skill-add action-pill"
|
||||
data-action="typedSkillAction"
|
||||
data-action-type="create">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{localize 'DG.Skills.AddTypedOrCustomSkill'}}
|
||||
</a>
|
||||
|
||||
<a class="item-control action-pill"
|
||||
data-action="specialTrainingAction"
|
||||
data-action-type="create">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{localize 'DG.SpecialTraining.Dialog.CreateSpecialTraining'}}
|
||||
</a>
|
||||
|
||||
<a class="item-control action-pill custom-skills-edit-toggle"
|
||||
data-action="toggleCustomSkillsEdit"
|
||||
data-tooltip="{{localize 'DG.Skills.ToggleEditDeleteSkills'}}">
|
||||
<i class="fas fa-edit"></i>
|
||||
{{localize 'DG.Skills.EditDeleteTypedSkills'}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="skill-util-buttons-row skill-util-buttons-row--full">
|
||||
<a class="item-control action-pill" data-action="applySkillImprovements">
|
||||
{{localize 'DG.Skills.ApplySkillImprovements.Title'}}
|
||||
<i class="fas fa-dice"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if showNotesInSkills }}
|
||||
{{> "systems/deltagreen/templates/actor/partials/notes-partial.html"}}
|
||||
|
||||
@@ -4,13 +4,22 @@
|
||||
src="{{item.img}}"
|
||||
data-action="editImage"
|
||||
title="{{item.name}}" />
|
||||
<div class="profession-sheet-name">
|
||||
<div class="profession-sheet-horizontal-grid-2col">
|
||||
<span class="resource-label">{{localize 'DG.ItemWindow.Profession.Name'}}:</span>
|
||||
<input type="text"
|
||||
name="name"
|
||||
value="{{item.name}}"
|
||||
placeholder="{{localize 'DG.ItemWindow.Profession.Name'}}"
|
||||
data-dtype="String" />
|
||||
<span class="resource-label">{{localize 'DG.ItemWindow.Profession.Bonds'}}:</span>
|
||||
<input type="number"
|
||||
class="item-sheet-short-input"
|
||||
name="system.bonds"
|
||||
value="{{item.system.bonds}}"
|
||||
min="1"
|
||||
max="5"
|
||||
data-dtype="Number"
|
||||
title="{{localize 'DG.ItemWindow.Profession.BondsTooltip'}}" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -2,74 +2,87 @@
|
||||
data-group="primary"
|
||||
data-tab="skills">
|
||||
<div class="profession-skills-scroll">
|
||||
<div class="profession-skills-section">
|
||||
<div class="profession-section-toolbar">
|
||||
<span class="profession-skills-toolbar-label">{{localize 'DG.ItemWindow.Profession.AutomaticSkills'}}</span>
|
||||
<button type="button"
|
||||
class="profession-skills-toolbar-control"
|
||||
data-action="addAutomaticSkill"
|
||||
title="{{localize 'DG.ItemWindow.Profession.AddSkill'}}">
|
||||
<i class="fas fa-plus"></i> {{localize 'DG.ItemWindow.Profession.AddSkill'}}
|
||||
</button>
|
||||
<span class="profession-skills-toolbar-label">{{localize 'DG.ItemWindow.Profession.Bonds'}}</span>
|
||||
<input type="number"
|
||||
class="profession-skills-toolbar-control profession-bonds-input"
|
||||
name="system.bonds"
|
||||
value="{{item.system.bonds}}"
|
||||
min="1"
|
||||
max="5"
|
||||
data-dtype="Number"
|
||||
title="{{localize 'DG.ItemWindow.Profession.BondsTooltip'}}" />
|
||||
</div>
|
||||
<ol class="profession-skill-list">
|
||||
{{#each automaticSkillRows}}
|
||||
<li class="profession-skill-row">
|
||||
<span class="profession-skill-label">{{label}}</span>
|
||||
<span class="profession-skill-rating">{{rating}}%</span>
|
||||
<button type="button"
|
||||
class="profession-skill-remove"
|
||||
data-action="removeAutomaticSkill"
|
||||
data-skill-key="{{key}}"
|
||||
title="{{localize 'DG.ItemWindow.Profession.RemoveSkill'}}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
<section class="profession-skills-section">
|
||||
<header class="profession-section-header">
|
||||
<h4 class="profession-section-title">{{localize 'DG.ItemWindow.Profession.AutomaticSkills'}}</h4>
|
||||
<button type="button"
|
||||
class="profession-section-add"
|
||||
data-action="addAutomaticSkill"
|
||||
title="{{localize 'DG.ItemWindow.Profession.AddSkill'}}">
|
||||
<i class="fas fa-plus"></i> {{localize 'DG.ItemWindow.Profession.AddSkill'}}
|
||||
</button>
|
||||
</header>
|
||||
<div class="profession-skill-list-columns-header">
|
||||
<div class="profession-skill-col-header">
|
||||
<span>{{localize 'DG.ItemWindow.Profession.Skill'}}</span>
|
||||
<span>{{localize 'DG.ItemWindow.Profession.Rating'}}</span>
|
||||
</div>
|
||||
<div class="profession-skill-col-header">
|
||||
<span>{{localize 'DG.ItemWindow.Profession.Skill'}}</span>
|
||||
<span>{{localize 'DG.ItemWindow.Profession.Rating'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="profession-skill-list">
|
||||
{{#each automaticSkillRows}}
|
||||
<li class="profession-skill-row">
|
||||
<span class="profession-skill-label">{{label}}</span>
|
||||
<span class="profession-skill-rating">{{rating}}%</span>
|
||||
<button type="button"
|
||||
class="profession-skill-remove"
|
||||
data-action="removeAutomaticSkill"
|
||||
data-skill-key="{{key}}"
|
||||
title="{{localize 'DG.ItemWindow.Profession.RemoveSkill'}}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<div class="profession-skills-section">
|
||||
<div class="profession-section-toolbar">
|
||||
<span class="profession-skills-toolbar-label">{{localize 'DG.ItemWindow.Profession.OptionSkills'}}</span>
|
||||
<button type="button"
|
||||
class="profession-skills-toolbar-control"
|
||||
data-action="addOptionSkill"
|
||||
title="{{localize 'DG.ItemWindow.Profession.AddSkill'}}">
|
||||
<i class="fas fa-plus"></i> {{localize 'DG.ItemWindow.Profession.AddSkill'}}
|
||||
</button>
|
||||
<span class="profession-skills-toolbar-label">{{localize 'DG.ItemWindow.Profession.OptionPicks'}}</span>
|
||||
<input type="number"
|
||||
class="profession-skills-toolbar-control profession-option-picks-input"
|
||||
name="system.optionSkills.optionPicks"
|
||||
value="{{optionPicks}}"
|
||||
min="0"
|
||||
data-dtype="Number" />
|
||||
</div>
|
||||
<ol class="profession-skill-list">
|
||||
{{#each optionSkillRows}}
|
||||
<li class="profession-skill-row">
|
||||
<span class="profession-skill-label">{{label}}</span>
|
||||
<span class="profession-skill-rating">{{rating}}%</span>
|
||||
<button type="button"
|
||||
class="profession-skill-remove"
|
||||
data-action="removeOptionSkill"
|
||||
data-skill-key="{{key}}"
|
||||
title="{{localize 'DG.ItemWindow.Profession.RemoveSkill'}}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
<section class="profession-skills-section">
|
||||
<header class="profession-section-header">
|
||||
<h4 class="profession-section-title">{{localize 'DG.ItemWindow.Profession.OptionSkills'}}</h4>
|
||||
<button type="button"
|
||||
class="profession-section-add"
|
||||
data-action="addOptionSkill"
|
||||
title="{{localize 'DG.ItemWindow.Profession.AddSkill'}}">
|
||||
<i class="fas fa-plus"></i> {{localize 'DG.ItemWindow.Profession.AddSkill'}}
|
||||
</button>
|
||||
</header>
|
||||
<div class="profession-section-config">
|
||||
<span class="resource-label">{{localize 'DG.ItemWindow.Profession.OptionPicks'}}:</span>
|
||||
<input type="number"
|
||||
class="item-sheet-short-input profession-option-picks-input"
|
||||
name="system.optionSkills.optionPicks"
|
||||
value="{{optionPicks}}"
|
||||
min="0"
|
||||
data-dtype="Number" />
|
||||
</div>
|
||||
<div class="profession-skill-list-columns-header">
|
||||
<div class="profession-skill-col-header">
|
||||
<span>{{localize 'DG.ItemWindow.Profession.Skill'}}</span>
|
||||
<span>{{localize 'DG.ItemWindow.Profession.Rating'}}</span>
|
||||
</div>
|
||||
<div class="profession-skill-col-header">
|
||||
<span>{{localize 'DG.ItemWindow.Profession.Skill'}}</span>
|
||||
<span>{{localize 'DG.ItemWindow.Profession.Rating'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="profession-skill-list">
|
||||
{{#each optionSkillRows}}
|
||||
<li class="profession-skill-row">
|
||||
<span class="profession-skill-label">{{label}}</span>
|
||||
<span class="profession-skill-rating">{{rating}}%</span>
|
||||
<button type="button"
|
||||
class="profession-skill-remove"
|
||||
data-action="removeOptionSkill"
|
||||
data-skill-key="{{key}}"
|
||||
title="{{localize 'DG.ItemWindow.Profession.RemoveSkill'}}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user