Large scale refactor to bring system in line with modern Foundry system guidelines and improve maintainability.
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
@@ -47,7 +47,6 @@ const DG = /** @type {const} */ ({
|
||||
"swim",
|
||||
"unarmed_combat",
|
||||
"unnatural",
|
||||
"ritual",
|
||||
],
|
||||
|
||||
skillImprovementFormulas: {
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 +0,0 @@
|
||||
export { default } from "../sheets/base-item-sheet.js";
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+23
-1226
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @param {...Function} mixins
|
||||
* @returns {Function}
|
||||
*/
|
||||
export default function composeMixins(...mixins) {
|
||||
return (Base) => mixins.reduceRight((acc, mixin) => mixin(acc), Base);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
+12
-33
@@ -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}}"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'}}">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
+2
-4
@@ -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"}}
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user