First round of changes

This commit is contained in:
2026-04-23 14:27:33 +02:00
parent f72230dd39
commit abe35cb537
45 changed files with 1860 additions and 56 deletions

View File

@@ -64,6 +64,52 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
parseInt(sheetData.data.system.xp_total) - parseInt(sheetData.data.system.xp_spent)
);
// Chiaroscuro: Skill ranks list for <select>
sheetData.data.skillRanksList = Object.keys(CONFIG.l5r5e.skillRanks).map((id) => ({
id,
label: game.i18n.localize(`chiaroscuro.skill_ranks.${id}`),
}));
// Chiaroscuro: Normalize skill values 0 (number) → "0" (string) for selectOptions matching
for (const category of Object.values(sheetData.data.system.skills)) {
for (const [key, value] of Object.entries(category)) {
if (value === 0) category[key] = "0";
}
}
// Chiaroscuro: Aspects gauge data
const aspectsData = sheetData.data.system.aspects?.aspects ?? {};
const gauge = aspectsData.gauge ?? 0;
sheetData.data.aspectsData = {
solar: aspectsData.solar ?? 0,
lunar: aspectsData.lunar ?? 0,
gauge,
gaugePercent: ((gauge + 10) / 20) * 100,
gaugeColor: gauge > 0 ? "#d4a855" : gauge < 0 ? "#5588aa" : "#888888",
};
// Chiaroscuro: État items active on the character
sheetData.data.etatItems = sheetData.items.filter((i) => i.type === "etat");
// Chiaroscuro: Invocations split by type (from splitTechniquesList)
const invocations = sheetData.data.splitTechniquesList["mot_invocation"] ?? [];
sheetData.data.splitInvocationsList = {
general: invocations.filter((t) => !t.system.invocation_type || t.system.invocation_type === "general"),
neutre: invocations.filter((t) => t.system.invocation_type === "neutre"),
precis: invocations.filter((t) => t.system.invocation_type === "precis"),
};
// Chiaroscuro: Arcane items
sheetData.data.arcaneItems = sheetData.items.filter((i) => i.type === "arcane");
// Chiaroscuro: Identity tabs enriched HTML
sheetData.data.enrichedHtml.identity_text1 = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.actor.system.identity_text1 ?? "", { async: true }
);
sheetData.data.enrichedHtml.identity_text2 = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.actor.system.identity_text2 ?? "", { async: true }
);
return sheetData;
}
@@ -120,6 +166,14 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
// XP +/-
html.find(".xp-control").on("click", this._modifyXP.bind(this));
// Chiaroscuro: set default ring on ring-name click
html.find(".ring-set-default").on("click", (event) => {
event.preventDefault();
const ring = $(event.currentTarget).data("ring");
this.actor.update({ "system.default_ring": ring });
});
// Advancements Tab to current rank onload
// TODO class "Active" Bug on load, dunno why :/
this._tabs
@@ -127,6 +181,18 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
.activate("advancement_rank_" + (this.actor.system.identity.school_rank || 0));
}
/**
* Override base dice picker to open Chiaroscuro d6 dialog.
* @param {Event} event
*/
_openDicePickerForSkill(event) {
event.preventDefault();
const el = $(event.currentTarget);
const skillId = el.data("skill");
const ringId = el.data("ring") || this.actor.system?.default_ring || "void";
new game.l5r5e.ChiaroscuroDiceDialog({ actor: this.actor, ringId, skillId }).render(true);
}
/**
* Split the school advancement, calculate the total xp spent and the current total xp spent by rank
*/
@@ -218,6 +284,13 @@ export class CharacterSheetL5r5e extends BaseCharacterSheetL5r5e {
delete formData["system.money.zeni"];
}
// Chiaroscuro: convert skill rank "0" (string from <select>) back to 0 (number)
for (const [key, value] of Object.entries(formData)) {
if (key.startsWith("system.skills.") && value === "0") {
formData[key] = 0;
}
}
// Save computed values
const currentData = this.object.system;
formData["system.focus"] = currentData.focus;

View File

