diff --git a/css/deltagreen.css b/css/deltagreen.css
index 09cf6e2..c2070f5 100644
--- a/css/deltagreen.css
+++ b/css/deltagreen.css
@@ -1044,6 +1044,32 @@ input.item-sheet-short-input {
padding-right: 5px;
}
+/*
+ Tome Item Sheet
+*/
+
+.tome-sheet-horizontal-grid-2col {
+ display: grid;
+ grid-template-columns: 100px auto;
+ align-items: center;
+ text-align: left;
+ column-gap: 10px;
+ row-gap: 10px;
+ padding-bottom: 10px;
+ padding-right: 5px;
+}
+
+.tome-sheet-handler-info-horizontal-grid-2col {
+ display: grid;
+ grid-template-columns: max-content auto;
+ align-items: left;
+ text-align: left;
+ column-gap: 10px;
+ row-gap: 10px;
+ padding-bottom: 10px;
+ padding-right: 5px;
+}
+
/*
Unnatural Character Sheet Specific Classes
*/
@@ -1146,3 +1172,11 @@ input.item-sheet-short-input {
align-self: flex-start;
text-align: left;
}
+
+.armor-piercing-chat-card-img {
+ border-radius: 10%;
+ height: 20px !important;
+ width: 20px !important;
+ margin: 0px;
+ vertical-align: text-bottom;
+}
diff --git a/lang/en.json b/lang/en.json
index 8aea9ad..ed7450d 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -287,5 +287,10 @@
"TYPES.Item.bond": "Bond",
"TYPES.Item.gear": "Gear",
"TYPES.Item.motivation": "Motivation",
- "TYPES.Item.weapon": "Weapon"
+ "TYPES.Item.weapon": "Weapon",
+
+ "DG.Tome.Name": "Name",
+ "DG.ItemWindow.Tome.Description": "Description",
+ "DG.ItemWindow.Tome.LanguagePlaceholder": "Language",
+ "DG.ItemWindow.Tome.StudyTime": "Study Time"
}
diff --git a/module/deltagreen.js b/module/deltagreen.js
index 6bdf9d7..88fa6a8 100644
--- a/module/deltagreen.js
+++ b/module/deltagreen.js
@@ -81,6 +81,14 @@ Hooks.on("preCreateItem", (item) => {
item.updateSource({
img: "systems/deltagreen/assets/icons/person-black-bg.svg",
});
+ } else if (item.type === "tome") {
+ item.updateSource({
+ img: "systems/deltagreen/assets/icons/book-cover.svg",
+ });
+ } else if (item.type === "ritual") {
+ item.updateSource({
+ img: "systems/deltagreen/assets/icons/bookmarklet.svg",
+ });
} else {
item.updateSource({
img: "systems/deltagreen/assets/icons/swap-bag-black-bg.svg",
diff --git a/module/item/item-sheet.js b/module/item/item-sheet.js
index 46c11c2..77ed2d7 100644
--- a/module/item/item-sheet.js
+++ b/module/item/item-sheet.js
@@ -1,3 +1,10 @@
+import {
+ DGPercentileRoll,
+ DGLethalityRoll,
+ DGDamageRoll,
+ DGSanityDamageRoll,
+} from "../roll/roll.js";
+
/**
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
@@ -32,10 +39,19 @@ export default class DeltaGreenItemSheet extends ItemSheet {
/** @override */
async getData() {
const data = super.getData();
+
data.enrichedDescription = await TextEditor.enrichHTML(
this.object.system.description,
{ async: true },
);
+
+ if (data.item.type === "tome" || data.type === "ritual") {
+ data.enrichedHandlerNotes = await TextEditor.enrichHTML(
+ this.object.system.handlerNotes,
+ { async: true },
+ );
+ }
+
return data;
}
@@ -60,6 +76,115 @@ export default class DeltaGreenItemSheet extends ItemSheet {
// eslint-disable-next-line no-useless-return
if (!this.options.editable) return;
- // Roll handlers, click handlers, etc. would go here.
+ // Rollable abilities - bind to everything with the 'Rollable' class
+ html.find(".rollable").click(this._onRoll.bind(this));
+ html.find(".rollable").contextmenu(this._onRoll.bind(this)); // this is for right-click, which triggers the roll modifier dialogue for most rolls
+
+ // toggle whether information is shown to a player for a tome or not
+ html.find(".tome-show-hide-info").click((event) => {
+ event.preventDefault();
+
+ this.item.system.revealed = !this.item.system.revealed;
+
+ this.item.update({ "system.revealed": this.item.system.revealed });
+ });
+ }
+
+ /**
+ * Handle clickable rolls.
+ *
+ * @param {Event} event The originating click event
+ * @async
+ * @private
+ */
+ async _onRoll(event) {
+ event.preventDefault();
+ const element = event.currentTarget;
+ const { dataset } = element;
+
+ if (event && event.which === 2) {
+ // probably don't want rolls to trigger from a middle mouse click so just kill it here
+ return;
+ }
+
+ //const item = this.actor.items.get(dataset.iid);
+ const rollOptions = {
+ rollType: dataset.rolltype,
+ key: dataset.key,
+ //actor: this,
+ specialTrainingName: dataset?.name || null, // Only applies to Special Training Rolls
+ item: this,
+ };
+
+ // Create a default 1d100 roll just in case.
+ let roll = new Roll("1d100", {});
+ switch (dataset.rolltype) {
+ case "stat":
+ case "skill":
+ case "sanity":
+ case "special-training":
+ case "weapon":
+ roll = new DGPercentileRoll("1D100", {}, rollOptions);
+ break;
+ case "lethality":
+ roll = new DGLethalityRoll("1D100", {}, rollOptions);
+ break;
+ case "damage": {
+ let diceFormula = item.system.damage;
+ const { skill } = item.system;
+ if (
+ this.actor.type === "agent" &&
+ (skill === "unarmed_combat" || skill === "melee_weapons")
+ ) {
+ diceFormula +=
+ this.actor.system.statistics.str.meleeDamageBonusFormula;
+ }
+ roll = new DGDamageRoll(diceFormula, {}, rollOptions);
+ break;
+ }
+ case "sanity-damage": {
+ let combinedFormula;
+ if (this.item.type === "tome") {
+ const { successLoss, failedLoss } = this.item.system.sanity;
+ combinedFormula = `{${successLoss}, ${failedLoss}}`;
+ }
+ roll = new DGSanityDamageRoll(combinedFormula, {}, rollOptions);
+ break;
+ }
+ default:
+ break;
+ }
+ this.processRoll(event, roll, rollOptions);
+ }
+
+ /**
+ * Show a dialog for the roll and then send to chat.
+ * Broke this logic out from `_onRoll()` so that other files can call it,
+ * namely the macro logic.
+ *
+ * TODO: Move this logic to the roll.js.
+ *
+ * @param {Event} event The originating click event
+ * @param {Event} roll The roll to show a dialog for and then send to chat.
+ * @async
+ */
+ async processRoll(event, roll) {
+ // Open dialog if user requests it (no dialog for Sanity Damage rolls)
+ if (
+ (event.shiftKey || event.which === 3) &&
+ !(roll instanceof DGSanityDamageRoll)
+ ) {
+ const dialogData = await roll.showDialog();
+ if (!dialogData) return;
+ if (dialogData.newFormula) {
+ roll = new DGDamageRoll(dialogData.newFormula, {}, roll.options);
+ }
+ roll.modifier += dialogData.targetModifier;
+ roll.options.rollMode = dialogData.rollMode;
+ }
+ // Evaluate the roll.
+ await roll.evaluate();
+ // Send the roll to chat.
+ roll.toChat();
}
}
diff --git a/module/roll/roll.js b/module/roll/roll.js
index 7fd7163..04ab32e 100644
--- a/module/roll/roll.js
+++ b/module/roll/roll.js
@@ -671,7 +671,9 @@ export class DGDamageRoll extends DGRoll {
.localize("DG.Roll.Damage")
.toUpperCase()} ${game.i18n.localize("DG.Roll.For")} ${
this.item.name
- }`;
+ } (${
+ this.item.system.armorPiercing
+ } )`;
} catch (ex) {
// console.log(ex);
label = `Rolling DAMAGE for ${label.toUpperCase()}`;
diff --git a/template.json b/template.json
index 1700db6..a38dd60 100644
--- a/template.json
+++ b/template.json
@@ -520,7 +520,15 @@
}
},
"Item": {
- "types": ["weapon", "armor", "motivation", "bond", "gear"],
+ "types": [
+ "weapon",
+ "armor",
+ "motivation",
+ "bond",
+ "gear",
+ "tome",
+ "ritual"
+ ],
"templates": {
"base": {
"name": "",
@@ -564,6 +572,31 @@
"templates": ["base"],
"equipped": true,
"expense": "Standard"
+ },
+ "tome": {
+ "templates": ["base"],
+ "language": "",
+ "studyTime": "days",
+ "unnaturalSkillIncrease": 5,
+ "occultSkillIncrease": 3,
+ "sanity": {
+ "notes": "",
+ "failedLoss": "1D6",
+ "successLoss": "1"
+ },
+ "handlerNotes": "",
+ "revealed": false
+ },
+ "ritual": {
+ "templates": ["base"],
+ "studyTime": "weeks",
+ "studySanCost": "1D10",
+ "studyUnnaturalIncrease": 1,
+ "activationSanCost": "1D10",
+ "activationCosts": "6 WP, 0 POW, 0 HP",
+ "complexity": "Simple",
+ "activationTime": "days",
+ "handlerNotes": ""
}
}
}
diff --git a/templates/item/item-tome-sheet.html b/templates/item/item-tome-sheet.html
new file mode 100644
index 0000000..70e934b
--- /dev/null
+++ b/templates/item/item-tome-sheet.html
@@ -0,0 +1,96 @@
+