Large scale refactor to bring system in line with modern Foundry system guidelines and improve maintainability.

This commit is contained in:
Aaron Lutomski
2026-06-21 13:44:54 -07:00
parent 57f794cbac
commit 4d556b3b3b
64 changed files with 2985 additions and 3818 deletions
+6
View File
@@ -17,11 +17,17 @@
- [#374](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/374) - Moving Age to Bio tab
- [#393](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/393) - Updated to support Foundry v14
- [#223](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/223) - Migrated item sheets to Foundry ApplicationV2 (`ItemSheetV2`)
key
### **Bug Fixes:**
- [#392](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/392) - Fixed sheet rolls
- [#394](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/394) - Unified actor and item description fields on schema with shared ProseMirror preparation
### **Under the Hood:**
- [#323](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/323) - Refactored roll logic and split into util functions
- Agent sheet refactor: modular actor sheet mixins, roll module split, skill improvement roll helpers, and consolidated utilities under `module/utils/`
- Removed obsolete world settings
## Version 1.6.6 - 2026-04-15
### **Bug Fixes:**
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+1 -19
View File
@@ -900,11 +900,6 @@ div.horizontal-bar-section-header {
min-height: 0;
}
div.motivation-editor-container div.editor {
height: 400px;
min-height: 100% !important;
}
.deltagreen .editor-host {
align-items: stretch;
display: flex;
@@ -933,19 +928,6 @@ div.motivation-editor-container div.editor {
min-height: 0;
}
/* Description fonts follow the selected sheet theme (see .cowboy-style / .program-style). */
.cowboy-style .editor-host prose-mirror .editor-content,
.cowboy-style .editor-host prose-mirror .editor-content .ProseMirror {
font-family: "SpecialElite", sans-serif !important;
}
.outlaw-style .editor-host prose-mirror .editor-content,
.outlaw-style .editor-host prose-mirror .editor-content .ProseMirror,
.program-style .editor-host prose-mirror .editor-content,
.program-style .editor-host prose-mirror .editor-content .ProseMirror {
font-family: "PublicSans", sans-serif !important;
}
/* Actor sheets: compact description when not editing. */
.deltagreen:not(.item) .editor-host prose-mirror:not(.active) .editor-content {
position: relative;
@@ -1509,7 +1491,7 @@ a[data-action="applySkillImprovements"] {
text-decoration: line-through;
}
.actor-sheet-v2 {
.agent-sheet {
.window-content {
display: grid;
grid-template-columns: 220px 1fr;
+2
View File
@@ -278,6 +278,8 @@
"TYPES.Item.motivation": "Motivació",
"TYPES.Item.bond": "Vincle",
"TYPES.Item.gear": "Equip",
"TYPES.Item.tome": "Tome",
"TYPES.Item.ritual": "Ritual",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "Activation SAN Damage",
"DG.ItemWindow.Ritual.ActivationTime": "Activation Time",
+2
View File
@@ -296,6 +296,8 @@
"TYPES.Item.motivation": "Motivation",
"TYPES.Item.bond": "Bindung",
"TYPES.Item.gear": "Ausrüstung",
"TYPES.Item.tome": "Buch",
"TYPES.Item.ritual": "Ritual",
"DG.ItemWindow.Ritual.ActivationSanityDamage": "Activation SAN Damage",
"DG.ItemWindow.Ritual.ActivationTime": "Activation Time",
-1
View File
@@ -355,7 +355,6 @@
"DG.Sheet.class.item": "Item Sheet",
"DG.Sheet.class.agent": "Agent Sheet",
"DG.Sheet.class.agentV2": "Agent Sheet V2",
"DG.Sheet.class.npc": "NPC Sheet",
"DG.Sheet.class.unnatural": "Unnatural Sheet",
"DG.Sheet.class.vehicle": "Vehicle Sheet",
-290
View File
@@ -3,296 +3,6 @@
* @extends {Actor}
*/
export default class DeltaGreenActor extends Actor {
/**
* Augment the basic actor data with additional dynamic data.
*/
prepareData() {
super.prepareData();
const actorData = this;
// console.log('actor.js prepareData');
// console.log(this);
// Make separate methods for each Actor type (character, npc, etc.) to keep
// things organized.
if (actorData.type === "agent") {
this._prepareAgentData(this);
} else if (actorData.type === "unnatural") {
this._prepareUnnaturalData(this);
} else if (actorData.type === "npc") {
this._prepareNpcData(this);
} else if (actorData.type === "vehicle") {
this._prepareVehicleData(this);
}
}
/**
*
* @param {*} agent
*/
_prepareVehicleData(actor) {
const actorData = actor;
// calculate total armor rating
let protection = 0;
for (const i of actor.items) {
if (i.type === "armor") {
if (i.system.equipped === true) {
protection += i.system.protection;
}
}
}
actorData.system.health.protection = protection;
// console.log(actor);
}
_prepareNpcData(actor) {
const { system } = actor;
// Loop through ability scores, and add their modifiers to our sheet output.
for (const statistic of Object.values(system.statistics)) {
// the x5 is just whatever the raw statistic is x 5 to turn it into a d100 percentile
statistic.x5 = statistic.value * 5;
}
// initialize sanity, don't set these afterwards, as they need to be manually edited
if (system.sanity.value >= 100) {
system.sanity.value = system.statistics.pow.x5;
system.sanity.currentBreakingPoint =
system.sanity.value - system.statistics.pow.value;
}
system.sanity.max = 99 - system.skills.unnatural.proficiency;
system.wp.max = system.statistics.pow.value;
try {
system.health.max = Math.ceil(
(system.statistics.con.value + system.statistics.str.value) / 2,
);
} catch {
system.health.max = 10;
}
const strength = system.statistics.str;
system.statistics.str.meleeDamageBonusFormula =
this._calculateMeleeDamageBonusFormula(strength.value);
try {
delete system.skills.ritual; // try to remove legacy skill for ritual if it exists
} catch {
// do nothing
}
system.sanity.ritual = 99 - system.sanity.value;
if (system.sanity.ritual > 99) {
system.sanity.ritual = 99;
} else if (system.sanity.ritual < 1) {
system.sanity.ritual = 1;
}
// calculate total armor rating
let protection = 0;
for (const i of actor.items) {
if (i.type === "armor") {
if (i.system.equipped === true) {
protection += i.system.protection;
}
}
}
system.health.protection = protection;
for (const skill of Object.values(system.skills)) {
skill.targetProficiency = skill.proficiency;
}
}
/**
*
* @param {*} agent
*/
_prepareUnnaturalData(actor) {
const { system } = actor;
// Loop through ability scores, and add their modifiers to our sheet output.
for (const statistic of Object.values(system.statistics)) {
// the x5 is just whatever the raw statistic is x 5 to turn it into a d100 percentile
statistic.x5 = statistic.value * 5;
}
if (system.wp.maxNeedsUpdate) {
system.wp.max = system.statistics.pow.value;
}
if (system.health.maxNeedsUpdate) {
try {
system.health.max = Math.ceil(
(system.statistics.con.value + system.statistics.str.value) / 2,
);
} catch {
system.health.max = 10;
}
}
// calculate total armor rating
let protection = 0;
for (const i of actor.items) {
if (i.type === "armor") {
if (i.system.equipped === true) {
protection += i.system.protection;
}
}
}
system.health.protection = protection;
for (const skill of Object.values(system.skills)) {
skill.targetProficiency = skill.proficiency;
}
}
/**
* Prepare Agent type specific data
*/
_prepareAgentData(agent) {
const { system } = agent;
// Make modifications to data here. For example:
// Loop through ability scores, and add their modifiers to our sheet output.
for (const statistic of Object.values(system.statistics)) {
// the x5 is just whatever the raw statistic is x 5 to turn it into a d100 percentile
statistic.x5 = statistic.value * 5;
}
try {
delete system.skills.ritual; // try to remove legacy skill for ritual if it exists
} catch {
// do nothing
}
system.sanity.ritual = 99 - system.sanity.value;
if (system.sanity.ritual > 99) {
system.sanity.ritual = 99;
} else if (system.sanity.ritual < 1) {
system.sanity.ritual = 1;
}
// The unnatural skill is sort of special
// It cannot be improved via failure, so add in a special property to reflect this
// Mostly to make it easy to deactivate the failure checkbox in the GUI
for (const [key, skill] of Object.entries(system.skills)) {
if (key === "unnatural") {
skill.cannotBeImprovedByFailure = true;
} else if (key === "luck") {
skill.cannotBeImprovedByFailure = true;
} else if (key === "ritual") {
skill.cannotBeImprovedByFailure = true;
} else {
skill.cannotBeImprovedByFailure = false;
}
// For ritual skill, it's calculated, so add some logic to turn off changing that entirely.
if (key === "ritual") {
skill.isCalculatedValue = true;
} else {
skill.isCalculatedValue = false;
}
// Add field for calculation
skill.targetProficiency = skill.proficiency;
}
system.wp.max = system.statistics.pow.value;
system.health.max = Math.ceil(
(system.statistics.con.value + system.statistics.str.value) / 2,
);
// initialize sanity, don't set these afterwards, as they need to be manually edited
if (system.sanity.value >= 100) {
system.sanity.value = system.statistics.pow.x5;
system.sanity.currentBreakingPoint =
system.sanity.value - system.statistics.pow.value;
}
system.sanity.max = 99 - system.skills.unnatural.proficiency;
// Sanity Loss Adaptations Logic
const { adaptations } = system.sanity;
if (
adaptations.violence.incident1 &&
adaptations.violence.incident2 &&
adaptations.violence.incident3
) {
system.sanity.adaptations.violence.isAdapted = true;
} else {
system.sanity.adaptations.violence.isAdapted = false;
}
if (
adaptations.helplessness.incident1 &&
adaptations.helplessness.incident2 &&
adaptations.helplessness.incident3
) {
system.sanity.adaptations.helplessness.isAdapted = true;
} else {
system.sanity.adaptations.helplessness.isAdapted = false;
}
if (system.sanity.value <= system.sanity.currentBreakingPoint) {
system.sanity.breakingPointHit = true;
} else {
system.sanity.breakingPointHit = false;
}
// calculate total armor rating
let protection = 0;
for (const i of agent.items) {
if (i.type === "armor") {
if (i.system.equipped === true) {
protection += i.system.protection;
}
}
}
system.health.protection = protection;
const strength = system.statistics.str;
system.statistics.str.meleeDamageBonusFormula =
this._calculateMeleeDamageBonusFormula(strength.value);
if (system.physical.exhaustedPenalty > 0) {
system.physical.exhaustedPenalty =
-1 * Math.abs(system.physical.exhaustedPenalty);
}
}
// Returns Bonus From Strength in Hand-to-hand Combat (melee/unarmed)
_calculateMeleeDamageBonusFormula(strengthValue) {
if (strengthValue < 5) {
return "-2";
}
if (strengthValue < 9) {
return "-1";
}
if (strengthValue > 12 && strengthValue < 17) {
return "+1";
}
if (strengthValue > 16) {
return "+2";
}
return "";
}
/** @override */
static async create(data, options = {}) {
data.prototypeToken = data.prototypeToken || {};
-1
View File
@@ -47,7 +47,6 @@ const DG = /** @type {const} */ ({
"swim",
"unarmed_combat",
"unnatural",
"ritual",
],
skillImprovementFormulas: {
+50 -22
View File
@@ -1,33 +1,61 @@
/**
* Rich-text field specs for actor sheets (by actor type).
* @type {Record<string, { path: string, key: string }[]>}
* Rich-text field specs keyed by actor sheet part id.
* @type {Record<string, Record<string, { path: string, key: string }[]>>}
*/
export const ACTOR_RICH_TEXT_BY_TYPE = {
agent: [{ path: "physical.description", key: "description" }],
npc: [{ path: "notes", key: "description" }],
unnatural: [{ path: "notes", key: "description" }],
vehicle: [{ path: "description", key: "description" }],
const ACTOR_RICH_TEXT_BY_PART = {
agent: {
rightBar: [{ path: "physical.description", key: "description" }],
},
npc: {
skills: [{ path: "notes", key: "description" }],
},
unnatural: {
skills: [{ path: "notes", key: "description" }],
},
vehicle: {
notes: [{ path: "description", key: "description" }],
},
};
/**
* Rich-text field specs for all item types.
* @type {{ path: string, key: string, types?: string[] }[]}
* Rich-text field specs keyed by item sheet part id.
* @type {Record<string, { path: string, key: string, types?: string[] }[]>}
*/
export const ITEM_RICH_TEXT_FIELDS = [
{ path: "description", key: "description" },
{
path: "handlerNotes",
key: "handlerNotes",
types: ["tome", "ritual"],
},
];
const ITEM_RICH_TEXT_BY_PART = {
description: [{ path: "description", key: "description" }],
handler: [
{
path: "handlerNotes",
key: "handlerNotes",
types: ["tome", "ritual"],
},
],
};
/**
* @param {string} itemType
* @param {"Actor"|"Item"} documentName
* @param {string} partId
* @param {object} [options]
* @param {string} [options.actorType]
* @param {string} [options.itemType]
* @param {boolean} [options.showNotesInSkills]
* @returns {{ path: string, key: string }[]}
*/
export function getItemRichTextFields(itemType) {
return ITEM_RICH_TEXT_FIELDS.filter(
(spec) => !spec.types || spec.types.includes(itemType),
);
export function getRichTextFieldsForPart(
documentName,
partId,
{ actorType, itemType, showNotesInSkills = true } = {},
) {
if (documentName === "Actor") {
if (partId === "skills" && !showNotesInSkills) return [];
return ACTOR_RICH_TEXT_BY_PART[actorType]?.[partId] ?? [];
}
if (documentName === "Item") {
return (ITEM_RICH_TEXT_BY_PART[partId] ?? []).filter(
(spec) => !spec.types || spec.types.includes(itemType),
);
}
return [];
}
+39 -1
View File
@@ -1,6 +1,18 @@
import HumanSkillsActorData from "./base/human-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
import {
calculateHealthMax,
calculateMeleeDamageBonusFormula,
clampSanityRitualValue,
computeEquippedArmorProtection,
initializeSanityIfUnset,
prepareAgentSkillFlags,
prepareBreakingPointHit,
prepareSanityAdaptations,
prepareStatisticsX5,
removeLegacyRitualSkill,
} from "../../utils/derived-actor-data.js";
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
foundry.data.fields;
@@ -12,7 +24,7 @@ export default class AgentData extends CharacterData {
return {
...superData,
...HumanSkillsActorData.defineSchema(),
schemaVersion: new NumberField({ initial: 1.0 }),
schemaVersion: new NumberField({ initial: 2.0 }),
sanity: new SchemaField({
value: new NumberField({ initial: 100 }),
currentBreakingPoint: new NumberField({ initial: 101 }),
@@ -52,4 +64,30 @@ export default class AgentData extends CharacterData {
}),
};
}
/** @inheritdoc */
prepareDerivedData() {
prepareStatisticsX5(this.statistics);
removeLegacyRitualSkill(this);
clampSanityRitualValue(this.sanity);
prepareAgentSkillFlags(this.skills);
this.wp.max = this.statistics.pow.value;
this.health.max = calculateHealthMax(this.statistics);
initializeSanityIfUnset(this.sanity, this.statistics);
this.sanity.max = 99 - this.skills.unnatural.proficiency;
prepareSanityAdaptations(this.sanity.adaptations);
prepareBreakingPointHit(this.sanity);
this.health.protection = computeEquippedArmorProtection(
this.parent.items,
);
this.statistics.str.meleeDamageBonusFormula =
calculateMeleeDamageBonusFormula(this.statistics.str.value);
if (this.physical.exhaustedPenalty > 0) {
this.physical.exhaustedPenalty =
-1 * Math.abs(this.physical.exhaustedPenalty);
}
}
}
+26 -1
View File
@@ -1,6 +1,16 @@
import HumanSkillsActorData from "./base/human-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
import {
calculateHealthMax,
calculateMeleeDamageBonusFormula,
clampSanityRitualValue,
computeEquippedArmorProtection,
initializeSanityIfUnset,
prepareStatisticsX5,
removeLegacyRitualSkill,
setSkillTargetProficiencies,
} from "../../utils/derived-actor-data.js";
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
foundry.data.fields;
@@ -12,7 +22,7 @@ export default class NPCData extends CharacterData {
return {
...superData,
...HumanSkillsActorData.defineSchema(),
schemaVersion: new NumberField({ initial: 1.0 }),
schemaVersion: new NumberField({ initial: 2.0 }),
sanity: new SchemaField({
value: new NumberField({ initial: 100 }),
currentBreakingPoint: new NumberField({ initial: 101 }),
@@ -25,4 +35,19 @@ export default class NPCData extends CharacterData {
showUntrainedSkills: new BooleanField({ initial: true }),
};
}
/** @inheritdoc */
prepareDerivedData() {
prepareStatisticsX5(this.statistics);
initializeSanityIfUnset(this.sanity, this.statistics);
this.sanity.max = 99 - this.skills.unnatural.proficiency;
this.wp.max = this.statistics.pow.value;
this.health.max = calculateHealthMax(this.statistics);
this.statistics.str.meleeDamageBonusFormula =
calculateMeleeDamageBonusFormula(this.statistics.str.value);
removeLegacyRitualSkill(this);
clampSanityRitualValue(this.sanity);
this.health.protection = computeEquippedArmorProtection(this.parent.items);
setSkillTargetProficiencies(this.skills);
}
}
+25 -1
View File
@@ -1,6 +1,13 @@
import UnnaturalSkillsActorData from "./base/unnatural-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
import {
calculateHealthMax,
computeEquippedArmorProtection,
prepareStatisticsX5,
removeLegacyRitualSkill,
setSkillTargetProficiencies,
} from "../../utils/derived-actor-data.js";
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
foundry.data.fields;
@@ -17,10 +24,27 @@ export default class UnnaturalData extends CharacterData {
failedLoss: new StringField({ initial: "1D4" }),
successLoss: new StringField({ initial: "1" }),
}),
schemaVersion: new NumberField({ initial: 1.0 }),
schemaVersion: new NumberField({ initial: 2.0 }),
notes: DGHTMLField(),
shortDescription: new StringField({ initial: "" }),
showUntrainedSkills: new BooleanField({ initial: true }),
};
}
/** @inheritdoc */
prepareDerivedData() {
prepareStatisticsX5(this.statistics);
if (this.wp.maxNeedsUpdate) {
this.wp.max = this.statistics.pow.value;
}
if (this.health.maxNeedsUpdate) {
this.health.max = calculateHealthMax(this.statistics);
}
removeLegacyRitualSkill(this);
this.health.protection = computeEquippedArmorProtection(this.parent.items);
setSkillTargetProficiencies(this.skills);
}
}
+6
View File
@@ -1,5 +1,6 @@
import SheetSettingsActorData from "./base/sheet-settings.js";
import DGHTMLField from "../fields/html-content-field.js";
import { computeEquippedArmorProtection } from "../../utils/derived-actor-data.js";
const { SchemaField, NumberField, StringField, ArrayField, HTMLField } =
foundry.data.fields;
@@ -21,4 +22,9 @@ export default class VehicleData extends foundry.abstract.TypeDataModel {
passengers: new ArrayField(new StringField()),
};
}
/** @inheritdoc */
prepareDerivedData() {
this.health.protection = computeEquippedArmorProtection(this.parent.items);
}
}
+21 -14
View File
@@ -4,23 +4,29 @@ import DeltaGreenActor from "./actor/actor.js";
import DGAgentSheet from "./sheets/agent-sheet.js";
import DeltaGreenItem from "./item/item.js";
import DGItemSheet from "./sheets/base-item-sheet.js";
import * as DGRolls from "./roll/roll.js";
import {
DGRoll,
DGPercentileRoll,
DGLethalityRoll,
DGDamageRoll,
DGSanityDamageRoll,
} from "./roll/roll-classes.js";
import registerSystemSettings from "./settings.js";
import preloadHandlebarsTemplates from "./templates.js";
import registerHandlebarsHelpers from "./other/register-helpers.js";
import ParseDeltaGreenStatBlock from "./other/stat-parser-macro.js";
import registerHandlebarsHelpers from "./utils/register-helpers.js";
import ParseDeltaGreenStatBlock from "./macros/stat-parser-macro.js";
import {
createDeltaGreenMacro,
rollItemMacro,
rollItemSkillCheckMacro,
rollSkillMacro,
rollSkillTestAndDamageForOwnedItem,
} from "./other/macro-functions.js";
import { handleInlineActions } from "./other/inline.js";
} from "./macros/macro-functions.js";
import { handleInlineActions } from "./chat/inline.js";
import { runWorldMigration } from "./utils/world-migration.js";
import DGNPCSheet from "./sheets/npc-sheet.js";
import DGUnnaturalSheet from "./sheets/unnatural-sheet.js";
import DGVehicleSheet from "./sheets/vehicle-sheet.js";
import DGAgentSheetV2 from "./sheets/agent-sheet-v2.js";
import AgentData from "./data/actor/agent.js";
import UnnaturalData from "./data/actor/unnatural.js";
import NPCData from "./data/actor/npc.js";
@@ -74,7 +80,13 @@ Hooks.once("init", async () => {
};
// Register custom dice rolls
Object.values(DGRolls).forEach((cls) => CONFIG.Dice.rolls.push(cls));
[
DGRoll,
DGPercentileRoll,
DGLethalityRoll,
DGDamageRoll,
DGSanityDamageRoll,
].forEach((cls) => CONFIG.Dice.rolls.push(cls));
// Register System Settings
registerSystemSettings();
@@ -103,13 +115,6 @@ Hooks.once("init", async () => {
});
});
Actors.registerSheet(DG.ID, DGAgentSheetV2, {
makeDefault: false,
themes: null,
label: "DG.Sheet.class.agentV2",
types: ["agent"],
});
// Register item sheet.
Items.registerSheet(DG.ID, DGItemSheet, {
makeDefault: true,
@@ -125,6 +130,8 @@ Hooks.once("init", async () => {
});
Hooks.once("ready", async () => {
await runWorldMigration();
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
// TODO: Fix eslint issue on next line
// eslint-disable-next-line consistent-return
-1
View File
@@ -1 +0,0 @@
export { default } from "../sheets/base-item-sheet.js";
-5
View File
@@ -10,11 +10,6 @@ export default class DeltaGreenItem extends Item {
*/
prepareData() {
super.prepareData();
// Get the Item's data
const itemData = this;
const actorData = this.actor || {};
const { system } = itemData;
}
/**
-264
View File
@@ -1,264 +0,0 @@
import DGUtils from "./utility-functions.js";
export default function registerHandlebarsHelpers() {
// Add Handlebars helpers
Handlebars.registerHelper("localizeWithFallback", (value, fallbackValue) => {
return DGUtils.localizeWithFallback(value, fallbackValue);
});
Handlebars.registerHelper("concat", (...args) => {
let outStr = "";
for (const arg in args) {
if (typeof args[arg] !== "object") {
outStr += args[arg];
}
}
return outStr;
});
Handlebars.registerHelper("toLowerCase", (str) => {
try {
return str.toLowerCase();
} catch (error) {
return "";
}
});
Handlebars.registerHelper("toUpperCase", (str) => {
try {
return str.toUpperCase();
} catch (error) {
return "";
}
});
Handlebars.registerHelper("if_eq", (a, b, opts) => {
if (a === b) {
return opts.fn(this);
}
return opts.inverse(this);
});
Handlebars.registerHelper("if_not_eq", (a, b, opts) => {
if (a !== b) {
return opts.fn(this);
}
return opts.inverse(this);
});
Handlebars.registerHelper("if_gt", (a, b, trueVal, falseVal) => {
if (a > b) {
return trueVal;
}
return falseVal;
});
Handlebars.registerHelper("cite_ahb", (page) => {
return `See page ${page} of the Agent's Handbook.`;
});
Handlebars.registerHelper("formatLethality", (lethality) => {
if (lethality > 0) {
return `${lethality.toString()}%`;
}
return "";
});
// Is this used anywhere?
Handlebars.registerHelper(
"getActorSkillProp",
(actorData, skillName, prop) => {
try {
if (skillName !== "" && prop !== "") {
const { skills } = actorData.data;
const skill = skills[skillName];
const propVal = skill[prop];
return propVal;
}
return "";
} catch (ex) {
console.log(ex);
return "";
}
},
);
/** Roll visibility modes for dialog selects (v14+ CONFIG.ChatMessage.modes keys). */
const ROLL_MESSAGE_MODE_KEYS = ["public", "gm", "blind", "self"];
Handlebars.registerHelper("getAvailableRollModes", () => {
try {
return Object.fromEntries(
ROLL_MESSAGE_MODE_KEYS.filter((key) => key in CONFIG.ChatMessage.modes).map(
(key) => [key, CONFIG.ChatMessage.modes[key]],
),
);
} catch (error) {
return console.log(error);
}
});
Handlebars.registerHelper("getDefaultRollMode", () => {
try {
return game.settings.get("core", "messageMode");
} catch (error) {
return console.log(error);
}
});
Handlebars.registerHelper(
"calculateHandToHandCombatDamageFormulaBonus",
(strength) => {
try {
let bonus = "";
if (strength.value < 5) {
bonus = " - 2";
} else if (strength.value < 9) {
bonus = " - 1";
} else if (strength.value > 12 && strength < 17) {
bonus = " + 1";
} else if (strength.value > 16) {
bonus = " + 2";
}
return bonus;
} catch (error) {
return console.log(error);
}
},
);
Handlebars.registerHelper("localizeWeaponSkill", (skill) => {
let label = skill;
try {
if (skill === "dex") {
label = game.i18n.localize("DG.Attributes.dex");
}
if (skill === "DG.Skills.custom") {
label = game.i18n.localize("DG.ItemWindow.Custom");
} else {
label = game.i18n.localize(`DG.Skills.${skill}`);
}
} catch (error) {
console.log(error);
}
return label;
});
Handlebars.registerHelper(
"hideSkillBasedOnProficiencyAndUserChoice",
(hideUntrainedSkills, proficiency) => {
let showValue = true;
try {
if (hideUntrainedSkills === false) {
showValue = true;
} else if (proficiency > 0) {
showValue = true;
} else {
showValue = false;
}
} catch (error) {
console.log(error);
}
return showValue;
},
);
// looks at system setting for what font to use and returns the class that is then used in the handlebars template that
// generates the character sheet.
Handlebars.registerHelper("getFontFamilySystemSettingClass", () => {
const setting = game.settings.get("deltagreen", "characterSheetFont");
const characterSheetStyle = game.settings.get(
"deltagreen",
"characterSheetStyle",
);
/*
if(setting === "TypeWriterCondensed"){
return "typewriter-condensed-font";
}
else if(setting === "Martel"){
return "martel-font";
}
else if(setting === "Signika"){
return "signika-font";
}
else if(setting === "atwriter"){
return "atwriter-font";
}
else if(setting === "SpecialElite"){
return "special-elite-font";
}
else if(setting === "PublicSans"){
return "public-sans-font";
}
else{
return "martel-font";
}
*/
/*
if(characterSheetStyle === "cowboy"){
return "special-elite-font";
}
else{
return "public-sans-font";
}
*/
});
Handlebars.registerHelper("getCharacterSheetStyle", () => {
const characterSheetStyle = game.settings.get(
"deltagreen",
"characterSheetStyle",
);
if (characterSheetStyle === "cowboy") {
return "cowboy-style";
}
if (characterSheetStyle === "outlaw") {
return "outlaw-style";
}
return "program-style";
});
Handlebars.registerHelper("keepSanityPrivate", () => {
let setting = false;
try {
setting = game.settings.get("deltagreen", "keepSanityPrivate");
if (game.user.isGM) {
setting = false;
}
} catch (ex) {
setting = false;
}
return setting;
});
Handlebars.registerHelper("playerHasGamemasterPrivileges", () => {
return game.user.isGM;
});
Handlebars.registerHelper("showImpossibleLandscapesContent", () => {
let result = false;
const setting = game.settings.get(
"deltagreen",
"showImpossibleLandscapesContent",
);
if (game.user.isGM === true && setting === true) {
result = true;
}
return result;
});
}
+669
View File
@@ -0,0 +1,669 @@
/* eslint-disable max-classes-per-file */
import DGUtils from "../utils/utility-functions.js";
import DG from "../config.js";
import {
showDamageRollModifyDialog,
showPercentileRollModifyDialog,
} from "./roll-dialogs.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;
}
/**
* Simple function that actually creates the message and sends it to chat.
* We override this to have a little more control over certain aspects of the message,
* right now, its `speaker` and `messageMode`.
*
* @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 } = {}) {
// eslint-disable-next-line no-param-reassign
messageData.speaker = ChatMessage.getSpeaker({ actor: this.actor });
const mode =
messageMode ??
this.options.messageMode ??
this.options.rollMode;
const opts = { create };
if (mode) {
opts.messageMode = foundry.dice.Roll._mapLegacyRollMode(mode);
}
return super.toMessage(messageData, opts);
}
}
export class DGPercentileRoll extends DGRoll {
/**
* Creates D100 rolls, the base die of the system.
*
* This constructor embeds the following info into the roll:
* 1. Target number that the roll needs to beat.
* 2. Localized name for the roll.
*
* Note: In order for all of our custom data to persist, our constructor must use the same parameters as its parent class.
* So, even though percentile rolls will always have a formula of "1d100" and we don't use the `data` object,
* we still have to keep them as parameters.
*
* @param {string} formula Unused - The string formula to parse (from Foundry) - Always "1d100" for percentile rolls.
* @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, 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.
* @param {DeltaGreenItem} [options.specialTrainingName] Optional - Special training rolls have names that are different from the roll key.
*/
// eslint-disable-next-line default-param-last, no-unused-vars
constructor(formula = "1D100", data = {}, options) {
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,
);
this.target = target;
this.localizedKey = localizedKey;
this.skillPath = skillPath;
// Set roll info for other Percentile rolls
switch (this.type) {
case "special-training":
this.specialTrainingName = options.specialTrainingName;
this.localizedKey = `${this.specialTrainingName} - (${this.localizedKey})`;
break;
case "weapon":
// If this weapon uses a custom target for rolls, we set that explicitly.
if (this.key === "custom") {
this.target = this.item.system.customSkillTarget;
this.localizedKey = game.i18n.localize("DG.ItemWindow.Custom");
}
// Add a the weapon's internal modifier.
this.modifier += this.item.system.skillModifier;
break;
case "sanity":
this.target = this.actor.system.sanity.value;
this.localizedKey = game.i18n.localize("DG.Attributes.SAN");
break;
case "luck":
this.target = 50;
this.localizedKey = game.i18n.localize("DG.Luck");
break;
default:
break;
}
}
/**
* Shows a dialog that can modify the roll.
*
* @returns {Promise<Object|void>} - the results of the dialog.
*/
async showDialog() {
const privateSanSetting = game.settings.get(
"deltagreen",
"keepSanityPrivate",
);
let hideSanTarget = false;
if (
privateSanSetting &&
(this.type === "sanity" || this.key === "ritual") &&
!game.user.isGM
) {
hideSanTarget = true;
}
let customModifierTarget = 20;
if (this.actor != null) {
try {
customModifierTarget = parseInt(
this.actor.system.settings.rolling.defaultPercentileModifier,
);
} catch {
// do nothing
}
}
return showPercentileRollModifyDialog({
label: this.localizedKey,
target: this.target,
hideTarget: hideSanTarget,
defaultModifier: customModifierTarget,
actor: this.actor,
});
}
/**
* 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.
*
* @returns {Promise<ChatMessage>} - the created chat message.
*/
async toChat() {
// if using private san rolls, must hide any SAN roll unless user is a GM
const privateSanSetting = game.settings.get(
"deltagreen",
"keepSanityPrivate",
);
if (
privateSanSetting &&
(this.type === "sanity" || this.key === "ritual") &&
!game.user.isGM
) {
this.options.messageMode = "blind";
}
const diceSoNice =
game.modules.has("dice-so-nice") &&
game.modules.get("dice-so-nice").active;
const label = this.createLabel();
let resultString = "";
let styleOverride = "";
if (this.isSuccess) {
if (this.isCritical) {
resultString = `${game.i18n.localize("DG.Roll.CriticalSuccess")}`;
resultString = `${resultString.toUpperCase()}`;
styleOverride = "color: green";
} else {
resultString = `${game.i18n.localize("DG.Roll.Success")}`;
}
} else if (this.isCritical) {
resultString = `${game.i18n.localize("DG.Roll.CriticalFailure")}`;
resultString = `${resultString.toUpperCase()}`;
styleOverride = "color: red";
} else {
resultString = `${game.i18n.localize("DG.Roll.Failure")}`;
}
const failureMark =
this.actor?.type === "agent" &&
!this.isSuccess &&
this.skillPath &&
this.key !== "unnatural" &&
!foundry.utils.getProperty(this.actor, `${this.skillPath}.failure`) &&
game.settings.get(DG.ID, "skillFailure");
const html = await renderTemplate(
"systems/deltagreen/templates/roll/percentile-roll.hbs",
{
styleOverride,
resultString,
formula: this.formula,
total: this.total,
failureMark,
},
);
// TODO: add setting for it?
if (failureMark) {
const keyForUpdate = `${this.skillPath}.failure`;
const message = await this.toMessage({
flags: {
deltagreen: {
rollbacks: {
[keyForUpdate]: false,
},
},
},
content: html,
flavor: label,
});
if (diceSoNice) {
await game.dice3d.waitFor3DAnimationByMessageID(message.id);
}
// TODO: auto-update actor or post icon with manual apply
await this.actor.update({
[keyForUpdate]: true,
});
return message;
}
return this.toMessage({ content: html, flavor: label });
}
/**
* Utility function, called in the DGPercentileRoll constructor.
* If this roll key corresponds to a stat, skill,
* or typedSkill, get pertinent info.
*
* This is used for Stat, Skill, Typed Skill, Weapon, and Special Training Rolls.
*
* @returns {Object} - Contains the roll target and localized version of the key.
*/
getRollInfoFromKey() {
const actorData = this.actor.system;
const skillKeys = Object.keys(actorData.skills);
const typedSkillKeys = Object.keys(actorData.typedSkills);
const statKeys = Object.keys(actorData.statistics);
let target = null;
let localizedKey = null;
let skillPath = null; // For optimization of failure checks
if (statKeys.includes(this.key)) {
target = actorData.statistics[this.key].x5;
localizedKey = game.i18n.localize(`DG.Attributes.${this.key}`);
}
if (skillKeys.includes(this.key)) {
// use calculated target proficiency (effects and etc like aim + 20%)
target =
actorData.skills[this.key].targetProficiency ||
actorData.skills[this.key].proficiency;
localizedKey = game.i18n.localize(`DG.Skills.${this.key}`);
skillPath = `system.skills.${this.key}`;
}
if (typedSkillKeys.includes(this.key)) {
const skill = actorData.typedSkills[this.key];
target = skill.proficiency;
localizedKey = `${skill.group} (${skill.label})`;
skillPath = `system.typedSkills.${this.key}`;
}
if (this.key === "ritual") {
target = actorData.sanity.ritual;
localizedKey = game.i18n.localize(`DG.Skills.ritual`);
}
return { target, localizedKey, skillPath };
}
/**
* Create label based on result of roll
*
* todo: do we want make isInhuman more similar to base label?
*
* @returns {string}
*/
createLabel() {
const startOfLabel = `${game.i18n.localize("DG.Roll.Rolling")} <b>${
this.localizedKey
}`;
const endOfLabel = `${game.i18n.localize("DG.Roll.Target")} ${
this.effectiveTarget
}`;
let label = this.isInhuman
? // "Inhuman" stat being rolled. See function for details.
`${startOfLabel} [${game.i18n
.localize("DG.Roll.Inhuman")
.toUpperCase()}]</b> ${endOfLabel}`
: `${startOfLabel}</b><br> ${endOfLabel}%`;
const { isExhausted, exhaustedCheckPenalty } = this.exhausted;
if (this.modifier || isExhausted) {
label += ` (${this.target}%`;
if (this.modifier) {
label += `${DGUtils.formatStringWithLeadingPlus(this.modifier)}%`;
}
if (isExhausted) {
label += `${DGUtils.formatStringWithLeadingPlus(
exhaustedCheckPenalty,
)}%`;
}
label += `)`;
}
return label;
}
get exhausted() {
let isExhausted = false;
let exhaustedCheckPenalty = -20;
try {
// I suspect (but am not entirely certain) that being tired doesn't make you less lucky)
if (this.type !== "luck") {
isExhausted = this.actor.system.physical.exhausted;
exhaustedCheckPenalty = this.actor.system.physical.exhaustedPenalty;
exhaustedCheckPenalty = -1 * Math.abs(exhaustedCheckPenalty);
}
} catch {
isExhausted = false;
exhaustedCheckPenalty = -20;
}
return { isExhausted, exhaustedCheckPenalty };
}
/**
* "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.
* For an inhuman check, the roll succeeds except on a roll of 100 which fails AND fumbles.
* If the roll is a matching digit roll, it is a critical as normal.
* Also, if the roll is below the regular (non-x5) value of the stat, it is a critical. E.g. a CON of 25, a d100 roll of 21 would be a critical.
*
* @returns {Boolean}
*/
get isInhuman() {
/*
Changing this to only consider the base x5 stat target for whether something is 'inhuman'
because I do not think the intent was an Agent with a high strength getting a +40% bonus to be considered 'inhuman'
and therefore benefit from the increased crit threshold, although could be wrong about this.
*/
if (this.target > 99 && this.type === "stat") {
return true;
}
return false;
}
/**
* Determines if a roll result is critical.
* If roll has not been evaluated, return null.
*
* @returns {null|Boolean}
*/
get isCritical() {
// If roll isn't evaluated, return null.
if (!this.total) {
return null;
}
let isCritical = false;
// 1, 100, or any matching dice are a crit, i.e. 11, 22, 33...99.
if (this.total === 1 || this.total === 100 || this.total % 11 === 0) {
// really good, or reeaaaally bad
isCritical = true;
}
// If inhuman and the roll is below the regular (non-x5) value of the stat, it is a critical.
// E.g. a CON of 25, a d100 roll of 21 would be a critical.
if (this.isInhuman && this.total <= this.target / 5) {
isCritical = true;
}
return isCritical;
}
/**
* Determines if a roll succeeded.
* If roll has not been evaluated, return null.
*
* @returns {null|Boolean}
*/
get isSuccess() {
// If roll isn't evaluated, return null.
if (!this.total) {
return null;
}
// A roll of 100 always (critically) fails, even for inhuman rolls.
if (this.total === 100) return false;
return this.total <= this.effectiveTarget;
}
/**
* Actual target for the roll accounting for modifier if present.
* Floored to 1 if a negative modifier would bring it below 1.
* Capped at 99 unless it is an inhuman stat test.
* Also worth noting, per page 47 of the Agent's Handbook, Exhaustion penalties
* affect not only skill and stat tests, but SAN tests as well...
*
* @returns {null|integer}
*/
get effectiveTarget() {
let target = 1;
const { isExhausted, exhaustedCheckPenalty } = this.exhausted;
if (!this.target || Number.isNaN(this.target)) {
return null;
}
target = parseInt(this.target);
if (isExhausted) {
target += exhaustedCheckPenalty;
}
if (this.modifier && !Number.isNaN(this.modifier)) {
const modifier = parseInt(this.modifier);
target += modifier;
// per agent's handbook (pg.43), a negative modifier can't lower a target below 1%
target = Math.max(target, 1);
}
// an 'inhuman' stat test can exceed 99% as a target, but skill tests otherwise cannot (agents handbook pg.43)
if (!this.isInhuman) {
target = Math.min(target, 99);
}
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 = `${game.i18n.localize("DG.Roll.Rolling")} <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, flavor: 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 = `${game.i18n.localize("DG.Roll.Rolling")} <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 = `Rolling <b>DAMAGE</b> for <b>${label.toUpperCase()}</b>`;
}
return this.toMessage({ content: this.total, flavor: 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 flavor = `Rolling <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, flavor });
}
/**
* 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];
}
}
+177
View File
@@ -0,0 +1,177 @@
import DGUtils from "../utils/utility-functions.js";
const { renderTemplate } = foundry.applications.handlebars;
const { DialogV2 } = foundry.applications.api;
const PERCENTILE_ROLL_DIALOG_TEMPLATE =
"systems/deltagreen/templates/dialog/modify-percentile-roll.html";
const DAMAGE_ROLL_DIALOG_TEMPLATE =
"systems/deltagreen/templates/dialog/modify-damage-roll.html";
/**
* @param {object} options
* @param {string} options.label
* @param {number} options.target
* @param {boolean} [options.hideTarget]
* @param {number} [options.defaultModifier]
* @param {Actor|null} [options.actor]
* @returns {Promise<{ targetModifier: number, messageMode?: string }|void>}
*/
export function showPercentileRollModifyDialog({
label,
target,
hideTarget = false,
defaultModifier = 20,
actor = null,
}) {
const backingData = {
data: {
label,
originalTarget: target,
targetModifier: defaultModifier,
hideTarget,
},
};
return renderTemplate(PERCENTILE_ROLL_DIALOG_TEMPLATE, backingData).then(
(content) =>
new Promise((resolve, reject) => {
const modButtons = [-40, -20, 20, 40].map((mod) => {
const sign = mod > 0 ? "Positive" : "Negative";
return {
action: `roll${Math.abs(mod)}${sign}`,
label: mod > 0 ? `+${mod}` : String(mod),
callback: (event, button, dialog) => {
try {
const messageMode =
dialog.element.querySelector("[name='messageMode']")?.value;
resolve({ targetModifier: mod, messageMode });
} catch (ex) {
reject(console.log(ex));
}
},
};
});
new DialogV2({
content,
window: {
title: DGUtils.localizeWithFallback(
"DG.ModifySkillRollDialogue.Title",
"Modify Roll",
),
},
buttons: [
{
default: true,
action: "roll",
label: DGUtils.localizeWithFallback("DG.Roll.Roll", "Roll"),
callback: (event, button, dialog) => {
try {
let targetModifier = dialog.element.querySelector(
"[name='targetModifier']",
)?.value;
const messageMode =
dialog.element.querySelector("[name='messageMode']")?.value;
const plusMinus = dialog.element.querySelector(
"[name='plusOrMinus']",
)?.value;
if (
targetModifier.trim() !== "" &&
!Number.isNaN(targetModifier)
) {
targetModifier = Math.abs(parseInt(targetModifier, 10));
if (plusMinus === "-") {
targetModifier *= -1;
}
if (actor != null) {
actor.update({
"system.settings.rolling.defaultPercentileModifier":
targetModifier,
});
}
}
resolve({ targetModifier, messageMode });
} catch (ex) {
reject(console.log(ex));
}
},
},
...modButtons,
],
}).render(true);
}),
);
}
/**
* @param {object} options
* @param {string} [options.itemName]
* @param {string} options.formula
* @returns {Promise<{ newFormula: string, messageMode?: string }|void>}
*/
export function showDamageRollModifyDialog({ itemName, formula }) {
const backingData = {
data: {
label: itemName,
originalFormula: formula,
outerModifier: "2 * ",
innerModifier: "+ 0",
},
};
return renderTemplate(DAMAGE_ROLL_DIALOG_TEMPLATE, backingData).then(
(content) =>
new Promise((resolve, reject) => {
new DialogV2({
content,
window: {
title: game.i18n.localize("DG.ModifySkillRollDialogue.Title"),
},
buttons: [
{
default: true,
label: game.i18n.translations.DG.Roll.Roll,
action: "roll",
callback: (event, button, dialog) => {
try {
const outerModifier = dialog.element.querySelector(
"[name='outerModifier']",
)?.value;
let innerModifier = dialog.element.querySelector(
"[name='innerModifier']",
)?.value;
const modifiedBaseRoll = dialog.element.querySelector(
"[name='originalFormula']",
)?.value;
const messageMode = dialog.element.querySelector(
"[name='targetRollMode']",
)?.value;
if (innerModifier.replace(" ", "") === "+0") {
innerModifier = "";
}
let newFormula = "";
if (outerModifier.trim() !== "") {
newFormula += `${outerModifier}(${modifiedBaseRoll}${innerModifier.trim()})`;
} else {
newFormula += modifiedBaseRoll + innerModifier.trim();
}
resolve({ newFormula, messageMode });
} catch (ex) {
reject(console.log(ex));
}
},
},
],
}).render(true);
}),
);
}
+17 -853
View File
@@ -1,855 +1,18 @@
/* eslint-disable max-classes-per-file */
import DGUtils from "../other/utility-functions.js";
import DG from "../config.js";
const { renderTemplate } = foundry.applications.handlebars;
const { DialogV2 } = foundry.applications.api;
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;
}
/**
* Simple function that actually creates the message and sends it to chat.
* We override this to have a little more control over certain aspects of the message,
* right now, its `speaker` and `messageMode`.
*
* @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 } = {}) {
// eslint-disable-next-line no-param-reassign
messageData.speaker = ChatMessage.getSpeaker({ actor: this.actor });
const mode =
messageMode ??
this.options.messageMode ??
this.options.rollMode;
const opts = { create };
if (mode) {
opts.messageMode = foundry.dice.Roll._mapLegacyRollMode(mode);
}
return super.toMessage(messageData, opts);
}
}
export class DGPercentileRoll extends DGRoll {
/**
* Creates D100 rolls, the base die of the system.
*
* This constructor embeds the following info into the roll:
* 1. Target number that the roll needs to beat.
* 2. Localized name for the roll.
*
* Note: In order for all of our custom data to persist, our constructor must use the same parameters as its parent class.
* So, even though percentile rolls will always have a formula of "1d100" and we don't use the `data` object,
* we still have to keep them as parameters.
*
* @param {string} formula Unused - The string formula to parse (from Foundry) - Always "1d100" for percentile rolls.
* @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, 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.
* @param {DeltaGreenItem} [options.specialTrainingName] Optional - Special training rolls have names that are different from the roll key.
*/
// eslint-disable-next-line default-param-last, no-unused-vars
constructor(formula = "1D100", data = {}, options) {
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,
);
this.target = target;
this.localizedKey = localizedKey;
this.skillPath = skillPath;
// Set roll info for other Percentile rolls
switch (this.type) {
case "special-training":
this.specialTrainingName = options.specialTrainingName;
this.localizedKey = `${this.specialTrainingName} - (${this.localizedKey})`;
break;
case "weapon":
// If this weapon uses a custom target for rolls, we set that explicitly.
if (this.key === "custom") {
this.target = this.item.system.customSkillTarget;
this.localizedKey = game.i18n.localize("DG.ItemWindow.Custom");
}
// Add a the weapon's internal modifier.
this.modifier += this.item.system.skillModifier;
break;
case "sanity":
this.target = this.actor.system.sanity.value;
this.localizedKey = game.i18n.localize("DG.Attributes.SAN");
break;
case "luck":
this.target = 50;
this.localizedKey = game.i18n.localize("DG.Luck");
break;
default:
break;
}
}
/**
* Shows a dialog that can modify the roll.
*
* @returns {Promise<Object|void>} - the results of the dialog.
*/
async showDialog() {
const privateSanSetting = game.settings.get(
"deltagreen",
"keepSanityPrivate",
);
let hideSanTarget = false;
if (
privateSanSetting &&
(this.type === "sanity" || this.key === "ritual") &&
!game.user.isGM
) {
hideSanTarget = true;
}
let customModifierTarget = 20;
if (this.actor != null) {
try {
customModifierTarget = parseInt(
this.actor.system.settings.rolling.defaultPercentileModifier,
);
} catch {
// do nothing
}
}
const backingData = {
data: {
label: this.localizedKey,
originalTarget: this.target,
targetModifier: customModifierTarget,
hideTarget: hideSanTarget,
},
};
const template =
"systems/deltagreen/templates/dialog/modify-percentile-roll.html";
const content = await renderTemplate(template, backingData);
return new Promise((resolve, reject) => {
const modButtons = [-40, -20, 20, 40].map((mod) => {
const sign = mod > 0 ? "Positive" : "Negative";
return {
action: `roll${Math.abs(mod)}${sign}`,
label: mod > 0 ? `+${mod}` : String(mod),
callback: (event, button, dialog) => {
try {
const messageMode =
dialog.element.querySelector("[name='rollMode']")?.value;
resolve({ targetModifier: mod, messageMode });
} catch (ex) {
reject(console.log(ex));
}
},
};
});
new DialogV2({
content,
window: {
title: DGUtils.localizeWithFallback(
"DG.ModifySkillRollDialogue.Title",
"Modify Roll",
),
},
buttons: [
{
default: true,
action: "roll",
label: DGUtils.localizeWithFallback("DG.Roll.Roll", "Roll"),
callback: (event, button, dialog) => {
try {
let targetModifier = dialog.element.querySelector(
"[name='targetModifier']",
)?.value; // this is text as a heads up
const messageMode =
dialog.element.querySelector("[name='rollMode']")?.value;
const plusMinus = dialog.element.querySelector(
"[name='plusOrMinus']",
)?.value;
if (
targetModifier.trim() !== "" &&
!Number.isNaN(targetModifier)
) {
targetModifier = Math.abs(parseInt(targetModifier));
if (plusMinus === "-") {
targetModifier *= -1;
}
// update the custom target modifier so it 'persists' between dialogs
if (this.actor != null) {
this.actor.update({
"system.settings.rolling.defaultPercentileModifier":
targetModifier,
});
}
}
resolve({ targetModifier, messageMode });
} catch (ex) {
reject(console.log(ex));
}
},
},
...modButtons,
],
}).render(true);
});
}
/**
* 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.
*
* @returns {Promise<ChatMessage>} - the created chat message.
*/
async toChat() {
// if using private san rolls, must hide any SAN roll unless user is a GM
const privateSanSetting = game.settings.get(
"deltagreen",
"keepSanityPrivate",
);
if (
privateSanSetting &&
(this.type === "sanity" || this.key === "ritual") &&
!game.user.isGM
) {
this.options.messageMode = "blind";
}
const diceSoNice =
game.modules.has("dice-so-nice") &&
game.modules.get("dice-so-nice").active;
const label = this.createLabel();
let resultString = "";
let styleOverride = "";
if (this.isSuccess) {
if (this.isCritical) {
resultString = `${game.i18n.localize("DG.Roll.CriticalSuccess")}`;
resultString = `${resultString.toUpperCase()}`;
styleOverride = "color: green";
} else {
resultString = `${game.i18n.localize("DG.Roll.Success")}`;
}
} else if (this.isCritical) {
resultString = `${game.i18n.localize("DG.Roll.CriticalFailure")}`;
resultString = `${resultString.toUpperCase()}`;
styleOverride = "color: red";
} else {
resultString = `${game.i18n.localize("DG.Roll.Failure")}`;
}
const failureMark =
this.actor?.type === "agent" &&
!this.isSuccess &&
this.skillPath &&
this.key !== "unnatural" &&
!foundry.utils.getProperty(this.actor, `${this.skillPath}.failure`) &&
game.settings.get(DG.ID, "skillFailure");
const html = await renderTemplate(
"systems/deltagreen/templates/roll/percentile-roll.hbs",
{
styleOverride,
resultString,
formula: this.formula,
total: this.total,
failureMark,
},
);
// TODO: add setting for it?
if (failureMark) {
const keyForUpdate = `${this.skillPath}.failure`;
const message = await this.toMessage({
flags: {
deltagreen: {
rollbacks: {
[keyForUpdate]: false,
},
},
},
content: html,
flavor: label,
});
if (diceSoNice) {
await game.dice3d.waitFor3DAnimationByMessageID(message.id);
}
// TODO: auto-update actor or post icon with manual apply
await this.actor.update({
[keyForUpdate]: true,
});
return message;
}
return this.toMessage({ content: html, flavor: label });
}
/**
* Utility function, called in the DGPercentileRoll constructor.
* If this roll key corresponds to a stat, skill,
* or typedSkill, get pertinent info.
*
* This is used for Stat, Skill, Typed Skill, Weapon, and Special Training Rolls.
*
* @returns {Object} - Contains the roll target and localized version of the key.
*/
getRollInfoFromKey() {
const actorData = this.actor.system;
const skillKeys = Object.keys(actorData.skills);
const typedSkillKeys = Object.keys(actorData.typedSkills);
const statKeys = Object.keys(actorData.statistics);
let target = null;
let localizedKey = null;
let skillPath = null; // For optimization of failure checks
if (statKeys.includes(this.key)) {
target = actorData.statistics[this.key].x5;
localizedKey = game.i18n.localize(`DG.Attributes.${this.key}`);
}
if (skillKeys.includes(this.key)) {
// use calculated target proficiency (effects and etc like aim + 20%)
target =
actorData.skills[this.key].targetProficiency ||
actorData.skills[this.key].proficiency;
localizedKey = game.i18n.localize(`DG.Skills.${this.key}`);
skillPath = `system.skills.${this.key}`;
}
if (typedSkillKeys.includes(this.key)) {
const skill = actorData.typedSkills[this.key];
target = skill.proficiency;
localizedKey = `${skill.group} (${skill.label})`;
skillPath = `system.typedSkills.${this.key}`;
}
if (this.key === "ritual") {
target = actorData.sanity.ritual;
localizedKey = game.i18n.localize(`DG.Skills.ritual`);
}
return { target, localizedKey, skillPath };
}
/**
* Create label based on result of roll
*
* todo: do we want make isInhuman more similar to base label?
*
* @returns {string}
*/
createLabel() {
const startOfLabel = `${game.i18n.localize("DG.Roll.Rolling")} <b>${
this.localizedKey
}`;
const endOfLabel = `${game.i18n.localize("DG.Roll.Target")} ${
this.effectiveTarget
}`;
let label = this.isInhuman
? // "Inhuman" stat being rolled. See function for details.
`${startOfLabel} [${game.i18n
.localize("DG.Roll.Inhuman")
.toUpperCase()}]</b> ${endOfLabel}`
: `${startOfLabel}</b><br> ${endOfLabel}%`;
const { isExhausted, exhaustedCheckPenalty } = this.exhausted;
if (this.modifier || isExhausted) {
label += ` (${this.target}%`;
if (this.modifier) {
label += `${DGUtils.formatStringWithLeadingPlus(this.modifier)}%`;
}
if (isExhausted) {
label += `${DGUtils.formatStringWithLeadingPlus(
exhaustedCheckPenalty,
)}%`;
}
label += `)`;
}
return label;
}
get exhausted() {
let isExhausted = false;
let exhaustedCheckPenalty = -20;
try {
// I suspect (but am not entirely certain) that being tired doesn't make you less lucky)
if (this.type !== "luck") {
isExhausted = this.actor.system.physical.exhausted;
exhaustedCheckPenalty = this.actor.system.physical.exhaustedPenalty;
exhaustedCheckPenalty = -1 * Math.abs(exhaustedCheckPenalty);
}
} catch {
isExhausted = false;
exhaustedCheckPenalty = -20;
}
return { isExhausted, exhaustedCheckPenalty };
}
/**
* "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.
* For an inhuman check, the roll succeeds except on a roll of 100 which fails AND fumbles.
* If the roll is a matching digit roll, it is a critical as normal.
* Also, if the roll is below the regular (non-x5) value of the stat, it is a critical. E.g. a CON of 25, a d100 roll of 21 would be a critical.
*
* @returns {Boolean}
*/
get isInhuman() {
/*
Changing this to only consider the base x5 stat target for whether something is 'inhuman'
because I do not think the intent was an Agent with a high strength getting a +40% bonus to be considered 'inhuman'
and therefore benefit from the increased crit threshold, although could be wrong about this.
*/
if (this.target > 99 && this.type === "stat") {
return true;
}
return false;
}
/**
* Determines if a roll result is critical.
* If roll has not been evaluated, return null.
*
* @returns {null|Boolean}
*/
get isCritical() {
// If roll isn't evaluated, return null.
if (!this.total) {
return null;
}
let isCritical = false;
// 1, 100, or any matching dice are a crit, i.e. 11, 22, 33...99.
if (this.total === 1 || this.total === 100 || this.total % 11 === 0) {
// really good, or reeaaaally bad
isCritical = true;
}
// If inhuman and the roll is below the regular (non-x5) value of the stat, it is a critical.
// E.g. a CON of 25, a d100 roll of 21 would be a critical.
if (this.isInhuman && this.total <= this.target / 5) {
isCritical = true;
}
return isCritical;
}
/**
* Determines if a roll succeeded.
* If roll has not been evaluated, return null.
*
* @returns {null|Boolean}
*/
get isSuccess() {
// If roll isn't evaluated, return null.
if (!this.total) {
return null;
}
// A roll of 100 always (critically) fails, even for inhuman rolls.
if (this.total === 100) return false;
return this.total <= this.effectiveTarget;
}
/**
* Actual target for the roll accounting for modifier if present.
* Floored to 1 if a negative modifier would bring it below 1.
* Capped at 99 unless it is an inhuman stat test.
* Also worth noting, per page 47 of the Agent's Handbook, Exhaustion penalties
* affect not only skill and stat tests, but SAN tests as well...
*
* @returns {null|integer}
*/
get effectiveTarget() {
let target = 1;
const { isExhausted, exhaustedCheckPenalty } = this.exhausted;
if (!this.target || Number.isNaN(this.target)) {
return null;
}
target = parseInt(this.target);
if (isExhausted) {
target += exhaustedCheckPenalty;
}
if (this.modifier && !Number.isNaN(this.modifier)) {
const modifier = parseInt(this.modifier);
target += modifier;
// per agent's handbook (pg.43), a negative modifier can't lower a target below 1%
target = Math.max(target, 1);
}
// an 'inhuman' stat test can exceed 99% as a target, but skill tests otherwise cannot (agents handbook pg.43)
if (!this.isInhuman) {
target = Math.min(target, 99);
}
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 = `${game.i18n.localize("DG.Roll.Rolling")} <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)}%)`;
}
let html = "";
html += `<div class="dice-roll" data-action="expandRoll">`;
html += ` <div class="dice-result">`;
html += ` <div style="${styleOverride}" class="dice-formula">${resultString}</div>`;
html += ` <div class="dice-tooltip">`;
html += ` <div class="wrapper">`;
html += ` <section class="tooltip-part">`;
html += ` <div class="dice">`;
html += ` <header class="part-header flexrow">`;
html += ` <span class="part-formula">`;
html += ` d100`;
html += ` </span>`;
html += ` <span class="part-total">`;
html += ` ${this.total}`;
html += ` </span>`;
html += ` </header>`;
html += ` <ol class="dice-rolls">`;
html += ` <li class="roll die d100">${this.total}`;
html += ` </ol>`;
html += ` <hr>`;
html += ` <header class="part-header flexrow">`;
html += ` <span class="part-formula">`;
html += ` 2d10 (d10 + d10)`;
html += ` </span>`;
html += ` <span class="part-total">`;
html += ` ${nonLethalDamage.total}`;
html += ` </span>`;
html += ` </header>`;
html += ` <ol class="dice-rolls">`;
html += ` <li class="roll die d10">${nonLethalDamage.die1}</li>`;
html += ` <li class="roll die d10">${nonLethalDamage.die2}</li>`;
html += ` </ol>`;
html += ` </div>`;
html += ` </section>`;
html += ` </div>`;
html += ` </div>`;
html += ` <h4 class="dice-total">${this.total} (${
nonLethalDamage.total
} ${game.i18n.localize("DG.Roll.Damage")})</h4>`;
html += ` </div>`;
html += `</div>`;
return this.toMessage({ content: html, flavor: 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 = `${game.i18n.localize("DG.Roll.Rolling")} <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 = `Rolling <b>DAMAGE</b> for <b>${label.toUpperCase()}</b>`;
}
return this.toMessage({ content: this.total, flavor: label });
}
async showDialog() {
const template =
"systems/deltagreen/templates/dialog/modify-damage-roll.html";
const backingData = {
data: {
label: this.item?.name,
originalFormula: this.formula,
outerModifier: "2 * ",
innerModifier: "+ 0",
},
};
const content = await renderTemplate(template, backingData);
return new Promise((resolve, reject) => {
new DialogV2({
content,
window: {
title: game.i18n.localize("DG.ModifySkillRollDialogue.Title"),
},
buttons: [
{
default: true,
label: game.i18n.translations.DG.Roll.Roll,
action: "roll",
callback: (event, button, dialog) => {
try {
const outerModifier = dialog.element.querySelector(
"[name='outerModifier']",
)?.value; // this is text as a heads up
let innerModifier = dialog.element.querySelector(
"[name='innerModifier']",
)?.value; // this is text as a heads up
const modifiedBaseRoll = dialog.element.querySelector(
"[name='originalFormula']",
)?.value; // this is text as a heads up
const messageMode = dialog.element.querySelector(
"[name='targetRollMode']",
)?.value;
if (innerModifier.replace(" ", "") === "+0") {
innerModifier = "";
}
let newFormula = "";
if (outerModifier.trim() !== "") {
newFormula += `${outerModifier}(${modifiedBaseRoll}${innerModifier.trim()})`;
} else {
newFormula += modifiedBaseRoll + innerModifier.trim();
}
resolve({ newFormula, messageMode });
} catch (ex) {
reject(console.log(ex));
}
},
},
],
}).render(true);
});
}
}
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 flavor = `Rolling <b>${DGUtils.localizeWithFallback(
"DG.Generic.SanDamage",
"SAN DAMAGE",
)}</b> For <b>${lowDie.formula} / ${highDie.formula}</b>`;
let html = "";
html += `<div class="dice-roll" data-action="expandRoll">`;
html += ` <div class="dice-result">`;
html += ` <div class="dice-formula">${lowDie.formula} / ${highDie.formula}</div>`;
html += ` <div class="dice-tooltip">`;
html += ` <div class="wrapper">`;
html += ` <section class="tooltip-part">`;
html += ` <div class="dice">`;
html += ` <header class="part-header flexrow">`;
html += ` <span class="part-formula">`;
html += ` ${lowDie.formula}`;
html += ` </span>`;
html += ` <span class="part-total">`;
html += ` ${lowResult}`;
html += ` </span>`;
html += ` </header>`;
html += ` <ol class="dice-rolls">`;
html += ` <li class="roll die d${lowDie.faces}">${lowResult}`;
html += ` </ol>`;
html += ` <hr>`;
html += ` <header class="part-header flexrow">`;
html += ` <span class="part-formula">`;
html += ` ${highDie.formula}`;
html += ` </span>`;
html += ` <span class="part-total">`;
html += ` ${highResult}`;
html += ` </span>`;
html += ` </header>`;
html += ` <ol class="dice-rolls">`;
html += ` <li class="roll die d${highDie.faces}">${highResult}</li>`;
html += ` </ol>`;
html += ` </div>`;
html += ` </section>`;
html += ` </div>`;
html += ` </div>`;
html += ` <h4 class="dice-total">${lowResult} / ${highResult}</h4>`;
html += ` </div>`;
html += `</div>`;
return this.toMessage({ content: html, flavor });
}
/**
* 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];
}
}
import {
DGRoll,
DGPercentileRoll,
DGLethalityRoll,
DGDamageRoll,
DGSanityDamageRoll,
} from "./roll-classes.js";
export {
DGRoll,
DGPercentileRoll,
DGLethalityRoll,
DGDamageRoll,
DGSanityDamageRoll,
};
/**
* Build a Delta Green roll from a click target's dataset.
@@ -881,6 +44,7 @@ export function createDGRollFromDataset(
case "sanity":
case "special-training":
case "weapon":
case "luck":
roll = new DGPercentileRoll("1D100", {}, rollOptions);
break;
case "lethality":
@@ -948,5 +112,5 @@ export async function processDGRoll(event, roll) {
}
}
await roll.evaluate();
roll.toChat();
await roll.toChat();
}
+106
View File
@@ -0,0 +1,106 @@
import DG from "../config.js";
/**
* @param {"1"|"d3"|"d4"|"d4-1"} baseFormula
* @returns {string}
*/
export function getSkillImprovementFormulaAsPercent(baseFormula) {
const flat = baseFormula === "1";
const formula = flat ? "1" : `1${baseFormula}`;
return `${formula}%`.toUpperCase();
}
/**
* Generates and evaluates skill improvement rolls for failed skills.
*
* @param {Actor} actor
* @param {string} baseFormula
* @param {object[]} failedSkills
* @param {object[]} failedTypedSkills
* @returns {Promise<Record<string, number>>}
*/
export async function evaluateSkillImprovementRolls(
actor,
baseFormula,
failedSkills,
failedTypedSkills,
) {
const totalFailures = failedSkills.length + failedTypedSkills.length;
if (!Object.keys(DG.skillImprovementFormulas).includes(baseFormula)) {
throw new Error(`Unknown roll formula: ${baseFormula}`);
}
const resultObj = {};
if (baseFormula === "1") {
return resultObj;
}
const rollPromises = [];
for (let i = 0; i < totalFailures; i++) {
rollPromises.push(new Roll(baseFormula, actor.system).evaluate());
}
const rolls = await Promise.all(rollPromises);
[...failedSkills, ...failedTypedSkills].forEach((skill, index) => {
resultObj[skill.key] = rolls[index].total;
});
return resultObj;
}
/**
* @param {object} params
* @param {Actor} params.actor
* @param {TokenDocument|null} [params.token]
* @param {string} params.baseFormula
* @param {object[]} params.failedSkills
* @param {object[]} params.failedTypedSkills
* @param {Record<string, number>} params.resultObj
* @returns {Promise<ChatMessage>}
*/
export async function createSkillImprovementChatMessage({
actor,
token = null,
baseFormula,
failedSkills,
failedTypedSkills,
resultObj,
}) {
const localizeFailedSkills = (skillsArray) => {
return skillsArray.map((skill) => {
const increment = resultObj[skill.key] ?? 1;
const label =
skill.label ?? game.i18n.localize(`DG.Skills.${skill.key}`);
const groupLabel = skill.group
? `${game.i18n.localize(
`DG.TypeSkills.${skill.group.replace(/\s+/g, "")}`,
)} (${label})`
: label;
return `${groupLabel}: <b>+${increment}%</b>`;
});
};
const failedSkillNames = localizeFailedSkills(failedSkills);
const failedTypedSkillNames = localizeFailedSkills(failedTypedSkills);
const content = [...failedSkillNames, ...failedTypedSkillNames].join(", ");
const flavor = game.i18n.format(
"DG.Skills.ApplySkillImprovements.ChatFlavor",
{
formula: getSkillImprovementFormulaAsPercent(baseFormula),
},
);
return ChatMessage.create({
speaker: ChatMessage.getSpeaker({
actor,
token,
alias: actor.name,
}),
content,
flavor,
messageMode: game.settings.get("core", "messageMode"),
});
}
+6 -42
View File
@@ -337,47 +337,11 @@ export default function registerSystemSettings() {
},
});
// obsolete - will be removed at some point
game.settings.register("deltagreen", "characterSheetFont", {
name: "World Font Choice",
hint: "Choose font style for use throughout this world.",
scope: "world", // This specifies a world-stored setting
config: false, // This specifies that the setting appears in the configuration view
type: String,
choices: {
// If choices are defined, the resulting setting will be a select menu
SpecialElite: "Special Elite (Classic Typewriters Font)",
Martel: "Martel (Clean Modern Font)",
Signika: "Signika (Foundry Default Font)",
TypeWriterCondensed:
"Condensed Typewriter (Modern, Small Typewriter Font)",
PublicSans: "Public Sans (US Government-style sans serif font)",
// "atwriter": "Another Typewriter (Alternate Old-style Typewriter Font)"
},
default: "SpecialElite", // The default value for the setting
onChange: (value) => {
// A callback function which triggers when the setting is changed
// console.log(value)
},
});
// obsolete - will be removed at some point
game.settings.register("deltagreen", "characterSheetBackgroundImageSetting", {
name: "World Sheet Background Image",
hint: "Choose background image for use throughout this world. (Refresh page to see change.)",
scope: "world", // This specifies a world-stored setting
config: false, // This specifies that the setting appears in the configuration view
type: String,
choices: {
// If choices are defined, the resulting setting will be a select menu
OldPaper1: "Old Dirty Paper (Good with Special Elite Font)",
IvoryPaper: "Ivory White Paper (Good with Martel Font)",
DefaultParchment: "Default Parchment (Good with Signika Font)",
},
default: "OldPaper1", // The default value for the setting
onChange: (value) => {
// A callback function which triggers when the setting is changed
// console.log(value)
},
game.settings.register(DG.ID, "schemaMigrationVersion", {
name: "Schema Migration Version",
scope: "world",
config: false,
type: Number,
default: 0,
});
}
-283
View File
@@ -1,283 +0,0 @@
import DGAgentSheet from "./agent-sheet.js";
import ActorEditStatForm from "./edit-stats.js";
/** @extends {DGAgentSheet} */
export default class DGAgentSheetV2 extends DGAgentSheet {
static DEFAULT_OPTIONS = /** @type {const} */ ({
classes: ["actor-sheet-v2"],
position: { width: 958, height: 700 },
actions: {
openStatsEdit: this._openStatsEdit,
toggleCustomSkillsEdit: function toggleCustomSkillsEdit(event) {
event.preventDefault();
event.stopPropagation();
this._customSkillsEditMode = !this._customSkillsEditMode;
this._syncCustomSkillsEditModeUi();
return this.render();
},
},
});
/** @type {boolean} */
_customSkillsEditMode = false;
/** @override */
static TABS = /** @type {const} */ ({
primary: {
initial: "skills",
labelPrefix: "DG.Navigation.Agent",
tabs: [
{ id: "skills" },
{ id: "combat" },
{ id: "motivations" },
{ id: "gear" },
{ id: "personal" },
{ id: "about", icon: "fas fa-question-circle", label: "" },
],
},
});
/** @override */
static PARTS = /** @type {const} */ ({
leftBar: {
template: `${this.TEMPLATE_PATH}/parts/left-bar.html`,
templates: [`${this.TEMPLATE_PATH}/partials/sanity-partial.html`],
},
tabs: this.BASE_PARTS.tabs,
rightBar: {
template: `${this.TEMPLATE_PATH}/parts/right-bar.html`,
templates: [
`${this.TEMPLATE_PATH}/parts/skills-tab-v2.html`,
`${this.TEMPLATE_PATH}/parts/combat-tab-v2.html`,
`${this.TEMPLATE_PATH}/parts/motivations-tab-v2.html`,
`${this.TEMPLATE_PATH}/parts/gear-tab-v2.html`,
`${this.TEMPLATE_PATH}/parts/personal-tab-v2.html`,
`${this.TEMPLATE_PATH}/parts/about-tab.html`,
`${this.TEMPLATE_PATH}/partials/custom-skills-partial-v2.html`,
`${this.TEMPLATE_PATH}/partials/bonds-section-partial.html`,
`${this.TEMPLATE_PATH}/partials/weapons-section-partial.html`,
`${this.TEMPLATE_PATH}/partials/armor-section-partial.html`,
`${this.TEMPLATE_PATH}/partials/injuries-section-partial.html`,
`${this.TEMPLATE_PATH}/partials/other-gear-section-partial.html`,
`${this.TEMPLATE_PATH}/partials/notes-partial.html`,
],
},
});
/** @override */
_configureRenderOptions(options) {
super._configureRenderOptions(options);
if (!this.actor.limited) return;
options.parts = ["leftBar", "tabs", "rightBar"];
}
/** @override */
_prepareTabs(group) {
const tabs = super._prepareTabs(group);
if (!this.actor.limited || this.actor.type !== "agent") return tabs;
return {
personal: { ...tabs.personal, active: true, cssClass: "active" },
};
}
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (this.actor.type !== "agent") return context;
context.typedSkillColumns = this._prepareTypedSkillColumns();
context.customSkillsEditMode = this._customSkillsEditMode;
return context;
}
/** @override */
async _onRender(context, options) {
await super._onRender(context, options);
this._syncCustomSkillsEditModeUi();
}
/** @returns {void} */
_syncCustomSkillsEditModeUi() {
this.element?.classList.toggle(
"custom-skills-edit-mode",
Boolean(this._customSkillsEditMode),
);
}
/**
* Packs skill groups into columns without splitting a group across columns.
* Respects the world "Sort Skills By Column" setting (same as core skills).
*
* @param {object[]} groups - Skill groups with skills and rowCount (sorted A–Z).
* @param {number} numCols - Number of columns.
* @returns {object[][]} Column arrays of render blocks.
*/
_packSkillGroupsIntoColumns(groups, numCols) {
const sortByColumn = game.settings.get("deltagreen", "sortSkills");
if (sortByColumn) {
return this._packSkillGroupsByColumn(groups, numCols);
}
return this._packSkillGroupsByRow(groups, numCols);
}
/**
* @param {object} column
* @param {object} group
* @returns {void}
*/
_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;
}
/**
* Column-first packing (read down each column, then the next).
*
* @param {object[]} groups
* @param {number} numCols
* @returns {object[][]}
*/
_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;
}
this._appendSkillGroupToColumn(columns[colIndex], group);
}
return columns.map((col) => col.blocks);
}
/**
* Row-first packing (groups round-robin across columns, like core skill row sort).
*
* @param {object[]} groups
* @param {number} numCols
* @returns {object[][]}
*/
_packSkillGroupsByRow(groups, numCols) {
const columns = Array.from({ length: numCols }, () => ({
blocks: [],
height: 0,
}));
for (let i = 0; i < groups.length; i++) {
this._appendSkillGroupToColumn(columns[i % numCols], groups[i]);
}
return columns.map((col) => col.blocks);
}
/**
* Builds grouped typed-skill columns for the V2 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 this._packSkillGroupsIntoColumns(groups, 3);
}
/**
* Sorted special-training blocks for the skills tab (single-column list).
*
* @returns {object[]}
*/
_prepareSpecialTrainingBlocks() {
const trainings = this._prepareSpecialTrainings().map((training) => ({
...training,
sortLabel: training.name.toUpperCase(),
actorType: this.actor.type,
}));
trainings.sort((a, b) =>
a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang),
);
return trainings;
}
static _openStatsEdit() {
const form = new ActorEditStatForm({
actor: this.actor,
});
form.render(true);
}
}
+186 -147
View File
@@ -1,5 +1,12 @@
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
import { packSkillGroupsIntoColumns } from "../utils/skill-layout.js";
import {
createSkillImprovementChatMessage,
evaluateSkillImprovementRolls,
getSkillImprovementFormulaAsPercent,
} from "../roll/skill-improvement-roll.js";
import DGActorSheet from "./base-actor-sheet.js";
import ActorEditStatForm from "../applications/edit-stats.js";
const { renderTemplate } = foundry.applications.handlebars;
@@ -7,15 +14,28 @@ const { renderTemplate } = foundry.applications.handlebars;
export default class DGAgentSheet extends DGActorSheet {
/** @override */
static DEFAULT_OPTIONS = /** @type {const} */ ({
classes: ["agent-sheet"],
position: { width: 958, height: 700 },
actions: {
// Resets.
clearBondDamage: DGAgentSheet._clearBondDamage,
resetBreakingPoint: DGAgentSheet._resetBreakingPoint,
// Other actions.
applySkillImprovements: DGAgentSheet._processSkillImprovements,
openStatsEdit: DGAgentSheet._openStatsEdit,
toggleCustomSkillsEdit: function toggleCustomSkillsEdit(event) {
event.preventDefault();
event.stopPropagation();
this._customSkillsEditMode = !this._customSkillsEditMode;
this._syncCustomSkillsEditModeUi();
return this.render();
},
},
});
/** @type {boolean} */
_customSkillsEditMode = false;
/** @override */
static TABS = /** @type {const} */ ({
primary: {
@@ -23,11 +43,10 @@ export default class DGAgentSheet extends DGActorSheet {
labelPrefix: "DG.Navigation.Agent",
tabs: [
{ id: "skills" },
{ id: "physical" },
{ id: "combat" },
{ id: "motivations" },
{ id: "gear" },
{ id: "bio" },
{ id: "bonds" },
{ id: "personal" },
{ id: "about", icon: "fas fa-question-circle", label: "" },
],
},
@@ -35,33 +54,88 @@ export default class DGAgentSheet extends DGActorSheet {
/** @override */
static PARTS = /** @type {const} */ ({
header: this.BASE_PARTS.header,
leftBar: {
template: `${this.TEMPLATE_PATH}/parts/left-bar.html`,
templates: [`${this.TEMPLATE_PATH}/partials/sanity-partial.html`],
},
tabs: this.BASE_PARTS.tabs,
skills: this.BASE_PARTS.skills,
physical: {
template: `${this.TEMPLATE_PATH}/parts/physical-tab.html`,
rightBar: {
template: `${this.TEMPLATE_PATH}/parts/right-bar.html`,
templates: [
`${this.TEMPLATE_PATH}/partials/attributes-grid-partial.html`,
`${this.TEMPLATE_PATH}/parts/skills-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/combat-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/motivations-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/gear-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/personal-tab-agent.html`,
`${this.TEMPLATE_PATH}/parts/about-tab.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`,
`${this.TEMPLATE_PATH}/partials/armor-section-partial.html`,
`${this.TEMPLATE_PATH}/partials/injuries-section-partial.html`,
`${this.TEMPLATE_PATH}/partials/other-gear-section-partial.html`,
`${this.TEMPLATE_PATH}/partials/notes-partial.html`,
],
scrollable: [""],
},
motivations: {
template: `${this.TEMPLATE_PATH}/parts/motivations-tab.html`,
},
gear: this.BASE_PARTS.gear,
bio: {
template: `${this.TEMPLATE_PATH}/parts/bio-tab.html`,
templates: [`${this.TEMPLATE_PATH}/partials/notes-partial.html`],
scrollable: [""],
},
bonds: {
template: `${this.TEMPLATE_PATH}/parts/bonds-tab.html`,
scrollable: [""],
},
about: this.BASE_PARTS.about,
});
/* -------------------------------------------- */
/** @override */
_configureRenderOptions(options) {
super._configureRenderOptions(options);
if (!this.actor.limited) return;
options.parts = ["leftBar", "tabs", "rightBar"];
}
/** @override */
_prepareTabs(group) {
const tabs = super._prepareTabs(group);
if (!this.actor.limited || this.actor.type !== "agent") return tabs;
return {
personal: { ...tabs.personal, active: true, cssClass: "active" },
};
}
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (this.actor.type !== "agent") return context;
context.typedSkillColumns = this._prepareTypedSkillColumns();
context.customSkillsEditMode = this._customSkillsEditMode;
return context;
}
/** @override */
_sortCustomSkills() {
// Agent sheet uses typedSkillColumns instead of sortedCustomSkills.
}
/** @override */
async _onRender(context, options) {
await super._onRender(context, options);
this._syncCustomSkillsEditModeUi();
}
/** @returns {void} */
_syncCustomSkillsEditModeUi() {
this.element?.classList.toggle(
"custom-skills-edit-mode",
Boolean(this._customSkillsEditMode),
);
}
static _clearBondDamage() {
for (const bond of this.actor.itemTypes.bond) {
if (!bond.system.hasBeenDamagedSinceLastHomeScene) continue;
bond.update({ "system.hasBeenDamagedSinceLastHomeScene": false });
}
}
/**
* Resets the actor's current breaking point by recalculating it as the difference
@@ -93,6 +167,86 @@ export default class DGAgentSheet extends DGActorSheet {
this.actor.update(dataToUpdate);
}
/**
* 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).
*
* @returns {object[]}
*/
_prepareSpecialTrainingBlocks() {
const trainings = this._prepareSpecialTrainings().map((training) => ({
...training,
sortLabel: training.name.toUpperCase(),
actorType: this.actor.type,
}));
trainings.sort((a, b) =>
a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang),
);
return trainings;
}
static _openStatsEdit() {
const form = new ActorEditStatForm({
actor: this.actor,
});
form.render(true);
}
/**
* Runs through the whole process of improving skills,
* i.e., prompting the user, rolling, and creating the chat card.
@@ -126,18 +280,21 @@ export default class DGAgentSheet extends DGActorSheet {
if (!prompt) return null;
const resultObj = await this._createSkillImprovementRolls(
const resultObj = await evaluateSkillImprovementRolls(
this.actor,
baseRollFormula,
failedSkills,
failedTypedSkills,
);
await this._createSkillImprovementChatCard(
baseRollFormula,
await createSkillImprovementChatMessage({
actor: this.actor,
token: this.token,
baseFormula: baseRollFormula,
failedSkills,
failedTypedSkills,
resultObj,
);
});
return this._applySkillImprovements(
failedSkills,
@@ -198,8 +355,7 @@ export default class DGAgentSheet extends DGActorSheet {
`${BASE_TEMPLATE_PATH}/dialog/apply-skill-improvements.html`,
{
failedSkillNames,
baseFormula:
DGAgentSheet._getSkillImprovementFormulaAsPercent(baseFormula),
baseFormula: getSkillImprovementFormulaAsPercent(baseFormula),
},
);
@@ -219,110 +375,6 @@ export default class DGAgentSheet extends DGActorSheet {
});
}
/**
* Generates and evaluates the rolls for skill improvements based on failed skills.
*
* @param {SkillImprovementFormula} baseFormula - The formula used to calculate skill improvements.
* @param {FailedSkill[]} failedSkills - An array of failed skills.
* @param {FailedTypedSkill[]} failedTypedSkills - An array of failed typed skills.
*
* @returns {Promise<{roll: Roll|undefined, resultObj: ResultObj}>} - An object containing the roll result and a map of skill keys to improvement values.
*
* @throws {Error} - Throws an error if the baseFormula is unknown.
*/
async _createSkillImprovementRolls(
baseFormula,
failedSkills,
failedTypedSkills,
) {
const totalFailures = failedSkills.length + failedTypedSkills.length;
if (!Object.keys(DG.skillImprovementFormulas).includes(baseFormula)) {
throw new Error(`Unknown roll formula: ${baseFormula}`);
}
const rollPromises = [];
const resultObj = {};
if (baseFormula !== "1") {
for (let i = 0; i < totalFailures; i++) {
rollPromises.push(new Roll(baseFormula, this.actor.system).evaluate());
}
const rolls = await Promise.all(rollPromises);
[...failedSkills, ...failedTypedSkills].forEach((skill, index) => {
resultObj[skill.key] = rolls[index].total;
});
}
return resultObj;
}
/**
* Create a chat card to record skill improvements.
*
* @param {FailedSkill[]} failedSkills - array of failed skills
* @param {FailedTypedSkill[]} failedTypedSkills - array of failed typed skills
* @param {Roll|undefined} roll - the improvement roll
* @param {ResultObj} resultObj
*
* @returns {Promise<ChatMessage>}
*/
_createSkillImprovementChatCard(
baseFormula,
failedSkills,
failedTypedSkills,
resultObj,
) {
const localizeFailedSkills = (skillsArray) => {
return skillsArray.map((skill) => {
const increment = resultObj[skill.key] ?? 1;
const label =
skill.label ?? game.i18n.localize(`DG.Skills.${skill.key}`); // fallback for regular skills
const groupLabel = skill.group
? `${game.i18n.localize(
`DG.TypeSkills.${skill.group.replace(/\s+/g, "")}`,
)} (${label})`
: label;
return `${groupLabel}: <b>+${increment}%</b>`;
});
};
const failedSkillNames = localizeFailedSkills(failedSkills);
const failedTypedSkillNames = localizeFailedSkills(failedTypedSkills);
// Prepare chat data
const content = [...failedSkillNames, ...failedTypedSkillNames].join(", ");
const flavor = game.i18n.format(
"DG.Skills.ApplySkillImprovements.ChatFlavor",
{
formula: DGAgentSheet._getSkillImprovementFormulaAsPercent(baseFormula),
},
);
const chatData = {
speaker: ChatMessage.getSpeaker({
actor: this.actor,
token: this.token,
alias: this.actor.name,
}),
content,
flavor,
messageMode: game.settings.get("core", "messageMode"),
};
return ChatMessage.create(chatData);
}
/**
* Updates the actor's skills / typed skills with the improvements,
* persisting the changes to the database.
*
* @param {FailedSkill[]} failedSkills - array of failed skills that need to be updated
* @param {FailedTypedSkill[]} failedTypedSkills - array of failed skills that need to be updated
* @param {ResultObj} resultObj
* @returns {Promise<DeltaGreenActor>} - the update promise
*/
_applySkillImprovements(failedSkills, failedTypedSkills, resultObj) {
const updateSkills = (skillsArray, updatedTarget) => {
skillsArray.forEach((skill) => {
@@ -344,17 +396,4 @@ export default class DGAgentSheet extends DGActorSheet {
system: { skills: updatedSkills, typedSkills: updatedTypedSkills },
});
}
/**
* Takes a base formula and returns a string representing the skill improvement formula as a percentage.
* TODO: Move to the roll class itself, probably even a "SkillImprovementRoll".
*
* @param {SkillImprovementFormula} baseFormula - the base formula to convert
* @returns {string} the skill improvement formula as a percentage (e.g. "1D3%" or "1%")
*/
static _getSkillImprovementFormulaAsPercent(baseFormula) {
const flat = baseFormula === "1";
const formula = flat ? "1" : `1${baseFormula}`;
return `${formula}%`.toUpperCase();
}
}
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,5 +1,4 @@
import { BASE_TEMPLATE_PATH } from "../config.js";
import { getItemRichTextFields } from "../config/rich-text-fields.js";
import DGSheetMixin from "./base-sheet.js";
import { createDGRollFromDataset, processDGRoll } from "../roll/roll.js";
@@ -183,10 +182,6 @@ export default class DGItemSheet extends DGSheetMixin(ItemSheetV2) {
}
context.owner = this.document.isOwner;
await this._prepareRichTextContext(
context,
getItemRichTextFields(this.item.type),
);
return context;
}
@@ -235,13 +230,4 @@ export default class DGItemSheet extends DGSheetMixin(ItemSheetV2) {
const revealed = !this.item.system.revealed;
this.item.update({ "system.revealed": revealed });
}
/**
* @param {Event|object} event
* @param {Roll} roll
* @returns {Promise<void>}
*/
async processRoll(event, roll) {
return processDGRoll(event, roll);
}
}
+26 -18
View File
@@ -1,5 +1,6 @@
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
import { prepareRichTextContext } from "./rich-text.js";
import { getRichTextFieldsForPart } from "../config/rich-text-fields.js";
import { prepareRichTextContext } from "../utils/rich-text.js";
const HbsAppMixin = foundry.applications.api.HandlebarsApplicationMixin;
@@ -30,8 +31,32 @@ const DGSheetMixin = (Base) => {
// Add the document to the context under a descriptive name (i.e. "actor", "item")
context[docName] = this.document;
context.richText = {};
return context;
}
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
const documentName = this.document.documentName;
const fieldSpecs = getRichTextFieldsForPart(documentName, partId, {
actorType: this.document.type,
itemType: this.document.type,
showNotesInSkills: context.showNotesInSkills,
});
if (fieldSpecs.length) {
Object.assign(
context.richText,
await prepareRichTextContext(this.document, fieldSpecs),
);
}
return context;
}
/** @inheritdoc */
async _onFirstRender(context, options) {
@@ -64,23 +89,6 @@ const DGSheetMixin = (Base) => {
resetPosition() {
this.setPosition(this.options.position);
}
/**
* Prepare `context.richText` from schema-driven ProseMirror field specs.
* @param {ApplicationRenderContext} context
* @param {{ path: string, key: string }[]} fieldSpecs
* @returns {Promise<void>}
*/
async _prepareRichTextContext(context, fieldSpecs) {
if (!fieldSpecs?.length) {
context.richText = {};
return;
}
context.richText = await prepareRichTextContext(
this.document,
fieldSpecs,
);
}
};
};
+7
View File
@@ -0,0 +1,7 @@
/**
* @param {...Function} mixins
* @returns {Function}
*/
export default function composeMixins(...mixins) {
return (Base) => mixins.reduceRight((acc, mixin) => mixin(acc), Base);
}
+239
View File
@@ -0,0 +1,239 @@
const { DialogV2 } = foundry.applications.api;
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
export default function ItemListMixin(Base) {
return class extends Base {
/** @param {object} [context] */
_prepareCharacterItems(context) {
const { actor } = this;
const armor = [];
const weapons = [];
const gear = [];
const tomes = [];
const rituals = [];
for (const i of actor.items) {
if (i.type === "armor") {
armor.push(i);
} else if (i.type === "weapon") {
weapons.push(i);
} else if (i.type === "gear") {
gear.push(i);
} else if (i.type === "tome") {
tomes.push(i);
} else if (i.type === "ritual") {
rituals.push(i);
}
}
const nameSort = (a, b) => {
const x = a.name.toLowerCase();
const y = b.name.toLowerCase();
if (x < y) return -1;
if (x > y) return 1;
return 0;
};
const sortSort = (a, b) => a.sort - b.sort;
if (actor.system.settings.sorting.armorSortAlphabetical) {
armor.sort(nameSort);
} else {
armor.sort(sortSort);
}
if (actor.system.settings.sorting.weaponSortAlphabetical) {
weapons.sort(nameSort);
} else {
weapons.sort(sortSort);
}
if (actor.system.settings.sorting.gearSortAlphabetical) {
gear.sort(nameSort);
} else {
gear.sort(sortSort);
}
if (actor.system.settings.sorting.tomeSortAlphabetical) {
tomes.sort(nameSort);
} else {
tomes.sort(sortSort);
}
if (actor.system.settings.sorting.ritualSortAlphabetical) {
rituals.sort(nameSort);
} else {
rituals.sort(sortSort);
}
actor.armor = armor;
actor.weapons = weapons;
actor.gear = gear;
actor.rituals = rituals;
actor.tomes = tomes;
}
/** @returns {void} */
_setupItemDragListeners() {
const { element } = this;
const handler = (ev) => this._onDragStart(ev);
element.querySelectorAll("li.item").forEach((li) => {
if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false);
});
}
/** @override */
_onDragStart(event) {
const li = event.currentTarget;
if (event.target.classList.contains("content-link")) return;
let dragData;
if (li.dataset.itemId) {
const item = this.actor.items.get(li.dataset.itemId);
dragData = item.toDragData();
}
if (li.dataset.effectId) {
const effect = this.actor.effects.get(li.dataset.effectId);
dragData = effect.toDragData();
}
if (!dragData) return;
if (li.dataset.itemId) {
const item = this.actor.items.get(li.dataset.itemId);
dragData.itemData = item;
}
event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
}
/** @override */
async _onDrop(event) {
super._onDrop(event);
if (event.altKey) {
const TextEditor = foundry.applications.ux.TextEditor.implementation;
const dragData = TextEditor.getDragEventData(event);
if (dragData.type === "Item") {
const item = fromUuidSync(dragData.uuid);
await item.delete();
}
}
}
static _onItemAction(event, target) {
const li = target.closest(".item");
const { itemId } = li.dataset;
const { actionType, itemType } = target.dataset;
switch (actionType) {
case "create":
this._onItemCreate(itemType);
break;
case "edit": {
const item = this.actor.items.get(itemId);
item.sheet.render(true);
break;
}
case "delete": {
this.actor.deleteEmbeddedDocuments("Item", [itemId]);
break;
}
default:
break;
}
}
/** @param {String} type */
_onItemCreate(type) {
const name = game.i18n.format(
game.i18n.translations.DOCUMENT?.New || "DG.FallbackText.newItem",
{
type: game.i18n.localize(`TYPES.Item.${type}`),
},
);
const itemData = {
name,
type,
system: {},
};
if (type === "bond") {
itemData.system.score = this.actor.system.statistics.cha.value;
}
return this.actor.createEmbeddedDocuments("Item", [itemData]);
}
/** Handler for generic toggle events */
static _toggleGeneric(event, target) {
const { prop } = target.dataset;
const itemId = target.closest("[data-item-id]")?.dataset.itemId;
this.toggle(prop, itemId);
}
/** Handler for item-sort-mode toggle events */
static _toggleItemSortMode(event, target) {
const itemType = target.dataset.gearType;
const propString = `${itemType}SortAlphabetical`;
const targetProp = `system.settings.sorting.${propString}`;
this.toggle(targetProp);
}
static _browsePack(event, target) {
const { packType } = target.dataset;
switch (packType) {
case "weapon": {
new DialogV2({
window: { title: "Select Compendium" },
buttons: [
{
action: "firearms",
label: game.i18n.localize("DG.Gear.WeaponTypes.Firearms"),
icon: '<i class="fas fa-crosshairs"></i>',
callback: () =>
game.packs
.find((k) => k.collection === "deltagreen.firearms")
.render(true),
},
{
action: "melee",
label: game.i18n.localize("DG.Gear.WeaponTypes.Melee"),
icon: '<i class="far fa-hand-rock"></i>',
callback: () =>
game.packs
.find(
(k) => k.collection === "deltagreen.hand-to-hand-weapons",
)
.render(true),
},
],
}).render(true);
break;
}
default:
game.packs
.find((k) => k.collection === `deltagreen.${packType}`)
.render(true);
break;
}
}
/**
* @param {string} prop
* @param {string} [itemId]
*/
toggle(prop, itemId) {
const item = this.actor.items.get(itemId);
const targetDoc = item ?? this.actor;
const currentVal = foundry.utils.getProperty(targetDoc, prop);
targetDoc.update({ [prop]: !currentVal });
}
};
}
+74
View File
@@ -0,0 +1,74 @@
import {
createDGRollFromDataset,
processDGRoll,
} from "../../roll/roll.js";
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
export default function RollSheetMixin(Base) {
return class extends Base {
/**
* Listens for right click events on the actor sheet and executes a regular roll,
* or luck roll, depending on the action.
*
* @returns {void}
*/
_setRightClickListeners() {
const { element } = this;
element.addEventListener("contextmenu", (event) => {
event.preventDefault();
const target = event.target.closest(
"[data-action='roll'],[data-action='rollLuck']",
);
if (!target) return;
if (target.dataset.action === "rollLuck") {
this.constructor._onRollLuck.call(this, event, target);
return;
}
this.constructor._onRoll.call(this, event, target);
});
}
/**
* Handle clickable rolls.
*
* @param {Event} event The originating click event
* @async
* @private
*/
static async _onRoll(event, target) {
if (target.classList.contains("not-rollable") || event.which === 2) return;
const item = this.actor.items.get(target.dataset.iid);
const roll = createDGRollFromDataset(target.dataset, {
actor: this.actor,
item,
element: target,
sanityDamageSource: "actor",
});
await this.processRoll(event, roll);
}
static async _onRollLuck(event) {
if (event.which === 2) return;
const roll = createDGRollFromDataset(
{ rolltype: "luck", key: "luck" },
{ actor: this.actor },
);
await this.processRoll(event, roll);
}
/**
* Show a dialog for the roll and then send to chat.
*
* @param {Event} event The originating click event
* @param {Roll} roll The roll to show a dialog for and then send to chat.
* @async
*/
async processRoll(event, roll) {
return processDGRoll(event, roll);
}
};
}
+281
View File
@@ -0,0 +1,281 @@
import DG from "../../config.js";
import { reorderForColumnSorting } from "../../utils/skill-layout.js";
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
export default function SkillPrepMixin(Base) {
return class extends Base {
/**
* Prepares the subname info placeholder based on the actor type.
*
* @returns {string}
*/
_prepareSubnameInfoPlaceholder() {
let subnameInfoPlaceholder = "";
switch (this.actor.type) {
case "unnatural":
subnameInfoPlaceholder =
"DG.UnnaturalSheet.ShortDescriptionPlaceholder";
break;
case "vehicle":
subnameInfoPlaceholder = "DG.VehicleSheet.DescriptionPlaceHolder";
break;
default:
subnameInfoPlaceholder = "DG.AgentSheet.ProfessionPlaceholder";
break;
}
return game.i18n.localize(subnameInfoPlaceholder);
}
/** @returns {void} */
_sortSkills() {
const sortedSkills = [];
for (const [key, skill] of Object.entries(this.actor.system.skills)) {
skill.key = key;
if (game.i18n.lang === "ja") {
skill.sortLabel = game.i18n.localize(`DG.Skills.ruby.${key}`);
} else {
skill.sortLabel = game.i18n.localize(`DG.Skills.${key}`);
}
if (skill.sortLabel === "" || skill.sortLabel === `DG.Skills.${key}`) {
skill.sortLabel = skill.key;
}
if (this.actor.type === "agent") {
skill.hasPendingImprovement =
skill.proficiency !== skill.targetProficiency;
skill.proficiencyInputClass = skill.hasPendingImprovement
? "target-proficiency-mod"
: "";
}
if (
!(
(this.actor.type === "npc" || this.actor.type === "unnatural") &&
this.actor.system.showUntrainedSkills &&
skill.proficiency < 1
)
) {
sortedSkills.push(skill);
}
}
sortedSkills.sort((a, b) => {
return a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang);
});
if (game.settings.get("deltagreen", "sortSkills")) {
const columnSortedSkills = reorderForColumnSorting(sortedSkills, 3);
this.actor.system.sortedSkills = columnSortedSkills;
} else {
this.actor.system.sortedSkills = sortedSkills;
}
}
/** @returns {void} */
_sortCustomSkills() {
const specialTrainings = this._prepareSpecialTrainings();
const sortedCustomSkills = [];
for (const [key, skill] of Object.entries(this.actor.system.typedSkills)) {
skill.type = "typeSkill";
skill.key = key;
skill.sortLabel = `${skill.group}.${skill.label}`;
skill.sortLabel = skill.sortLabel.toUpperCase();
skill.actorType = this.actor.type;
if (skill.sortLabel === "" || skill.sortLabel === `DG.Skills.${key}`) {
skill.sortLabel = skill.key;
}
sortedCustomSkills.push(skill);
}
for (let i = 0; i < specialTrainings.length; i++) {
const training = specialTrainings[i];
training.type = "training";
training.sortLabel = training.name.toUpperCase();
training.actorType = this.actor.type;
sortedCustomSkills.push(training);
}
sortedCustomSkills.sort((a, b) => {
return a.sortLabel.localeCompare(b.sortLabel, game.i18n.lang);
});
if (game.settings.get("deltagreen", "sortSkills")) {
const columnSortedSkills = reorderForColumnSorting(
sortedCustomSkills,
2,
);
this.actor.system.sortedCustomSkills = columnSortedSkills;
} else {
this.actor.system.sortedCustomSkills = sortedCustomSkills;
}
}
/** @returns {void} */
_prepareSkillTooltips() {
for (const skill of Object.values(this.actor.system.sortedSkills)) {
skill.tooltip = game.i18n.localize(`DG.Skills.Tooltip.${skill.key}`);
if (!skill.proficiency) {
skill.tooltip = skill.tooltip.concat(
"<br><br>",
game.i18n.localize("DG.Tooltip.CannotRollSkillLabel"),
);
}
}
}
/** @returns {object[]} */
_prepareSpecialTrainings() {
return this.actor.system.specialTraining.map((training) => {
const simplifiedTraining = {
name: training.name,
id: training.id,
key: training.attribute,
};
let attributeLabel;
let attributeTooltip = null;
switch (true) {
case DG.statistics.includes(training.attribute):
attributeLabel = `${training.attribute.toUpperCase()}x5`;
attributeTooltip = `DG.Attributes.Tooltip.${training.attribute}`;
simplifiedTraining.attribute = attributeLabel;
simplifiedTraining.targetNumber =
this.actor.system.statistics[training.attribute].x5;
break;
case DG.skills.includes(training.attribute):
attributeLabel = game.i18n.localize(
`DG.Skills.${training.attribute}`,
);
if (attributeLabel === `DG.Skills.${training.attribute}`) {
attributeLabel =
this.actor.system.skills[training.attribute].label;
}
attributeTooltip = `DG.Skills.Tooltip.${training.attribute}`;
simplifiedTraining.attribute = attributeLabel;
simplifiedTraining.targetNumber =
this.actor.system.skills[training.attribute].proficiency;
break;
default:
attributeLabel =
this.actor.system.typedSkills[training.attribute].label;
simplifiedTraining.attribute = attributeLabel;
simplifiedTraining.targetNumber =
this.actor.system.typedSkills[training.attribute].proficiency;
break;
}
simplifiedTraining.attributeLabel = attributeLabel;
simplifiedTraining.attributeTooltip = attributeTooltip;
simplifiedTraining.displayLabel = `${simplifiedTraining.name} (${attributeLabel}, ${simplifiedTraining.targetNumber}%)`;
return simplifiedTraining;
});
}
shouldShowHyperGeometrySection(actor) {
if (game.user.isGM) {
return true;
}
if (
game.settings.get(
"deltagreen",
"alwaysShowHypergeometrySectionForPlayers",
)
) {
return true;
}
for (const i of actor.items) {
if (i.type === "tome" || i.type === "ritual") {
return true;
}
}
return false;
}
/**
* Require Shift while hovering to show tooltips.
*
* @param {HTMLElement} root
*/
_tooltipsSettings(root) {
const mode = game.settings.get("deltagreen", "skillTooltipDisplay");
if (mode !== "hoverShift" && mode !== "never") return;
const nodes = root.querySelectorAll("[data-tooltip],[title]");
if (mode === "never") {
nodes.forEach((el) => {
if (el.dataset.shiftTooltipInstalled === "true") return;
el.removeAttribute("data-tooltip");
el.removeAttribute("title");
el.dataset.shiftTooltipInstalled = "true";
});
return;
}
nodes.forEach((el) => {
if (el.dataset.shiftTooltipInstalled === "true") return;
let html = el.getAttribute("data-tooltip");
let isHtml = true;
if (!html) {
const title = el.getAttribute("title");
if (title) {
html = foundry.utils.escapeHTML(title);
isHtml = false;
}
}
if (!html) return;
el.removeAttribute("data-tooltip");
el.removeAttribute("title");
el.dataset.shiftTooltipInstalled = "true";
const opts = isHtml ? { html } : { text: html };
const show = () => game.tooltip.activate(el, opts);
const hide = () => game.tooltip.deactivate();
const onKey = (ev) => {
if (ev.key !== "Shift") return;
if (!document.body.contains(el)) {
window.removeEventListener("keydown", onKey);
window.removeEventListener("keyup", onKey);
return;
}
if (ev.type === "keydown") show();
else hide();
};
const onEnter = (ev) => {
if (ev.shiftKey) show();
window.addEventListener("keydown", onKey);
window.addEventListener("keyup", onKey);
};
const onLeave = () => {
hide();
window.removeEventListener("keydown", onKey);
window.removeEventListener("keyup", onKey);
};
el.addEventListener("pointerenter", onEnter, { passive: true });
el.addEventListener("pointerleave", onLeave, { passive: true });
});
}
};
}
@@ -0,0 +1,157 @@
const { DialogV2 } = foundry.applications.api;
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
export default function SpecialTrainingMixin(Base) {
return class extends Base {
static _onSpecialTrainingAction(event, target) {
const { actionType, id } = target.dataset;
switch (actionType) {
case "delete":
{
const specialTrainingArray = foundry.utils.duplicate(
this.actor.system.specialTraining,
);
const index = specialTrainingArray.findIndex(
(training) => training.id === id,
);
specialTrainingArray.splice(index, 1);
this.actor.update({
"system.specialTraining": specialTrainingArray,
});
}
break;
default:
this._showSpecialTrainingDialog(actionType, id);
break;
}
}
async _showSpecialTrainingDialog(action, targetID) {
const specialTraining = this.actor.system.specialTraining.find(
(training) => training.id === targetID,
);
const optionGroups = {
stats: game.i18n.localize(
"DG.SpecialTraining.Dialog.DropDown.Statistics",
),
skills: game.i18n.localize("DG.SpecialTraining.Dialog.DropDown.Skills"),
typedSkills: game.i18n.localize(
"DG.SpecialTraining.Dialog.DropDown.CustomSkills",
),
};
const statList = Object.entries(this.actor.system.statistics).map(
([key, stat]) => ({
value: key,
group: optionGroups.stats,
label: game.i18n.localize(`DG.Attributes.${key}`),
targetNumber: stat.value * 5,
}),
);
const skillList = Object.entries(this.actor.system.skills).map(
([key, skill]) => ({
value: key,
group: optionGroups.skills,
label: game.i18n.localize(`DG.Skills.${key}`),
targetNumber: skill.proficiency,
}),
);
const typedSkillList = Object.entries(this.actor.system.typedSkills).map(
([key, skill]) => ({
value: key,
group: optionGroups.typedSkills,
label: `${game.i18n.localize(`DG.TypeSkills.${skill.group}`)} (${
skill.label
})`,
targetNumber: skill.proficiency,
}),
);
const selectElement = foundry.applications.fields.createSelectInput({
name: "special-training-skill",
options: [...statList, ...skillList, ...typedSkillList],
groups: Object.values(optionGroups),
}).outerHTML;
const { renderTemplate } = foundry.applications.handlebars;
const content = await renderTemplate(
"systems/deltagreen/templates/dialog/special-training.html",
{
name: specialTraining?.name || "",
selectElement,
currentAttribute: specialTraining?.attribute || "",
statList,
skillList,
typedSkillList,
},
);
const buttonLabel = game.i18n.localize(
`DG.SpecialTraining.Dialog.${action.capitalize()}SpecialTraining`,
);
new DialogV2({
content,
window: {
title: game.i18n.localize("DG.SpecialTraining.Dialog.Title"),
},
default: "confirm",
buttons: [
{
default: true,
action: "confirm",
label: buttonLabel,
callback: (event, button, dialog) => {
const specialTrainingLabel = dialog.element.querySelector(
"[name='special-training-label']",
)?.value;
const specialTrainingAttribute = dialog.element.querySelector(
"[name='special-training-skill']",
)?.value;
if (action === "create")
this._createSpecialTraining(
specialTrainingLabel,
specialTrainingAttribute,
);
if (action === "edit")
this._editSpecialTraining(
specialTrainingLabel,
specialTrainingAttribute,
targetID,
);
},
},
],
}).render(true);
}
_createSpecialTraining(label, attribute) {
const specialTrainingArray = foundry.utils.duplicate(
this.actor.system.specialTraining,
);
specialTrainingArray.push({
name: label,
attribute,
id: foundry.utils.randomID(),
});
this.actor.update({ "system.specialTraining": specialTrainingArray });
}
_editSpecialTraining(label, attribute, id) {
const specialTrainingArray = foundry.utils.duplicate(
this.actor.system.specialTraining,
);
const specialTraining = specialTrainingArray.find(
(training) => training.id === id,
);
specialTraining.name = label;
specialTraining.attribute = attribute;
this.actor.update({ "system.specialTraining": specialTrainingArray });
}
};
}
+276
View File
@@ -0,0 +1,276 @@
const { DialogV2 } = foundry.applications.api;
/** @param {typeof foundry.applications.api.ApplicationV2} Base */
export default function TypedSkillMixin(Base) {
return class extends Base {
static _onTypedSkillAction(event, target) {
const { actionType, typedskill } = target.dataset;
switch (actionType) {
case "create":
this._showNewTypeSkillDialog();
break;
case "edit":
this._showNewEditTypeSkillDialog(typedskill);
break;
case "delete":
this.actor.update({ [`system.typedSkills.-=${typedskill}`]: null });
break;
default:
break;
}
}
_showNewEditTypeSkillDialog(targetSkill) {
const { typedSkills } = this.actor.system;
const currentLabel = typedSkills[targetSkill].label;
const currentGroup = typedSkills[targetSkill].group;
let htmlContent = `<div>`;
htmlContent += ` <label>${
game.i18n.translations.DG?.Skills?.SkillGroup ?? "Skill Group"
}:</label>`;
htmlContent += ` <select name="new-type-skill-group" />`;
if (currentGroup === game.i18n.translations.DG?.TypeSkills?.Art ?? "Art") {
htmlContent += ` <option value="Art" selected>${
game.i18n.translations.DG?.TypeSkills?.Art ?? "Art"
}</option>`;
} else {
htmlContent += ` <option value="Art">${
game.i18n.translations.DG?.TypeSkills?.Art ?? "Art"
}</option>`;
}
if (
currentGroup === game.i18n.translations.DG?.TypeSkills?.Craft ??
"Craft"
) {
htmlContent += ` <option value="Craft" selected>${
game.i18n.translations.DG?.TypeSkills?.Craft ?? "Craft"
}</option>`;
} else {
htmlContent += ` <option value="Craft">${
game.i18n.translations.DG?.TypeSkills?.Craft ?? "Craft"
}</option>`;
}
if (
currentGroup ===
(game.i18n.translations.DG?.TypeSkills?.ForeignLanguage ??
"Foreign Language")
) {
htmlContent += ` <option value="ForeignLanguage" selected>${
game.i18n.translations.DG?.TypeSkills?.ForeignLanguage ??
"Foreign Language"
}</option>`;
} else {
htmlContent += ` <option value="ForeignLanguage">${
game.i18n.translations.DG?.TypeSkills?.ForeignLanguage ??
"Foreign Language"
}</option>`;
}
if (
currentGroup ===
(game.i18n.translations.DG?.TypeSkills?.MilitaryScience ??
"Military Science")
) {
htmlContent += ` <option value="MilitaryScience" selected>${
game.i18n.translations.DG?.TypeSkills?.MilitaryScience ??
"Military Science"
}</option>`;
} else {
htmlContent += ` <option value="MilitaryScience">${
game.i18n.translations.DG?.TypeSkills?.MilitaryScience ??
"Military Science"
}</option>`;
}
if (
currentGroup === game.i18n.translations.DG?.TypeSkills?.Pilot ?? "Pilot"
) {
htmlContent += ` <option value="Pilot" selected>${
game.i18n.translations.DG?.TypeSkills?.Pilot ?? "Pilot"
}</option>`;
} else {
htmlContent += ` <option value="Pilot">${
game.i18n.translations.DG?.TypeSkills?.Pilot ?? "Pilot"
}</option>`;
}
if (
currentGroup ===
(game.i18n.translations.DG?.TypeSkills?.Science ?? "Science")
) {
htmlContent += ` <option value="Science" selected>${
game.i18n.translations.DG?.TypeSkills?.Science ?? "Science"
}</option>`;
} else {
htmlContent += ` <option value="Science">${
game.i18n.translations.DG?.TypeSkills?.Science ?? "Science"
}</option>`;
}
if (
currentGroup === game.i18n.translations.DG?.TypeSkills?.Other ?? "Other"
) {
htmlContent += ` <option value="Other" selected>${
game.i18n.translations.DG?.TypeSkills?.Other ?? "Other"
}</option>`;
} else {
htmlContent += ` <option value="Other">${
game.i18n.translations.DG?.TypeSkills?.Other ?? "Other"
}</option>`;
}
htmlContent += ` </select>`;
htmlContent += `</div>`;
htmlContent += `<div>`;
htmlContent += ` <label>${
game.i18n.translations.DG?.Skills.SkillName ?? "Skill Name"
}</label>`;
htmlContent += ` <input type="text" name="new-type-skill-label" value="${currentLabel}" />`;
htmlContent += `</div>`;
new DialogV2({
content: htmlContent,
window: {
title:
game.i18n.translations.DG?.Skills?.EditTypedOrCustomSkill ??
"Edit Typed or Custom Skill",
},
buttons: [
{
default: true,
action: "add",
label: game.i18n.translations.DG?.Skills?.EditSkill ?? "Edit Skill",
callback: (event, button, dialog) => {
const newTypeSkillLabel = dialog.element.querySelector(
"[name='new-type-skill-label']",
)?.value;
const newTypeSkillGroup = dialog.element.querySelector(
"[name='new-type-skill-group']",
)?.value;
this._updateTypedSkill(
targetSkill,
newTypeSkillLabel,
newTypeSkillGroup,
);
},
},
],
}).render(true);
}
_showNewTypeSkillDialog() {
let htmlContent = "";
htmlContent += `<div>`;
htmlContent += ` <label>${
game.i18n.translations.DG?.Skills?.SkillGroup ?? "Skill Group"
}:</label>`;
htmlContent += ` <select name="new-type-skill-group" />`;
htmlContent += ` <option value="Art">${
game.i18n.translations.DG?.TypeSkills?.Art ?? "Art"
}</option>`;
htmlContent += ` <option value="Craft">${
game.i18n.translations.DG?.TypeSkills?.Craft ?? "Craft"
}</option>`;
htmlContent += ` <option value="ForeignLanguage">${
game.i18n.translations.DG?.TypeSkills?.ForeignLanguage ??
"Foreign Language"
}</option>`;
htmlContent += ` <option value="MilitaryScience">${
game.i18n.translations.DG?.TypeSkills?.MilitaryScience ??
"Military Science"
}</option>`;
htmlContent += ` <option value="Pilot">${
game.i18n.translations.DG?.TypeSkills?.Pilot ?? "Pilot"
}</option>`;
htmlContent += ` <option value="Science">${
game.i18n.translations.DG?.TypeSkills?.Science ?? "Science"
}</option>`;
htmlContent += ` <option value="Other">${
game.i18n.translations.DG?.TypeSkills?.Other ?? "Other"
}</option>`;
htmlContent += ` </select>`;
htmlContent += `</div>`;
htmlContent += `<div>`;
htmlContent += ` <label>${
game.i18n.translations.DG?.Skills.SkillName ?? "Skill Name"
}</label>`;
htmlContent += ` <input type="text" name="new-type-skill-label" />`;
htmlContent += `</div>`;
new DialogV2({
content: htmlContent,
window: {
title:
game.i18n.translations.DG?.Skills?.AddTypedOrCustomSkill ??
"Add Typed or Custom Skill",
},
default: "add",
buttons: [
{
default: true,
action: "add",
label: game.i18n.translations.DG?.Skills?.AddSkill ?? "Add Skill",
callback: (event, button, dialog) => {
const newTypeSkillLabel = dialog.element.querySelector(
"[name='new-type-skill-label']",
)?.value;
const newTypeSkillGroup = dialog.element.querySelector(
"[name='new-type-skill-group']",
)?.value;
this._addNewTypedSkill(newTypeSkillLabel, newTypeSkillGroup);
},
},
],
}).render(true);
}
_addNewTypedSkill(newSkillLabel, newSkillGroup) {
const updatedData = foundry.utils.duplicate(this.actor.system);
const { typedSkills } = updatedData;
const d = new Date();
const newSkillPropertyName =
d.getFullYear().toString() +
(d.getMonth() + 1).toString() +
d.getDate().toString() +
d.getHours().toString() +
d.getMinutes().toString() +
d.getSeconds().toString();
typedSkills[newSkillPropertyName] = {
label: newSkillLabel,
group: newSkillGroup,
proficiency: 10,
failure: false,
};
updatedData.typedSkills = typedSkills;
this.actor.update({ system: updatedData });
}
_updateTypedSkill(targetSkill, newSkillLabel, newSkillGroup) {
if (
newSkillLabel !== null &&
newSkillLabel !== "" &&
newSkillGroup !== null &&
newSkillGroup !== ""
) {
const updatedData = foundry.utils.duplicate(this.actor.system);
updatedData.typedSkills[targetSkill].label = newSkillLabel;
updatedData.typedSkills[targetSkill].group = newSkillGroup;
this.actor.update({ system: updatedData });
}
}
};
}
+2 -13
View File
@@ -1,3 +1,4 @@
import { flagUnnaturalResourceMaxUpdates } from "../utils/actor-submit.js";
import DGNPCSheet from "./npc-sheet.js";
/** @extends {DGNPCSheet} */
@@ -5,19 +6,7 @@ export default class DGUnnaturalSheet extends DGNPCSheet {
/** @override */
async _processSubmitData(event, form, submitData, options) {
const submittedData = foundry.utils.expandObject(submitData);
// we need to update max WP if POW has changed (only for unnatural, but doesn't hurt others)
submittedData.system.wp.maxNeedsUpdate =
submittedData.system?.statistics.pow.value !==
this.actor.system.statistics.pow.value;
// we need to update max HP if STR or CON have changed (only for unnatural, but doesn't hurt others)
submittedData.system.health.maxNeedsUpdate =
submittedData.system?.statistics.str.value !==
this.actor.system.statistics.str.value ||
submittedData.system?.statistics.con.value !==
this.actor.system.statistics.con.value;
flagUnnaturalResourceMaxUpdates(submittedData, this.actor);
return super._processSubmitData(event, form, submittedData, options);
}
}
+16
View File
@@ -0,0 +1,16 @@
/**
* @param {object} submittedData
* @param {Actor} actor
* @returns {void}
*/
export function flagUnnaturalResourceMaxUpdates(submittedData, actor) {
submittedData.system.wp.maxNeedsUpdate =
submittedData.system?.statistics.pow.value !==
actor.system.statistics.pow.value;
submittedData.system.health.maxNeedsUpdate =
submittedData.system?.statistics.str.value !==
actor.system.statistics.str.value ||
submittedData.system?.statistics.con.value !==
actor.system.statistics.con.value;
}
+138
View File
@@ -0,0 +1,138 @@
/**
* Shared derived-data helpers for actor TypeDataModels.
*/
/**
* @param {Item[]} items
* @returns {number}
*/
export function computeEquippedArmorProtection(items) {
let protection = 0;
for (const item of items) {
if (item.type === "armor" && item.system.equipped === true) {
protection += item.system.protection;
}
}
return protection;
}
/**
* @param {number} strengthValue
* @returns {string}
*/
export function calculateMeleeDamageBonusFormula(strengthValue) {
if (strengthValue < 5) return "-2";
if (strengthValue < 9) return "-1";
if (strengthValue > 12 && strengthValue < 17) return "+1";
if (strengthValue > 16) return "+2";
return "";
}
/**
* @param {object} statistics
* @returns {void}
*/
export function prepareStatisticsX5(statistics) {
for (const statistic of Object.values(statistics)) {
statistic.x5 = statistic.value * 5;
}
}
/**
* @param {object} skills
* @returns {void}
*/
export function setSkillTargetProficiencies(skills) {
for (const skill of Object.values(skills)) {
skill.targetProficiency = skill.proficiency;
}
}
/**
* @param {object} sanity
* @param {object} statistics
* @returns {void}
*/
export function initializeSanityIfUnset(sanity, statistics) {
if (sanity.value >= 100) {
sanity.value = statistics.pow.x5;
sanity.currentBreakingPoint = sanity.value - statistics.pow.value;
}
}
/**
* @param {object} sanity
* @returns {void}
*/
export function clampSanityRitualValue(sanity) {
sanity.ritual = 99 - sanity.value;
if (sanity.ritual > 99) {
sanity.ritual = 99;
} else if (sanity.ritual < 1) {
sanity.ritual = 1;
}
}
/**
* @param {object} adaptations
* @returns {void}
*/
export function prepareSanityAdaptations(adaptations) {
adaptations.violence.isAdapted =
adaptations.violence.incident1 &&
adaptations.violence.incident2 &&
adaptations.violence.incident3;
adaptations.helplessness.isAdapted =
adaptations.helplessness.incident1 &&
adaptations.helplessness.incident2 &&
adaptations.helplessness.incident3;
}
/**
* @param {object} sanity
* @returns {void}
*/
export function prepareBreakingPointHit(sanity) {
sanity.breakingPointHit = sanity.value <= sanity.currentBreakingPoint;
}
/**
* @param {object} skills
* @returns {void}
*/
export function prepareAgentSkillFlags(skills) {
for (const [key, skill] of Object.entries(skills)) {
skill.cannotBeImprovedByFailure =
key === "unnatural" || key === "luck" || key === "ritual";
skill.isCalculatedValue = key === "ritual";
skill.targetProficiency = skill.proficiency;
}
}
/**
* @param {object} system
* @returns {void}
*/
export function removeLegacyRitualSkill(system) {
try {
delete system.skills.ritual;
} catch {
// Legacy field may be absent or non-configurable.
}
}
/**
* @param {object} statistics
* @returns {number}
*/
export function calculateHealthMax(statistics) {
try {
return Math.ceil(
(statistics.con.value + statistics.str.value) / 2,
);
} catch {
return 10;
}
}
+131
View File
@@ -0,0 +1,131 @@
import DGUtils from "./utility-functions.js";
export default function registerHandlebarsHelpers() {
// Add Handlebars helpers
Handlebars.registerHelper("localizeWithFallback", (value, fallbackValue) => {
return DGUtils.localizeWithFallback(value, fallbackValue);
});
Handlebars.registerHelper("concat", (...args) => {
let outStr = "";
for (const arg in args) {
if (typeof args[arg] !== "object") {
outStr += args[arg];
}
}
return outStr;
});
Handlebars.registerHelper("toLowerCase", (str) => {
try {
return str.toLowerCase();
} catch (error) {
return "";
}
});
Handlebars.registerHelper("toUpperCase", (str) => {
try {
return str.toUpperCase();
} catch (error) {
return "";
}
});
Handlebars.registerHelper("if_eq", (a, b, opts) => {
if (a === b) {
return opts.fn(this);
}
return opts.inverse(this);
});
Handlebars.registerHelper("if_not_eq", (a, b, opts) => {
if (a !== b) {
return opts.fn(this);
}
return opts.inverse(this);
});
Handlebars.registerHelper("if_gt", (a, b, trueVal, falseVal) => {
if (a > b) {
return trueVal;
}
return falseVal;
});
/** Roll visibility modes for dialog selects (v14+ CONFIG.ChatMessage.modes keys). */
const ROLL_MESSAGE_MODE_KEYS = ["public", "gm", "blind", "self"];
Handlebars.registerHelper("getAvailableRollModes", () => {
try {
return Object.fromEntries(
ROLL_MESSAGE_MODE_KEYS.filter((key) => key in CONFIG.ChatMessage.modes).map(
(key) => [key, CONFIG.ChatMessage.modes[key]],
),
);
} catch (error) {
return console.log(error);
}
});
Handlebars.registerHelper("getDefaultRollMode", () => {
try {
return game.settings.get("core", "messageMode");
} catch (error) {
return console.log(error);
}
});
Handlebars.registerHelper("localizeWeaponSkill", (skill) => {
let label = skill;
try {
if (skill === "dex") {
label = game.i18n.localize("DG.Attributes.dex");
}
if (skill === "DG.Skills.custom") {
label = game.i18n.localize("DG.ItemWindow.Custom");
} else {
label = game.i18n.localize(`DG.Skills.${skill}`);
}
} catch (error) {
console.log(error);
}
return label;
});
Handlebars.registerHelper("keepSanityPrivate", () => {
let setting = false;
try {
setting = game.settings.get("deltagreen", "keepSanityPrivate");
if (game.user.isGM) {
setting = false;
}
} catch (ex) {
setting = false;
}
return setting;
});
Handlebars.registerHelper("playerHasGamemasterPrivileges", () => {
return game.user.isGM;
});
Handlebars.registerHelper("showImpossibleLandscapesContent", () => {
let result = false;
const setting = game.settings.get(
"deltagreen",
"showImpossibleLandscapesContent",
);
if (game.user.isGM === true && setting === true) {
result = true;
}
return result;
});
}
+126
View File
@@ -0,0 +1,126 @@
/**
* Reorders a flat array into column-major order for grid display.
*
* @param {object[]} arr
* @param {number} 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);
const extraColumns = arr.length % numCols;
const colHeights = new Array(numCols).fill(baseRowCount);
for (let i = 0; i < extraColumns; i++) {
colHeights[i] += 1;
}
let index = 0;
for (let col = 0; col < numCols; col++) {
const rowCount = colHeights[col];
for (let row = 0; row < rowCount; row++) {
const newIndex = numCols * row + col;
if (newIndex < arr.length) {
reordered[newIndex] = arr[index];
index += 1;
}
}
}
return reordered;
}
/**
* @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.
*
* @param {object[]} groups
* @param {number} numCols
* @param {boolean} sortByColumn
* @returns {object[][]}
*/
export function packSkillGroupsIntoColumns(groups, numCols, sortByColumn) {
if (sortByColumn) {
return packSkillGroupsByColumn(groups, numCols);
}
return packSkillGroupsByRow(groups, numCols);
}
+58
View File
@@ -0,0 +1,58 @@
import DG from "../config.js";
const MIGRATION_VERSION = 2;
const ACTOR_TYPES_WITH_SKILLS = ["agent", "npc", "unnatural"];
const OBSOLETE_WORLD_SETTINGS = [
"characterSheetFont",
"characterSheetBackgroundImageSetting",
];
/**
* Run one-time world migrations for the Delta Green system.
*
* @returns {Promise<void>}
*/
export async function runWorldMigration() {
if (!game.user.isGM) return;
const currentVersion =
game.settings.get(DG.ID, "schemaMigrationVersion") ?? 0;
if (currentVersion >= MIGRATION_VERSION) return;
let migratedActors = 0;
const actors = game.actors.filter((actor) =>
ACTOR_TYPES_WITH_SKILLS.includes(actor.type),
);
for (const actor of actors) {
if (!actor.system.skills?.ritual) continue;
await actor.update({
"system.skills.-=ritual": null,
"system.schemaVersion": MIGRATION_VERSION,
});
migratedActors += 1;
}
removeObsoleteWorldSettings();
await game.settings.set(DG.ID, "schemaMigrationVersion", MIGRATION_VERSION);
console.log(
`Delta Green | World migration v${MIGRATION_VERSION} complete. Removed legacy ritual skill from ${migratedActors} actor(s).`,
);
}
/**
* @returns {void}
*/
function removeObsoleteWorldSettings() {
const worldConfig = game.settings.storage.get("world")?.document?.config;
const dgConfig = worldConfig?.[DG.ID];
if (!dgConfig) return;
for (const key of OBSOLETE_WORLD_SETTINGS) {
delete dgConfig[key];
}
}
@@ -108,39 +108,18 @@
{{#if_eq block.type "training" }}
<div class="item flexrow flex-group-left flex-thin-border special-training-box">
<div class="flexrow">
{{#if_not_eq (localize (concat 'DG.Attributes.' block.key)) (concat "DG.Attributes." block.key) }}
<label class="{{if_gt block.targetNumber 0 'rollable' 'not-rollable'}} skill-label"
data-action="roll"
data-key="{{block.key}}"
data-name="{{block.name}}"
data-rolltype="special-training"
{{#unless (eq ../../skillTooltipDisplay "never") }}
data-tooltip="{{localize (concat 'DG.Attributes.Tooltip.' block.key)}}"
{{/unless }}>
{{block.name}} ({{localize (concat 'DG.Attributes.' block.key)}}, {{block.targetNumber}}%)
</label>
{{else}}
{{#if_not_eq (localize (concat 'DG.Skills.' block.key)) (concat "DG.Skills." block.key) }}
<label class="{{if_gt block.targetNumber 0 'rollable' 'not-rollable'}} skill-label"
data-action="roll"
data-key="{{block.key}}"
data-name="{{block.name}}"
data-rolltype="special-training"
{{#unless (eq ../../skillTooltipDisplay "never") }}
data-tooltip="{{localize (concat 'DG.Skills.Tooltip.' block.key)}}"
{{/unless }}>
{{block.name}} ({{localize (concat 'DG.Skills.' block.key)}}, {{block.targetNumber}}%)
</label>
{{else}}
<label class="{{if_gt block.targetNumber 0 'rollable' 'not-rollable'}} skill-label"
data-action="roll"
data-key="{{block.key}}"
data-name="{{block.name}}"
data-rolltype="special-training">
{{block.name}} ({{block.attribute}}, {{block.targetNumber}}%)
</label>
{{/if_not_eq}}
{{/if_not_eq}}
<label class="{{if_gt block.targetNumber 0 'rollable' 'not-rollable'}} skill-label"
data-action="roll"
data-key="{{block.key}}"
data-name="{{block.name}}"
data-rolltype="special-training"
{{#if block.attributeTooltip }}
{{#unless (eq ../../skillTooltipDisplay "never") }}
data-tooltip="{{localize block.attributeTooltip}}"
{{/unless }}
{{/if }}>
{{block.displayLabel}}
</label>
<div class="item-controls skill-and-training-controls custom-skill-edit-controls">
<a class="item-control"
data-action="specialTrainingAction"
@@ -92,23 +92,13 @@
<div class="item flexrow flex-group-left flex-thin-border special-training-box">
<div class="flexrow">
{{#if_not_eq (localize (concat 'DG.Attributes.' skill.key)) (concat "DG.Attributes." skill.key) }}
<label {{#unless (eq ../../skillTooltipDisplay "never") }}
data-tooltip="{{localize (concat 'DG.Attributes.Tooltip.' skill.key)}}"
{{/unless }}>
{{skill.name}} ({{localize (concat 'DG.Attributes.' skill.key)}}, {{skill.targetNumber}}%)
</label>
{{else}}
{{#if_not_eq (localize (concat 'DG.Skills.' skill.key)) (concat "DG.Skills." skill.key) }}
<label {{#unless (eq ../../skillTooltipDisplay "never") }}
data-tooltip="{{localize (concat 'DG.Skills.Tooltip.' skill.key)}}"
{{/unless }}>
{{skill.name}} ({{localize (concat 'DG.Skills.' skill.key)}}, {{skill.targetNumber}}%)
</label>
{{else}}
<label>{{skill.name}} ({{skill.attribute}}, {{skill.targetNumber}}%)</label>
{{/if_not_eq}}
{{/if_not_eq}}
<label {{#if skill.attributeTooltip }}
{{#unless (eq ../../skillTooltipDisplay "never") }}
data-tooltip="{{localize skill.attributeTooltip}}"
{{/unless }}
{{/if }}>
{{skill.displayLabel}}
</label>
<label class="{{if_gt skill.targetNumber 0 'rollable' 'not-rollable'}}"
data-action="roll"
data-key="{{skill.key}}"
-98
View File
@@ -1,98 +0,0 @@
{{!-- djlint:off H020 --}}
{{!-- PERSONAL (Biography) Tab --}}
<div class="tab bio {{tabs.bio.cssClass}}"
data-group="primary"
data-tab="bio">
<div class="horizontal-bar-section-divider">
<span>{{localize "DG.Sheet.BlockHeaders.PersonalInfoCV"}}</span>
</div>
<div class="bio-grid-5col bio-grid-top-border">
<span>{{localize 'DG.CV.Sex'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.sex"
value="{{actor.system.biography.sex}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.SexPlaceholder'}}" />
<span></span>
<span class="bio-grid-label">{{localize 'DG.CV.AgeAndDoB'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.age"
value="{{actor.system.biography.age}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.AgeAndDoBPlaceholder'}}" />
</div>
<div class="bio-grid-2col">
<span>{{localize 'DG.CV.Employer'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.employer"
value="{{actor.system.biography.employer}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.EmployerPlaceholder'}}" />
<span>{{localize 'DG.CV.Nationality'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.nationality"
value="{{actor.system.biography.nationality}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.NationalityPlaceholder'}}" />
</div>
<div class="bio-grid-2col">
<span>{{localize 'DG.CV.Education'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.education"
value="{{actor.system.biography.education}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.EducationPlaceholder'}}" />
</div>
{{#if (showImpossibleLandscapesContent) }}
<div class="yellow-king yellow-king-grid-9col">
<span class="yellow-king yellow-king-rotate2">{{localize 'DG.ImpossibleLanscapes.IHaveSeenThe'}}</span>
<span></span>
<span class="yellow-king yellow-sign">{{localize 'DG.ImpossibleLanscapes.YellowSign'}}</span>
<input type="checkbox"
name="system.corruption.haveSeenTheYellowSign"
{{checked actor.system.corruption.haveSeenTheYellowSign}}
data-dtype="Boolean" />
<span></span>
<span class="yellow-king yellow-king-rotate1">{{localize 'DG.ImpossibleLanscapes.CorruptionFirstHalf'}}</span>
<span class="yellow-king yellow-king-rotate4">{{localize 'DG.ImpossibleLanscapes.CorruptionSecondHalf'}}</span>
<span></span>
<input class="yellow-king"
type="number"
placeholder="0"
name="system.corruption.value"
value="{{actor.system.corruption.value}}" />
</div>
<div class="yellow-king yellow-king-grid-5col">
<span class="yellow-king-label">{{localize 'DG.ImpossibleLanscapes.Gift'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.corruption.gift"
value="{{actor.system.corruption.gift}}"
data-dtype="String" />
<span></span>
<span class="yellow-king-label">{{localize 'DG.ImpossibleLanscapes.Insight'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.corruption.insight"
value="{{actor.system.corruption.insight}}"
data-dtype="String" />
</div>
{{/if}}
{{> "systems/deltagreen/templates/actor/partials/notes-partial.html"}}
</div>
-98
View File
@@ -1,98 +0,0 @@
{{!-- djlint:off H020 --}}
{{!-- PERSONAL (Biography) Tab --}}
<div class="tab bio {{tabs.bio.cssClass}}"
data-group="primary"
data-tab="bio">
<div class="horizontal-bar-section-divider">
<span>{{localize "DG.Sheet.BlockHeaders.PersonalInfoCV"}}</span>
</div>
<div class="bio-grid-5col bio-grid-top-border">
<span>{{localize 'DG.CV.Sex'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.sex"
value="{{actor.system.biography.sex}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.SexPlaceholder'}}" />
<span></span>
<span class="bio-grid-label">{{localize 'DG.CV.AgeAndDoB'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.age"
value="{{actor.system.biography.age}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.AgeAndDoBPlaceholder'}}" />
</div>
<div class="bio-grid-2col">
<span>{{localize 'DG.CV.Employer'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.employer"
value="{{actor.system.biography.employer}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.EmployerPlaceholder'}}" />
<span>{{localize 'DG.CV.Nationality'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.nationality"
value="{{actor.system.biography.nationality}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.NationalityPlaceholder'}}" />
</div>
<div class="bio-grid-2col">
<span>{{localize 'DG.CV.Education'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.biography.education"
value="{{actor.system.biography.education}}"
data-dtype="String"
placeholder="{{localize 'DG.CV.EducationPlaceholder'}}" />
</div>
{{#if (showImpossibleLandscapesContent) }}
<div class="yellow-king yellow-king-grid-9col">
<span class="yellow-king yellow-king-rotate2">{{localize 'DG.ImpossibleLanscapes.IHaveSeenThe'}}</span>
<span></span>
<span class="yellow-king yellow-sign">{{localize 'DG.ImpossibleLanscapes.YellowSign'}}</span>
<input type="checkbox"
name="system.corruption.haveSeenTheYellowSign"
{{checked actor.system.corruption.haveSeenTheYellowSign}}
data-dtype="Boolean" />
<span></span>
<span class="yellow-king yellow-king-rotate1">{{localize 'DG.ImpossibleLanscapes.CorruptionFirstHalf'}}</span>
<span class="yellow-king yellow-king-rotate4">{{localize 'DG.ImpossibleLanscapes.CorruptionSecondHalf'}}</span>
<span></span>
<input class="yellow-king"
type="number"
placeholder="0"
name="system.corruption.value"
value="{{actor.system.corruption.value}}" />
</div>
<div class="yellow-king yellow-king-grid-5col">
<span class="yellow-king-label">{{localize 'DG.ImpossibleLanscapes.Gift'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.corruption.gift"
value="{{actor.system.corruption.gift}}"
data-dtype="String" />
<span></span>
<span class="yellow-king-label">{{localize 'DG.ImpossibleLanscapes.Insight'}}</span>
<input class="fill-remaining-space"
type="text"
name="system.corruption.insight"
value="{{actor.system.corruption.insight}}"
data-dtype="String" />
</div>
{{/if}}
{{> "systems/deltagreen/templates/actor/partials/notes-partial.html"}}
</div>
-73
View File
@@ -1,73 +0,0 @@
<div class="tab bonds {{tabs.bonds.cssClass}}"
data-group="primary"
data-tab="bonds">
<div class="horizontal-bar-section-divider">
<span>{{localize "DG.Sheet.BlockHeaders.BondsRelationshipsContacts"}}</span>
</div>
<div>
<ol class="items-list">
<li class="item flexrow item-header">
<div class="item-bond-damage">
<a data-action="clearBondDamage"
title="{{localizeWithFallback 'DG.Generic.ClearAll' 'Clear All'}}"><i class="fas fa-arrow-rotate-left"></i></a>
</div>
<div class="item-image"></div>
<div class="item-name">{{localize 'DG.Bonds.Name'}}</div>
<div class="item-name">{{localize 'DG.Bonds.Relationship'}}</div>
<div class="item-name">{{localize 'DG.Bonds.Score'}}</div>
<div class="item-controls">
<a class="item-control action-pill"
data-action="itemAction"
data-action-type="create"
data-item-type="bond"
title="Create item">
<i class="fas fa-plus"></i>
{{localize 'DG.Generic.AddButtonLabel'}}
</a>
</div>
</li>
{{#each actor.items as |item id| }}
{{#if_eq item.type "bond" }}
<li class="item flexrow" data-item-id="{{item.id}}">
<input class="item-bond-damage"
type="checkbox"
data-action="toggle"
data-prop="system.hasBeenDamagedSinceLastHomeScene"
title="Check if bond has been damaged since last home scene."
{{checked item.system.hasBeenDamagedSinceLastHomeScene}} />
<div class="item-image">
<img src="{{item.img}}" title="{{item.name}}" width="24" height="24" />
</div>
<span class="item-name">{{item.name}}</span>
<span>{{item.system.relationship}}</span>
<span>{{item.system.score}}</span>
<div class="item-controls">
<a class="item-control"
data-action="itemAction"
data-action-type="edit"
title="Edit Item">
<i class="fas fa-edit"></i>
</a>
<a class="item-control"
data-action="itemAction"
data-action-type="delete"
title="Delete Item">
<i class="fas fa-trash"></i>
</a>
</div>
</li>
{{/if_eq}}
{{/each}}
</ol>
<div>
<span>{{localizeWithFallback "DG.Generic.CheckDamagedBondsBoxUntilNextHomeScene" "Check a damaged Bond's box until the next Home scene ends." }}</span>
</div>
</div>
</div>
+4
View File
@@ -144,6 +144,10 @@
data-dtype="Number" />
</div>
<div class="horizontal-bar-section-divider">
<span>{{localize "DG.Sheet.BlockHeaders.Adaptations"}}</span>
</div>
<div class="adaptation-grid-4col">
<label data-tooltip="{{localize 'DG.Mental.Tooltip.Violence'}}">
-149
View File
@@ -1,149 +0,0 @@
{{!-- Psychological Tab (formerly 'Mental', formerly 'Motivations') --}}
<div class="tab motivations {{tabs.motivations.cssClass}}"
data-group="primary"
data-tab="motivations">
<div class="horizontal-bar-section-divider">
<span>{{localize "DG.Sheet.BlockHeaders.BreakingPointAndAdaptations"}}</span>
</div>
<div>
<div class="breaking-point-grid-3col">
<label class="bolded-label"
data-tooltip="{{localize 'DG.Mental.Tooltip.BreakingPoint'}}">
{{localize 'DG.Mental.BreakingPoint'}}
</label>
<input class="centered"
title="{{localize 'DG.Tooltip.BreakPoint'}}"
type="text"
name="system.sanity.currentBreakingPoint"
value="{{actor.system.sanity.currentBreakingPoint}}"
data-dtype="Number" />
<a class="btn-tiny"
data-action="resetBreakingPoint"
title="{{localize 'DG.Tooltip.BreakPointReset'}}">{{localize 'DG.Mental.Reset'}}</a>
</div>
<div>
<label class="bolded-label">{{localize 'DG.Mental.SanityLossIncidentsLabel'}}</label>
<div class="adaptation-grid-5col">
<label data-tooltip="{{localize 'DG.Mental.Tooltip.Violence'}}">
{{localize 'DG.Mental.Violence'}}
</label>
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.sanity.adaptations.violence.incident1"
data-tooltip="{{localize 'DG.Mental.Tooltip.Checkbox.Violence'}}"
{{checked actor.system.sanity.adaptations.violence.incident1}}
data-dtype="Boolean" />
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.sanity.adaptations.violence.incident2"
data-tooltip="{{localize 'DG.Mental.Tooltip.Checkbox.Violence'}}"
{{checked actor.system.sanity.adaptations.violence.incident2}}
data-dtype="Boolean" />
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.sanity.adaptations.violence.incident3"
data-tooltip="{{localize 'DG.Mental.Tooltip.Checkbox.Violence'}}"
{{checked actor.system.sanity.adaptations.violence.incident3}}
data-dtype="Boolean" />
<label class="adapted-label"
data-tooltip="{{localize 'DG.Tooltip.AdaptedToViolence'}}">
{{#if actor.system.sanity.adaptations.violence.isAdapted }}
{{localize 'DG.Mental.AdaptedToViolence'}}
{{/if}}
</label>
<label data-tooltip="{{localize 'DG.Mental.Tooltip.Helplessness'}}">
{{localize 'DG.Mental.Helplessness'}}
</label>
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.sanity.adaptations.helplessness.incident1"
data-tooltip="{{localize 'DG.Mental.Tooltip.Checkbox.Helplessness'}}"
{{checked actor.system.sanity.adaptations.helplessness.incident1}}
data-dtype="Boolean" />
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.sanity.adaptations.helplessness.incident2"
data-tooltip="{{localize 'DG.Mental.Tooltip.Checkbox.Helplessness'}}"
{{checked actor.system.sanity.adaptations.helplessness.incident2}}
data-dtype="Boolean" />
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.sanity.adaptations.helplessness.incident3"
data-tooltip="{{localize 'DG.Mental.Tooltip.Checkbox.Helplessness'}}"
{{checked actor.system.sanity.adaptations.helplessness.incident3}}
data-dtype="Boolean" />
<label class="adapted-label"
data-tooltip="{{localize 'DG.Tooltip.AdaptedToHelplessness'}}">
{{#if actor.system.sanity.adaptations.helplessness.isAdapted }}
{{localize 'DG.Mental.AdaptedToHelplessness'}}
{{/if}}
</label>
</div>
</div>
</div>
<div class="horizontal-bar-section-divider">
<span>{{localize "DG.Sheet.BlockHeaders.MotivationsAndDisorders"}}</span>
</div>
<div>
<ol class="items-list">
<li class="item flexrow item-header">
<div class="item-name">{{localize 'DG.Mental.Motivations'}}</div>
<div class="item-name">{{localize 'DG.Mental.Disorders'}}</div>
<div class="item-controls">
<a class="item-control action-pill"
data-action="itemAction"
data-action-type="create"
data-item-type="motivation"
title="Create item">
<i class="fas fa-plus"></i>
{{localize 'DG.Generic.AddButtonLabel'}}
</a>
</div>
</li>
{{#each actor.items as |item id| }}
{{#if_eq item.type 'motivation' }}
<li class="item flexrow" data-item-id="{{item.id}}">
<span class="item-name"
{{#if item.system.crossedOut }}
style="text-decoration: line-through"
{{/if }}>
{{item.name}}
</span>
{{#if item.system.crossedOut }}
<span class="item-name">{{item.system.disorder}}</span>
{{/if}}
<div class="item-controls">
<a class="item-control"
data-action="itemAction"
data-action-type="edit"
title="Edit Item">
<i class="fas fa-edit"></i>
</a>
<a class="item-control"
data-action="itemAction"
data-action-type="delete"
title="Delete Item">
<i class="fas fa-trash"></i>
</a>
</div>
</li>
{{/if_eq}}
{{/each}}
</ol>
</div>
</div>
@@ -1,59 +0,0 @@
{{!-- Physical Statistics like STR, CON, etc. --}}
<div class="tab physical {{tabs.physical.cssClass}}"
data-group="primary"
data-tab="physical">
<div class="horizontal-bar-section-divider">
<span>{{localize "DG.Sheet.BlockHeaders.Injuries"}}</span>
</div>
<div>
<div class="first-aid-received-section resource">
<label class="bolded-label first-aid-label"
title="{{localize 'DG.Tooltip.FirstAidAttempted'}}">
{{localize 'DG.Physical.FirstAidLabel'}}
</label>
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.physical.firstAidAttempted"
{{checked actor.system.physical.firstAidAttempted}}
data-dtype="Boolean"
title="{{localize 'DG.Tooltip.FirstAidAttempted'}}" />
</div>
<div class="exhausted-section resource">
<label class="bolded-label exhaustion-label"
title="{{localize 'DG.Tooltip.Exhaustion'}}">
{{localize 'DG.Physical.Exhausted'}}
</label>
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.physical.exhausted"
{{checked actor.system.physical.exhausted}}
data-dtype="Boolean"
title="{{localize 'DG.Tooltip.Exhaustion'}}" />
{{#if actor.system.physical.exhausted }}
<label class="bolded-label exhaustion-label"
title="{{localize 'DG.Tooltip.Exhaustion'}}">
{{localize 'DG.Physical.ExhaustionPenalty'}}
</label>
<input type="number"
name="system.physical.exhaustedPenalty"
value="{{actor.system.physical.exhaustedPenalty}}"
data-dtype="Number"
title="{{localize 'DG.Tooltip.Exhaustion'}}"
min="-99"
max="0" />
{{/if}}
</div>
<div>
<label class="bolded-label">{{localize 'DG.Physical.Wounds'}}</label>
<!-- This seems to save values, but not retrieve them... -->
<textarea class="wounds" name="system.physical.wounds">{{actor.system.physical.wounds}}</textarea>
</div>
</div>
</div>
-61
View File
@@ -1,61 +0,0 @@
{{!-- Physical Statistics like STR, CON, etc. --}}
<div class="tab physical {{tabs.physical.cssClass}}"
data-group="primary"
data-tab="physical">
{{> "systems/deltagreen/templates/actor/partials/attributes-grid-partial.html"}}
<div class="horizontal-bar-section-divider">
<span>{{localize "DG.Sheet.BlockHeaders.Injuries"}}</span>
</div>
<div>
<div class="first-aid-received-section resource">
<label class="bolded-label first-aid-label"
title="{{localize 'DG.Tooltip.FirstAidAttempted'}}">
{{localize 'DG.Physical.FirstAidLabel'}}
</label>
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.physical.firstAidAttempted"
{{checked actor.system.physical.firstAidAttempted}}
data-dtype="Boolean"
title="{{localize 'DG.Tooltip.FirstAidAttempted'}}" />
</div>
<div class="exhausted-section resource">
<label class="bolded-label exhaustion-label"
title="{{localize 'DG.Tooltip.Exhaustion'}}">
{{localize 'DG.Physical.Exhausted'}}
</label>
<input class="labeled-resource-checkbox"
type="checkbox"
name="system.physical.exhausted"
{{checked actor.system.physical.exhausted}}
data-dtype="Boolean"
title="{{localize 'DG.Tooltip.Exhaustion'}}" />
{{#if actor.system.physical.exhausted }}
<label class="bolded-label exhaustion-label"
title="{{localize 'DG.Tooltip.Exhaustion'}}">
{{localize 'DG.Physical.ExhaustionPenalty'}}
</label>
<input type="number"
name="system.physical.exhaustedPenalty"
value="{{actor.system.physical.exhaustedPenalty}}"
data-dtype="Number"
title="{{localize 'DG.Tooltip.Exhaustion'}}"
min="-99"
max="0" />
{{/if}}
</div>
<div>
<label class="bolded-label">{{localize 'DG.Physical.Wounds'}}</label>
<!-- This seems to save values, but not retrieve them... -->
<textarea class="wounds" name="system.physical.wounds">{{actor.system.physical.wounds}}</textarea>
</div>
</div>
</div>
+5 -5
View File
@@ -1,8 +1,8 @@
<div class="right-bar">
{{> "systems/deltagreen/templates/actor/parts/skills-tab-v2.html" }}
{{> "systems/deltagreen/templates/actor/parts/combat-tab-v2.html" }}
{{> "systems/deltagreen/templates/actor/parts/motivations-tab-v2.html" }}
{{> "systems/deltagreen/templates/actor/parts/gear-tab-v2.html" }}
{{> "systems/deltagreen/templates/actor/parts/personal-tab-v2.html" }}
{{> "systems/deltagreen/templates/actor/parts/skills-tab-agent.html" }}
{{> "systems/deltagreen/templates/actor/parts/combat-tab-agent.html" }}
{{> "systems/deltagreen/templates/actor/parts/motivations-tab-agent.html" }}
{{> "systems/deltagreen/templates/actor/parts/gear-tab-agent.html" }}
{{> "systems/deltagreen/templates/actor/parts/personal-tab-agent.html" }}
{{> "systems/deltagreen/templates/actor/parts/about-tab.html" }}
</div>
@@ -27,9 +27,7 @@
{{localizeWithFallback (concat "DG.Skills." skill.key) skill.label }}
</label>
<input class="percentile-skill-input {{!-- This use of helpers is a bit hacky and should be consolidated in _prepareContext --}}
{{#if (and (ne skill.proficiency skill.targetProficiency) (eq ../actor.type "agent")) }}
target-proficiency-mod{{/if }}"
<input class="percentile-skill-input {{skill.proficiencyInputClass}}"
type="text"
name="system.skills.{{skill.key }}.proficiency"
value="{{skill.proficiency }}"
@@ -47,7 +45,7 @@
{{/each}}
</div>
<div>{{> "systems/deltagreen/templates/actor/partials/custom-skills-partial-v2.html" }}</div>
<div>{{> "systems/deltagreen/templates/actor/partials/custom-skills-partial-agent.html" }}</div>
{{#if showNotesInSkills }}
{{> "systems/deltagreen/templates/actor/partials/notes-partial.html"}}
+1 -3
View File
@@ -29,9 +29,7 @@
<i class="fas fa-dice"></i>
</label>
<input class="percentile-skill-input {{!-- This use of helpers is a bit hacky and should be consolidated in _prepareContext --}}
{{#if (and (ne skill.proficiency skill.targetProficiency) (eq ../actor.type "agent")) }}
target-proficiency-mod{{/if }}"
<input class="percentile-skill-input {{skill.proficiencyInputClass}}"
type="text"
name="system.skills.{{skill.key }}.proficiency"
value="{{skill.proficiency }}"
+2 -2
View File
@@ -24,8 +24,8 @@
<input class="numeric-label-field" type="number" name="targetModifier" value="{{data.targetModifier}}" title="{{localize 'DG.ModifySkillRollDialogue.ModifierTooltip'}}" min="0" max="99" step="1" oninput="validity.valid||(value='');"/>
</div>
<label for="rollMode">{{ localize "DG.RollMode" }}:</label>
<select name="rollMode">
<label for="messageMode">{{ localize "DG.RollMode" }}:</label>
<select name="messageMode">
{{selectOptions (getAvailableRollModes) selected=(getDefaultRollMode) localize=true}}
</select>
</div>
+30
View File
@@ -0,0 +1,30 @@
<div class="dice-roll" data-action="expandRoll">
<div class="dice-result">
<div style="{{styleOverride}}" class="dice-formula">{{resultString}}</div>
<div class="dice-tooltip">
<div class="wrapper">
<section class="tooltip-part">
<div class="dice">
<header class="part-header flexrow">
<span class="part-formula">d100</span>
<span class="part-total">{{total}}</span>
</header>
<ol class="dice-rolls">
<li class="roll die d100">{{total}}</li>
</ol>
<hr>
<header class="part-header flexrow">
<span class="part-formula">2d10 (d10 + d10)</span>
<span class="part-total">{{nonLethalTotal}}</span>
</header>
<ol class="dice-rolls">
<li class="roll die d10">{{die1}}</li>
<li class="roll die d10">{{die2}}</li>
</ol>
</div>
</section>
</div>
</div>
<h4 class="dice-total">{{total}} ({{nonLethalTotal}} {{damageLabel}})</h4>
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
<div class="dice-roll" data-action="expandRoll">
<div class="dice-result">
<div class="dice-formula">{{lowFormula}} / {{highFormula}}</div>
<div class="dice-tooltip">
<div class="wrapper">
<section class="tooltip-part">
<div class="dice">
<header class="part-header flexrow">
<span class="part-formula">{{lowFormula}}</span>
<span class="part-total">{{lowResult}}</span>
</header>
<ol class="dice-rolls">
<li class="roll die d{{lowFaces}}">{{lowResult}}</li>
</ol>
<hr>
<header class="part-header flexrow">
<span class="part-formula">{{highFormula}}</span>
<span class="part-total">{{highResult}}</span>
</header>
<ol class="dice-rolls">
<li class="roll die d{{highFaces}}">{{highResult}}</li>
</ol>
</div>
</section>
</div>
</div>
<h4 class="dice-total">{{lowResult}} / {{highResult}}</h4>
</div>
</div>