@@ -50,6 +50,21 @@ export class NpcSheetL5r5e extends BaseCharacterSheetL5r5e {
label: game.i18n.localize("l5r5e.character_types." + e),
}));
// Danger levels for martial/social danger selects
const dangerLevels = ["simple", "moyenne", "assez_difficile", "difficile"];
sheetData.data.dangerList = dangerLevels.map((id) => ({
id,
label: game.i18n.localize(`chiaroscuro.danger.${id}`),
}));
// Invocations list (mot_invocation techniques, split by type like character sheet)
const invocations = sheetData.data.splitTechniquesList?.["mot_invocation"] ?? [];
sheetData.data.splitInvocationsList = {
general: invocations.filter((t) => !t.system.invocation_type || t.system.invocation_type === "general"),
neutre: invocations.filter((t) => t.system.invocation_type === "neutre"),
precis: invocations.filter((t) => t.system.invocation_type === "precis"),
};
return sheetData;
}
@@ -101,4 +116,16 @@ export class NpcSheetL5r5e extends BaseCharacterSheetL5r5e {
return super._updateObject(event, formData);
}
/**
* Override base dice picker to open Chiaroscuro d6 dialog.
* @param {Event} event
*/
_openDicePickerForSkill(event) {
event.preventDefault();
const el = $(event.currentTarget);
const skillId = el.data("skill");
const ringId = el.data("ring") || this.actor.system?.default_ring || "void";
new game.l5r5e.ChiaroscuroDiceDialog({ actor: this.actor, ringId, skillId }).render(true);
}
}

View File

