diff --git a/css/cde-theme.css b/css/cde-theme.css
index 180a346..75a307b 100644
--- a/css/cde-theme.css
+++ b/css/cde-theme.css
@@ -981,6 +981,346 @@ section.npc .cde-neon-tabs .item.active {
.cde-npc-tracks .cde-track-note input {
width: 100%;
}
+.cde-chat-app-buttons {
+ display: flex;
+ gap: 6px;
+ padding: 6px 8px 4px;
+ border-top: 1px solid #1a2436;
+}
+.cde-chat-app-buttons .cde-chat-btn {
+ flex: 1 1 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 5px 8px;
+ font-size: 11px;
+ font-family: "Orbitron", "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ background: #101622;
+ border: 1px solid #1a2436;
+ border-radius: 4px;
+ color: #7d94b8;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.cde-chat-app-buttons .cde-chat-btn i {
+ font-size: 12px;
+}
+.cde-chat-app-buttons .cde-chat-btn:hover {
+ background: rgba(74, 158, 255, 0.1);
+ border-color: #4a9eff;
+ color: #4a9eff;
+ box-shadow: 0 0 8px rgba(74, 158, 255, 0.3);
+}
+.cde-chat-app-buttons .cde-chat-btn--tinji:hover {
+ background: rgba(255, 61, 90, 0.1);
+ border-color: #ff3d5a;
+ color: #ff3d5a;
+ box-shadow: 0 0 8px rgba(255, 61, 90, 0.3);
+}
+.cde-loksyu-standalone .cde-loksyu-app-body {
+ padding: 12px;
+}
+.cde-loksyu-standalone .cde-loksyu-elements {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 8px;
+ margin-bottom: 12px;
+}
+.cde-loksyu-standalone .cde-lok-card {
+ background: rgba(16, 22, 34, 0.8);
+ border: 1px solid #1a2436;
+ border-radius: 6px;
+ padding: 8px 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ transition: border-color 0.2s;
+}
+.cde-loksyu-standalone .cde-lok-card--wood:hover {
+ border-color: #4a9e3f;
+}
+.cde-loksyu-standalone .cde-lok-card--wood .cde-lok-input:focus {
+ border-bottom-color: #4a9e3f;
+}
+.cde-loksyu-standalone .cde-lok-card--fire:hover {
+ border-color: #ff3d5a;
+}
+.cde-loksyu-standalone .cde-lok-card--fire .cde-lok-input:focus {
+ border-bottom-color: #ff3d5a;
+}
+.cde-loksyu-standalone .cde-lok-card--earth:hover {
+ border-color: #c88a3a;
+}
+.cde-loksyu-standalone .cde-lok-card--earth .cde-lok-input:focus {
+ border-bottom-color: #c88a3a;
+}
+.cde-loksyu-standalone .cde-lok-card--metal:hover {
+ border-color: #7d94b8;
+}
+.cde-loksyu-standalone .cde-lok-card--metal .cde-lok-input:focus {
+ border-bottom-color: #7d94b8;
+}
+.cde-loksyu-standalone .cde-lok-card--water:hover {
+ border-color: #4a9eff;
+}
+.cde-loksyu-standalone .cde-lok-card--water .cde-lok-input:focus {
+ border-bottom-color: #4a9eff;
+}
+.cde-loksyu-standalone .cde-lok-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.cde-loksyu-standalone .cde-lok-header img.cde-lok-icon {
+ border-radius: 4px;
+ flex-shrink: 0;
+}
+.cde-loksyu-standalone .cde-lok-titles {
+ flex: 1 1 0;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+.cde-loksyu-standalone .cde-lok-titles .cde-lok-name {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #e2e8f4;
+}
+.cde-loksyu-standalone .cde-lok-titles .cde-lok-qual {
+ font-size: 9px;
+ color: #7d94b8;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.cde-loksyu-standalone .cde-lok-reset {
+ color: #7d94b8;
+ font-size: 11px;
+ flex-shrink: 0;
+ cursor: pointer;
+}
+.cde-loksyu-standalone .cde-lok-reset:hover {
+ color: #e2e8f4;
+}
+.cde-loksyu-standalone .cde-lok-values {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.cde-loksyu-standalone .cde-lok-polarity {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+.cde-loksyu-standalone .cde-lok-polarity .cde-lok-pol-label {
+ font-size: 9px;
+ color: #7d94b8;
+ width: 30px;
+ flex-shrink: 0;
+}
+.cde-loksyu-standalone .cde-lok-polarity--yang .cde-lok-pol-label {
+ color: #e2e8f4;
+}
+.cde-loksyu-standalone .cde-lok-input {
+ flex: 1 1 0;
+ background: transparent;
+ border: none;
+ border-bottom: 1px solid #1a2436;
+ color: #e2e8f4;
+ font-size: 13px;
+ font-weight: 700;
+ text-align: center;
+ padding: 2px 0;
+ transition: border-bottom-color 0.2s;
+ width: 100%;
+}
+.cde-loksyu-standalone .cde-lok-input:focus {
+ outline: none;
+}
+.cde-loksyu-standalone .cde-lok-input[disabled] {
+ opacity: 0.5;
+ cursor: default;
+}
+.cde-loksyu-standalone .cde-loksyu-visual-row {
+ text-align: center;
+ margin: 6px 0;
+}
+.cde-loksyu-standalone .cde-loksyu-visual-row .cde-lok-visual {
+ max-width: 100%;
+ max-height: 80px;
+ opacity: 0.6;
+}
+.cde-loksyu-standalone .cde-lok-footer {
+ display: flex;
+ justify-content: center;
+ padding-top: 6px;
+ border-top: 1px solid #1a2436;
+ margin-top: 6px;
+}
+.cde-loksyu-standalone .cde-lok-reset-all {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 14px;
+ font-size: 11px;
+ background: rgba(255, 61, 90, 0.12);
+ border: 1px solid rgba(255, 61, 90, 0.3);
+ border-radius: 4px;
+ color: #7d94b8;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.cde-loksyu-standalone .cde-lok-reset-all:hover {
+ background: rgba(255, 61, 90, 0.2);
+ border-color: #ff3d5a;
+ color: #e2e8f4;
+}
+.cde-tinji-standalone .cde-tinji-app-body {
+ padding: 16px 12px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+.cde-tinji-standalone .cde-tinji-display {
+ flex: 1 1 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+}
+.cde-tinji-standalone .cde-tinji-chinese-large {
+ font-size: 40px;
+ color: #ff3d5a;
+ text-shadow: 0 0 20px rgba(255, 61, 90, 0.6);
+ line-height: 1;
+ font-family: serif;
+}
+.cde-tinji-standalone .cde-tinji-label {
+ font-size: 10px;
+ font-family: "Orbitron", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: #7d94b8;
+}
+.cde-tinji-standalone .cde-tinji-counter {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 4px 0;
+}
+.cde-tinji-standalone .cde-tinji-step {
+ width: 28px;
+ height: 28px;
+ background: #101622;
+ border: 1px solid #1a2436;
+ border-radius: 50%;
+ color: #7d94b8;
+ font-size: 18px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+ line-height: 1;
+ padding: 0;
+}
+.cde-tinji-standalone .cde-tinji-step:hover {
+ background: rgba(255, 61, 90, 0.15);
+ border-color: #ff3d5a;
+ color: #ff3d5a;
+}
+.cde-tinji-standalone .cde-tinji-direct {
+ width: 72px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid #ff3d5a;
+ color: #ff3d5a;
+ font-size: 36px;
+ font-weight: 700;
+ text-align: center;
+ text-shadow: 0 0 12px rgba(255, 61, 90, 0.5);
+ padding: 0;
+}
+.cde-tinji-standalone .cde-tinji-direct:focus {
+ outline: none;
+}
+.cde-tinji-standalone .cde-tinji-direct[disabled] {
+ opacity: 0.7;
+ cursor: default;
+}
+.cde-tinji-standalone .cde-tinji-hint {
+ font-size: 10px;
+ color: #7d94b8;
+ text-align: center;
+}
+.cde-tinji-standalone .cde-tinji-actions {
+ display: flex;
+ gap: 6px;
+ margin-top: 6px;
+}
+.cde-tinji-standalone .cde-tinji-spend-btn,
+.cde-tinji-standalone .cde-tinji-reset-btn {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 5px 12px;
+ font-size: 11px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.cde-tinji-standalone .cde-tinji-spend-btn {
+ background: rgba(255, 61, 90, 0.15);
+ border: 1px solid #ff3d5a;
+ color: #ff3d5a;
+}
+.cde-tinji-standalone .cde-tinji-spend-btn:hover {
+ background: rgba(255, 61, 90, 0.3);
+}
+.cde-tinji-standalone .cde-tinji-spend-btn[disabled] {
+ opacity: 0.4;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+.cde-tinji-standalone .cde-tinji-reset-btn {
+ background: #101622;
+ border: 1px solid #1a2436;
+ color: #7d94b8;
+}
+.cde-tinji-standalone .cde-tinji-reset-btn:hover {
+ border-color: #e2e8f4;
+ color: #e2e8f4;
+}
+.cde-tinji-standalone .cde-tinji-visual {
+ width: 90px;
+ height: auto;
+ opacity: 0.6;
+ flex-shrink: 0;
+}
+.cde-tinji-spend-msg {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ padding: 6px 8px;
+ background: rgba(255, 61, 90, 0.08);
+ border-left: 3px solid #ff3d5a;
+ border-radius: 4px;
+ color: #e2e8f4;
+}
+.cde-tinji-spend-msg i {
+ color: #ff3d5a;
+}
+.cde-tinji-spend-msg .cde-tinji-remain {
+ margin-left: auto;
+ font-size: 10px;
+ color: #7d94b8;
+}
.cde-loksyu-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
diff --git a/css/cde-theme.less b/css/cde-theme.less
index 4734f1f..081a451 100644
--- a/css/cde-theme.less
+++ b/css/cde-theme.less
@@ -1032,6 +1032,351 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
// ============================================================
// Loksyu — 5 elemental cards grid
// ============================================================
+// ============================================================
+// Chat buttons — Loksyu / TinJi quick-access
+// ============================================================
+.cde-chat-app-buttons {
+ display: flex;
+ gap: 6px;
+ padding: 6px 8px 4px;
+ border-top: 1px solid @cde-border;
+
+ .cde-chat-btn {
+ flex: 1 1 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 5px 8px;
+ font-size: 11px;
+ font-family: "Orbitron", "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ background: @cde-surface;
+ border: 1px solid @cde-border;
+ border-radius: 4px;
+ color: @cde-muted;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ i { font-size: 12px; }
+
+ &:hover {
+ background: fade(@cde-spell, 10%);
+ border-color: @cde-spell;
+ color: @cde-spell;
+ box-shadow: 0 0 8px fade(@cde-spell, 30%);
+ }
+
+ &--tinji:hover {
+ background: fade(@cde-kungfu, 10%);
+ border-color: @cde-kungfu;
+ color: @cde-kungfu;
+ box-shadow: 0 0 8px fade(@cde-kungfu, 30%);
+ }
+ }
+}
+
+// ============================================================
+// Loksyu standalone app
+// ============================================================
+.cde-loksyu-standalone {
+ .cde-loksyu-app-body {
+ padding: 12px;
+ }
+
+ .cde-loksyu-elements {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 8px;
+ margin-bottom: 12px;
+ }
+
+ .cde-lok-card {
+ background: fade(@cde-surface, 80%);
+ border: 1px solid @cde-border;
+ border-radius: 6px;
+ padding: 8px 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ transition: border-color 0.2s;
+
+ &--wood { &:hover { border-color: #4a9e3f; } .cde-lok-input:focus { border-bottom-color: #4a9e3f; } }
+ &--fire { &:hover { border-color: @cde-kungfu; } .cde-lok-input:focus { border-bottom-color: @cde-kungfu; } }
+ &--earth { &:hover { border-color: #c88a3a; } .cde-lok-input:focus { border-bottom-color: #c88a3a; } }
+ &--metal { &:hover { border-color: @cde-muted; } .cde-lok-input:focus { border-bottom-color: @cde-muted; } }
+ &--water { &:hover { border-color: @cde-spell; } .cde-lok-input:focus { border-bottom-color: @cde-spell; } }
+ }
+
+ .cde-lok-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+
+ img.cde-lok-icon { border-radius: 4px; flex-shrink: 0; }
+ }
+
+ .cde-lok-titles {
+ flex: 1 1 0;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+
+ .cde-lok-name {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: @cde-text;
+ }
+
+ .cde-lok-qual {
+ font-size: 9px;
+ color: @cde-muted;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .cde-lok-reset {
+ color: @cde-muted;
+ font-size: 11px;
+ flex-shrink: 0;
+ cursor: pointer;
+ &:hover { color: @cde-text; }
+ }
+
+ .cde-lok-values {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .cde-lok-polarity {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ .cde-lok-pol-label {
+ font-size: 9px;
+ color: @cde-muted;
+ width: 30px;
+ flex-shrink: 0;
+ }
+
+ &--yang .cde-lok-pol-label { color: @cde-text; }
+ }
+
+ .cde-lok-input {
+ flex: 1 1 0;
+ background: transparent;
+ border: none;
+ border-bottom: 1px solid @cde-border;
+ color: @cde-text;
+ font-size: 13px;
+ font-weight: 700;
+ text-align: center;
+ padding: 2px 0;
+ transition: border-bottom-color 0.2s;
+ width: 100%;
+
+ &:focus { outline: none; }
+ &[disabled] { opacity: 0.5; cursor: default; }
+ }
+
+ .cde-loksyu-visual-row {
+ text-align: center;
+ margin: 6px 0;
+
+ .cde-lok-visual {
+ max-width: 100%;
+ max-height: 80px;
+ opacity: 0.6;
+ }
+ }
+
+ .cde-lok-footer {
+ display: flex;
+ justify-content: center;
+ padding-top: 6px;
+ border-top: 1px solid @cde-border;
+ margin-top: 6px;
+ }
+
+ .cde-lok-reset-all {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 14px;
+ font-size: 11px;
+ background: fade(@cde-kungfu, 12%);
+ border: 1px solid fade(@cde-kungfu, 30%);
+ border-radius: 4px;
+ color: @cde-muted;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover {
+ background: fade(@cde-kungfu, 20%);
+ border-color: @cde-kungfu;
+ color: @cde-text;
+ }
+ }
+}
+
+// ============================================================
+// TinJi standalone app
+// ============================================================
+.cde-tinji-standalone {
+ .cde-tinji-app-body {
+ padding: 16px 12px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ .cde-tinji-display {
+ flex: 1 1 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .cde-tinji-chinese-large {
+ font-size: 40px;
+ color: @cde-kungfu;
+ text-shadow: 0 0 20px fade(@cde-kungfu, 60%);
+ line-height: 1;
+ font-family: serif;
+ }
+
+ .cde-tinji-label {
+ font-size: 10px;
+ font-family: "Orbitron", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: @cde-muted;
+ }
+
+ .cde-tinji-counter {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 4px 0;
+ }
+
+ .cde-tinji-step {
+ width: 28px;
+ height: 28px;
+ background: @cde-surface;
+ border: 1px solid @cde-border;
+ border-radius: 50%;
+ color: @cde-muted;
+ font-size: 18px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s;
+ line-height: 1;
+ padding: 0;
+
+ &:hover {
+ background: fade(@cde-kungfu, 15%);
+ border-color: @cde-kungfu;
+ color: @cde-kungfu;
+ }
+ }
+
+ .cde-tinji-direct {
+ width: 72px;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid @cde-kungfu;
+ color: @cde-kungfu;
+ font-size: 36px;
+ font-weight: 700;
+ text-align: center;
+ text-shadow: 0 0 12px fade(@cde-kungfu, 50%);
+ padding: 0;
+
+ &:focus { outline: none; }
+ &[disabled] { opacity: 0.7; cursor: default; }
+ }
+
+ .cde-tinji-hint {
+ font-size: 10px;
+ color: @cde-muted;
+ text-align: center;
+ }
+
+ .cde-tinji-actions {
+ display: flex;
+ gap: 6px;
+ margin-top: 6px;
+ }
+
+ .cde-tinji-spend-btn,
+ .cde-tinji-reset-btn {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 5px 12px;
+ font-size: 11px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+
+ .cde-tinji-spend-btn {
+ background: fade(@cde-kungfu, 15%);
+ border: 1px solid @cde-kungfu;
+ color: @cde-kungfu;
+
+ &:hover { background: fade(@cde-kungfu, 30%); }
+ &[disabled] { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
+ }
+
+ .cde-tinji-reset-btn {
+ background: @cde-surface;
+ border: 1px solid @cde-border;
+ color: @cde-muted;
+
+ &:hover { border-color: @cde-text; color: @cde-text; }
+ }
+
+ .cde-tinji-visual {
+ width: 90px;
+ height: auto;
+ opacity: 0.6;
+ flex-shrink: 0;
+ }
+}
+
+// Chat Tin Ji spend message
+.cde-tinji-spend-msg {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ padding: 6px 8px;
+ background: fade(@cde-kungfu, 8%);
+ border-left: 3px solid @cde-kungfu;
+ border-radius: 4px;
+ color: @cde-text;
+
+ i { color: @cde-kungfu; }
+
+ .cde-tinji-remain {
+ margin-left: auto;
+ font-size: 10px;
+ color: @cde-muted;
+ }
+}
+
.cde-loksyu-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
diff --git a/dist/system.js b/dist/system.js
index 8bffcff..b90255d 100644
--- a/dist/system.js
+++ b/dist/system.js
@@ -100,7 +100,9 @@ var TEMPLATE_PARTIALS = [
"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-supernaturals.html",
"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-spells.html",
"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-kungfus.html",
- "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-items.html"
+ "systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-items.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-loksyu-app.html",
+ "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-tinji-app.html"
];
// src/config/localize.js
@@ -927,6 +929,61 @@ async function rollInitiativeNPC(actor) {
});
}
+// src/ui/apps/singletons.js
+var WU_XING_CYCLE = {
+ wood: ["wood", "fire", "water", "earth", "metal"],
+ fire: ["fire", "earth", "wood", "metal", "water"],
+ earth: ["earth", "metal", "fire", "water", "wood"],
+ metal: ["metal", "water", "earth", "wood", "fire"],
+ water: ["water", "wood", "metal", "fire", "earth"]
+};
+var ASPECT_FACES = {
+ metal: [3, 8],
+ water: [1, 6],
+ earth: [0, 5],
+ fire: [2, 7],
+ wood: [4, 9]
+};
+async function getSingletonActor(type) {
+ const existing = game.actors.find((a) => a.type === type);
+ if (existing) return existing;
+ if (!game.user.isGM) {
+ ui.notifications.warn(game.i18n.localize(type === ACTOR_TYPES.loksyu ? "CDE.LoksyuNotFound" : "CDE.TinjiNotFound"));
+ return null;
+ }
+ const nameKey = type === ACTOR_TYPES.loksyu ? "CDE.UpperCaseLoksyu" : "CDE.UpperCaseTinJi";
+ const actor = await Actor.create({
+ name: game.i18n.localize(nameKey),
+ type,
+ img: type === ACTOR_TYPES.loksyu ? "systems/fvtt-chroniques-de-l-etrange/images/loksyu_long.webp" : "systems/fvtt-chroniques-de-l-etrange/images/tinji.webp"
+ });
+ return actor ?? null;
+}
+async function updateLoksyuFromRoll(activeAspect, faces) {
+ const cycle = WU_XING_CYCLE[activeAspect];
+ if (!cycle) return;
+ const lokAspect = cycle[3];
+ const [yinFace, yangFace] = ASPECT_FACES[lokAspect] ?? [];
+ if (yinFace === void 0) return;
+ const yinCount = faces[yinFace] ?? 0;
+ const yangCount = faces[yangFace] ?? 0;
+ if (yinCount === 0 && yangCount === 0) return;
+ const actor = await getSingletonActor(ACTOR_TYPES.loksyu);
+ if (!actor) return;
+ const current = actor.system[lokAspect] ?? { yin: { value: 0 }, yang: { value: 0 } };
+ await actor.update({
+ [`system.${lokAspect}.yin.value`]: (current.yin.value ?? 0) + yinCount,
+ [`system.${lokAspect}.yang.value`]: (current.yang.value ?? 0) + yangCount
+ });
+}
+async function updateTinjiFromRoll(count) {
+ if (!count || count <= 0) return;
+ const actor = await getSingletonActor(ACTOR_TYPES.tinji);
+ if (!actor) return;
+ const current = actor.system.value ?? 0;
+ await actor.update({ "system.value": current + count });
+}
+
// src/ui/rolling.js
var RESULT_TEMPLATE2 = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html";
var SKILL_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-skill-dice-prompt.html";
@@ -955,7 +1012,7 @@ var ASPECT_ICONS = {
fire: "systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp",
wood: "systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp"
};
-var ASPECT_FACES = {
+var ASPECT_FACES2 = {
metal: [3, 8],
water: [1, 6],
earth: [0, 5],
@@ -963,7 +1020,7 @@ var ASPECT_FACES = {
fire: [2, 7],
wood: [4, 9]
};
-var WU_XING_CYCLE = {
+var WU_XING_CYCLE2 = {
wood: ["wood", "fire", "water", "earth", "metal"],
fire: ["fire", "earth", "wood", "metal", "water"],
earth: ["earth", "metal", "fire", "water", "wood"],
@@ -993,14 +1050,14 @@ function countFaces(rollResults) {
return counts;
}
function computeWuXingResults(faces, aspectName, bonusAuspicious = 0) {
- const cycle = WU_XING_CYCLE[aspectName];
+ const cycle = WU_XING_CYCLE2[aspectName];
if (!cycle) return null;
const [succAspect, ausAspect, noxAspect, lokAspect, tinAspect] = cycle;
- const [succYin, succYang] = ASPECT_FACES[succAspect];
- const [ausYin, ausYang] = ASPECT_FACES[ausAspect];
- const [noxYin, noxYang] = ASPECT_FACES[noxAspect];
- const [lokYin, lokYang] = ASPECT_FACES[lokAspect];
- const [tinYin, tinYang] = ASPECT_FACES[tinAspect];
+ const [succYin, succYang] = ASPECT_FACES2[succAspect];
+ const [ausYin, ausYang] = ASPECT_FACES2[ausAspect];
+ const [noxYin, noxYang] = ASPECT_FACES2[noxAspect];
+ const [lokYin, lokYang] = ASPECT_FACES2[lokAspect];
+ const [tinYin, tinYang] = ASPECT_FACES2[tinAspect];
const yin = game.i18n.localize("CDE.Yin");
const yang = game.i18n.localize("CDE.Yang");
return {
@@ -1296,6 +1353,8 @@ async function rollForActor(actor, rollKey) {
if (game.modules.get("dice-so-nice")?.active && wpMsg?.id) {
await game.dice3d.waitFor3DAnimationByMessageID(wpMsg.id);
}
+ if ((wpResults.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wpAspectName, wpFaces);
+ if ((wpResults.tinjidice ?? 0) > 0) await updateTinjiFromRoll(wpResults.tinjidice);
return;
}
default:
@@ -1449,6 +1508,8 @@ async function rollForActor(actor, rollKey) {
if (game.modules.get("dice-so-nice")?.active && msg?.id) {
await game.dice3d.waitFor3DAnimationByMessageID(msg.id);
}
+ if ((results.loksyudice ?? 0) > 0) await updateLoksyuFromRoll(wuXingAspectName, faces);
+ if ((results.tinjidice ?? 0) > 0) await updateTinjiFromRoll(results.tinjidice);
}
// src/ui/sheets/actors/base.js
@@ -1769,6 +1830,118 @@ var CDENpcSheet = class extends CDEBaseActorSheet {
}
};
+// src/ui/apps/tinji-app.js
+var SYSTEM_ID2 = "fvtt-chroniques-de-l-etrange";
+var CDETinjiApp = class _CDETinjiApp extends foundry.applications.api.HandlebarsApplicationMixin(
+ foundry.applications.api.ApplicationV2
+) {
+ static DEFAULT_OPTIONS = {
+ id: "cde-tinji-app",
+ tag: "div",
+ window: {
+ title: "CDE.TinJi2",
+ icon: "fas fa-star",
+ resizable: false
+ },
+ classes: ["cde-app", "cde-tinji-standalone"],
+ position: { width: 320, height: "auto" },
+ actions: {
+ increment: _CDETinjiApp.#onIncrement,
+ decrement: _CDETinjiApp.#onDecrement,
+ reset: _CDETinjiApp.#onReset,
+ spend: _CDETinjiApp.#onSpend
+ }
+ };
+ static PARTS = {
+ main: {
+ template: `systems/${SYSTEM_ID2}/templates/apps/cde-tinji-app.html`
+ }
+ };
+ /** @type {Actor|null} */
+ #actor = null;
+ /** @type {Function|null} */
+ #updateHook = null;
+ static open() {
+ const existing = Object.values(foundry.applications.instances ?? {}).find(
+ (app2) => app2 instanceof _CDETinjiApp
+ );
+ if (existing) {
+ existing.bringToFront();
+ return existing;
+ }
+ const app = new _CDETinjiApp();
+ app.render(true);
+ return app;
+ }
+ async _prepareContext() {
+ this.#actor = await getSingletonActor(ACTOR_TYPES.tinji);
+ if (!this.#actor) return { hasActor: false, value: 0 };
+ return {
+ hasActor: true,
+ canEdit: this.#actor.isOwner,
+ value: this.#actor.system.value ?? 0
+ };
+ }
+ _onRender(context, options) {
+ super._onRender(context, options);
+ this.#bindDirectInput();
+ this.#updateHook = Hooks.on("updateActor", (actor) => {
+ if (actor.id === this.#actor?.id) this.render();
+ });
+ }
+ _onClose(options) {
+ if (this.#updateHook !== null) {
+ Hooks.off("updateActor", this.#updateHook);
+ this.#updateHook = null;
+ }
+ super._onClose(options);
+ }
+ #bindDirectInput() {
+ const input = this.element?.querySelector("input.cde-tinji-direct");
+ if (!input) return;
+ input.addEventListener("change", async (ev) => {
+ const val = parseInt(ev.currentTarget.value, 10);
+ if (!isNaN(val) && this.#actor) {
+ await this.#actor.update({ "system.value": Math.max(0, val) });
+ }
+ });
+ }
+ static async #onIncrement() {
+ if (!this.#actor) return;
+ const current = this.#actor.system.value ?? 0;
+ await this.#actor.update({ "system.value": current + 1 });
+ }
+ static async #onDecrement() {
+ if (!this.#actor) return;
+ const current = this.#actor.system.value ?? 0;
+ if (current <= 0) return;
+ await this.#actor.update({ "system.value": current - 1 });
+ }
+ static async #onReset() {
+ if (!this.#actor) return;
+ await this.#actor.update({ "system.value": 0 });
+ }
+ /** Spend 1 Tin Ji die and announce it in chat */
+ static async #onSpend() {
+ if (!this.#actor) return;
+ const current = this.#actor.system.value ?? 0;
+ if (current <= 0) {
+ ui.notifications.warn(game.i18n.localize("CDE.TinjiEmpty"));
+ return;
+ }
+ await this.#actor.update({ "system.value": current - 1 });
+ ChatMessage.create({
+ user: game.user.id,
+ content: `
+
+ ${game.i18n.localize("CDE.TinJi2")}
+ ${game.i18n.format("CDE.TinjiSpent", { name: game.user.name })}
+ (${current - 1} ${game.i18n.localize("CDE.TinjiRemaining")})
+
`
+ });
+ }
+};
+
// src/ui/sheets/actors/tinji.js
var CDETinjiSheet = class extends CDEBaseActorSheet {
static DEFAULT_OPTIONS = {
@@ -1778,6 +1951,120 @@ var CDETinjiSheet = class extends CDEBaseActorSheet {
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-tinji-sheet.html" }
};
tabGroups = { primary: "tinji" };
+ /** Redirect any direct actor-sheet open to the standalone app instead */
+ async _onFirstRender(context, options) {
+ await this.close({ animate: false });
+ CDETinjiApp.open();
+ }
+};
+
+// src/ui/apps/loksyu-app.js
+var SYSTEM_ID3 = "fvtt-chroniques-de-l-etrange";
+var CDELoksyuApp = class _CDELoksyuApp extends foundry.applications.api.HandlebarsApplicationMixin(
+ foundry.applications.api.ApplicationV2
+) {
+ static DEFAULT_OPTIONS = {
+ id: "cde-loksyu-app",
+ tag: "div",
+ window: {
+ title: "CDE.Loksyu",
+ icon: "fas fa-yin-yang",
+ resizable: false
+ },
+ classes: ["cde-app", "cde-loksyu-standalone"],
+ position: { width: 540, height: "auto" },
+ actions: {
+ resetElement: _CDELoksyuApp.#onResetElement,
+ resetAll: _CDELoksyuApp.#onResetAll
+ }
+ };
+ static PARTS = {
+ main: {
+ template: `systems/${SYSTEM_ID3}/templates/apps/cde-loksyu-app.html`
+ }
+ };
+ /** @type {Actor|null} */
+ #actor = null;
+ /** @type {Function|null} bound hook handler */
+ #updateHook = null;
+ /** Singleton accessor — open or bring to front */
+ static open() {
+ const existing = Object.values(foundry.applications.instances ?? {}).find(
+ (app2) => app2 instanceof _CDELoksyuApp
+ );
+ if (existing) {
+ existing.bringToFront();
+ return existing;
+ }
+ const app = new _CDELoksyuApp();
+ app.render(true);
+ return app;
+ }
+ async _prepareContext() {
+ this.#actor = await getSingletonActor(ACTOR_TYPES.loksyu);
+ if (!this.#actor) return { hasActor: false };
+ const sys = this.#actor.system;
+ const ELEMENTS = [
+ { key: "wood", nameKey: "CDE.Wood", qualKey: "CDE.WoodQualities", img: `systems/${SYSTEM_ID3}/images/cde_bois.webp` },
+ { key: "fire", nameKey: "CDE.Fire", qualKey: "CDE.FireQualities", img: `systems/${SYSTEM_ID3}/images/cde_feu.webp` },
+ { key: "earth", nameKey: "CDE.Earth", qualKey: "CDE.EarthQualities", img: `systems/${SYSTEM_ID3}/images/cde_terre.webp` },
+ { key: "metal", nameKey: "CDE.Metal", qualKey: "CDE.MetalQualities", img: `systems/${SYSTEM_ID3}/images/cde_metal.webp` },
+ { key: "water", nameKey: "CDE.Water", qualKey: "CDE.WaterQualities", img: `systems/${SYSTEM_ID3}/images/cde_eau.webp` }
+ ];
+ return {
+ hasActor: true,
+ canEdit: this.#actor.isOwner,
+ elements: ELEMENTS.map((el) => ({
+ ...el,
+ yang: sys[el.key]?.yang?.value ?? 0,
+ yin: sys[el.key]?.yin?.value ?? 0
+ }))
+ };
+ }
+ _onRender(context, options) {
+ super._onRender(context, options);
+ this.#bindInputs();
+ this.#updateHook = Hooks.on("updateActor", (actor) => {
+ if (actor.id === this.#actor?.id) this.render();
+ });
+ }
+ _onClose(options) {
+ if (this.#updateHook !== null) {
+ Hooks.off("updateActor", this.#updateHook);
+ this.#updateHook = null;
+ }
+ super._onClose(options);
+ }
+ #bindInputs() {
+ const inputs = this.element?.querySelectorAll("input[data-field]");
+ if (!inputs?.length) return;
+ inputs.forEach((input) => {
+ input.addEventListener("change", async (ev) => {
+ const field = ev.currentTarget.dataset.field;
+ const val = parseInt(ev.currentTarget.value, 10);
+ if (!field || isNaN(val)) return;
+ await this.#actor?.update({ [field]: Math.max(0, val) });
+ });
+ });
+ }
+ static async #onResetElement(event, target) {
+ const key = target.dataset.element;
+ if (!key || !this.#actor) return;
+ await this.#actor.update({
+ [`system.${key}.yin.value`]: 0,
+ [`system.${key}.yang.value`]: 0
+ });
+ }
+ static async #onResetAll(_event, _target) {
+ if (!this.#actor) return;
+ const KEYS = ["wood", "fire", "earth", "metal", "water"];
+ const update = {};
+ for (const k of KEYS) {
+ update[`system.${k}.yin.value`] = 0;
+ update[`system.${k}.yang.value`] = 0;
+ }
+ await this.#actor.update(update);
+ }
};
// src/ui/sheets/actors/loksyu.js
@@ -1789,6 +2076,11 @@ var CDELoksyuSheet = class extends CDEBaseActorSheet {
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/actor/cde-loksyu-sheet.html" }
};
tabGroups = { primary: "loksyu" };
+ /** Redirect any direct actor-sheet open to the standalone app instead */
+ async _onFirstRender(context, options) {
+ await this.close({ animate: false });
+ CDELoksyuApp.open();
+ }
};
// src/ui/sheets/items/base.js
@@ -2177,6 +2469,24 @@ Hooks.once("init", async () => {
Hooks.once("ready", async () => {
await migrateIfNeeded();
});
+Hooks.on("renderChatLog", (_app, html) => {
+ const el = html instanceof HTMLElement ? html : html[0];
+ const controls = el?.querySelector?.(".chat-controls");
+ if (!controls) return;
+ const wrapper = document.createElement("div");
+ wrapper.classList.add("cde-chat-app-buttons");
+ wrapper.innerHTML = `
+
+
+ `;
+ controls.appendChild(wrapper);
+ wrapper.querySelector(".cde-chat-btn--loksyu")?.addEventListener("click", () => CDELoksyuApp.open());
+ wrapper.querySelector(".cde-chat-btn--tinji")?.addEventListener("click", () => CDETinjiApp.open());
+});
function injectCompendiumLink(html) {
const header = html[0]?.querySelector?.("h4.divider");
if (!header) return;
diff --git a/dist/system.js.map b/dist/system.js.map
index f8b5d2b..ad19f5a 100644
--- a/dist/system.js.map
+++ b/dist/system.js.map
@@ -1,7 +1,7 @@
{
"version": 3,
- "sources": ["../src/config/constants.js", "../src/config/localize.js", "../src/config/runtime.js", "../src/data/actors/character.js", "../src/data/actors/npc.js", "../src/data/actors/tinji.js", "../src/data/actors/loksyu.js", "../src/data/items/item.js", "../src/data/items/kungfu.js", "../src/data/items/spell.js", "../src/data/items/supernatural.js", "../src/data/items/weapon.js", "../src/data/items/armor.js", "../src/data/items/sanhei.js", "../src/data/items/ingredient.js", "../src/documents/chat-message.js", "../src/documents/actor.js", "../src/documents/item.js", "../src/ui/dice.js", "../src/ui/helpers.js", "../src/ui/templates.js", "../src/ui/initiative.js", "../src/ui/rolling.js", "../src/ui/sheets/actors/base.js", "../src/ui/sheets/actors/character.js", "../src/ui/sheets/actors/npc.js", "../src/ui/sheets/actors/tinji.js", "../src/ui/sheets/actors/loksyu.js", "../src/ui/sheets/items/base.js", "../src/ui/sheets/items/item.js", "../src/ui/sheets/items/kungfu.js", "../src/ui/sheets/items/spell.js", "../src/ui/sheets/items/supernatural.js", "../src/ui/sheets/items/weapon.js", "../src/ui/sheets/items/armor.js", "../src/ui/sheets/items/sanhei.js", "../src/ui/sheets/items/ingredient.js", "../src/migration.js", "../src/system.js"],
- "sourcesContent": ["export const SYSTEM_ID = \"fvtt-chroniques-de-l-etrange\"\n\nexport const ACTOR_TYPES = {\n character: \"character\",\n npc: \"npc\",\n tinji: \"tinji\",\n loksyu: \"loksyu\",\n}\n\nexport const ITEM_TYPES = {\n item: \"item\",\n kungfu: \"kungfu\",\n spell: \"spell\",\n supernatural: \"supernatural\",\n weapon: \"weapon\",\n armor: \"armor\",\n sanhei: \"sanhei\",\n ingredient: \"ingredient\",\n}\n\nexport const SUBTYPES = {\n weapon: { id: \"weapon\", label: \"CDE.Weapon\" },\n armor: { id: \"armor\", label: \"CDE.Armor\" },\n sanhei: { id: \"sanhei\", label: \"CDE.Sanhei\" },\n other: { id: \"other\", label: \"CDE.Other\" },\n}\n\nexport const MAGICS = {\n internalcinnabar: {\n id: \"internalcinnabar\",\n background: \"linear-grey\",\n label: \"CDE.InternalCinnabar\",\n aspectlabel: \"CDE.Metal\",\n speciality: {\n essence: { label: \"CDE.Essence\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n mind: { label: \"CDE.Mind\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n purification: { label: \"CDE.Purification\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n manipulation: { label: \"CDE.Manipulation\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n aura: { label: \"CDE.Aura\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n alchemy: {\n id: \"alchemy\",\n background: \"linear-blue\",\n label: \"CDE.Alchemy\",\n aspectlabel: \"CDE.Water\",\n speciality: {\n acupuncture: { label: \"CDE.Acupuncture\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n elixirs: { label: \"CDE.Elixirs\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n poisons: { label: \"CDE.Poisons\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n arsenal: { label: \"CDE.Arsenal\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n potions: { label: \"CDE.Potions\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n masteryoftheway: {\n id: \"masteryoftheway\",\n background: \"linear-brown\",\n label: \"CDE.MasteryOfTheWay\",\n aspectlabel: \"CDE.Earth\",\n speciality: {\n curse: { label: \"CDE.Curse\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n transfiguration: { label: \"CDE.Transfiguration\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n necromancy: { label: \"CDE.Necromancy\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n climatecontrol: { label: \"CDE.ClimateControl\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n goldenmagic: { label: \"CDE.GoldenMagic\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n exorcism: {\n id: \"exorcism\",\n background: \"linear-red\",\n label: \"CDE.Exorcism\",\n aspectlabel: \"CDE.Fire\",\n speciality: {\n invocation: { label: \"CDE.Invocation\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n tracking: { label: \"CDE.Tracking\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n protection: { label: \"CDE.Protection\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n punishment: { label: \"CDE.Punishment\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n domination: { label: \"CDE.Domination\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n geomancy: {\n id: \"geomancy\",\n background: \"linear-green\",\n label: \"CDE.Geomancy\",\n aspectlabel: \"CDE.Wood\",\n speciality: {\n neutralization: { label: \"CDE.Neutralization\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n divination: { label: \"CDE.Divination\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n earthlyprayer: { label: \"CDE.EarthlyPrayer\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n heavenlyprayer: { label: \"CDE.HeavenlyPrayer\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n fungseoi: { label: \"CDE.Fungseoi\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\", labelicon: \"Yang\", labelelement: \"CDE.Wood\" },\n },\n },\n}\n\nexport const TEMPLATE_PARTIALS = [\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-skills.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-magics.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-nghang.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-treasures.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-items.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-kungfus.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-character-spells.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-supernaturals.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-spells.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-kungfus.html\",\n \"systems/fvtt-chroniques-de-l-etrange/templates/actor/parts/cde-npc-items.html\",\n]\n", "import { MAGICS, SUBTYPES } from \"./constants.js\"\n\nexport function preLocalizeConfig() {\n const localizeConfigObject = (obj, keys) => {\n for (const o of Object.values(obj)) {\n for (const key of keys) {\n o[key] = game.i18n.localize(o[key])\n }\n }\n }\n\n localizeConfigObject(SUBTYPES, [\"label\"])\n Object.values(MAGICS).forEach((magic) => {\n magic.label = game.i18n.localize(magic.label)\n magic.aspectlabel = game.i18n.localize(magic.aspectlabel)\n Object.values(magic.speciality).forEach((spec) => {\n spec.label = game.i18n.localize(spec.label)\n spec.labelelement = game.i18n.localize(spec.labelelement)\n })\n })\n}\n", "export function configureRuntime() {\n CONFIG.Actor.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/actor-banner.webp\"\n CONFIG.Adventure.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/adventure-banner.webp\"\n CONFIG.Cards.compendiumBanner = \"ui/banners/cards-banner.webp\"\n CONFIG.Item.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/item-banner.webp\"\n CONFIG.JournalEntry.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/journalentry-banner.webp\"\n CONFIG.Macro.compendiumBanner = \"ui/banners/macro-banner.webp\"\n CONFIG.Playlist.compendiumBanner = \"ui/banners/playlist-banner.webp\"\n CONFIG.RollTable.compendiumBanner = \"ui/banners/rolltable-banner.webp\"\n CONFIG.Scene.compendiumBanner = \"/systems/fvtt-chroniques-de-l-etrange/images/banners/scene-banner.webp\"\n}\n", "export default class CharacterDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra })\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const boolField = (initial = false) => new fields.BooleanField({ required: true, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n const aspectField = (label, chinese) =>\n new fields.SchemaField({\n chinese: stringField(chinese),\n label: stringField(label),\n value: numberField(15, { min: 0 }),\n })\n\n const skillField = (label) =>\n new fields.SchemaField({\n label: stringField(label),\n specialities: stringField(\"\"),\n value: numberField(0, { min: 0 }),\n })\n\n const resourceField = (label) =>\n new fields.SchemaField({\n label: stringField(label),\n specialities: stringField(\"\"),\n value: numberField(0, { min: 0 }),\n debt: boolField(false),\n })\n\n const componentField = () =>\n new fields.SchemaField({\n value: stringField(\"\"),\n })\n\n const magicSpecialityField = () =>\n new fields.SchemaField({\n check: boolField(false),\n })\n\n const magicField = () =>\n new fields.SchemaField({\n visible: boolField(true),\n value: numberField(0, { min: 0 }),\n speciality: new fields.SchemaField({\n essence: magicSpecialityField(),\n mind: magicSpecialityField(),\n purification: magicSpecialityField(),\n manipulation: magicSpecialityField(),\n aura: magicSpecialityField(),\n acupuncture: magicSpecialityField(),\n elixirs: magicSpecialityField(),\n poisons: magicSpecialityField(),\n arsenal: magicSpecialityField(),\n potions: magicSpecialityField(),\n curse: magicSpecialityField(),\n transfiguration: magicSpecialityField(),\n necromancy: magicSpecialityField(),\n climatecontrol: magicSpecialityField(),\n goldenmagic: magicSpecialityField(),\n invocation: magicSpecialityField(),\n tracking: magicSpecialityField(),\n protection: magicSpecialityField(),\n punishment: magicSpecialityField(),\n domination: magicSpecialityField(),\n neutralization: magicSpecialityField(),\n divination: magicSpecialityField(),\n earthlyprayer: magicSpecialityField(),\n heavenlyprayer: magicSpecialityField(),\n fungseoi: magicSpecialityField(),\n }),\n })\n\n const treasureBranch = () =>\n new fields.SchemaField({\n value: numberField(0, { min: 0 }),\n max: numberField(0, { min: 0 }),\n min: numberField(0, { min: 0 }),\n })\n\n const treasureLevel = () =>\n new fields.SchemaField({\n san: treasureBranch(),\n zing: treasureBranch(),\n })\n\n const schema = {\n concept: stringField(\"\"),\n guardian: numberField(0, { min: 0, max: 5 }),\n initiative: numberField(1, { min: 0 }),\n anti_initiative: numberField(24, { min: 0 }),\n description: htmlField(\"\"),\n prefs: new fields.SchemaField({\n typeofthrow: new fields.SchemaField({\n check: boolField(true),\n choice: stringField(\"0\"),\n }),\n }),\n prompt: new fields.SchemaField({\n typeofthrow: new fields.SchemaField({\n check: boolField(true),\n choice: stringField(\"0\"),\n }),\n configure: new fields.SchemaField({\n numberofdice: numberField(0),\n aspect: numberField(0),\n bonus: numberField(0),\n bonusauspiciousdice: numberField(0),\n typeofthrow: numberField(0),\n aspectskill: numberField(0),\n bonusmalusskill: numberField(0),\n aspectspeciality: numberField(0),\n rolldifficulty: numberField(0),\n bonusmalusspeciality: numberField(0),\n }),\n }),\n aspect: new fields.SchemaField({\n fire: aspectField(\"CDE.Fire\", \"\u328B\"),\n earth: aspectField(\"CDE.Earth\", \"\u328F\"),\n metal: aspectField(\"CDE.Metal\", \"\u328E\"),\n water: aspectField(\"CDE.Water\", \"\u328C\"),\n wood: aspectField(\"CDE.Wood\", \"\u328D\"),\n }),\n skills: new fields.SchemaField({\n art: skillField(\"CDE.Art\"),\n investigation: skillField(\"CDE.Investigation\"),\n erudition: skillField(\"CDE.Erudition\"),\n knavery: skillField(\"CDE.Knavery\"),\n wordliness: skillField(\"CDE.Wordliness\"),\n prowess: skillField(\"CDE.Prowess\"),\n sciences: skillField(\"CDE.Sciences\"),\n technologies: skillField(\"CDE.Technologies\"),\n kungfu: skillField(\"CDE.KungFu\"),\n rangedcombat: skillField(\"CDE.RangedCombat\"),\n }),\n resources: new fields.SchemaField({\n supply: resourceField(\"CDE.Supply\"),\n inquiry: resourceField(\"CDE.Inquiry\"),\n influence: resourceField(\"CDE.Influence\"),\n }),\n component: new fields.SchemaField({\n one: componentField(),\n two: componentField(),\n three: componentField(),\n four: componentField(),\n five: componentField(),\n six: componentField(),\n seven: componentField(),\n eight: componentField(),\n nine: componentField(),\n zero: componentField(),\n }),\n magics: new fields.SchemaField({\n internalcinnabar: magicField(),\n alchemy: magicField(),\n masteryoftheway: magicField(),\n exorcism: magicField(),\n geomancy: magicField(),\n }),\n threetreasures: new fields.SchemaField({\n heiyang: new fields.SchemaField({ value: numberField(0, { min: 0 }), max: numberField(0, { min: 0 }) }),\n heiyin: new fields.SchemaField({ value: numberField(0, { min: 0 }), max: numberField(0, { min: 0 }) }),\n dicelevel: new fields.SchemaField({\n level0d: treasureLevel(),\n level1d: treasureLevel(),\n level2d: treasureLevel(),\n }),\n }),\n experience: new fields.SchemaField({\n value: numberField(0, { min: 0 }),\n max: numberField(0, { min: 0 }),\n min: numberField(0, { min: 0 }),\n }),\n }\n\n return schema\n }\n}\n", "export default class NpcDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra })\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const boolField = (initial = false) => new fields.BooleanField({ required: true, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n const aptitudeField = () =>\n new fields.SchemaField({\n value: numberField(0, { min: 0 }),\n speciality: stringField(\"\"),\n })\n\n const trackedField = () =>\n new fields.SchemaField({\n value: numberField(0, { min: 0 }),\n calcul: numberField(0, { min: 0 }),\n note: stringField(\"\"),\n })\n\n return {\n type: stringField(\"\"),\n threat: numberField(0, { min: 0, max: 4 }), // profane(0) | apprentice(1) | initiate(2) | accomplished(3) | renowned(4)\n nuisance: numberField(0, { min: 0, max: 5 }), // figurant(0) | minion(1) | adversary(2) | ally(3) | boss(4) | divinity(5)\n initiative: numberField(1, { min: 0 }),\n anti_initiative: numberField(24, { min: 0 }),\n aptitudes: new fields.SchemaField({\n physical: aptitudeField(),\n martial: aptitudeField(),\n mental: aptitudeField(),\n social: aptitudeField(),\n spiritual: aptitudeField(),\n }),\n vitality: trackedField(),\n hei: trackedField(),\n description: htmlField(\"\"),\n prefs: new fields.SchemaField({\n typeofthrow: new fields.SchemaField({\n check: boolField(false),\n choice: stringField(\"0\"),\n }),\n }),\n }\n }\n}\n", "export default class TinjiDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n return {\n value: numberField(0, { min: 0 }),\n description: htmlField(\"\"),\n }\n }\n}\n", "export default class LoksyuDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n const polarity = () =>\n new fields.SchemaField({\n yin: new fields.SchemaField({ value: numberField(0, { min: 0 }) }),\n yang: new fields.SchemaField({ value: numberField(0, { min: 0 }) }),\n })\n\n return {\n fire: polarity(),\n earth: polarity(),\n metal: polarity(),\n water: polarity(),\n wood: polarity(),\n description: htmlField(\"\"),\n }\n }\n}\n", "export default class EquipmentDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const numberField = (initial = 0, extra = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...extra })\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n quantity: numberField(1, { min: 0 }),\n weight: numberField(0, { min: 0 }),\n notes: htmlField(\"\"),\n }\n }\n}\n", "export default class KungfuDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const boolField = (initial = false) => new fields.BooleanField({ required: true, initial })\n\n const techniqueField = () =>\n new fields.SchemaField({\n check: boolField(false),\n name: stringField(\"\"),\n activation: stringField(\"action-attack\"), // action-attack | action-defense | action-aid | action-attack-defense | reaction | dice | damage-inflicted | damage-received\n technique: htmlField(\"\"),\n })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n orientation: stringField(\"yin\"), // yin | yang | yinyang\n aspect: stringField(\"metal\"), // metal | eau | terre | feu | bois\n skill: stringField(\"kungfu\"), // kungfu | rangedcombat\n speciality: stringField(\"\"),\n style: stringField(\"\"),\n techniques: new fields.SchemaField({\n technique1: techniqueField(),\n technique2: techniqueField(),\n technique3: techniqueField(),\n }),\n notes: htmlField(\"\"),\n }\n }\n}\n", "export default class SpellDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n specialityname: stringField(\"\"),\n associatedelement: stringField(\"metal\"), // metal | eau | terre | feu | bois\n hei: stringField(\"\"),\n realizationtimeritual: stringField(\"\"),\n realizationtimeaccelerated: stringField(\"\"),\n flashback: stringField(\"\"),\n components: htmlField(\"\"),\n effects: htmlField(\"\"),\n examples: htmlField(\"\"),\n notes: htmlField(\"\"),\n discipline: stringField(\"internalcinnabar\"),\n heiType: stringField(\"yin\"),\n heiCost: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 }),\n difficulty: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 }),\n }\n }\n}\n", "export default class SupernaturalDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n notes: htmlField(\"\"),\n heiType: stringField(\"yin\"),\n heiCost: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 0 }),\n trigger: stringField(\"\"),\n effects: htmlField(\"\"),\n }\n }\n}\n", "export default class WeaponDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n weaponType: stringField(\"melee\"),\n material: stringField(\"\"),\n damageAspect: stringField(\"metal\"),\n damageBase: intField(1),\n range: stringField(\"contact\"), // contact | courte | mediane | longue | extreme\n obtainLevel: intField(0, { min: 0, max: 5 }),\n obtainDifficulty: intField(0, { min: 0, max: 3 }),\n quantity: intField(1),\n notes: htmlField(\"\"),\n }\n }\n}\n", "export default class ArmorDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n protectionValue: intField(0),\n domain: stringField(\"\"),\n obtainLevel: intField(0, { min: 0, max: 5 }),\n obtainDifficulty: intField(0, { min: 0, max: 3 }),\n quantity: intField(1),\n notes: htmlField(\"\"),\n }\n }\n}\n", "export default class SanheiDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })\n\n const propertySchema = () => new fields.SchemaField({\n name: stringField(\"\"),\n heiCost: intField(0),\n heiType: stringField(\"yin\"),\n description: htmlField(\"\"),\n })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n heiType: stringField(\"yin\"),\n properties: new fields.SchemaField({\n prop1: propertySchema(),\n prop2: propertySchema(),\n prop3: propertySchema(),\n }),\n notes: htmlField(\"\"),\n }\n }\n}\n", "export default class IngredientDataModel extends foundry.abstract.TypeDataModel {\n static defineSchema() {\n const { fields } = foundry.data\n const stringField = (initial = \"\") => new fields.StringField({ required: true, nullable: false, initial })\n const htmlField = (initial = \"\") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })\n const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })\n\n return {\n reference: stringField(\"\"),\n description: htmlField(\"\"),\n school: stringField(\"all\"),\n obtainLevel: intField(0, { min: 0, max: 5 }),\n obtainDifficulty: intField(0, { min: 0, max: 3 }),\n quantity: intField(1),\n notes: htmlField(\"\"),\n }\n }\n}\n", "export class CDEMessage extends ChatMessage {\n async renderHTML({ canDelete, canClose = false, ...rest } = {}) {\n const html = await super.renderHTML({ canDelete, canClose, ...rest })\n this.#enrichChatCard(html)\n return html\n }\n\n getAssociatedActor() {\n if (this.speaker.scene && this.speaker.token) {\n const scene = game.scenes.get(this.speaker.scene)\n const token = scene?.tokens.get(this.speaker.token)\n if (token) return token.actor\n }\n return game.actors.get(this.speaker.actor)\n }\n\n #enrichChatCard(html) {\n const actor = this.getAssociatedActor()\n\n let img\n let nameText\n if (this.isContentVisible) {\n img = actor?.img ?? this.author.avatar\n nameText = this.alias\n } else {\n img = this.author.avatar\n nameText = this.author.name\n }\n\n const avatar = document.createElement(\"a\")\n avatar.classList.add(\"avatar\")\n if (actor) avatar.dataset.uuid = actor.uuid\n const avatarImg = document.createElement(\"img\")\n Object.assign(avatarImg, { src: img, alt: nameText })\n avatar.append(avatarImg)\n\n const name = document.createElement(\"span\")\n name.classList.add(\"name-stacked\")\n const title = document.createElement(\"span\")\n title.classList.add(\"title\")\n title.append(nameText)\n name.append(title)\n\n const sender = html.querySelector(\".message-sender\")\n sender?.replaceChildren(avatar, name)\n }\n}\n", "import { ACTOR_TYPES } from \"../config/constants.js\"\n\nexport class CDEActor extends Actor {\n getRollData() {\n const data = this.toObject(false).system\n return data\n }\n\n prepareBaseData() {\n super.prepareBaseData()\n\n if (this.type === ACTOR_TYPES.character) {\n this.system.anti_initiative = 25 - (this.system.initiative ?? 0)\n }\n\n if (this.type === ACTOR_TYPES.npc) {\n this.system.vitality.calcul = (this.system.aptitudes.physical.value ?? 0) * 4\n this.system.hei.calcul = (this.system.aptitudes.spiritual.value ?? 0) * 4\n this.system.anti_initiative = 25 - (this.system.initiative ?? 0)\n }\n }\n}\n", "export class CDEItem extends Item {\n get isWeapon() {\n return this.system.subtype === \"weapon\"\n }\n\n get isArmor() {\n return this.system.subtype === \"armor\"\n }\n\n get isSanhei() {\n return this.system.subtype === \"sanhei\"\n }\n\n get isOther() {\n return this.system.subtype === \"other\"\n }\n}\n", "const DIGIT_LABELS = [\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-1.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-2.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-3.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-4.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-5.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-6.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-7.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-8.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-9.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/digit/d10-10.webp\",\n]\n\nconst CLASSIC_LABELS = [\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-1.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-2.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-3.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-4.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-5.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-6.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-7.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-8.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-9.webp\",\n \"systems/fvtt-chroniques-de-l-etrange/images/dice-so-nice/d10-10.webp\",\n]\n\nexport function registerDice() {\n Hooks.once(\"diceSoNiceReady\", (dice3d) => {\n dice3d.addColorset(\n {\n name: \"cde\",\n description: \"CdE\",\n foreground: \"#000000\",\n background: \"#ffffff\",\n edge: \"#ffffff\",\n font: \"DeliusUnicase\",\n texture: \"ice\",\n material: \"plastic\",\n },\n \"preferred\",\n )\n\n dice3d.addSystem({ id: \"fvtt-chroniques-de-l-etrangedigit\", name: \"Chroniques de l'\u00E9trange digits\" }, \"preferred\")\n dice3d.addDicePreset({ type: \"d10\", labels: DIGIT_LABELS, system: \"fvtt-chroniques-de-l-etrangedigit\" })\n\n dice3d.addSystem({ id: \"fvtt-chroniques-de-l-etrange\", name: \"Chroniques de l'\u00E9trange\" }, \"preferred\")\n dice3d.addDicePreset({ type: \"d10\", labels: CLASSIC_LABELS, system: \"fvtt-chroniques-de-l-etrange\" })\n })\n}\n", "import { MAGICS } from \"../config/constants.js\"\n\nexport function registerHandlebarsHelpers() {\n const { Handlebars } = globalThis\n if (!Handlebars) return\n\n Handlebars.registerHelper(\"select\", function (selected, options) {\n const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected))\n const rgx = new RegExp(` value=[\"']${escapedValue}[\"']`)\n const html = options.fn(this)\n return html.replace(rgx, \"$& selected\")\n })\n\n Handlebars.registerHelper(\"getMagicBackground\", function (magic) {\n return game.i18n.localize(MAGICS[magic]?.background ?? \"\")\n })\n\n Handlebars.registerHelper(\"getMagicLabel\", function (magic) {\n return game.i18n.localize(MAGICS[magic]?.label ?? \"\")\n })\n\n Handlebars.registerHelper(\"getMagicAspectLabel\", function (magic) {\n return game.i18n.localize(MAGICS[magic]?.aspectlabel ?? \"\")\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityLabel\", function (magic, speciality) {\n return game.i18n.localize(MAGICS[magic]?.speciality?.[speciality]?.label ?? \"\")\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityClassIcon\", function (magic, speciality) {\n return MAGICS[magic]?.speciality?.[speciality]?.classicon ?? \"\"\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityIcon\", function (magic, speciality) {\n return MAGICS[magic]?.speciality?.[speciality]?.icon ?? \"\"\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityElementIcon\", function (magic, speciality) {\n return MAGICS[magic]?.speciality?.[speciality]?.elementicon ?? \"\"\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityLabelIcon\", function (magic, speciality) {\n return MAGICS[magic]?.speciality?.[speciality]?.labelicon ?? \"\"\n })\n\n Handlebars.registerHelper(\"getMagicSpecialityLabelElement\", function (magic, speciality) {\n return game.i18n.localize(MAGICS[magic]?.speciality?.[speciality]?.labelelement ?? \"\")\n })\n\n Handlebars.registerHelper(\"getMagicAspectIcon\", function (magic) {\n const icons = {\n internalcinnabar: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\",\n alchemy: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\",\n masteryoftheway: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\",\n exorcism: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\",\n geomancy: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\",\n }\n return icons[magic] ?? \"\"\n })\n\n Handlebars.registerHelper(\"getElementIcon\", function (aspect) {\n const icons = {\n metal: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\",\n water: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\",\n earth: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\",\n fire: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\",\n wood: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\",\n // legacy French keys\n eau: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\",\n terre: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\",\n feu: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\",\n bois: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\",\n }\n return icons[aspect] ?? \"\"\n })\n\n Handlebars.registerHelper(\"getOrientationIcon\", function (orientation) {\n const icons = {\n yin: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.webp\",\n yang: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.webp\",\n yinyang: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.webp\",\n }\n return icons[orientation] ?? \"\"\n })\n\n Handlebars.registerHelper(\"getOrientationLabel\", function (orientation) {\n const keys = {\n yin: \"CDE.OrientationYin\",\n yang: \"CDE.OrientationYang\",\n yinyang: \"CDE.OrientationYinYang\",\n }\n return game.i18n.localize(keys[orientation] ?? \"CDE.Orientation\")\n })\n\n Handlebars.registerHelper(\"getActivationLabel\", function (activation) {\n const keys = {\n \"action-attack\": \"CDE.ActivationAttack\",\n \"action-defense\": \"CDE.ActivationDefense\",\n \"action-aid\": \"CDE.ActivationAid\",\n \"action-attack-defense\": \"CDE.ActivationAttackOrDefense\",\n reaction: \"CDE.ActivationReaction\",\n dice: \"CDE.ActivationDice\",\n \"damage-inflicted\": \"CDE.ActivationDamageInflicted\",\n \"damage-received\": \"CDE.ActivationDamageReceived\",\n }\n return game.i18n.localize(keys[activation] ?? \"CDE.Activation\")\n })\n}\n", "import { TEMPLATE_PARTIALS } from \"../config/constants.js\"\n\nexport async function preloadPartials() {\n return loadTemplates(TEMPLATE_PARTIALS)\n}\n", "/**\n * Initiative determination system for Chroniques de l'\u00C9trange.\n *\n * PJ formula: Initiative = Prouesse + Premi\u00E8re action (comp\u00E9tence/ressource/magie)\n * PNJ formula: Initiative = Aptitude physique + Premi\u00E8re action (aptitude)\n *\n * Range 1-24 ; anti-initiative = 25 \u2212 initiative.\n * Combat order is ascending (low initiative acts first).\n */\n\nconst PC_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-prompt.html\"\nconst NPC_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-prompt-npc.html\"\nconst RESULT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-result.html\"\n\n/** Skills, resources and magics available as \"premi\u00E8re action\" for a PC. */\nfunction buildPCOptions(sys) {\n const sk = sys.skills ?? {}\n const rs = sys.resources ?? {}\n const mg = sys.magics ?? {}\n return [\n { key: \"art\", label: game.i18n.localize(\"CDE.Art\"), value: sk.art?.value ?? 0 },\n { key: \"investigation\", label: game.i18n.localize(\"CDE.Investigation\"), value: sk.investigation?.value ?? 0 },\n { key: \"erudition\", label: game.i18n.localize(\"CDE.Erudition\"), value: sk.erudition?.value ?? 0 },\n { key: \"knavery\", label: game.i18n.localize(\"CDE.Knavery\"), value: sk.knavery?.value ?? 0 },\n { key: \"wordliness\", label: game.i18n.localize(\"CDE.Wordliness\"), value: sk.wordliness?.value ?? 0 },\n { key: \"prowess\", label: game.i18n.localize(\"CDE.Prowess\"), value: sk.prowess?.value ?? 0 },\n { key: \"sciences\", label: game.i18n.localize(\"CDE.Sciences\"), value: sk.sciences?.value ?? 0 },\n { key: \"technologies\", label: game.i18n.localize(\"CDE.Technologies\"), value: sk.technologies?.value ?? 0 },\n { key: \"kungfu\", label: game.i18n.localize(\"CDE.KungFu\"), value: sk.kungfu?.value ?? 0 },\n { key: \"rangedcombat\", label: game.i18n.localize(\"CDE.RangedCombat\"), value: sk.rangedcombat?.value ?? 0 },\n { key: \"supply\", label: game.i18n.localize(\"CDE.Supply\"), value: rs.supply?.value ?? 0 },\n { key: \"inquiry\", label: game.i18n.localize(\"CDE.Inquiry\"), value: rs.inquiry?.value ?? 0 },\n { key: \"influence\", label: game.i18n.localize(\"CDE.Influence\"), value: rs.influence?.value ?? 0 },\n { key: \"internalcinnabar\", label: game.i18n.localize(\"CDE.InternalCinnabar\"), value: mg.internalcinnabar?.value ?? 0 },\n { key: \"alchemy\", label: game.i18n.localize(\"CDE.Alchemy\"), value: mg.alchemy?.value ?? 0 },\n { key: \"masteryoftheway\", label: game.i18n.localize(\"CDE.MasteryOfTheWay\"), value: mg.masteryoftheway?.value ?? 0 },\n { key: \"exorcism\", label: game.i18n.localize(\"CDE.Exorcism\"), value: mg.exorcism?.value ?? 0 },\n { key: \"geomancy\", label: game.i18n.localize(\"CDE.Geomancy\"), value: mg.geomancy?.value ?? 0 },\n ]\n}\n\n/** Aptitudes available as \"premi\u00E8re action\" for an NPC. */\nfunction buildNPCOptions(sys) {\n const ap = sys.aptitudes ?? {}\n return [\n { key: \"physical\", label: game.i18n.localize(\"CDE.Physical\"), value: ap.physical?.value ?? 0 },\n { key: \"martial\", label: game.i18n.localize(\"CDE.Martial\"), value: ap.martial?.value ?? 0 },\n { key: \"mental\", label: game.i18n.localize(\"CDE.Mental\"), value: ap.mental?.value ?? 0 },\n { key: \"social\", label: game.i18n.localize(\"CDE.Social\"), value: ap.social?.value ?? 0 },\n { key: \"spiritual\", label: game.i18n.localize(\"CDE.Spiritual\"), value: ap.spiritual?.value ?? 0 },\n ]\n}\n\n/** Parse the dialog element and extract firstaction + modifier. */\nfunction readInitFields(dialog) {\n const root = dialog.element ?? dialog\n const selectedKey = root.querySelector(\"select[name='firstaction']\")?.value ?? \"\"\n const modifier = parseInt(root.querySelector(\"input[name='modifier']\")?.value ?? \"0\", 10) || 0\n return { selectedKey, modifier }\n}\n\n/** Post a styled initiative chat message. */\nasync function sendInitChatMessage({ actor, baseName, baseValue, actionName, actionValue, modifier, initiative, antiInitiative }) {\n const html = await foundry.applications.handlebars.renderTemplate(RESULT_TEMPLATE, {\n actorName: actor.name,\n actorImg: actor.img,\n baseName,\n baseValue,\n actionName,\n actionValue,\n modifier,\n hasModifier: modifier !== 0,\n initiative,\n antiInitiative,\n })\n await ChatMessage.create({\n user: game.user.id,\n speaker: ChatMessage.getSpeaker({ actor }),\n content: html,\n })\n}\n\n/**\n * Open the PC initiative dialog, compute initiative (Prouesse + Premi\u00E8re action + modificateur)\n * and update the actor, then post a chat card.\n */\nexport async function rollInitiativePC(actor) {\n const sys = actor.system\n const prowess = sys.skills?.prowess?.value ?? 0\n const options = buildPCOptions(sys)\n const baseName = game.i18n.localize(\"CDE.Prowess\")\n\n const content = await foundry.applications.handlebars.renderTemplate(PC_PROMPT_TEMPLATE, {\n prowessValue: prowess,\n options,\n modifier: 0,\n })\n\n const result = await foundry.applications.api.DialogV2.prompt({\n window: { title: game.i18n.localize(\"CDE.InitiativeRoll\") },\n content,\n rejectClose: false,\n ok: {\n label: game.i18n.localize(\"CDE.Validate\"),\n callback: (_ev, _btn, dialog) => readInitFields(dialog),\n },\n })\n if (!result) return\n\n const { selectedKey, modifier } = result\n const selected = options.find((o) => o.key === selectedKey) ?? options[0]\n const rawValue = prowess + selected.value + modifier\n const initiative = Math.max(1, Math.min(24, rawValue))\n const antiInit = 25 - initiative\n\n await actor.update({ \"system.initiative\": initiative })\n await sendInitChatMessage({\n actor,\n baseName,\n baseValue: prowess,\n actionName: selected.label,\n actionValue: selected.value,\n modifier,\n initiative,\n antiInitiative: antiInit,\n })\n}\n\n/**\n * Open the NPC initiative dialog, compute initiative (Aptitude physique + Premi\u00E8re action + modificateur)\n * and update the actor, then post a chat card.\n */\nexport async function rollInitiativeNPC(actor) {\n const sys = actor.system\n const physical = sys.aptitudes?.physical?.value ?? 0\n const options = buildNPCOptions(sys)\n const baseName = game.i18n.localize(\"CDE.Physical\")\n\n const content = await foundry.applications.handlebars.renderTemplate(NPC_PROMPT_TEMPLATE, {\n physicalValue: physical,\n options,\n modifier: 0,\n })\n\n const result = await foundry.applications.api.DialogV2.prompt({\n window: { title: game.i18n.localize(\"CDE.InitiativeRoll\") },\n content,\n rejectClose: false,\n ok: {\n label: game.i18n.localize(\"CDE.Validate\"),\n callback: (_ev, _btn, dialog) => readInitFields(dialog),\n },\n })\n if (!result) return\n\n const { selectedKey, modifier } = result\n const selected = options.find((o) => o.key === selectedKey) ?? options[0]\n const rawValue = physical + selected.value + modifier\n const initiative = Math.max(1, Math.min(24, rawValue))\n const antiInit = 25 - initiative\n\n await actor.update({ \"system.initiative\": initiative })\n await sendInitChatMessage({\n actor,\n baseName,\n baseValue: physical,\n actionName: selected.label,\n actionValue: selected.value,\n modifier,\n initiative,\n antiInitiative: antiInit,\n })\n}\n", "/**\n * Wu Xing rolling system for Chroniques de l'\u00C9trange.\n *\n * The Wu Xing cycle maps each aspect (by index 0-4) to die face groups:\n * - metal=0 : faces 3,8\n * - water=1 : faces 1,6\n * - earth=2 : faces 0/10,5\n * - fire=3 : faces 2,7\n * - wood=4 : faces 4,9\n *\n * For a given active aspect the five result categories are:\n * successes / auspicious / noxious / loksyu (yin face, yang face) / tinji\n * Each category is associated with one of the five aspects in Wu Xing cycle order.\n */\n\nimport { MAGICS } from \"../config/constants.js\"\n\nconst RESULT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html\"\nconst SKILL_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-skill-dice-prompt.html\"\nconst SKILL_SPECIAL_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-skill-special-dice-prompt.html\"\nconst MAGIC_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-magic-dice-prompt.html\"\nconst WEAPON_PROMPT_TEMPLATE = \"systems/fvtt-chroniques-de-l-etrange/templates/form/cde-weapon-dice-prompt.html\"\n\n/** Maps i18n element label \u2192 aspect name (for speciality default aspect lookup) */\nconst LABELELEMENT_TO_ASPECT = {\n \"CDE.Metal\": \"metal\",\n \"CDE.Water\": \"water\",\n \"CDE.Earth\": \"earth\",\n \"CDE.Fire\": \"fire\",\n \"CDE.Wood\": \"wood\",\n}\n\n/** Map aspect index \u2192 string name used in result template */\nconst ASPECT_NAMES = [\"metal\", \"water\", \"earth\", \"fire\", \"wood\"]\n\n/** Map aspect name \u2192 i18n label key */\nconst ASPECT_LABELS = {\n metal: \"CDE.Metal\",\n water: \"CDE.Water\",\n earth: \"CDE.Earth\",\n fire: \"CDE.Fire\",\n wood: \"CDE.Wood\",\n}\n\n/** Map aspect name \u2192 image path */\nconst ASPECT_ICONS = {\n metal: \"systems/fvtt-chroniques-de-l-etrange/images/cde_metal.webp\",\n water: \"systems/fvtt-chroniques-de-l-etrange/images/cde_eau.webp\",\n earth: \"systems/fvtt-chroniques-de-l-etrange/images/cde_terre.webp\",\n fire: \"systems/fvtt-chroniques-de-l-etrange/images/cde_feu.webp\",\n wood: \"systems/fvtt-chroniques-de-l-etrange/images/cde_bois.webp\",\n}\n\n/** Map aspect index \u2192 die face pair [yin, yang] (face=10 stored as 0) */\nconst ASPECT_FACES = {\n metal: [3, 8],\n water: [1, 6],\n earth: [0, 5], // 0 = face \"10\"\n fire: [2, 7],\n wood: [4, 9],\n}\n\n/**\n * Wu Xing generating/overcoming cycle:\n * wood \u2192 fire \u2192 earth \u2192 metal \u2192 water \u2192 wood (generating)\n * For each active aspect, the five categories in order:\n * [successes, auspicious, noxious, loksyu, tinji]\n */\nconst WU_XING_CYCLE = {\n wood: [\"wood\", \"fire\", \"water\", \"earth\", \"metal\"],\n fire: [\"fire\", \"earth\", \"wood\", \"metal\", \"water\"],\n earth: [\"earth\", \"metal\", \"fire\", \"water\", \"wood\"],\n metal: [\"metal\", \"water\", \"earth\", \"wood\", \"fire\"],\n water: [\"water\", \"wood\", \"metal\", \"fire\", \"earth\"],\n}\n\n/** Maps weapon range string \u2192 dice malus applied to the attack pool */\nconst RANGE_MALUS = {\n contact: 0,\n courte: 0,\n mediane: -1,\n longue: -2,\n extreme: -3,\n}\n\n/** Maps weapon type string \u2192 default skill key */\nconst WEAPON_TYPE_SKILL = {\n melee: \"kungfu\",\n thrown: \"rangedcombat\",\n ranged: \"rangedcombat\",\n firearm: \"rangedcombat\",\n}\n\n/** Maps weapon damageAspect name \u2192 ASPECT_NAMES index */\nconst WEAPON_ASPECT_INDEX = { metal: 0, eau: 1, water: 1, terre: 2, earth: 2, feu: 3, fire: 3, bois: 4, wood: 4 }\n\n/** Count how many times each die face appeared in the roll results */\nfunction countFaces(rollResults) {\n const counts = { 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 0:0 }\n for (const die of rollResults) {\n const face = die.result === 10 ? 0 : die.result\n counts[face]++\n }\n return counts\n}\n\n/**\n * Compute Wu Xing result categories from face counts and active aspect.\n * Returns { successesdice, auspiciousdice, noxiousdice, loksyudice, tinjidice, loksyurepartition }\n */\nfunction computeWuXingResults(faces, aspectName, bonusAuspicious = 0) {\n const cycle = WU_XING_CYCLE[aspectName]\n if (!cycle) return null\n\n const [succAspect, ausAspect, noxAspect, lokAspect, tinAspect] = cycle\n const [succYin, succYang] = ASPECT_FACES[succAspect]\n const [ausYin, ausYang] = ASPECT_FACES[ausAspect]\n const [noxYin, noxYang] = ASPECT_FACES[noxAspect]\n const [lokYin, lokYang] = ASPECT_FACES[lokAspect]\n const [tinYin, tinYang] = ASPECT_FACES[tinAspect]\n\n const yin = game.i18n.localize(\"CDE.Yin\")\n const yang = game.i18n.localize(\"CDE.Yang\")\n\n return {\n successesdice: faces[succYin] + faces[succYang],\n auspiciousdice: faces[ausYin] + faces[ausYang] + bonusAuspicious,\n noxiousdice: faces[noxYin] + faces[noxYang],\n loksyudice: faces[lokYin] + faces[lokYang],\n loksyurepartition: `[${yin}(${faces[lokYin]}) ${yang}(${faces[lokYang]})]`,\n tinjidice: faces[tinYin] + faces[tinYang],\n }\n}\n\n/** Read a named field from a dialog DOM element */\nfunction readField(dlg, name) {\n const el = dlg.querySelector(`[name=\"${name}\"]`)\n if (!el) return null\n return el.type === \"checkbox\" ? el.checked : el.value\n}\n\n/**\n * Open a DialogV2.prompt with the given template + data and return the resolved form values.\n * The callback receives the DialogV2 application instance; fields are read from its .element.\n * @returns {Promise|null>}\n */\nasync function showRollPrompt({ title, template, data, fields }) {\n const content = await foundry.applications.handlebars.renderTemplate(template, data)\n return foundry.applications.api.DialogV2.prompt({\n window: { title },\n content,\n rejectClose: false,\n ok: {\n label: game.i18n.localize(\"CDE.Validate\"),\n callback: (event, button, dialog) => {\n // In AppV2, dialog is the application instance; .element is the root HTMLElement\n const root = dialog.element ?? dialog\n const result = {}\n for (const field of fields) {\n result[field] = readField(root, field)\n }\n return result\n },\n },\n })\n}\n\n/**\n * Open the skill roll prompt and return the user-confirmed parameters.\n * @param {object} params - Initial values\n * @returns {Promise