Correction compendiums

This commit is contained in:
2026-04-27 21:30:33 +02:00
parent 1e252ff6f2
commit bc49286f91
76 changed files with 1645 additions and 73 deletions
+355
View File
@@ -3833,3 +3833,358 @@ ol.item-list li.item .item-controls a.item-control:hover {
color: #ff3d5a;
text-shadow: 0 0 5px rgba(255, 61, 90, 0.4);
}
/* ============================================================
ROUE D'INITIATIVE — CDEWheelApp
============================================================ */
.cde-wheel-app {
color: #e2e8f4;
background: #080c14;
font-family: "Averia", "Averia Regular", sans-serif;
}
.cde-wheel-app .window-content {
padding: 0;
overflow: hidden;
}
/* Two-column layout: SVG wheel left, panel right */
.cde-wheel-layout {
display: flex;
height: 100%;
min-height: 520px;
}
/* ---- Left: SVG wheel ---- */
.cde-wheel-svg-container {
flex: 0 0 480px;
width: 480px;
padding: 12px 12px 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #0d1520;
border-right: 1px solid #1a2436;
}
.cde-wheel-svg-container svg {
flex: 1 1 auto;
width: 100%;
max-width: 456px;
max-height: 456px;
overflow: visible;
}
.cde-wheel-svg-container .cde-wheel-legend {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0 2px;
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
flex-shrink: 0;
}
.cde-wheel-svg-container .cde-wheel-legend-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.cde-wheel-svg-container .cde-wheel-segment {
stroke: #080c14;
stroke-width: 1.5;
transition: opacity 0.2s;
}
.cde-wheel-svg-container .cde-wheel-cran-label {
font-family: "Averia Regular", sans-serif;
font-size: 12px;
fill: rgba(255, 255, 255, 0.55);
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
user-select: none;
}
.cde-wheel-svg-container .cde-wheel-fighter-circle {
stroke-width: 2;
cursor: pointer;
transition: r 0.2s, stroke-width 0.2s;
}
.cde-wheel-svg-container .cde-wheel-fighter-circle:hover {
stroke-width: 3;
}
.cde-wheel-svg-container .cde-wheel-fighter-circle.is-active {
r: 18;
stroke-width: 3;
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.7));
}
.cde-wheel-svg-container .cde-wheel-fighter-circle.is-turn {
stroke-dasharray: 3 2;
animation: cde-spin 4s linear infinite;
}
.cde-wheel-svg-container .cde-wheel-fighter-initial {
font-size: 13px;
font-weight: 700;
fill: #fff;
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
}
/* ---- Right: panel ---- */
.cde-wheel-panel {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
min-width: 0;
}
.cde-wheel-section-title {
padding: 7px 12px 6px;
border-bottom: 1px solid #1a2436;
background: #0d1520;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: #7d94b8;
border-left: 3px solid #4a9eff;
flex-shrink: 0;
}
.cde-wheel-section-title em {
font-style: normal;
color: #e2e8f4;
text-transform: none;
letter-spacing: 0;
font-size: 11px;
}
/* Combatant list */
.cde-wheel-combatants {
flex: 0 0 auto;
max-height: 130px;
overflow-y: auto;
border-bottom: 1px solid #1a2436;
}
.cde-wheel-combatant {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px 6px 9px;
cursor: pointer;
border-bottom: 1px solid rgba(26, 36, 54, 0.5);
border-left: 3px solid transparent;
transition: background 0.15s, border-color 0.15s;
}
.cde-wheel-combatant:last-child {
border-bottom: none;
}
.cde-wheel-combatant:hover {
background: #101622;
}
.cde-wheel-combatant.cde-wheel-combatant--selected {
background: rgba(74, 158, 255, 0.1);
border-left-color: #4a9eff;
}
.cde-wheel-combatant.cde-wheel-combatant--selected .cde-wheel-combatant-name {
color: #e2e8f4;
font-weight: 600;
}
.cde-wheel-combatant.cde-wheel-combatant--active .cde-wheel-active-marker {
color: #f0c040;
filter: drop-shadow(0 0 3px #f0c040);
}
.cde-wheel-combatant-img {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
border: 1.5px solid #263853;
flex-shrink: 0;
}
.cde-wheel-combatant-name {
flex: 1 1 auto;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #7d94b8;
}
.cde-wheel-combatant-cran {
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
min-width: 22px;
text-align: center;
padding: 2px 6px;
border-radius: 12px;
line-height: 1.3;
}
.cde-wheel-active-marker {
font-size: 10px;
color: #7d94b8;
flex-shrink: 0;
}
/* Action area */
.cde-wheel-actions {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 8px 10px 10px;
gap: 6px;
}
.cde-wheel-actions.cde-wheel-actions--hint {
justify-content: center;
align-items: center;
}
.cde-wheel-hint {
color: #7d94b8;
font-size: 12px;
text-align: center;
font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.cde-wheel-hint i {
font-size: 22px;
opacity: 0.4;
}
.cde-wheel-action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
}
.cde-wheel-action-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
background: #101622;
border: 1px solid #263853;
border-radius: 5px;
color: #7d94b8;
font-size: 11px;
padding: 5px 7px;
cursor: pointer;
text-align: left;
transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
font-family: inherit;
}
.cde-wheel-action-btn:hover {
color: #e2e8f4;
background: #1a2436;
border-color: #263853;
}
.cde-wheel-action-btn .cde-wheel-action-name {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cde-wheel-action-btn .cde-wheel-action-cost {
flex-shrink: 0;
font-weight: 700;
font-size: 10px;
padding: 1px 5px;
border-radius: 10px;
background: #263853;
color: #7d94b8;
line-height: 1.4;
}
.cde-wheel-action-btn[data-cost="1"]:hover {
box-shadow: 0 0 6px rgba(74, 158, 255, 0.22);
}
.cde-wheel-action-btn[data-cost="1"] .cde-wheel-action-cost {
background: #1a3d6a;
color: #6aadff;
}
.cde-wheel-action-btn[data-cost="2"]:hover {
box-shadow: 0 0 6px rgba(212, 160, 80, 0.25);
}
.cde-wheel-action-btn[data-cost="2"] .cde-wheel-action-cost {
background: #4a3200;
color: #d4a050;
}
.cde-wheel-action-btn[data-cost="3"]:hover {
box-shadow: 0 0 6px rgba(224, 96, 48, 0.28);
}
.cde-wheel-action-btn[data-cost="3"] .cde-wheel-action-cost {
background: #4a1800;
color: #e07840;
}
.cde-wheel-action-btn[data-cost="6"] {
border-color: rgba(204, 32, 64, 0.4);
}
.cde-wheel-action-btn[data-cost="6"]:hover {
box-shadow: 0 0 6px rgba(204, 32, 64, 0.35);
}
.cde-wheel-action-btn[data-cost="6"] .cde-wheel-action-cost {
background: #4a0814;
color: #e03050;
}
/* Special action buttons */
.cde-wheel-special-actions {
display: flex;
gap: 6px;
margin-top: 2px;
}
.cde-wheel-btn-roll,
.cde-wheel-btn-surprise {
flex: 1 1 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: 5px;
font-size: 11px;
font-weight: 600;
padding: 7px 8px;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, box-shadow 0.15s;
border: 1px solid;
}
.cde-wheel-btn-roll {
background: rgba(192, 138, 0, 0.14);
border-color: rgba(192, 138, 0, 0.5);
color: #e0b030;
}
.cde-wheel-btn-roll:hover {
background: rgba(192, 138, 0, 0.26);
box-shadow: 0 0 8px rgba(192, 138, 0, 0.4);
}
.cde-wheel-btn-surprise {
background: rgba(255, 61, 90, 0.12);
border-color: rgba(255, 61, 90, 0.45);
color: #ff3d5a;
}
.cde-wheel-btn-surprise:hover {
background: rgba(255, 61, 90, 0.24);
box-shadow: 0 0 8px rgba(255, 61, 90, 0.35);
}
/* No-combat empty state */
.cde-wheel-no-combat {
flex: 1 1 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #7d94b8;
gap: 8px;
padding: 20px;
text-align: center;
font-size: 12px;
}
.cde-wheel-no-combat i {
font-size: 28px;
opacity: 0.4;
}
/* Spin animation for active-turn token */
@keyframes cde-spin {
from {
transform-origin: var(--fx) var(--fy);
transform: rotate(0deg);
}
to {
transform-origin: var(--fx) var(--fy);
transform: rotate(360deg);
}
}
+393
View File
@@ -3951,3 +3951,396 @@ ol.item-list {
text-shadow: 0 0 5px fade(@cde-kungfu, 40%);
}
}
/* ============================================================
ROUE D'INITIATIVE — CDEWheelApp
============================================================ */
// Wu Xing segment colours (match JS constants)
@wu-metal: #b8c4cc;
@wu-water: #3a7bd5;
@wu-earth: #c8a84b;
@wu-fire: #d94f3d;
@wu-wood: #4a9b5a;
@wu-reference: #2c1f6b;
.cde-wheel-app {
color: @cde-text;
background: @cde-bg;
font-family: "Averia", "Averia Regular", sans-serif;
.window-content {
padding: 0;
overflow: hidden;
}
}
/* Two-column layout: SVG wheel left, panel right */
.cde-wheel-layout {
display: flex;
height: 100%;
min-height: 520px;
}
/* ---- Left: SVG wheel ---- */
.cde-wheel-svg-container {
flex: 0 0 480px;
width: 480px;
padding: 12px 12px 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: @cde-surface2;
border-right: 1px solid @cde-border;
svg {
flex: 1 1 auto;
width: 100%;
max-width: 456px;
max-height: 456px;
overflow: visible;
}
.cde-wheel-legend {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0 2px;
font-size: 11px;
color: rgba(255,255,255,0.55);
flex-shrink: 0;
}
.cde-wheel-legend-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.cde-wheel-segment {
stroke: @cde-bg;
stroke-width: 1.5;
transition: opacity 0.2s;
}
.cde-wheel-cran-label {
font-family: "Averia Regular", sans-serif;
font-size: 12px;
fill: rgba(255,255,255,0.55);
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
user-select: none;
}
// Combatant token circle on the wheel
.cde-wheel-fighter-circle {
stroke-width: 2;
cursor: pointer;
transition: r 0.2s, stroke-width 0.2s;
&:hover {
stroke-width: 3;
}
&.is-active {
r: 18;
stroke-width: 3;
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.7));
}
&.is-turn {
stroke-dasharray: 3 2;
animation: cde-spin 4s linear infinite;
}
}
.cde-wheel-fighter-initial {
font-size: 13px;
font-weight: 700;
fill: #fff;
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
}
}
/* ---- Right: panel ---- */
.cde-wheel-panel {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
min-width: 0;
}
.cde-wheel-section-title {
padding: 7px 12px 6px;
border-bottom: 1px solid @cde-border;
background: @cde-surface2;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: @cde-muted;
border-left: 3px solid @cde-spell;
flex-shrink: 0;
em {
font-style: normal;
color: @cde-text;
text-transform: none;
letter-spacing: 0;
font-size: 11px;
}
}
/* Combatant list */
.cde-wheel-combatants {
flex: 0 0 auto;
max-height: 130px;
overflow-y: auto;
border-bottom: 1px solid @cde-border;
}
.cde-wheel-combatant {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px 6px 9px;
cursor: pointer;
border-bottom: 1px solid fade(@cde-border, 50%);
border-left: 3px solid transparent;
transition: background 0.15s, border-color 0.15s;
&:last-child { border-bottom: none; }
&:hover {
background: @cde-surface;
}
&.cde-wheel-combatant--selected {
background: fade(@cde-spell, 10%);
border-left-color: @cde-spell;
.cde-wheel-combatant-name {
color: @cde-text;
font-weight: 600;
}
}
&.cde-wheel-combatant--active {
.cde-wheel-active-marker {
color: #f0c040;
filter: drop-shadow(0 0 3px #f0c040);
}
}
}
.cde-wheel-combatant-img {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
border: 1.5px solid @cde-border-hi;
flex-shrink: 0;
}
.cde-wheel-combatant-name {
flex: 1 1 auto;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: @cde-muted;
}
.cde-wheel-combatant-cran {
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
min-width: 22px;
text-align: center;
padding: 2px 6px;
border-radius: 12px;
line-height: 1.3;
}
.cde-wheel-active-marker {
font-size: 10px;
color: @cde-muted;
flex-shrink: 0;
}
/* Action area */
.cde-wheel-actions {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 8px 10px 10px;
gap: 6px;
&.cde-wheel-actions--hint {
justify-content: center;
align-items: center;
}
}
.cde-wheel-hint {
color: @cde-muted;
font-size: 12px;
text-align: center;
font-style: italic;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
i {
font-size: 22px;
opacity: 0.4;
}
}
.cde-wheel-action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
}
.cde-wheel-action-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
background: @cde-surface;
border: 1px solid @cde-border-hi;
border-radius: @cde-radius-sm;
color: @cde-muted;
font-size: 11px;
padding: 5px 7px;
cursor: pointer;
text-align: left;
transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
font-family: inherit;
&:hover {
color: @cde-text;
background: @cde-border;
border-color: @cde-border-hi;
}
.cde-wheel-action-name {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cde-wheel-action-cost {
flex-shrink: 0;
font-weight: 700;
font-size: 10px;
padding: 1px 5px;
border-radius: 10px;
background: @cde-border-hi;
color: @cde-muted;
line-height: 1.4;
}
&[data-cost="1"] {
&:hover { box-shadow: 0 0 6px fade(@cde-spell, 22%); }
.cde-wheel-action-cost { background: #1a3d6a; color: #6aadff; }
}
&[data-cost="2"] {
&:hover { box-shadow: 0 0 6px fade(#d4a050, 25%); }
.cde-wheel-action-cost { background: #4a3200; color: #d4a050; }
}
&[data-cost="3"] {
&:hover { box-shadow: 0 0 6px fade(#e06030, 28%); }
.cde-wheel-action-cost { background: #4a1800; color: #e07840; }
}
&[data-cost="6"] {
border-color: fade(#cc2040, 40%);
&:hover { box-shadow: 0 0 6px fade(#cc2040, 35%); }
.cde-wheel-action-cost { background: #4a0814; color: #e03050; }
}
}
/* Special action buttons */
.cde-wheel-special-actions {
display: flex;
gap: 6px;
margin-top: 2px;
}
.cde-wheel-btn-roll,
.cde-wheel-btn-surprise {
flex: 1 1 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: @cde-radius-sm;
font-size: 11px;
font-weight: 600;
padding: 7px 8px;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, box-shadow 0.15s;
border: 1px solid;
}
.cde-wheel-btn-roll {
background: fade(#c08a00, 14%);
border-color: fade(#c08a00, 50%);
color: #e0b030;
&:hover {
background: fade(#c08a00, 26%);
box-shadow: 0 0 8px fade(#c08a00, 40%);
}
}
.cde-wheel-btn-surprise {
background: fade(@cde-kungfu, 12%);
border-color: fade(@cde-kungfu, 45%);
color: @cde-kungfu;
&:hover {
background: fade(@cde-kungfu, 24%);
box-shadow: 0 0 8px fade(@cde-kungfu, 35%);
}
}
/* No-combat empty state */
.cde-wheel-no-combat {
flex: 1 1 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: @cde-muted;
gap: 8px;
padding: 20px;
text-align: center;
font-size: 12px;
i {
font-size: 28px;
opacity: 0.4;
}
}
/* Spin animation for active-turn token */
@keyframes cde-spin {
from { transform-origin: var(--fx) var(--fy); transform: rotate(0deg); }
to { transform-origin: var(--fx) var(--fy); transform: rotate(360deg); }
}
+244 -2
View File
@@ -130,7 +130,8 @@ var TEMPLATE_PARTIALS = [
"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/apps/cde-loksyu-app.html",
"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-tinji-app.html"
"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-tinji-app.html",
"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html"
];
// src/config/settings.js
@@ -807,6 +808,20 @@ function registerHandlebarsHelpers() {
};
return game.i18n.localize(keys[activation] ?? "CDE.Activation");
});
Handlebars.registerHelper("cranPosition", function(cran, cx, cy, r) {
const angleDeg = 90 + cran * 15;
const angleRad = angleDeg * Math.PI / 180;
const x = Math.round(cx + r * Math.cos(angleRad));
const y = Math.round(cy - r * Math.sin(angleRad));
return { x, y };
});
Handlebars.registerHelper("fighterX", function(cx, index, total) {
const offset = total > 1 ? (index - (total - 1) / 2) * 34 : 0;
return Math.round(cx - 15 + offset);
});
Handlebars.registerHelper("fighterY", function(cy, index, total) {
return Math.round(cy - 50);
});
}
// src/ui/templates.js
@@ -2209,6 +2224,205 @@ var CDETinjiApp = class _CDETinjiApp extends foundry.applications.api.Handlebars
}
};
// src/documents/combat.js
var CDECombat = class extends Combat {
/**
* Override rollInitiative to open the PC or NPC initiative dialog
* for each selected combatant, then sync the result to the Combatant document.
*/
async rollInitiative(ids, options = {}) {
const combatantIds = typeof ids === "string" ? [ids] : ids;
for (const id of combatantIds) {
const combatant = this.combatants.get(id);
if (!combatant) continue;
const actor = combatant.actor;
if (!actor) continue;
if (actor.type === ACTOR_TYPES.character) {
await rollInitiativePC(actor);
} else {
await rollInitiativeNPC(actor);
}
}
return this;
}
/**
* Sort combatants: highest initiative first (furthest counter-clockwise = acts first).
* Ties: PCs before NPCs; among PCs, by name; among NPCs, by name.
* Calls super.setupTurns() first to ensure this.current is properly initialized.
*/
setupTurns() {
super.setupTurns();
this.turns = this.turns.slice().sort((a, b) => {
const ia = a.initiative ?? 0;
const ib = b.initiative ?? 0;
if (ia !== ib) return ib - ia;
const aIsPC = a.actor?.type === ACTOR_TYPES.character ? 1 : 0;
const bIsPC = b.actor?.type === ACTOR_TYPES.character ? 1 : 0;
if (aIsPC !== bIsPC) return bIsPC - aIsPC;
return (a.name ?? "").localeCompare(b.name ?? "");
});
return this.turns;
}
};
async function advanceCombatantPosition(combatant, cranCost) {
const current = combatant.initiative ?? combatant.actor?.system?.initiative ?? 1;
const newValue = (current - cranCost - 1 + 48) % 24 + 1;
await combatant.update({ initiative: newValue });
}
// src/ui/apps/wheel-app.js
var WHEEL_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html";
var ACTION_COSTS = [
{ key: "draw", label: "CDE.ActionCostDraw", cost: 1 },
{ key: "changestyle", label: "CDE.ActionCostChangeStyle", cost: 1 },
{ key: "defense", label: "CDE.ActionCostDefense", cost: 1 },
{ key: "aim", label: "CDE.ActionCostAim", cost: 2 },
{ key: "help", label: "CDE.ActionCostHelp", cost: 2 },
{ key: "defally", label: "CDE.ActionCostDefendAlly", cost: 2 },
{ key: "move", label: "CDE.ActionCostMove", cost: 2 },
{ key: "attack", label: "CDE.ActionCostAttack", cost: 3 },
{ key: "delay", label: "CDE.ActionCostDelay", cost: 6 }
];
var WHEEL_SEGMENTS = [
{ label: "M\xE9tal", color: "#b8c4cc", textColor: "#1a1a1a", crans: [1, 2, 3, 4] },
{ label: "Eau", color: "#3a7bd5", textColor: "#ffffff", crans: [5, 6, 7, 8] },
{ label: "Terre", color: "#c8a84b", textColor: "#1a1a1a", crans: [9, 10, 11, 12] },
{ label: "Feu", color: "#d94f3d", textColor: "#ffffff", crans: [13, 14, 15, 16] },
{ label: "Bois", color: "#4a9b5a", textColor: "#ffffff", crans: [17, 18, 19, 20] },
{ label: "Rep\xE8re", color: "#1a1a2e", textColor: "#aaaaaa", crans: [21, 22, 23, 24] }
];
function segmentForCran(cran) {
return WHEEL_SEGMENTS.find((s) => s.crans.includes(cran)) ?? WHEEL_SEGMENTS[0];
}
var CDEWheelApp = class _CDEWheelApp extends foundry.applications.api.ApplicationV2 {
static DEFAULT_OPTIONS = {
id: "cde-wheel-app",
classes: ["cde-wheel-app"],
tag: "div",
window: {
title: "CDE.InitiativeWheel",
icon: "fas fa-circle-notch",
resizable: true
},
position: { width: 820, height: 620 },
actions: {
advanceCran: _CDEWheelApp.#advanceCran,
setSurprised: _CDEWheelApp.#setSurprised,
rollInitiative: _CDEWheelApp.#rollInitiative
}
};
/** @type {CDEWheelApp|null} */
static #instance = null;
/** Open (or bring to front) the singleton instance. */
static open() {
if (!_CDEWheelApp.#instance || _CDEWheelApp.#instance.rendered === false) {
_CDEWheelApp.#instance = new _CDEWheelApp();
_CDEWheelApp.#instance.render(true);
} else {
_CDEWheelApp.#instance.bringToFront();
}
return _CDEWheelApp.#instance;
}
/** Currently selected combatant id (for action panel). */
#selectedId = null;
async _prepareContext(options) {
const combat = game.combat;
const combatants = combat ? [...combat.combatants.values()] : [];
const sorted = [...combatants].sort((a, b) => (b.initiative ?? 0) - (a.initiative ?? 0));
const cranData = this.#buildCranData(combatants);
const selected = this.#selectedId ? combatants.find((c) => c.id === this.#selectedId) : null;
const actionCosts = ACTION_COSTS.map((a) => ({
...a,
label: game.i18n.localize(a.label)
}));
return {
hasCombat: !!combat,
combatants: sorted.map((c) => ({
id: c.id,
name: c.name,
img: c.token?.texture?.src ?? c.actor?.img ?? "icons/svg/mystery-man.svg",
initiative: c.initiative ?? "\u2014",
segment: segmentForCran(c.initiative ?? 1),
isActive: combat?.current?.combatantId === c.id,
isSelected: c.id === this.#selectedId,
hasInitiative: c.initiative != null
})),
cranData,
selected,
selectedName: selected?.name ?? null,
actionCosts
};
}
async _renderHTML(context, options) {
return foundry.applications.handlebars.renderTemplate(WHEEL_TEMPLATE, context);
}
_replaceHTML(result, content, options) {
content.innerHTML = result;
this.#bindEvents(content);
}
/** Build per-cran data for the SVG wheel. */
#buildCranData(combatants) {
const data = [];
for (let cran = 1; cran <= 24; cran++) {
const segment = segmentForCran(cran);
const fighters = combatants.filter((c) => Math.round(c.initiative) === cran);
data.push({ cran, segment, fighters });
}
return data;
}
/** Bind click events for combatant selection. */
#bindEvents(content) {
content.querySelectorAll("[data-select-combatant]").forEach((el) => {
el.addEventListener("click", () => {
this.#selectedId = el.dataset.selectCombatant;
this.render();
});
});
}
/** Action: advance selected combatant by given cran cost. */
static async #advanceCran(event, element) {
const app = _CDEWheelApp.#instance;
if (!app?.#selectedId) return;
const cost = parseInt(element.dataset.cost, 10);
if (!cost || isNaN(cost)) return;
const combatant = game.combat?.combatants.get(app.#selectedId);
if (!combatant) return;
await advanceCombatantPosition(combatant, cost);
}
/** Action: set selected combatant to surprised (position 1 = reference). */
static async #setSurprised(event, element) {
const app = _CDEWheelApp.#instance;
if (!app?.#selectedId) return;
const combatant = game.combat?.combatants.get(app.#selectedId);
if (!combatant) return;
await combatant.update({ initiative: 1 });
}
/** Action: open the initiative dialog for the selected combatant. */
static async #rollInitiative(event, element) {
const app = _CDEWheelApp.#instance;
if (!app?.#selectedId) return;
const combatant = game.combat?.combatants.get(app.#selectedId);
if (!combatant) return;
await game.combat.rollInitiative([app.#selectedId]);
}
/** Re-render when combat state changes. */
static registerHooks() {
const refresh = () => {
if (_CDEWheelApp.#instance?.rendered) _CDEWheelApp.#instance.render();
};
Hooks.on("updateCombat", refresh);
Hooks.on("updateCombatant", refresh);
Hooks.on("createCombatant", refresh);
Hooks.on("deleteCombatant", refresh);
Hooks.on("updateActor", (_actor, diff) => {
if (foundry.utils.hasProperty(diff, "system.initiative")) refresh();
});
Hooks.on("deleteCombat", () => {
if (_CDEWheelApp.#instance?.rendered) _CDEWheelApp.#instance.render();
});
}
};
// src/ui/roll-actions.js
var RESULT_TEMPLATE3 = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html";
function injectRollActions(message, html) {
@@ -2362,7 +2576,8 @@ Hooks.once("init", async () => {
console.info(`CHRONIQUESDELETRANGE | Initializing ${SYSTEM_ID}`);
registerSettings();
game.system.CONST = { MAGICS, SUBTYPES };
game.cde = { CDELoksyuApp, CDETinjiApp };
game.cde = { CDELoksyuApp, CDETinjiApp, CDEWheelApp };
CONFIG.Combat.documentClass = CDECombat;
CONFIG.Actor.dataModels = {
[ACTOR_TYPES.character]: CharacterDataModel,
[ACTOR_TYPES.npc]: NpcDataModel
@@ -2440,6 +2655,7 @@ Hooks.once("init", async () => {
});
Hooks.once("ready", async () => {
await migrateIfNeeded();
CDEWheelApp.registerHooks();
});
Hooks.on("renderChatLog", (_app, html) => {
const el = html instanceof HTMLElement ? html : html[0] ?? html;
@@ -2454,10 +2670,14 @@ Hooks.on("renderChatLog", (_app, html) => {
<button type="button" class="cde-chat-btn cde-chat-btn--tinji">
<i class="fas fa-star"></i> ${game.i18n.localize("CDE.TinJi2")}
</button>
<button type="button" class="cde-chat-btn cde-chat-btn--wheel">
<i class="fas fa-circle-notch"></i> ${game.i18n.localize("CDE.InitiativeWheel")}
</button>
`;
wrapper.addEventListener("click", (ev) => {
if (ev.target.closest(".cde-chat-btn--loksyu")) CDELoksyuApp.open();
if (ev.target.closest(".cde-chat-btn--tinji")) CDETinjiApp.open();
if (ev.target.closest(".cde-chat-btn--wheel")) CDEWheelApp.open();
});
const anchor = el.querySelector(".chat-form") ?? el.querySelector(".chat-message-form") ?? el.querySelector("form");
if (anchor) anchor.parentElement.insertBefore(wrapper, anchor);
@@ -2472,6 +2692,28 @@ Hooks.on("updateSetting", (setting) => {
refreshAllRollActions();
}
});
Hooks.on("updateActor", (actor, diff) => {
if (!foundry.utils.hasProperty(diff, "system.initiative")) return;
if (!game.combat) return;
const initiative = actor.system.initiative;
const combatant = game.combat.combatants.find((c) => c.actor?.id === actor.id);
if (combatant && combatant.initiative !== initiative) {
combatant.update({ initiative }).catch(() => {
});
}
});
Hooks.on("updateCombatant", (combatant, diff) => {
if (!("initiative" in diff)) return;
const initiative = combatant.initiative;
if (initiative == null) return;
setTimeout(() => {
const actor = combatant.actor;
if (actor && actor.system?.initiative !== initiative) {
actor.update({ "system.initiative": initiative }).catch(() => {
});
}
}, 0);
});
/**
* Chroniques de l'Étrange — Système FoundryVTT
*
+3 -3
View File
File diff suppressed because one or more lines are too long
+22
View File
@@ -131,6 +131,28 @@
"CDE.InitiativeNPCSpeciality": "Première action (Aptitude) que vous escomptez effectuer",
"CDE.InitiativeRoll": "Jet d'initiative",
"CDE.InitiativeSpeciality": "Première action (Compétence) que vous escomptez effectuer",
"CDE.InitiativeWheel": "Roue d'Initiative",
"CDE.InitiativeWheelOpen": "Ouvrir la Roue d'Initiative",
"CDE.InitiativeWheelHint": "Roue d'initiative Les Chroniques de l'Étrange",
"CDE.ActionCostAttack": "Attaque",
"CDE.ActionCostDefense": "Défense",
"CDE.ActionCostDefendAlly": "Défendre un allié",
"CDE.ActionCostMove": "Déplacement",
"CDE.ActionCostHelp": "Aide",
"CDE.ActionCostAim": "Viser",
"CDE.ActionCostChangeStyle": "Changer d'art",
"CDE.ActionCostDraw": "Dégainer",
"CDE.ActionCostDelay": "Retarder (événement)",
"CDE.ActionCostAdvance": "Avancer de {n} crans",
"CDE.ActiveCombatant": "Combat en cours — ce personnage agit maintenant",
"CDE.AdvanceCombatant": "Avancer sur la roue",
"CDE.Combatants": "Combattants",
"CDE.NoCombatActive": "Aucun combat en cours",
"CDE.SelectCombatantHint": "Cliquez sur un combattant pour sélectionner ses actions",
"CDE.SurprisedAction": "Pris par surprise",
"CDE.SurprisedHint": "Place le personnage sur le cran de référence (position 1)",
"CDE.WheelCran": "Cran",
"CDE.WheelReference": "Repère",
"CDE.Inquiry": "Renseignement",
"CDE.InternalCinnabar": "Cinabre Interne",
"CDE.Investigation": "Enquête",
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/04/27-17:47:13.055628 7f2779bff6c0 Delete type=3 #1
2026/04/27-17:47:13.058468 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.061813 7f272b7fe6c0 Level-0 table #5: 1330 bytes OK
2026/04/27-17:47:13.067956 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.068111 7f272b7fe6c0 Manual compaction at level-0 from '!items!3aig6MWvZCRoWXPW' @ 72057594037927935 : 1 .. '!items!cXaQG1TBE0jzrbNt' @ 0 : 0; will stop at (end)
2026/04/27-20:01:11.390845 7fed927fc6c0 Recovering log #4
2026/04/27-20:01:11.400505 7fed927fc6c0 Delete type=3 #2
2026/04/27-20:01:11.400599 7fed927fc6c0 Delete type=0 #4
+5
View File
@@ -0,0 +1,5 @@
2026/04/27-17:47:13.055628 7f2779bff6c0 Delete type=3 #1
2026/04/27-17:47:13.058468 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.061813 7f272b7fe6c0 Level-0 table #5: 1330 bytes OK
2026/04/27-17:47:13.067956 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.068111 7f272b7fe6c0 Manual compaction at level-0 from '!items!3aig6MWvZCRoWXPW' @ 72057594037927935 : 1 .. '!items!cXaQG1TBE0jzrbNt' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/04/27-17:47:13.085519 7f27793fe6c0 Delete type=3 #1
2026/04/27-17:47:13.087023 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.091030 7f272b7fe6c0 Level-0 table #5: 5923 bytes OK
2026/04/27-17:47:13.097545 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.097759 7f272b7fe6c0 Manual compaction at level-0 from '!items!0NDBw1YB54q3hLH0' @ 72057594037927935 : 1 .. '!items!ykekdZlirabRobEF' @ 0 : 0; will stop at (end)
2026/04/27-20:01:11.418750 7fed93fff6c0 Recovering log #4
2026/04/27-20:01:11.428738 7fed93fff6c0 Delete type=3 #2
2026/04/27-20:01:11.428793 7fed93fff6c0 Delete type=0 #4
+5
View File
@@ -0,0 +1,5 @@
2026/04/27-17:47:13.085519 7f27793fe6c0 Delete type=3 #1
2026/04/27-17:47:13.087023 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.091030 7f272b7fe6c0 Level-0 table #5: 5923 bytes OK
2026/04/27-17:47:13.097545 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.097759 7f272b7fe6c0 Manual compaction at level-0 from '!items!0NDBw1YB54q3hLH0' @ 72057594037927935 : 1 .. '!items!ykekdZlirabRobEF' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/04/27-17:47:13.116591 7f272bfff6c0 Delete type=3 #1
2026/04/27-17:47:13.117666 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.121072 7f272b7fe6c0 Level-0 table #5: 559 bytes OK
2026/04/27-17:47:13.127453 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.127641 7f272b7fe6c0 Manual compaction at level-0 from '!items!HKq5ANSGiBIdcnki' @ 72057594037927935 : 1 .. '!items!HKq5ANSGiBIdcnki' @ 0 : 0; will stop at (end)
2026/04/27-20:01:11.433002 7fed92ffd6c0 Recovering log #4
2026/04/27-20:01:11.442974 7fed92ffd6c0 Delete type=3 #2
2026/04/27-20:01:11.443041 7fed92ffd6c0 Delete type=0 #4
+5
View File
@@ -0,0 +1,5 @@
2026/04/27-17:47:13.116591 7f272bfff6c0 Delete type=3 #1
2026/04/27-17:47:13.117666 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.121072 7f272b7fe6c0 Level-0 table #5: 559 bytes OK
2026/04/27-17:47:13.127453 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.127641 7f272b7fe6c0 Manual compaction at level-0 from '!items!HKq5ANSGiBIdcnki' @ 72057594037927935 : 1 .. '!items!HKq5ANSGiBIdcnki' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/04/27-17:47:13.145290 7f2778bfd6c0 Delete type=3 #1
2026/04/27-17:47:13.146592 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.150681 7f272b7fe6c0 Level-0 table #5: 32988 bytes OK
2026/04/27-17:47:13.157088 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.157210 7f272b7fe6c0 Manual compaction at level-0 from '!items!2nKXEHLG0fXtSOdy' @ 72057594037927935 : 1 .. '!items!tlIc1bmIAbQeUwj7' @ 0 : 0; will stop at (end)
2026/04/27-20:01:11.327150 7fed93fff6c0 Recovering log #4
2026/04/27-20:01:11.338223 7fed93fff6c0 Delete type=3 #2
2026/04/27-20:01:11.338311 7fed93fff6c0 Delete type=0 #4
+5
View File
@@ -0,0 +1,5 @@
2026/04/27-17:47:13.145290 7f2778bfd6c0 Delete type=3 #1
2026/04/27-17:47:13.146592 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.150681 7f272b7fe6c0 Level-0 table #5: 32988 bytes OK
2026/04/27-17:47:13.157088 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.157210 7f272b7fe6c0 Manual compaction at level-0 from '!items!2nKXEHLG0fXtSOdy' @ 72057594037927935 : 1 .. '!items!tlIc1bmIAbQeUwj7' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/04/27-17:47:13.175381 7f2778bfd6c0 Delete type=3 #1
2026/04/27-17:47:13.176524 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.180270 7f272b7fe6c0 Level-0 table #5: 21686 bytes OK
2026/04/27-17:47:13.186334 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.186548 7f272b7fe6c0 Manual compaction at level-0 from '!actors!4ZjFZ1HoJV9mJStt' @ 72057594037927935 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at (end)
2026/04/27-20:01:11.445769 7fed937fe6c0 Recovering log #4
2026/04/27-20:01:11.456194 7fed937fe6c0 Delete type=3 #2
2026/04/27-20:01:11.456260 7fed937fe6c0 Delete type=0 #4
+5
View File
@@ -0,0 +1,5 @@
2026/04/27-17:47:13.175381 7f2778bfd6c0 Delete type=3 #1
2026/04/27-17:47:13.176524 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.180270 7f272b7fe6c0 Level-0 table #5: 21686 bytes OK
2026/04/27-17:47:13.186334 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.186548 7f272b7fe6c0 Manual compaction at level-0 from '!actors!4ZjFZ1HoJV9mJStt' @ 72057594037927935 : 1 .. '!actors!zVpmacwoWEG8YTCQ' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/04/27-17:47:13.202702 7f27793fe6c0 Delete type=3 #1
2026/04/27-17:47:13.203697 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.207185 7f272b7fe6c0 Level-0 table #5: 4830 bytes OK
2026/04/27-17:47:13.213948 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.214169 7f272b7fe6c0 Manual compaction at level-0 from '!items!DC2kimCi9sWxqhXG' @ 72057594037927935 : 1 .. '!items!qzfAEhmvVxEMzm0k' @ 0 : 0; will stop at (end)
2026/04/27-20:01:11.404989 7fed937fe6c0 Recovering log #4
2026/04/27-20:01:11.415714 7fed937fe6c0 Delete type=3 #2
2026/04/27-20:01:11.415769 7fed937fe6c0 Delete type=0 #4
+5
View File
@@ -0,0 +1,5 @@
2026/04/27-17:47:13.202702 7f27793fe6c0 Delete type=3 #1
2026/04/27-17:47:13.203697 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.207185 7f272b7fe6c0 Level-0 table #5: 4830 bytes OK
2026/04/27-17:47:13.213948 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.214169 7f272b7fe6c0 Manual compaction at level-0 from '!items!DC2kimCi9sWxqhXG' @ 72057594037927935 : 1 .. '!items!qzfAEhmvVxEMzm0k' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/04/27-17:47:13.236474 7f2778bfd6c0 Delete type=3 #1
2026/04/27-17:47:13.238827 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.244095 7f272b7fe6c0 Level-0 table #5: 120353 bytes OK
2026/04/27-17:47:13.250468 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.250702 7f272b7fe6c0 Manual compaction at level-0 from '!items!2f51pcvFkcZjaxDk' @ 72057594037927935 : 1 .. '!items!yVN7PZw35iIaBl0H' @ 0 : 0; will stop at (end)
2026/04/27-20:01:11.343717 7fed927fc6c0 Recovering log #4
2026/04/27-20:01:11.353301 7fed927fc6c0 Delete type=3 #2
2026/04/27-20:01:11.353373 7fed927fc6c0 Delete type=0 #4
+5
View File
@@ -0,0 +1,5 @@
2026/04/27-17:47:13.236474 7f2778bfd6c0 Delete type=3 #1
2026/04/27-17:47:13.238827 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.244095 7f272b7fe6c0 Level-0 table #5: 120353 bytes OK
2026/04/27-17:47:13.250468 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.250702 7f272b7fe6c0 Manual compaction at level-0 from '!items!2f51pcvFkcZjaxDk' @ 72057594037927935 : 1 .. '!items!yVN7PZw35iIaBl0H' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/04/27-17:47:13.267650 7f272bfff6c0 Delete type=3 #1
2026/04/27-17:47:13.268718 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.272210 7f272b7fe6c0 Level-0 table #5: 8622 bytes OK
2026/04/27-17:47:13.278603 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.278822 7f272b7fe6c0 Manual compaction at level-0 from '!items!APN91pQL0NBfZsG7' @ 72057594037927935 : 1 .. '!items!xxZKGqDVxAfr140W' @ 0 : 0; will stop at (end)
2026/04/27-20:01:11.363742 7fed937fe6c0 Recovering log #4
2026/04/27-20:01:11.374069 7fed937fe6c0 Delete type=3 #2
2026/04/27-20:01:11.374139 7fed937fe6c0 Delete type=0 #4
+5
View File
@@ -0,0 +1,5 @@
2026/04/27-17:47:13.267650 7f272bfff6c0 Delete type=3 #1
2026/04/27-17:47:13.268718 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.272210 7f272b7fe6c0 Level-0 table #5: 8622 bytes OK
2026/04/27-17:47:13.278603 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.278822 7f272b7fe6c0 Manual compaction at level-0 from '!items!APN91pQL0NBfZsG7' @ 72057594037927935 : 1 .. '!items!xxZKGqDVxAfr140W' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000002
MANIFEST-000006
+3 -5
View File
@@ -1,5 +1,3 @@
2026/04/27-17:47:13.302798 7f2779bff6c0 Delete type=3 #1
2026/04/27-17:47:13.303703 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.307266 7f272b7fe6c0 Level-0 table #5: 4359 bytes OK
2026/04/27-17:47:13.314111 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.314243 7f272b7fe6c0 Manual compaction at level-0 from '!items!2IYbyCPF9LJojzsj' @ 72057594037927935 : 1 .. '!items!uOpWyMGK3oiUJ1Sl' @ 0 : 0; will stop at (end)
2026/04/27-20:01:11.377813 7fed93fff6c0 Recovering log #4
2026/04/27-20:01:11.387818 7fed93fff6c0 Delete type=3 #2
2026/04/27-20:01:11.387923 7fed93fff6c0 Delete type=0 #4
+5
View File
@@ -0,0 +1,5 @@
2026/04/27-17:47:13.302798 7f2779bff6c0 Delete type=3 #1
2026/04/27-17:47:13.303703 7f272b7fe6c0 Level-0 table #5: started
2026/04/27-17:47:13.307266 7f272b7fe6c0 Level-0 table #5: 4359 bytes OK
2026/04/27-17:47:13.314111 7f272b7fe6c0 Delete type=0 #3
2026/04/27-17:47:13.314243 7f272b7fe6c0 Manual compaction at level-0 from '!items!2IYbyCPF9LJojzsj' @ 72057594037927935 : 1 .. '!items!uOpWyMGK3oiUJ1Sl' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1
View File
@@ -161,4 +161,5 @@ export const TEMPLATE_PARTIALS = [
"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",
"systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html",
]
+87
View File
@@ -0,0 +1,87 @@
/**
* Chroniques de l'Étrange — Système FoundryVTT
*
* Chroniques de l'Étrange est un jeu de rôle édité par Antre-Monde Éditions.
* Ce système FoundryVTT est une implémentation indépendante et n'est pas
* affilié à Antre-Monde Éditions,
* mais a été réalisé avec l'autorisation d'Antre-Monde Éditions.
*
* @author LeRatierBretonnien
* @copyright 20242026 LeRatierBretonnien
* @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
*/
import { rollInitiativePC, rollInitiativeNPC } from "../ui/initiative.js"
import { ACTOR_TYPES } from "../config/constants.js"
/**
* Custom Combat document for Chroniques de l'Étrange.
*
* The initiative wheel has 24 crans (positions). The character with the
* highest initiative acts first (furthest counter-clockwise from reference).
* After each action, their position advances clockwise by the action's cran cost
* (initiative decreases, wrapping from 1 → 24).
*
* Sort order: descending by initiative (highest acts first).
*/
export class CDECombat extends Combat {
/**
* Override rollInitiative to open the PC or NPC initiative dialog
* for each selected combatant, then sync the result to the Combatant document.
*/
async rollInitiative(ids, options = {}) {
const combatantIds = typeof ids === "string" ? [ids] : ids
for (const id of combatantIds) {
const combatant = this.combatants.get(id)
if (!combatant) continue
const actor = combatant.actor
if (!actor) continue
if (actor.type === ACTOR_TYPES.character) {
await rollInitiativePC(actor)
} else {
await rollInitiativeNPC(actor)
}
// combatant.initiative is synced by the updateActor hook in system.js
// (triggered by actor.update inside rollInitiativePC/NPC)
}
return this
}
/**
* Sort combatants: highest initiative first (furthest counter-clockwise = acts first).
* Ties: PCs before NPCs; among PCs, by name; among NPCs, by name.
* Calls super.setupTurns() first to ensure this.current is properly initialized.
*/
setupTurns() {
super.setupTurns()
this.turns = this.turns.slice().sort((a, b) => {
const ia = a.initiative ?? 0
const ib = b.initiative ?? 0
if (ia !== ib) return ib - ia // descending — highest acts first
// Tie-break: PCs before NPCs
const aIsPC = a.actor?.type === ACTOR_TYPES.character ? 1 : 0
const bIsPC = b.actor?.type === ACTOR_TYPES.character ? 1 : 0
if (aIsPC !== bIsPC) return bIsPC - aIsPC
return (a.name ?? "").localeCompare(b.name ?? "")
})
return this.turns
}
}
/**
* Advance a combatant's wheel position by the given action cran cost.
* Position wraps: after reaching 1, it continues from 24.
*
* @param {Combatant} combatant
* @param {number} cranCost
*/
export async function advanceCombatantPosition(combatant, cranCost) {
const current = combatant.initiative ?? combatant.actor?.system?.initiative ?? 1
const newValue = ((current - cranCost - 1 + 48) % 24) + 1
// Update combatant only; the updateCombatant hook in system.js syncs actor.initiative.
await combatant.update({ initiative: newValue })
}
+42 -1
View File
@@ -27,7 +27,9 @@ import { CDECharacterSheet, CDENpcSheet } from "./ui/sheets/actors/index.js"
import { CDEItemSheet, CDEKungfuSheet, CDESpellSheet, CDESupernaturalSheet, CDEWeaponSheet, CDEArmorSheet, CDESanheiSheet, CDEIngredientSheet } from "./ui/sheets/items/index.js"
import { CDELoksyuApp } from "./ui/apps/loksyu-app.js"
import { CDETinjiApp } from "./ui/apps/tinji-app.js"
import { CDEWheelApp } from "./ui/apps/wheel-app.js"
import { injectRollActions, refreshAllRollActions } from "./ui/roll-actions.js"
import { CDECombat } from "./documents/combat.js"
Hooks.once("i18nInit", preLocalizeConfig)
@@ -39,7 +41,9 @@ Hooks.once("init", async () => {
game.system.CONST = { MAGICS, SUBTYPES }
// Expose standalone apps globally for macros
game.cde = { CDELoksyuApp, CDETinjiApp }
game.cde = { CDELoksyuApp, CDETinjiApp, CDEWheelApp }
CONFIG.Combat.documentClass = CDECombat
CONFIG.Actor.dataModels = {
[ACTOR_TYPES.character]: CharacterDataModel,
@@ -126,6 +130,7 @@ Hooks.once("init", async () => {
Hooks.once("ready", async () => {
await migrateIfNeeded()
CDEWheelApp.registerHooks()
})
/** Add Loksyu + Tin Ji quick-access buttons to the chat panel (FoundryVTT v13) */
@@ -145,12 +150,16 @@ Hooks.on("renderChatLog", (_app, html) => {
<button type="button" class="cde-chat-btn cde-chat-btn--tinji">
<i class="fas fa-star"></i> ${game.i18n.localize("CDE.TinJi2")}
</button>
<button type="button" class="cde-chat-btn cde-chat-btn--wheel">
<i class="fas fa-circle-notch"></i> ${game.i18n.localize("CDE.InitiativeWheel")}
</button>
`
// Use event delegation to avoid being swallowed by Foundry's own handlers
wrapper.addEventListener("click", (ev) => {
if (ev.target.closest(".cde-chat-btn--loksyu")) CDELoksyuApp.open()
if (ev.target.closest(".cde-chat-btn--tinji")) CDETinjiApp.open()
if (ev.target.closest(".cde-chat-btn--wheel")) CDEWheelApp.open()
})
// Insert before the chat form — works on v12 and v13
@@ -173,3 +182,35 @@ Hooks.on("updateSetting", setting => {
refreshAllRollActions()
}
})
/**
* When an actor's initiative changes (via +/- buttons on the sheet),
* sync the corresponding combatant in the active combat.
*/
Hooks.on("updateActor", (actor, diff) => {
if (!foundry.utils.hasProperty(diff, "system.initiative")) return
if (!game.combat) return
const initiative = actor.system.initiative
const combatant = game.combat.combatants.find(c => c.actor?.id === actor.id)
if (combatant && combatant.initiative !== initiative) {
combatant.update({ initiative }).catch(() => {})
}
})
/**
* When a combatant's initiative changes (via wheel action buttons),
* sync the actor's system.initiative to match.
* Uses setTimeout to defer until after Foundry's update chain resolves,
* avoiding concurrent #recordPreviousState errors on the combat document.
*/
Hooks.on("updateCombatant", (combatant, diff) => {
if (!("initiative" in diff)) return
const initiative = combatant.initiative
if (initiative == null) return
setTimeout(() => {
const actor = combatant.actor
if (actor && actor.system?.initiative !== initiative) {
actor.update({ "system.initiative": initiative }).catch(() => {})
}
}, 0)
})
+1
View File
@@ -14,3 +14,4 @@
export { CDELoksyuApp } from "./loksyu-app.js"
export { CDETinjiApp } from "./tinji-app.js"
export { updateLoksyuFromRoll, updateTinjiFromRoll } from "./singletons.js"
export { CDEWheelApp } from "./wheel-app.js"
+204
View File
@@ -0,0 +1,204 @@
/**
* Chroniques de l'Étrange — Système FoundryVTT
*
* Chroniques de l'Étrange est un jeu de rôle édité par Antre-Monde Éditions.
* Ce système FoundryVTT est une implémentation indépendante et n'est pas
* affilié à Antre-Monde Éditions,
* mais a été réalisé avec l'autorisation d'Antre-Monde Éditions.
*
* @author LeRatierBretonnien
* @copyright 20242026 LeRatierBretonnien
* @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
*/
import { advanceCombatantPosition } from "../../documents/combat.js"
const WHEEL_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html"
/**
* Action costs for common combat actions (in crans).
* Listed in order from cheapest to most expensive.
*/
export const ACTION_COSTS = [
{ key: "draw", label: "CDE.ActionCostDraw", cost: 1 },
{ key: "changestyle", label: "CDE.ActionCostChangeStyle", cost: 1 },
{ key: "defense", label: "CDE.ActionCostDefense", cost: 1 },
{ key: "aim", label: "CDE.ActionCostAim", cost: 2 },
{ key: "help", label: "CDE.ActionCostHelp", cost: 2 },
{ key: "defally", label: "CDE.ActionCostDefendAlly", cost: 2 },
{ key: "move", label: "CDE.ActionCostMove", cost: 2 },
{ key: "attack", label: "CDE.ActionCostAttack", cost: 3 },
{ key: "delay", label: "CDE.ActionCostDelay", cost: 6 },
]
/**
* Wu Xing color segments for the 24-cran wheel.
* 6 colors × 4 crans = 24. Each colour covers crans [start..start+3].
* An effect lasting 6 crans returns to the same colour.
*/
const WHEEL_SEGMENTS = [
{ label: "Métal", color: "#b8c4cc", textColor: "#1a1a1a", crans: [1, 2, 3, 4] },
{ label: "Eau", color: "#3a7bd5", textColor: "#ffffff", crans: [5, 6, 7, 8] },
{ label: "Terre", color: "#c8a84b", textColor: "#1a1a1a", crans: [9, 10, 11, 12] },
{ label: "Feu", color: "#d94f3d", textColor: "#ffffff", crans: [13, 14, 15, 16] },
{ label: "Bois", color: "#4a9b5a", textColor: "#ffffff", crans: [17, 18, 19, 20] },
{ label: "Repère", color: "#1a1a2e", textColor: "#aaaaaa", crans: [21, 22, 23, 24] },
]
/** Return the segment data for a given cran (124). */
function segmentForCran(cran) {
return WHEEL_SEGMENTS.find(s => s.crans.includes(cran)) ?? WHEEL_SEGMENTS[0]
}
/**
* Roue d'Initiative — visual initiative wheel for CDE combat.
*
* Shows all combatants in the current combat scene on a 24-cran wheel.
* Provides action-cost buttons to advance a combatant's position.
*
* Singleton: open via CDEWheelApp.open().
*/
export class CDEWheelApp extends foundry.applications.api.ApplicationV2 {
static DEFAULT_OPTIONS = {
id: "cde-wheel-app",
classes: ["cde-wheel-app"],
tag: "div",
window: {
title: "CDE.InitiativeWheel",
icon: "fas fa-circle-notch",
resizable: true,
},
position: { width: 820, height: 620 },
actions: {
advanceCran: CDEWheelApp.#advanceCran,
setSurprised: CDEWheelApp.#setSurprised,
rollInitiative: CDEWheelApp.#rollInitiative,
},
}
/** @type {CDEWheelApp|null} */
static #instance = null
/** Open (or bring to front) the singleton instance. */
static open() {
if (!CDEWheelApp.#instance || CDEWheelApp.#instance.rendered === false) {
CDEWheelApp.#instance = new CDEWheelApp()
CDEWheelApp.#instance.render(true)
} else {
CDEWheelApp.#instance.bringToFront()
}
return CDEWheelApp.#instance
}
/** Currently selected combatant id (for action panel). */
#selectedId = null
async _prepareContext(options) {
const combat = game.combat
const combatants = combat ? [...combat.combatants.values()] : []
const sorted = [...combatants].sort((a, b) => (b.initiative ?? 0) - (a.initiative ?? 0))
const cranData = this.#buildCranData(combatants)
const selected = this.#selectedId
? combatants.find(c => c.id === this.#selectedId)
: null
const actionCosts = ACTION_COSTS.map(a => ({
...a,
label: game.i18n.localize(a.label),
}))
return {
hasCombat: !!combat,
combatants: sorted.map(c => ({
id: c.id,
name: c.name,
img: c.token?.texture?.src ?? c.actor?.img ?? "icons/svg/mystery-man.svg",
initiative: c.initiative ?? "—",
segment: segmentForCran(c.initiative ?? 1),
isActive: combat?.current?.combatantId === c.id,
isSelected: c.id === this.#selectedId,
hasInitiative: c.initiative != null,
})),
cranData,
selected,
selectedName: selected?.name ?? null,
actionCosts,
}
}
async _renderHTML(context, options) {
return foundry.applications.handlebars.renderTemplate(WHEEL_TEMPLATE, context)
}
_replaceHTML(result, content, options) {
content.innerHTML = result
this.#bindEvents(content)
}
/** Build per-cran data for the SVG wheel. */
#buildCranData(combatants) {
const data = []
for (let cran = 1; cran <= 24; cran++) {
const segment = segmentForCran(cran)
const fighters = combatants.filter(c => Math.round(c.initiative) === cran)
data.push({ cran, segment, fighters })
}
return data
}
/** Bind click events for combatant selection. */
#bindEvents(content) {
content.querySelectorAll("[data-select-combatant]").forEach(el => {
el.addEventListener("click", () => {
this.#selectedId = el.dataset.selectCombatant
this.render()
})
})
}
/** Action: advance selected combatant by given cran cost. */
static async #advanceCran(event, element) {
const app = CDEWheelApp.#instance
if (!app?.#selectedId) return
const cost = parseInt(element.dataset.cost, 10)
if (!cost || isNaN(cost)) return
const combatant = game.combat?.combatants.get(app.#selectedId)
if (!combatant) return
await advanceCombatantPosition(combatant, cost)
}
/** Action: set selected combatant to surprised (position 1 = reference). */
static async #setSurprised(event, element) {
const app = CDEWheelApp.#instance
if (!app?.#selectedId) return
const combatant = game.combat?.combatants.get(app.#selectedId)
if (!combatant) return
// Update combatant only — updateCombatant hook in system.js syncs actor
await combatant.update({ initiative: 1 })
}
/** Action: open the initiative dialog for the selected combatant. */
static async #rollInitiative(event, element) {
const app = CDEWheelApp.#instance
if (!app?.#selectedId) return
const combatant = game.combat?.combatants.get(app.#selectedId)
if (!combatant) return
await game.combat.rollInitiative([app.#selectedId])
}
/** Re-render when combat state changes. */
static registerHooks() {
const refresh = () => {
if (CDEWheelApp.#instance?.rendered) CDEWheelApp.#instance.render()
}
Hooks.on("updateCombat", refresh)
Hooks.on("updateCombatant", refresh)
Hooks.on("createCombatant", refresh)
Hooks.on("deleteCombatant", refresh)
Hooks.on("updateActor", (_actor, diff) => {
if (foundry.utils.hasProperty(diff, "system.initiative")) refresh()
})
Hooks.on("deleteCombat", () => {
if (CDEWheelApp.#instance?.rendered) CDEWheelApp.#instance.render()
})
}
}
+25
View File
@@ -118,4 +118,29 @@ export function registerHandlebarsHelpers() {
}
return game.i18n.localize(keys[activation] ?? "CDE.Activation")
})
/**
* Compute the SVG x,y coordinates for a cran on the initiative wheel.
* Cran 124 are arranged counter-clockwise from the bottom (reference at 6 o'clock).
* angle = 90° + cran * 15° (counter-clockwise = positive in standard math, negative in SVG).
* In SVG coords: x = cx + r*cos(a), y = cy - r*sin(a) [y-axis is flipped in SVG].
*/
Handlebars.registerHelper("cranPosition", function (cran, cx, cy, r) {
const angleDeg = 90 + cran * 15 // counter-clockwise from bottom
const angleRad = (angleDeg * Math.PI) / 180
const x = Math.round(cx + r * Math.cos(angleRad))
const y = Math.round(cy - r * Math.sin(angleRad))
return { x, y }
})
/** X offset for overlapping fighters on the same cran. Centres a 30px image on the cran cx. */
Handlebars.registerHelper("fighterX", function (cx, index, total) {
const offset = total > 1 ? (index - (total - 1) / 2) * 34 : 0
return Math.round(cx - 15 + offset)
})
/** Y offset for fighters — positions image just above the cran circle. */
Handlebars.registerHelper("fighterY", function (cy, index, total) {
return Math.round(cy - 50)
})
}
+38 -12
View File
@@ -25,7 +25,10 @@
"path": "packs/cde-kungfus",
"system": "fvtt-chroniques-de-l-etrange",
"flags": {},
"ownership": { "PLAYER": "OBSERVER", "ASSISTANT": "OWNER" }
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "cde-spells",
@@ -34,7 +37,10 @@
"path": "packs/cde-spells",
"system": "fvtt-chroniques-de-l-etrange",
"flags": {},
"ownership": { "PLAYER": "OBSERVER", "ASSISTANT": "OWNER" }
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "cde-supernaturals",
@@ -43,7 +49,10 @@
"path": "packs/cde-supernaturals",
"system": "fvtt-chroniques-de-l-etrange",
"flags": {},
"ownership": { "PLAYER": "OBSERVER", "ASSISTANT": "OWNER" }
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "cde-weapons",
@@ -52,7 +61,10 @@
"path": "packs/cde-weapons",
"system": "fvtt-chroniques-de-l-etrange",
"flags": {},
"ownership": { "PLAYER": "OBSERVER", "ASSISTANT": "OWNER" }
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "cde-armors",
@@ -61,7 +73,10 @@
"path": "packs/cde-armors",
"system": "fvtt-chroniques-de-l-etrange",
"flags": {},
"ownership": { "PLAYER": "OBSERVER", "ASSISTANT": "OWNER" }
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "cde-sanhei",
@@ -70,7 +85,10 @@
"path": "packs/cde-sanhei",
"system": "fvtt-chroniques-de-l-etrange",
"flags": {},
"ownership": { "PLAYER": "OBSERVER", "ASSISTANT": "OWNER" }
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "cde-ingredients",
@@ -79,7 +97,10 @@
"path": "packs/cde-ingredients",
"system": "fvtt-chroniques-de-l-etrange",
"flags": {},
"ownership": { "PLAYER": "OBSERVER", "ASSISTANT": "OWNER" }
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "cde-items",
@@ -88,7 +109,10 @@
"path": "packs/cde-items",
"system": "fvtt-chroniques-de-l-etrange",
"flags": {},
"ownership": { "PLAYER": "OBSERVER", "ASSISTANT": "OWNER" }
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "cde-npcs",
@@ -97,7 +121,10 @@
"path": "packs/cde-npcs",
"system": "fvtt-chroniques-de-l-etrange",
"flags": {},
"ownership": { "PLAYER": "NONE", "ASSISTANT": "OWNER" }
"ownership": {
"PLAYER": "NONE",
"ASSISTANT": "OWNER"
}
}
],
"languages": [
@@ -185,14 +212,13 @@
"minimum": "13",
"verified": "13"
},
"relationships": {
},
"relationships": {},
"background": "/systems/fvtt-chroniques-de-l-etrange/images/background/accueil.webp",
"grid": {
"distance": 5,
"units": "m"
},
"initiative": "@anti_initiative",
"initiative": "@initiative",
"primaryTokenAttribute": "threetreasures.heiyang",
"secondaryTokenAttribute": "threetreasures.heiyin",
"manifest": "https://www.uberwald.me/gitea/uberwald/fvtt-chroniques-de-l-etrange/raw/branch/main/system.json",
+148
View File
@@ -0,0 +1,148 @@
{{! Roue d'Initiative — Chroniques de l'Étrange }}
<div class="cde-wheel-layout">
{{! ── SVG Wheel ── }}
<div class="cde-wheel-svg-container">
{{#if hasCombat}}
<svg class="cde-wheel-svg" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
{{! Background circle }}
<circle cx="200" cy="200" r="190" fill="#0d0d1a" stroke="#2a2a4a" stroke-width="1"/>
{{! Cran slots — 24 positions, arranged counter-clockwise from bottom }}
{{#each cranData}}
{{#with (cranPosition cran 200 200 158)}}
<g class="cde-wheel-cran {{#if ../fighters.length}}cde-wheel-cran--occupied{{/if}}"
data-cran="{{../cran}}">
<circle cx="{{x}}" cy="{{y}}" r="19"
fill="{{../segment.color}}"
stroke="{{../segment.textColor}}"
stroke-width="{{#if ../fighters.length}}2.5{{else}}1.5{{/if}}"
opacity="{{#if ../fighters.length}}1{{else}}0.65{{/if}}"/>
<text x="{{x}}" y="{{y}}"
text-anchor="middle"
dominant-baseline="central"
font-size="12"
font-family="var(--cde-font-primary, sans-serif)"
fill="{{../segment.textColor}}"
font-weight="bold">{{../cran}}</text>
{{! Combatant avatar(s) on this cran — placed above the cran circle }}
{{#each ../fighters}}
<image href="{{img}}"
x="{{fighterX ../x @index ../fighters.length}}"
y="{{fighterY ../y @index ../fighters.length}}"
width="30" height="30"
clip-path="circle()"
class="cde-wheel-fighter {{#if isActive}}cde-wheel-fighter--active{{/if}}"
style="filter: drop-shadow(0 0 4px #fff)"/>
{{/each}}
</g>
{{/with}}
{{/each}}
{{! Centre: system name }}
<text x="200" y="192" text-anchor="middle" font-size="13"
font-family="var(--cde-font-primary, sans-serif)"
fill="#5a5a9a" letter-spacing="1">CHRONIQUES</text>
<text x="200" y="210" text-anchor="middle" font-size="11"
font-family="var(--cde-font-primary, sans-serif)"
fill="#4a4a7a" letter-spacing="1">DE L'ÉTRANGE</text>
{{! Reference marker at bottom (cran 0 / position between 24 and 1) }}
<polygon points="200,378 194,393 206,393"
fill="#ff6633" opacity="0.9"/>
<text x="200" y="400" text-anchor="middle" font-size="9"
fill="#ff6633" font-family="var(--cde-font-primary, sans-serif)">
{{ localize "CDE.WheelReference" }}
</text>
</svg>
{{! Wu Xing colour legend — HTML strip below SVG }}
<div class="cde-wheel-legend">
<span class="cde-wheel-legend-dot" style="background:#b8c4cc"></span>Métal
<span class="cde-wheel-legend-dot" style="background:#3a7bd5"></span>Eau
<span class="cde-wheel-legend-dot" style="background:#c8a84b"></span>Terre
<span class="cde-wheel-legend-dot" style="background:#d94f3d"></span>Feu
<span class="cde-wheel-legend-dot" style="background:#4a9b5a"></span>Bois
</div>
{{else}}
<div class="cde-wheel-no-combat">
<i class="fa-solid fa-circle-notch cde-wheel-no-combat-icon"></i>
<p>{{ localize "CDE.NoCombatActive" }}</p>
</div>
{{/if}}
</div>
{{! ── Right panel: combatant list + actions ── }}
<div class="cde-wheel-panel">
{{! Combatant list }}
<div class="cde-wheel-combatants">
<div class="cde-wheel-section-title">{{ localize "CDE.Combatants" }}</div>
{{#if hasCombat}}
{{#each combatants}}
<div class="cde-wheel-combatant {{#if isSelected}}cde-wheel-combatant--selected{{/if}} {{#if isActive}}cde-wheel-combatant--active{{/if}}"
data-select-combatant="{{id}}">
<img class="cde-wheel-combatant-img" src="{{img}}" alt="{{name}}"/>
<span class="cde-wheel-combatant-name">{{name}}</span>
<span class="cde-wheel-combatant-cran"
style="background: {{segment.color}}; color: {{segment.textColor}}">
{{initiative}}
</span>
{{#if isActive}}<i class="fa-solid fa-bolt cde-wheel-active-marker" title="{{ localize 'CDE.ActiveCombatant' }}"></i>{{/if}}
</div>
{{/each}}
{{else}}
<p class="cde-wheel-hint">{{ localize "CDE.NoCombatActive" }}</p>
{{/if}}
</div>
{{! Action cost panel — only shown when a combatant is selected }}
{{#if selected}}
<div class="cde-wheel-actions">
<div class="cde-wheel-section-title">
{{ localize "CDE.AdvanceCombatant" }} — <em>{{selectedName}}</em>
</div>
<div class="cde-wheel-action-grid">
{{#each actionCosts}}
<button type="button"
class="cde-wheel-action-btn"
data-action="advanceCran"
data-cost="{{cost}}"
title="{{label}} ({{cost}} crans)">
<span class="cde-wheel-action-name">{{label}}</span>
<span class="cde-wheel-action-cost">{{cost}}</span>
</button>
{{/each}}
</div>
<div class="cde-wheel-special-actions">
<button type="button"
class="cde-wheel-btn-roll"
data-action="rollInitiative"
title="{{ localize 'CDE.DeterminateInitiative' }}">
<i class="fa-solid fa-dice-d10"></i>
{{ localize "CDE.InitiativeRoll" }}
</button>
<button type="button"
class="cde-wheel-btn-surprise"
data-action="setSurprised"
title="{{ localize 'CDE.SurprisedHint' }}">
<i class="fa-solid fa-exclamation-triangle"></i>
{{ localize "CDE.SurprisedAction" }}
</button>
</div>
</div>
{{else}}
<div class="cde-wheel-actions cde-wheel-actions--hint">
<p class="cde-wheel-hint">
<i class="fa-solid fa-hand-pointer"></i>
{{ localize "CDE.SelectCombatantHint" }}
</p>
</div>
{{/if}}
</div>{{! end panel }}
</div>{{! end layout }}