@@ -14,6 +14,50 @@ export const L5R5E = {
skillCostMultiplier: 2,
techniqueCost: 3,
},
// --- Chiaroscuro additions ---
/** Skill rank enum values and associated flat bonus */
skillRanks: {
0: { bonus: 0 },
initie: { bonus: 1 },
expert: { bonus: 2 },
maitre: { bonus: 3 },
parangon1: { bonus: 3, passive: 1 }, // dice results of 1 count as 2
parangon2: { bonus: 3, passive: 2 }, // dice results of 1-2 count as 3
parangon3: { bonus: 3, passive: 3 }, // dice results of 1-3 count as 4
},
/** Difficulty thresholds (Chiaroscuro scale) */
difficulties: {
simple: 7,
moyenne: 10,
assez_difficile: 13,
difficile: 16,
tres_difficile: 22,
heroique: 28,
improbable: 32,
},
/** Ring colors for Chiaroscuro visual style */
ringColors: {
air: "rgb(145, 120, 150)",
water: "rgb(95, 145, 155)",
fire: "rgb(155, 115, 80)",
earth: "rgb(105, 150, 120)",
void: "rgb(75, 70, 65)",
},
/** Aspect gauge configuration */
aspects: {
solarConditionId: "desequilibre_lunaire",
lunarConditionId: "desequilibre_solaire",
imbalanceThreshold: 5,
resetThreshold: 10,
},
// --- End Chiaroscuro additions ---
// For rings wound to be aligned, add them first
conditions: [{
id: "lightly_wounded_fire",
@@ -180,6 +224,16 @@ export const L5R5E = {
name: "l5r5e.conditions.unconscious",
img: "systems/l5r5e/assets/icons/conditions/unconscious.webp",
system: { id: "L5RCoreCon000015" }
},{
id: "desequilibre_solaire",
name: "chiaroscuro.aspects.desequilibre_solaire",
img: "systems/l5r5e/assets/icons/conditions/desequilibre_solaire.webp",
system: { id: "ChiaCon000001" }
},{
id: "desequilibre_lunaire",
name: "chiaroscuro.aspects.desequilibre_lunaire",
img: "systems/l5r5e/assets/icons/conditions/desequilibre_lunaire.webp",
system: { id: "ChiaCon000002" }
}],
regex: {
techniqueDifficulty: /^@([TS]):([^|]+?)(?:\|(min|max)(?:\(([^)]+?)\))?)?$/,
@@ -344,6 +398,8 @@ L5R5E.techniques.set("mastery_ability", { type: "school", displayInTypes: false
L5R5E.techniques.set("title_ability", { type: "title", displayInTypes: false });
// Custom
L5R5E.techniques.set("specificity", { type: "custom", displayInTypes: false });
// Chiaroscuro
L5R5E.techniques.set("mot_invocation", { type: "chiaroscuro", displayInTypes: true });
// *** SkillId - CategoryId ***
L5R5E.skills = new Map();

View File

@@ -0,0 +1,327 @@
/**
* Chiaroscuro Dice Dialog
*
* d6 pool system: ring value × multiplier d6, sum vs difficulty.
* Multiplier: ×1 base, ×2 if aspect or assistance, ×3 if both.
* Parangon passives adjust individual die results before summing.
*/
export class ChiaroscuroDiceDialog extends FormApplication {
/**
* Current Actor
* @type {ActorL5r5e}
* @private
*/
_actor = null;
/**
* Payload Object
*/
object = {
ring: { id: "void", value: 1 },
skill: { id: "", name: "", bonus: 0, rank: "0" },
difficulty: { id: "moyenne", value: 10 },
modifier: 0,
useAspectPoint: false,
aspectType: "solar",
useAssistance: false,
};
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "l5r5e-chiaroscuro-dice-dialog",
classes: ["l5r5e", "chiaroscuro-dice-dialog"],
template: CONFIG.l5r5e.paths.templates + "dice/chiaroscuro-dice-dialog.html",
title: game.i18n.localize("chiaroscuro.dice.title"),
width: 440,
height: "auto",
});
}
get id() {
return `l5r5e-chiaroscuro-dice-dialog-${this._actor?.id ?? "no-actor"}`;
}
get title() {
return game.i18n.localize("chiaroscuro.dice.title") + (this._actor ? " — " + this._actor.name : "");
}
/**
* Total dice to roll (ring value × multiplier)
* @return {number}
*/
get totalDice() {
const base = this.object.ring.value;
const both = this.object.useAspectPoint && this.object.useAssistance;
const either = this.object.useAspectPoint || this.object.useAssistance;
return base * (both ? 3 : either ? 2 : 1);
}
/**
* @param options actor, actorId, ringId, skillId
*/
constructor(options = {}) {
super({}, options);
// Resolve actor
[
options?.actor,
game.actors.get(options?.actorId),
canvas.tokens.controlled[0]?.actor,
game.user.character,
].forEach((actor) => {
if (!this._actor && actor instanceof Actor && actor.isOwner) {
this._actor = actor;
}
});
// Default ring: options > actor default_ring > void
const ringId = options.ringId ?? this._actor?.system?.default_ring ?? "void";
this.ringId = ringId;
// Skill
if (options.skillId) {
this.skillId = options.skillId;
}
}
/**
* Set ring (id + value from actor)
* @param {string} ringId
*/
set ringId(ringId) {
this.object.ring.id = CONFIG.l5r5e.stances.includes(ringId) ? ringId : "void";
this.object.ring.value = this._actor?.system?.rings?.[this.object.ring.id] || 1;
// Auto-derive aspect type from ring (fire/earth → solar, air/water → lunar; void = manual)
if (this.object.ring.id !== "void") {
this.object.aspectType = ["fire", "earth"].includes(this.object.ring.id) ? "solar" : "lunar";
}
}
/**
* Set skill (id, name, rank, bonus)
* @param {string} skillId
*/
set skillId(skillId) {
if (!skillId) return;
const catId = CONFIG.l5r5e.skills.get(skillId.toLowerCase().trim());
const rank = this._actor?.system?.skills?.[catId]?.[skillId] ?? "0";
this.object.skill = {
...this.object.skill,
id: skillId,
name: catId ? game.i18n.localize(`l5r5e.skills.${catId}.${skillId}`) : skillId,
rank,
bonus: CONFIG.l5r5e.skillRanks?.[rank]?.bonus ?? 0,
};
}
async getData(options = null) {
const difficultiesList = Object.entries(CONFIG.l5r5e.difficulties).map(([id, value]) => ({
id,
label: game.i18n.localize(`chiaroscuro.difficulties.${id}`),
value,
}));
const aspectsList = [
{ id: "solar", label: game.i18n.localize("chiaroscuro.aspects.solar") },
{ id: "lunar", label: game.i18n.localize("chiaroscuro.aspects.lunar") },
];
return {
...(await super.getData(options)),
actor: this._actor,
data: this.object,
totalDice: this.totalDice,
ringsList: game.l5r5e.HelpersL5r5e.getRingsList(this._actor),
difficultiesList,
aspectsList,
isVoidRing: this.object.ring.id === "void",
quickInfo: this._actor?.system?.quick_info ?? "",
};
}
activateListeners(html) {
super.activateListeners(html);
// Ring selector
html.find(".ring-selection-chi").on("click", async (event) => {
event.preventDefault();
event.stopPropagation();
this.ringId = event.currentTarget.dataset.ringid;
this.render(false);
});
// Difficulty select
html.find("select[name='difficulty.id']").on("change", (event) => {
this.object.difficulty.id = event.target.value;
this.object.difficulty.value = CONFIG.l5r5e.difficulties[this.object.difficulty.id];
this.render(false);
});
// Flat modifier
html.find("input[name='modifier']").on("change", (event) => {
this.object.modifier = parseInt(event.target.value) || 0;
});
// Aspect point checkbox
html.find("#use_aspect_point").on("change", (event) => {
this.object.useAspectPoint = event.target.checked;
this.render(false);
});
// Aspect type select (solar / lunar)
html.find("select[name='aspectType']").on("change", (event) => {
this.object.aspectType = event.target.value;
});
// Assistance checkbox
html.find("#use_assistance").on("change", (event) => {
this.object.useAssistance = event.target.checked;
this.render(false);
});
}
async _updateObject(event, formData) {
const nbDice = this.totalDice;
const skillRank = this.object.skill.rank;
const skillBonus = this.object.skill.bonus;
const flatModifier = this.object.modifier;
const difficulty = this.object.difficulty.value;
// Roll the dice using FoundryVTT Roll API
const roll = await new Roll(`${nbDice}d6`).evaluate();
const rawResults = roll.dice[0].results.map((r) => r.result);
// Apply parangon passive adjustments
const adjustedResults = rawResults.map((r) => this._applyParangon(r, skillRank));
const diceAdjustedFlags = rawResults.map((r, i) => adjustedResults[i] !== r);
const wasAdjusted = diceAdjustedFlags.some(Boolean);
// Compute total
const rawSum = adjustedResults.reduce((a, b) => a + b, 0);
const total = rawSum + skillBonus + flatModifier;
const success = total >= difficulty;
const bonus = success ? total - difficulty : 0;
// Update aspect gauge after roll
if (this._actor && this.object.useAspectPoint) {
await this._updateAspectGauge();
}
// Post chat message
await this._sendChatMessage({
nbDice,
rawResults,
adjustedResults,
diceAdjustedFlags,
wasAdjusted,
rawSum,
total,
skillBonus,
flatModifier,
difficulty,
success,
bonus,
});
return this.close();
}
/**
* Apply parangon rank passive: replace low die results with higher value.
* parangon1: 1 → 2
* parangon2: 12 → 3
* parangon3: 13 → 4
* @param {number} result
* @param {string} rank
* @return {number}
*/
_applyParangon(result, rank) {
if (rank === "parangon3" && result <= 3) return 4;
if (rank === "parangon2" && result <= 2) return 3;
if (rank === "parangon1" && result <= 1) return 2;
return result;
}
/**
* Update the aspect gauge on the actor after an aspect point roll.
* Gauge positive = solar side, negative = lunar side.
* ±5 → apply Déséquilibre. ±10 → full reset.
*/
async _updateAspectGauge() {
// Support both single-nested (system.aspects) and double-nested (system.aspects.aspects)
const aspectsPath = this._actor.system.aspects?.aspects !== undefined
? "system.aspects.aspects"
: "system.aspects";
const aspects = foundry.utils.getProperty(this._actor, aspectsPath) ?? {};
const gaugeDirection = this.object.aspectType === "solar" ? 1 : -1;
const newGauge = (aspects.gauge ?? 0) + gaugeDirection;
if (Math.abs(newGauge) >= 10) {
// Full reset
await this._actor.update({
[`${aspectsPath}.gauge`]: 0,
[`${aspectsPath}.solar`]: 0,
[`${aspectsPath}.lunar`]: 0,
});
// Remove all desequilibre conditions
const toRemove = this._actor.items
.filter((i) => i.type === "etat" && ["desequilibre_solaire", "desequilibre_lunaire"].includes(i.system?.condition_type))
.map((i) => i.id);
if (toRemove.length) {
await this._actor.deleteEmbeddedDocuments("Item", toRemove);
}
} else {
await this._actor.update({ [`${aspectsPath}.gauge`]: newGauge });
if (Math.abs(newGauge) >= 5) {
// Apply opposing desequilibre
const condType = this.object.aspectType === "solar" ? "desequilibre_lunaire" : "desequilibre_solaire";
const existing = this._actor.items.find(
(i) => i.type === "etat" && i.system?.condition_type === condType
);
if (!existing) {
await this._actor.createEmbeddedDocuments("Item", [
{
type: "etat",
name: game.i18n.localize(`chiaroscuro.aspects.${condType}`),
system: { condition_type: condType },
},
]);
}
}
}
}
/**
* Create and send the chat message.
*/
async _sendChatMessage(rollData) {
const content = await foundry.applications.handlebars.renderTemplate(
CONFIG.l5r5e.paths.templates + "dice/chiaroscuro-chat-roll.html",
{
actor: this._actor,
profileImg: this._actor?.img ?? "icons/svg/mystery-man.svg",
ring: this.object.ring,
skill: this.object.skill,
difficulty: this.object.difficulty,
useAspectPoint: this.object.useAspectPoint,
aspectType: this.object.aspectType,
useAssistance: this.object.useAssistance,
modifier: this.object.modifier,
quickInfo: this._actor?.system?.quick_info ?? "",
...rollData,
}
);
return ChatMessage.implementation.create({
user: game.user.id,
speaker: {
actor: this._actor?.id ?? null,
alias: this._actor?.name ?? null,
},
content,
sound: CONFIG.sounds.dice,
});
}
}

View File

@@ -64,6 +64,11 @@ export const RegisterHandlebars = function () {
return objects.join("");
});
// Chiaroscuro: return the flat bonus for a given skill rank id
Handlebars.registerHelper("skillRankBonus", (rankId) => {
return CONFIG.l5r5e.skillRanks?.[rankId]?.bonus ?? 0;
});
// Add a setter
Handlebars.registerHelper("setVar", function (varName, varValue, options) {
options.data.root[varName] = varValue;

View File

@@ -0,0 +1,44 @@
import { BaseItemSheetL5r5e } from "./base-item-sheet.js";
/**
* Sheet for Arcane items (Chiaroscuro).
* @extends {BaseItemSheetL5r5e}
*/
export class ArcaneSheetL5r5e extends BaseItemSheetL5r5e {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["l5r5e", "sheet", "arcane"],
template: CONFIG.l5r5e.paths.templates + "items/arcane/arcane-sheet.html",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "attributes" }],
});
}
/** @override */
async getData(options = {}) {
const sheetData = await super.getData(options);
// Convert application array to comma-separated string for display
const app = sheetData.data.system.application;
sheetData.data.system.applicationDisplay = Array.isArray(app) ? app.join(", ") : (app ?? "");
sheetData.data.enrichedHtml = {
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(
sheetData.data.system.description ?? "",
{ async: true }
),
};
return sheetData;
}
/** @override */
async _updateObject(event, formData) {
// Convert comma-separated application string back to array
const raw = formData["system.applicationDisplay"] ?? "";
formData["system.application"] = raw.split(",").map((s) => s.trim()).filter(Boolean);
delete formData["system.applicationDisplay"];
return super._updateObject(event, formData);
}
}

View File

@@ -11,4 +11,16 @@ export class ArmorSheetL5r5e extends ItemSheetL5r5e {
template: CONFIG.l5r5e.paths.templates + "items/armor/armor-sheet.html",
});
}
/** @override */
async getData(options = {}) {
const sheetData = await super.getData(options);
const catObj = game.l5r5e.HelpersL5r5e.getLocalizedRawObject("chiaroscuro.armor.categories") ?? {};
sheetData.data.armorCategories = Object.entries(catObj)
.filter(([k]) => k !== "label")
.map(([id, label]) => ({ id, label }));
return sheetData;
}
}

View File

@@ -0,0 +1,30 @@
import { BaseItemSheetL5r5e } from "./base-item-sheet.js";
/**
* Sheet for État items (Chiaroscuro).
* @extends {BaseItemSheetL5r5e}
*/
export class EtatSheetL5r5e extends BaseItemSheetL5r5e {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["l5r5e", "sheet", "etat"],
template: CONFIG.l5r5e.paths.templates + "items/etat/etat-sheet.html",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "attributes" }],
});
}
/** @override */
async getData(options = {}) {
const sheetData = await super.getData(options);
sheetData.data.enrichedHtml = {
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(
sheetData.data.system.description ?? "",
{ async: true }
),
};
return sheetData;
}
}

View File

@@ -0,0 +1,35 @@
import { BaseItemSheetL5r5e } from "./base-item-sheet.js";
/**
* Sheet for Mystère items (Chiaroscuro).
* @extends {BaseItemSheetL5r5e}
*/
export class MystereSheetL5r5e extends BaseItemSheetL5r5e {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["l5r5e", "sheet", "mystere"],
template: CONFIG.l5r5e.paths.templates + "items/mystere/mystere-sheet.html",
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "attributes" }],
});
}
/** @override */
async getData(options = {}) {
const sheetData = await super.getData(options);
sheetData.data.mystereTypes = [
{ id: "mineur", label: game.i18n.localize("chiaroscuro.mystere.mineur") },
{ id: "majeur", label: game.i18n.localize("chiaroscuro.mystere.majeur") },
];
sheetData.data.enrichedHtml = {
description: await foundry.applications.ux.TextEditor.implementation.enrichHTML(
sheetData.data.system.description ?? "",
{ async: true }
),
};
return sheetData;
}
}

View File

@@ -17,12 +17,27 @@ export class TechniqueSheetL5r5e extends ItemSheetL5r5e {
const sheetData = await super.getData(options);
// List all available techniques type
const types = ["core", "school", "title"];
const types = ["core", "school", "title", "chiaroscuro"];
if (game.settings.get(CONFIG.l5r5e.namespace, "techniques-customs")) {
types.push("custom");
}
sheetData.data.techniquesList = game.l5r5e.HelpersL5r5e.getTechniquesList({ types });
// Invocation sub-type fields (visible only for mot_invocation)
sheetData.data.isMotInvocation = sheetData.data.system.technique_type === "mot_invocation";
sheetData.data.invocationTypes = [
{ id: "general", label: game.i18n.localize("chiaroscuro.technique.invocation_types.general") },
{ id: "neutre", label: game.i18n.localize("chiaroscuro.technique.invocation_types.neutre") },
{ id: "precis", label: game.i18n.localize("chiaroscuro.technique.invocation_types.precis") },
];
sheetData.data.modeInvocationValues = [
{ id: "-3", label: "-3" },
{ id: "0", label: "0" },
{ id: "3", label: "+3" },
];
// Convert mode_invocation to string for selectOptions matching
sheetData.data.system.mode_invocation_str = String(sheetData.data.system.mode_invocation ?? 0);
// Sanitize Difficulty and Skill list
sheetData.data.system.difficulty = TechniqueSheetL5r5e.formatDifficulty(sheetData.data.system.difficulty);
sheetData.data.system.skill = TechniqueSheetL5r5e.translateSkillsList(
@@ -55,6 +70,12 @@ export class TechniqueSheetL5r5e extends ItemSheetL5r5e {
TechniqueSheetL5r5e.translateSkillsList(formData["system.skill"].split(","), true)
).join(",");
// Convert mode_invocation_str back to number
if ("system.mode_invocation_str" in formData) {
formData["system.mode_invocation"] = parseInt(formData["system.mode_invocation_str"] ?? "0", 10);
delete formData["system.mode_invocation_str"];
}
return super._updateObject(event, formData);
}

View File

@@ -23,6 +23,12 @@ export class WeaponSheetL5r5e extends ItemSheetL5r5e {
label: "l5r5e.skills." + cat.toLowerCase() + "." + id.toLowerCase(),
}));
// Weapon categories (Chiaroscuro)
const catObj = game.l5r5e.HelpersL5r5e.getLocalizedRawObject("chiaroscuro.weapon.categories") ?? {};
sheetData.data.weaponCategories = [{ id: "", label: "—" }].concat(
Object.entries(catObj).map(([id, label]) => ({ id, label }))
);
return sheetData;
}
}

View File

@@ -19,6 +19,7 @@ import { RingDie } from "./dice/dietype/ring-die.js";
import { RollL5r5e } from "./dice/roll.js";
import { DicePickerDialog } from "./dice/dice-picker-dialog.js";
import { RollnKeepDialog } from "./dice/roll-n-keep-dialog.js";
import { ChiaroscuroDiceDialog } from "./dice/chiaroscuro-dice-dialog.js";
import { CombatL5r5e } from "./combat.js";
// Items
import { ItemL5r5e } from "./item.js";
@@ -33,6 +34,9 @@ import { TitleSheetL5r5e } from "./items/title-sheet.js";
import { BondSheetL5r5e } from "./items/bond-sheet.js";
import { SignatureScrollSheetL5r5e } from "./items/signature-scroll-sheet.js";
import { ItemPatternSheetL5r5e } from "./items/item-pattern-sheet.js";
import { ArcaneSheetL5r5e } from "./items/arcane-sheet.js";
import { EtatSheetL5r5e } from "./items/etat-sheet.js";
import { MystereSheetL5r5e } from "./items/mystere-sheet.js";
import { ArmyCohortSheetL5r5e } from "./items/army-cohort-sheet.js";
import { ArmyFortificationSheetL5r5e } from "./items/army-fortification-sheet.js";
// JournalEntry
@@ -118,6 +122,7 @@ Hooks.once("init", async () => {
ActorL5r5e,
DicePickerDialog,
RollnKeepDialog,
ChiaroscuroDiceDialog,
GmToolbox,
GmMonitor,
storage: new Storage(),
@@ -223,6 +228,21 @@ Hooks.once("init", async () => {
label: "TYPES.Item.army_fortification",
makeDefault: true,
});
fdc.Items.registerSheet(L5R5E.namespace, ArcaneSheetL5r5e, {
types: ["arcane"],
label: "TYPES.Item.arcane",
makeDefault: true,
});
fdc.Items.registerSheet(L5R5E.namespace, EtatSheetL5r5e, {
types: ["etat"],
label: "TYPES.Item.etat",
makeDefault: true,
});
fdc.Items.registerSheet(L5R5E.namespace, MystereSheetL5r5e, {
types: ["mystere"],
label: "TYPES.Item.mystere",
makeDefault: true,
});
// Journal
fdc.Journal.unregisterSheet("core", fav1s.JournalSheet);

View File

@@ -309,6 +309,29 @@ export class MigrationL5r5e {
}
// ***** End of 1.3.0 *****
// ***** Start of 2.0.0 (Chiaroscuro) *****
if (options?.force || MigrationL5r5e.needUpdate("2.0.0")) {
// Migrate character skill ranks from numeric (1-5) to Chiaroscuro string enum.
// Rank 0 (unranked) stays as the number 0 — this is intentional: the template
// defaults to 0 (number) and JS coerces it correctly when keying into skillRanks.
if (actor.type === "character" && system.skills) {
const rankMap = { 1: "initie", 2: "expert", 3: "maitre", 4: "parangon1", 5: "parangon2" };
const groups = ["artisan", "martial", "scholar", "social", "trade"];
for (const group of groups) {
const groupSkills = system.skills[group];
if (!groupSkills) continue;
for (const [skillName, rank] of Object.entries(groupSkills)) {
const numRank = Number(rank);
// Only migrate non-zero numeric ranks; 0 is already valid as-is
if (numRank > 0 && rankMap[numRank]) {
updateData[`system.skills.${group}.${skillName}`] = rankMap[numRank];
}
}
}
}
}
// ***** End of 2.0.0 (Chiaroscuro) *****
return updateData;
}

View File

@@ -5,12 +5,15 @@ export const PreloadTemplates = async function () {
// *** Actors : PC ***
`${tpl}actors/character/advancement-school.html`,
`${tpl}actors/character/advancement-others.html`,
`${tpl}actors/character/aspects.html`,
`${tpl}actors/character/attributes.html`,
`${tpl}actors/character/category.html`,
`${tpl}actors/character/conflict.html`,
`${tpl}actors/character/experience.html`,
`${tpl}actors/character/identity.html`,
`${tpl}actors/character/identity-text.html`,
`${tpl}actors/character/inventory.html`,
`${tpl}actors/character/invocations.html`,
`${tpl}actors/character/narrative.html`,
`${tpl}actors/character/rings.html`,
`${tpl}actors/character/effects.html`,
@@ -74,5 +77,6 @@ export const PreloadTemplates = async function () {
`${tpl}items/weapon/weapon-sheet.html`,
`${tpl}items/army-cohort/army-cohort-entry.html`,
`${tpl}items/army-fortification/army-fortification-entry.html`,
`${tpl}dice/chiaroscuro-chat-roll.html`,
]);
};