diff --git a/README.md b/README.md
index d804147..4d080f8 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-# Les Chroniques de l'étrange
+# Les Chroniques de l'étrange pour FoundryVTT
-A rudimentary implementation of Les Chroniques de l'étrange for Foundry VTT.
-
-The 2 integrated CdE d10 die sets – for dice-so-nice extension – look nicer with 'white', 'paper', and plastic' dice options selected in this very extension.
\ No newline at end of file
+Implémentation du JDR Les Chroniques de l'Etrange de Antre-Monde éditions.
diff --git a/css/cde-theme.css b/css/cde-theme.css
index 33803da..5b7e093 100644
--- a/css/cde-theme.css
+++ b/css/cde-theme.css
@@ -29,12 +29,21 @@
overflow: hidden;
}
.cde-sheet input,
-.cde-sheet select,
.cde-sheet textarea {
font-family: inherit;
color: #e2e8f4;
background: transparent;
}
+.cde-sheet select {
+ font-family: inherit;
+ color: #e2e8f4;
+ background: #101622;
+ border-radius: 2px;
+}
+.cde-sheet select option {
+ background: #080c14;
+ color: #e2e8f4;
+}
.cde-neon-header {
position: relative;
background: #101622;
@@ -182,8 +191,7 @@
color: #e2e8f4;
letter-spacing: 0.02em;
}
-.cde-stat-cell input,
-.cde-stat-cell select {
+.cde-stat-cell input {
width: 100%;
background: transparent;
border: none;
@@ -194,7 +202,18 @@
outline: none;
transition: border-color 0.15s;
}
-.cde-stat-cell input:focus,
+.cde-stat-cell input:focus {
+ border-bottom-color: #00d4d4;
+}
+.cde-stat-cell select {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid #1a2436;
+ font-size: 14px;
+ padding: 4px 0;
+ outline: none;
+ transition: border-color 0.15s;
+}
.cde-stat-cell select:focus {
border-bottom-color: #00d4d4;
}
@@ -492,8 +511,7 @@ section.npc .cde-neon-tabs .item.active {
color: #7d94b8;
margin: 0;
}
-.cde-chip input,
-.cde-chip select {
+.cde-chip input {
width: 100%;
border: none;
border-bottom: 1px solid #1a2436;
@@ -502,7 +520,16 @@ section.npc .cde-neon-tabs .item.active {
padding: 4px 0;
outline: none;
}
-.cde-chip input:focus,
+.cde-chip input:focus {
+ border-bottom-color: #00d4d4;
+}
+.cde-chip select {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid #1a2436;
+ padding: 4px 0;
+ outline: none;
+}
.cde-chip select:focus {
border-bottom-color: #00d4d4;
}
@@ -738,7 +765,6 @@ section.npc .cde-neon-tabs .item.active {
color: #7d94b8;
}
.cde-field input,
-.cde-field select,
.cde-field textarea {
width: 100%;
border: none;
@@ -749,10 +775,19 @@ section.npc .cde-neon-tabs .item.active {
outline: none;
}
.cde-field input:focus,
-.cde-field select:focus,
.cde-field textarea:focus {
border-bottom-color: #00d4d4;
}
+.cde-field select {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid #1a2436;
+ padding: 5px 0;
+ outline: none;
+}
+.cde-field select:focus {
+ border-bottom-color: #00d4d4;
+}
.cde-section-title {
font-family: "Averia", sans-serif;
font-size: 10px;
@@ -793,8 +828,7 @@ section.npc .cde-neon-tabs .item.active {
.cde-data-table tr:hover {
background: rgba(38, 56, 83, 0.2);
}
-.cde-data-table input,
-.cde-data-table select {
+.cde-data-table input {
width: 100%;
border: none;
border-bottom: 1px solid #1a2436;
@@ -803,6 +837,13 @@ section.npc .cde-neon-tabs .item.active {
padding: 4px 0;
outline: none;
}
+.cde-data-table select {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid #1a2436;
+ padding: 4px 0;
+ outline: none;
+}
.cde-centered-card {
display: flex;
gap: 12px;
@@ -1222,6 +1263,763 @@ section.npc .cde-neon-tabs .item.active {
letter-spacing: 0.06em;
color: #7d94b8;
}
+.cde-section-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 10px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: #7d94b8;
+ border-bottom: 1px solid #1a2436;
+ padding-bottom: 6px;
+ margin-bottom: 10px;
+}
+.cde-section-label i {
+ font-size: 11px;
+}
+.cde-section-label--top-margin {
+ margin-top: 18px;
+}
+.cde-components-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+ margin-bottom: 10px;
+}
+.cde-component-cell {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: #101622;
+ border: 1px solid #1a2436;
+ border-radius: 8px;
+ padding: 4px 8px;
+}
+.cde-component-cell:hover {
+ border-color: #263853;
+}
+.cde-component-die {
+ width: 28px;
+ height: 28px;
+ object-fit: contain;
+ flex-shrink: 0;
+ opacity: 0.85;
+}
+.cde-component-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ border-bottom: 1px solid transparent;
+ color: #e2e8f4;
+ font-size: 12px;
+ padding: 2px 0;
+}
+.cde-component-input:focus {
+ outline: none;
+ border-bottom-color: #4a9eff;
+}
+.cde-component-input::placeholder {
+ color: #7d94b8;
+ font-style: italic;
+ font-size: 11px;
+}
+.cde-component-random-row {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 4px;
+}
+.cde-btn-random-component {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 16px;
+ background: rgba(74, 158, 255, 0.08);
+ border: 1px solid rgba(74, 158, 255, 0.35);
+ border-radius: 8px;
+ color: #4a9eff;
+ font-family: "Averia", sans-serif;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ cursor: pointer;
+ transition: background 0.15s, box-shadow 0.15s;
+}
+.cde-btn-random-component i {
+ font-size: 14px;
+}
+.cde-btn-random-component:hover {
+ background: rgba(74, 158, 255, 0.16);
+ box-shadow: 0 0 8px rgba(74, 158, 255, 0.3);
+}
+.cde-magic-card {
+ background: #101622;
+ border: 1px solid #1a2436;
+ border-left: 3px solid #263853;
+ border-radius: 8px;
+ margin-bottom: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
+}
+.cde-magic-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+}
+.cde-magic-aspect-icon {
+ width: 32px;
+ height: 32px;
+ object-fit: contain;
+ flex-shrink: 0;
+}
+.cde-magic-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ min-width: 0;
+}
+.cde-magic-name {
+ font-family: "Averia", sans-serif;
+ font-size: 13px;
+ font-weight: 700;
+ color: #e2e8f4;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.cde-magic-aspect-name {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #7d94b8;
+ font-family: "Averia", sans-serif;
+}
+.cde-magic-value-input {
+ width: 52px;
+ flex-shrink: 0;
+}
+.cde-magic-roll-btn {
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background 0.15s;
+ flex-shrink: 0;
+}
+.cde-magic-roll-btn i {
+ font-size: 15px;
+}
+.cde-magic-roll-btn:hover {
+ background: rgba(74, 158, 255, 0.15);
+}
+.cde-magic-toggle {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ color: #7d94b8;
+ flex-shrink: 0;
+}
+.cde-magic-toggle input[type="checkbox"] {
+ display: none;
+}
+.cde-magic-toggle i {
+ font-size: 11px;
+ transition: color 0.15s;
+}
+.cde-magic-toggle:hover i {
+ color: #e2e8f4;
+}
+.cde-magic-specialities {
+ border-top: 1px solid #1a2436;
+ padding: 4px 0;
+}
+.cde-magic-spec-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 10px 5px 42px;
+ opacity: 0.55;
+ transition: opacity 0.12s, background 0.12s;
+}
+.cde-magic-spec-row:hover {
+ opacity: 1;
+ background: rgba(38, 56, 83, 0.3);
+}
+.cde-magic-spec-row--active {
+ opacity: 1;
+}
+.cde-magic-spec-check-label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+.cde-magic-spec-check-label input[type="checkbox"] {
+ display: none;
+}
+.cde-magic-spec-check-label .cde-spec-checkbox-ui {
+ width: 14px;
+ height: 14px;
+ border: 1px solid #7d94b8;
+ border-radius: 3px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: border-color 0.12s, background 0.12s;
+}
+.cde-magic-spec-check-label input:checked + .cde-spec-checkbox-ui {
+ background: #4a9eff;
+ border-color: #4a9eff;
+}
+.cde-magic-spec-check-label input:checked + .cde-spec-checkbox-ui::after {
+ content: "✓";
+ font-size: 9px;
+ color: #080c14;
+ line-height: 1;
+}
+.cde-magic-spec-element {
+ width: 18px;
+ height: 18px;
+ object-fit: contain;
+ flex-shrink: 0;
+}
+.cde-magic-spec-polarity {
+ font-size: 10px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: #7d94b8;
+ width: 42px;
+ flex-shrink: 0;
+}
+.cde-magic-spec-polarity.icon-yin {
+ color: #cc44ff;
+}
+.cde-magic-spec-polarity.icon-yang {
+ color: #00d4d4;
+}
+.cde-magic-spec-polarity.icon-yinyang {
+ color: #4a9eff;
+}
+.cde-magic-spec-name {
+ flex: 1;
+ font-size: 12px;
+ color: #e2e8f4;
+ font-family: "Averia", sans-serif;
+}
+.cde-magic-spec-roll-btn {
+ width: 26px;
+ height: 26px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ cursor: pointer;
+ color: #7d94b8;
+ flex-shrink: 0;
+ transition: color 0.15s, background 0.15s;
+}
+.cde-magic-spec-roll-btn i {
+ font-size: 12px;
+}
+.cde-magic-spec-roll-btn:hover {
+ color: #4a9eff;
+ background: rgba(74, 158, 255, 0.12);
+}
+.cde-magic--internalcinnabar {
+ border-left-color: #70706e;
+}
+.cde-magic--internalcinnabar .cde-magic-name {
+ color: #a3a3a1;
+}
+.cde-magic--internalcinnabar .cde-magic-roll-btn i {
+ color: #70706e;
+}
+.cde-magic--alchemy {
+ border-left-color: #009fe2;
+}
+.cde-magic--alchemy .cde-magic-name {
+ color: #30c1ff;
+}
+.cde-magic--alchemy .cde-magic-roll-btn i {
+ color: #009fe2;
+}
+.cde-magic--masteryoftheway {
+ border-left-color: #a85747;
+}
+.cde-magic--masteryoftheway .cde-magic-name {
+ color: #cd9488;
+}
+.cde-magic--masteryoftheway .cde-magic-roll-btn i {
+ color: #be7364;
+}
+.cde-magic--exorcism {
+ border-left-color: #cd171a;
+}
+.cde-magic--exorcism .cde-magic-name {
+ color: #ed5d60;
+}
+.cde-magic--exorcism .cde-magic-roll-btn i {
+ color: #cd171a;
+}
+.cde-magic--geomancy {
+ border-left-color: #41a436;
+}
+.cde-magic--geomancy .cde-magic-name {
+ color: #68ca5d;
+}
+.cde-magic--geomancy .cde-magic-roll-btn i {
+ color: #41a436;
+}
+.cde-grimoire-section {
+ border-top: 1px dashed rgba(26, 36, 54, 0.6);
+ margin-top: 6px;
+ padding-top: 6px;
+}
+.cde-grimoire-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ font-size: 10px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: #7d94b8;
+}
+.cde-grimoire-header i {
+ font-size: 10px;
+ color: #4a9eff;
+}
+.cde-grimoire-header span {
+ flex: 1;
+}
+.cde-grimoire-add {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 3px;
+ cursor: pointer;
+ color: #7d94b8;
+ transition: color 0.12s, background 0.12s;
+}
+.cde-grimoire-add i {
+ font-size: 10px;
+}
+.cde-grimoire-add:hover {
+ color: #4a9eff;
+ background: rgba(74, 158, 255, 0.15);
+}
+.cde-grimoire-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+.cde-grimoire-entry {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px 4px 14px;
+ border-bottom: 1px solid rgba(26, 36, 54, 0.4);
+ transition: background 0.1s;
+}
+.cde-grimoire-entry:last-child {
+ border-bottom: none;
+}
+.cde-grimoire-entry:hover {
+ background: rgba(26, 36, 54, 0.25);
+}
+.cde-grimoire-entry:hover .cde-grimoire-controls {
+ opacity: 1;
+}
+.cde-grimoire-img {
+ width: 18px;
+ height: 18px;
+ object-fit: contain;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+.cde-grimoire-name {
+ flex: 1;
+ font-size: 12px;
+ color: #e2e8f4;
+ font-family: "Averia", sans-serif;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.cde-grimoire-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 10px;
+ color: #7d94b8;
+ flex-shrink: 0;
+}
+.cde-grimoire-meta em {
+ color: #4a9eff;
+ font-style: normal;
+ font-size: 10px;
+ max-width: 80px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.cde-grimoire-diff,
+.cde-grimoire-hei {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+}
+.cde-grimoire-diff i,
+.cde-grimoire-hei i {
+ font-size: 9px;
+}
+.cde-grimoire-diff {
+ color: #7d94b8;
+}
+.cde-grimoire-hei {
+ color: #ff3d5a;
+}
+.cde-grimoire-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity 0.12s;
+ flex-shrink: 0;
+}
+.cde-grimoire-controls a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ border-radius: 3px;
+ color: #7d94b8;
+ cursor: pointer;
+ transition: color 0.12s, background 0.12s;
+}
+.cde-grimoire-controls a i {
+ font-size: 10px;
+}
+.cde-grimoire-controls a:hover {
+ color: #e2e8f4;
+ background: rgba(38, 56, 83, 0.3);
+}
+.cde-grimoire-empty {
+ padding: 4px 14px 8px;
+ font-size: 11px;
+ color: #7d94b8;
+ font-style: italic;
+ margin: 0;
+}
+.cde-kf-add-row {
+ display: flex;
+ justify-content: flex-end;
+ padding: 4px 0 8px;
+}
+.cde-kf-add-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #7d94b8;
+ cursor: pointer;
+ padding: 4px 10px;
+ border-radius: 8px;
+ border: 1px solid #1a2436;
+ transition: color 0.12s, border-color 0.12s, background 0.12s;
+}
+.cde-kf-add-btn i {
+ font-size: 10px;
+}
+.cde-kf-add-btn:hover {
+ color: #ff3d5a;
+ border-color: #ff3d5a;
+ background: rgba(255, 61, 90, 0.08);
+}
+.cde-kf-card {
+ border: 1px solid #1a2436;
+ border-left: 3px solid #ff3d5a;
+ border-radius: 8px;
+ background: rgba(16, 22, 34, 0.7);
+ margin-bottom: 10px;
+ overflow: hidden;
+ transition: box-shadow 0.15s;
+}
+.cde-kf-card:hover {
+ box-shadow: 0 0 8px rgba(255, 61, 90, 0.2);
+}
+.cde-kf-card.cde-kf--metal {
+ border-left-color: #70706e;
+}
+.cde-kf-card.cde-kf--eau {
+ border-left-color: #009fe2;
+}
+.cde-kf-card.cde-kf--terre {
+ border-left-color: #be7364;
+}
+.cde-kf-card.cde-kf--feu {
+ border-left-color: #cd171a;
+}
+.cde-kf-card.cde-kf--bois {
+ border-left-color: #41a436;
+}
+.cde-kf-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+}
+.cde-kf-header:hover .cde-kf-controls {
+ opacity: 1;
+}
+.cde-kf-aspect-icon {
+ width: 26px;
+ height: 26px;
+ object-fit: contain;
+ flex-shrink: 0;
+}
+.cde-kf-orient-icon {
+ width: 18px;
+ height: 18px;
+ object-fit: contain;
+ flex-shrink: 0;
+ opacity: 0.75;
+}
+.cde-kf-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+.cde-kf-name {
+ font-size: 14px;
+ font-weight: 700;
+ font-family: "Averia", sans-serif;
+ color: #e2e8f4;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.cde-kf-meta {
+ font-size: 10px;
+ color: #7d94b8;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+.cde-kf-meta em {
+ color: #e2e8f4;
+ font-style: normal;
+}
+.cde-kf-roll-btn {
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ cursor: pointer;
+ color: #7d94b8;
+ flex-shrink: 0;
+ transition: color 0.15s, background 0.15s;
+}
+.cde-kf-roll-btn i {
+ font-size: 14px;
+}
+.cde-kf-roll-btn:hover {
+ color: #ff3d5a;
+ background: rgba(255, 61, 90, 0.15);
+}
+.cde-kf-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity 0.12s;
+ flex-shrink: 0;
+}
+.cde-kf-controls a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ border-radius: 3px;
+ color: #7d94b8;
+ cursor: pointer;
+ transition: color 0.12s, background 0.12s;
+}
+.cde-kf-controls a i {
+ font-size: 11px;
+}
+.cde-kf-controls a:hover {
+ color: #e2e8f4;
+ background: rgba(38, 56, 83, 0.3);
+}
+.cde-kf-style-row {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ padding: 4px 12px 6px 44px;
+ border-top: 1px solid rgba(26, 36, 54, 0.6);
+ background: rgba(16, 22, 34, 0.4);
+}
+.cde-kf-style-label {
+ font-size: 10px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #ff3d5a;
+ flex-shrink: 0;
+}
+.cde-kf-style-label i {
+ font-size: 9px;
+}
+.cde-kf-style-text {
+ font-size: 11px;
+ color: #e2e8f4;
+ font-family: "Averia", sans-serif;
+ font-style: italic;
+}
+.cde-kf-techniques {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ border-top: 1px solid rgba(26, 36, 54, 0.5);
+}
+.cde-kf-tech {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 12px 5px 44px;
+ border-bottom: 1px solid rgba(26, 36, 54, 0.3);
+ opacity: 0.5;
+ transition: opacity 0.1s, background 0.1s;
+}
+.cde-kf-tech:last-child {
+ border-bottom: none;
+}
+.cde-kf-tech--mastered {
+ opacity: 1;
+}
+.cde-kf-tech:hover {
+ background: rgba(26, 36, 54, 0.2);
+ opacity: 1;
+}
+.cde-kf-tech-mastered {
+ font-size: 11px;
+ flex-shrink: 0;
+ width: 14px;
+ text-align: center;
+}
+.cde-kf-tech--mastered .cde-kf-tech-mastered {
+ color: #ff3d5a;
+}
+.cde-kf-tech:not(.cde-kf-tech--mastered) .cde-kf-tech-mastered {
+ color: #7d94b8;
+}
+.cde-act-badge {
+ font-size: 9px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ padding: 2px 5px;
+ border-radius: 3px;
+ flex-shrink: 0;
+ max-width: 110px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border: 1px solid currentColor;
+}
+.cde-act-badge.cde-act--action-attack {
+ color: #cd171a;
+ background: rgba(205, 23, 26, 0.1);
+}
+.cde-act-badge.cde-act--action-defense {
+ color: #009fe2;
+ background: rgba(0, 159, 226, 0.1);
+}
+.cde-act-badge.cde-act--action-aid {
+ color: #41a436;
+ background: rgba(65, 164, 54, 0.1);
+}
+.cde-act-badge.cde-act--action-attack-defense {
+ color: #70706e;
+ background: rgba(112, 112, 110, 0.12);
+}
+.cde-act-badge.cde-act--reaction {
+ color: #a85747;
+ background: rgba(168, 87, 71, 0.12);
+}
+.cde-act-badge.cde-act--dice {
+ color: #4a9eff;
+ background: rgba(74, 158, 255, 0.1);
+}
+.cde-act-badge.cde-act--damage-inflicted {
+ color: #ff6b35;
+ background: rgba(255, 107, 53, 0.1);
+}
+.cde-act-badge.cde-act--damage-received {
+ color: #7d94b8;
+ background: rgba(26, 36, 54, 0.2);
+}
+.cde-kf-tech-name {
+ flex: 1;
+ font-size: 12px;
+ color: #e2e8f4;
+ font-family: "Averia", sans-serif;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.cde-kf-tech-name em {
+ color: #7d94b8;
+}
+.cde-kf-empty {
+ padding: 16px;
+ text-align: center;
+ font-size: 12px;
+ color: #7d94b8;
+ font-style: italic;
+}
+.cde-chat-random-component {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ padding: 8px 12px;
+}
+.cde-chat-component-label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: #7d94b8;
+ font-family: "Averia", sans-serif;
+}
+.cde-chat-component-value {
+ font-size: 16px;
+ font-weight: 700;
+ color: #4a9eff;
+ text-shadow: 0 0 8px rgba(74, 158, 255, 0.5);
+}
.cde-tabs {
margin-top: 12px;
border-bottom: 1px solid #1a2436;
diff --git a/css/cde-theme.less b/css/cde-theme.less
index a7cc157..545f9dc 100644
--- a/css/cde-theme.less
+++ b/css/cde-theme.less
@@ -69,11 +69,24 @@
height: 100%;
overflow: hidden;
- input, select, textarea {
+ input, textarea {
font-family: inherit;
color: @cde-text;
background: transparent;
}
+
+ // Selects need an explicit dark background — transparent fails on native dropdowns
+ select {
+ font-family: inherit;
+ color: @cde-text;
+ background: @cde-surface;
+ border-radius: 2px;
+ }
+
+ select option {
+ background: @cde-bg;
+ color: @cde-text;
+ }
}
// ============================================================
@@ -213,8 +226,7 @@
letter-spacing: 0.02em;
}
-.cde-stat-cell input,
-.cde-stat-cell select {
+.cde-stat-cell input {
width: 100%;
background: transparent;
border: none;
@@ -225,9 +237,19 @@
outline: none;
transition: border-color 0.15s;
- &:focus {
- border-bottom-color: @cde-item;
- }
+ &:focus { border-bottom-color: @cde-item; }
+}
+
+.cde-stat-cell select {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid @cde-border;
+ font-size: 14px;
+ padding: 4px 0;
+ outline: none;
+ transition: border-color 0.15s;
+
+ &:focus { border-bottom-color: @cde-item; }
}
.cde-neon-header.kungfu .cde-stat-cell input:focus,
@@ -504,7 +526,7 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
margin: 0;
}
- input, select {
+ input {
width: 100%;
border: none;
border-bottom: 1px solid @cde-border;
@@ -516,6 +538,16 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
&:focus { border-bottom-color: @cde-item; }
}
+ select {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid @cde-border;
+ padding: 4px 0;
+ outline: none;
+
+ &:focus { border-bottom-color: @cde-item; }
+ }
+
input[type="checkbox"] {
width: auto;
align-self: flex-start;
@@ -754,7 +786,7 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
color: @cde-muted;
}
- input, select, textarea {
+ input, textarea {
width: 100%;
border: none;
border-bottom: 1px solid @cde-border;
@@ -765,6 +797,16 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
&:focus { border-bottom-color: @cde-item; }
}
+
+ select {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid @cde-border;
+ padding: 5px 0;
+ outline: none;
+
+ &:focus { border-bottom-color: @cde-item; }
+ }
}
.cde-section-title {
@@ -809,7 +851,7 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
tr:nth-child(even) { background: fade(@cde-surface, 50%); }
tr:hover { background: fade(@cde-border-hi, 20%); }
- input, select {
+ input {
width: 100%;
border: none;
border-bottom: 1px solid @cde-border;
@@ -818,6 +860,14 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
padding: 4px 0;
outline: none;
}
+
+ select {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid @cde-border;
+ padding: 4px 0;
+ outline: none;
+ }
}
.cde-centered-card {
@@ -1227,6 +1277,727 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
}
}
+// ============================================================
+// Magics tab — components grid + magic cards
+// ============================================================
+
+.cde-section-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 10px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: @cde-muted;
+ border-bottom: 1px solid @cde-border;
+ padding-bottom: 6px;
+ margin-bottom: 10px;
+
+ i { font-size: 11px; }
+
+ &--top-margin { margin-top: 18px; }
+}
+
+// 2-column grid for the 10 components
+.cde-components-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+ margin-bottom: 10px;
+}
+
+.cde-component-cell {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: @cde-surface;
+ border: 1px solid @cde-border;
+ border-radius: @cde-radius;
+ padding: 4px 8px;
+
+ &:hover { border-color: @cde-border-hi; }
+}
+
+.cde-component-die {
+ width: 28px;
+ height: 28px;
+ object-fit: contain;
+ flex-shrink: 0;
+ opacity: 0.85;
+}
+
+.cde-component-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ border-bottom: 1px solid transparent;
+ color: @cde-text;
+ font-size: 12px;
+ padding: 2px 0;
+
+ &:focus {
+ outline: none;
+ border-bottom-color: @cde-spell;
+ }
+
+ &::placeholder { color: @cde-muted; font-style: italic; font-size: 11px; }
+}
+
+.cde-component-random-row {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 4px;
+}
+
+.cde-btn-random-component {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 16px;
+ background: fade(@cde-spell, 8%);
+ border: 1px solid fade(@cde-spell, 35%);
+ border-radius: @cde-radius;
+ color: @cde-spell;
+ font-family: "Averia", sans-serif;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ cursor: pointer;
+ transition: background 0.15s, box-shadow 0.15s;
+
+ i { font-size: 14px; }
+
+ &:hover {
+ background: fade(@cde-spell, 16%);
+ box-shadow: 0 0 8px fade(@cde-spell, 30%);
+ }
+}
+
+// === Magic type cards ===
+.cde-magic-card {
+ background: @cde-surface;
+ border: 1px solid @cde-border;
+ border-left: 3px solid @cde-border-hi;
+ border-radius: @cde-radius;
+ margin-bottom: 8px;
+ overflow: hidden;
+ box-shadow: @cde-shadow-sm;
+}
+
+.cde-magic-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+}
+
+.cde-magic-aspect-icon {
+ width: 32px;
+ height: 32px;
+ object-fit: contain;
+ flex-shrink: 0;
+}
+
+.cde-magic-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ min-width: 0;
+}
+
+.cde-magic-name {
+ font-family: "Averia", sans-serif;
+ font-size: 13px;
+ font-weight: 700;
+ color: @cde-text;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.cde-magic-aspect-name {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: @cde-muted;
+ font-family: "Averia", sans-serif;
+}
+
+.cde-magic-value-input {
+ width: 52px;
+ flex-shrink: 0;
+}
+
+.cde-magic-roll-btn {
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: @cde-radius;
+ cursor: pointer;
+ transition: background 0.15s;
+ flex-shrink: 0;
+
+ i { font-size: 15px; }
+
+ &:hover { background: fade(@cde-spell, 15%); }
+}
+
+.cde-magic-toggle {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ color: @cde-muted;
+ flex-shrink: 0;
+
+ input[type="checkbox"] { display: none; }
+
+ i { font-size: 11px; transition: color 0.15s; }
+
+ &:hover i { color: @cde-text; }
+}
+
+// Specialities list
+.cde-magic-specialities {
+ border-top: 1px solid @cde-border;
+ padding: 4px 0;
+}
+
+.cde-magic-spec-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 10px 5px 42px; // indent aligned with aspect icon
+ opacity: 0.55;
+ transition: opacity 0.12s, background 0.12s;
+
+ &:hover { opacity: 1; background: fade(@cde-border-hi, 30%); }
+ &--active { opacity: 1; }
+}
+
+.cde-magic-spec-check-label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ flex-shrink: 0;
+
+ input[type="checkbox"] { display: none; }
+
+ .cde-spec-checkbox-ui {
+ width: 14px;
+ height: 14px;
+ border: 1px solid @cde-muted;
+ border-radius: 3px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: border-color 0.12s, background 0.12s;
+ }
+
+ input:checked + .cde-spec-checkbox-ui {
+ background: @cde-spell;
+ border-color: @cde-spell;
+
+ &::after {
+ content: "✓";
+ font-size: 9px;
+ color: @cde-bg;
+ line-height: 1;
+ }
+ }
+}
+
+.cde-magic-spec-element {
+ width: 18px;
+ height: 18px;
+ object-fit: contain;
+ flex-shrink: 0;
+}
+
+.cde-magic-spec-polarity {
+ font-size: 10px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: @cde-muted;
+ width: 42px;
+ flex-shrink: 0;
+
+ &.icon-yin { color: @cde-supernatural; }
+ &.icon-yang { color: @cde-item; }
+ &.icon-yinyang { color: @cde-spell; }
+}
+
+.cde-magic-spec-name {
+ flex: 1;
+ font-size: 12px;
+ color: @cde-text;
+ font-family: "Averia", sans-serif;
+}
+
+.cde-magic-spec-roll-btn {
+ width: 26px;
+ height: 26px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: @cde-radius;
+ cursor: pointer;
+ color: @cde-muted;
+ flex-shrink: 0;
+ transition: color 0.15s, background 0.15s;
+
+ i { font-size: 12px; }
+
+ &:hover { color: @cde-spell; background: fade(@cde-spell, 12%); }
+}
+
+// Per-magic accent colors using direct Wu Xing LESS variables
+.cde-magic--internalcinnabar {
+ border-left-color: @wu-gray;
+ .cde-magic-name { color: lighten(@wu-gray, 20%); }
+ .cde-magic-roll-btn i { color: @wu-gray; }
+}
+.cde-magic--alchemy {
+ border-left-color: @wu-blue;
+ .cde-magic-name { color: lighten(@wu-blue, 15%); }
+ .cde-magic-roll-btn i { color: @wu-blue; }
+}
+.cde-magic--masteryoftheway {
+ border-left-color: @wu-brown;
+ .cde-magic-name { color: lighten(@wu-brown, 20%); }
+ .cde-magic-roll-btn i { color: lighten(@wu-brown, 10%); }
+}
+.cde-magic--exorcism {
+ border-left-color: @wu-red;
+ .cde-magic-name { color: lighten(@wu-red, 20%); }
+ .cde-magic-roll-btn i { color: @wu-red; }
+}
+.cde-magic--geomancy {
+ border-left-color: @wu-green;
+ .cde-magic-name { color: lighten(@wu-green, 15%); }
+ .cde-magic-roll-btn i { color: @wu-green; }
+}
+
+// =====================================================================
+// GRIMOIRE (spell list integrated in magic discipline cards)
+// =====================================================================
+
+.cde-grimoire-section {
+ border-top: 1px dashed fade(@cde-border, 60%);
+ margin-top: 6px;
+ padding-top: 6px;
+}
+
+.cde-grimoire-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ font-size: 10px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: @cde-muted;
+
+ i { font-size: 10px; color: @cde-spell; }
+
+ span { flex: 1; }
+}
+
+.cde-grimoire-add {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 3px;
+ cursor: pointer;
+ color: @cde-muted;
+ transition: color 0.12s, background 0.12s;
+
+ i { font-size: 10px; }
+
+ &:hover { color: @cde-spell; background: fade(@cde-spell, 15%); }
+}
+
+.cde-grimoire-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.cde-grimoire-entry {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px 4px 14px;
+ border-bottom: 1px solid fade(@cde-border, 40%);
+ transition: background 0.1s;
+
+ &:last-child { border-bottom: none; }
+
+ &:hover {
+ background: fade(@cde-border, 25%);
+
+ .cde-grimoire-controls { opacity: 1; }
+ }
+}
+
+.cde-grimoire-img {
+ width: 18px;
+ height: 18px;
+ object-fit: contain;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.cde-grimoire-name {
+ flex: 1;
+ font-size: 12px;
+ color: @cde-text;
+ font-family: "Averia", sans-serif;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.cde-grimoire-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 10px;
+ color: @cde-muted;
+ flex-shrink: 0;
+
+ em {
+ color: @cde-spell;
+ font-style: normal;
+ font-size: 10px;
+ max-width: 80px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
+.cde-grimoire-diff, .cde-grimoire-hei {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+
+ i { font-size: 9px; }
+}
+
+.cde-grimoire-diff { color: @cde-muted; }
+.cde-grimoire-hei { color: @cde-kungfu; }
+
+.cde-grimoire-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity 0.12s;
+ flex-shrink: 0;
+
+ a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ border-radius: 3px;
+ color: @cde-muted;
+ cursor: pointer;
+ transition: color 0.12s, background 0.12s;
+
+ i { font-size: 10px; }
+
+ &:hover { color: @cde-text; background: fade(@cde-border-hi, 30%); }
+ }
+}
+
+.cde-grimoire-empty {
+ padding: 4px 14px 8px;
+ font-size: 11px;
+ color: @cde-muted;
+ font-style: italic;
+ margin: 0;
+}
+
+// =====================================================================
+// KUNG-FU CARDS (actor tab redesign)
+// =====================================================================
+
+.cde-kf-add-row {
+ display: flex;
+ justify-content: flex-end;
+ padding: 4px 0 8px;
+}
+
+.cde-kf-add-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: @cde-muted;
+ cursor: pointer;
+ padding: 4px 10px;
+ border-radius: @cde-radius;
+ border: 1px solid @cde-border;
+ transition: color 0.12s, border-color 0.12s, background 0.12s;
+
+ i { font-size: 10px; }
+
+ &:hover { color: @cde-kungfu; border-color: @cde-kungfu; background: fade(@cde-kungfu, 8%); }
+}
+
+.cde-kf-card {
+ border: 1px solid @cde-border;
+ border-left: 3px solid @cde-kungfu;
+ border-radius: @cde-radius;
+ background: fade(@cde-surface, 70%);
+ margin-bottom: 10px;
+ overflow: hidden;
+ transition: box-shadow 0.15s;
+
+ &:hover { box-shadow: 0 0 8px fade(@cde-kungfu, 20%); }
+
+ &.cde-kf--metal { border-left-color: @wu-gray; }
+ &.cde-kf--eau { border-left-color: @wu-blue; }
+ &.cde-kf--terre { border-left-color: lighten(@wu-brown, 10%); }
+ &.cde-kf--feu { border-left-color: @wu-red; }
+ &.cde-kf--bois { border-left-color: @wu-green; }
+}
+
+.cde-kf-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+
+ &:hover .cde-kf-controls { opacity: 1; }
+}
+
+.cde-kf-aspect-icon {
+ width: 26px;
+ height: 26px;
+ object-fit: contain;
+ flex-shrink: 0;
+}
+
+.cde-kf-orient-icon {
+ width: 18px;
+ height: 18px;
+ object-fit: contain;
+ flex-shrink: 0;
+ opacity: 0.75;
+}
+
+.cde-kf-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.cde-kf-name {
+ font-size: 14px;
+ font-weight: 700;
+ font-family: "Averia", sans-serif;
+ color: @cde-text;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.cde-kf-meta {
+ font-size: 10px;
+ color: @cde-muted;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+
+ em { color: @cde-text; font-style: normal; }
+}
+
+.cde-kf-roll-btn {
+ width: 30px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: @cde-radius;
+ cursor: pointer;
+ color: @cde-muted;
+ flex-shrink: 0;
+ transition: color 0.15s, background 0.15s;
+
+ i { font-size: 14px; }
+
+ &:hover { color: @cde-kungfu; background: fade(@cde-kungfu, 15%); }
+}
+
+.cde-kf-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity 0.12s;
+ flex-shrink: 0;
+
+ a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ border-radius: 3px;
+ color: @cde-muted;
+ cursor: pointer;
+ transition: color 0.12s, background 0.12s;
+
+ i { font-size: 11px; }
+
+ &:hover { color: @cde-text; background: fade(@cde-border-hi, 30%); }
+ }
+}
+
+.cde-kf-style-row {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ padding: 4px 12px 6px 44px;
+ border-top: 1px solid fade(@cde-border, 60%);
+ background: fade(@cde-surface, 40%);
+}
+
+.cde-kf-style-label {
+ font-size: 10px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: @cde-kungfu;
+ flex-shrink: 0;
+
+ i { font-size: 9px; }
+}
+
+.cde-kf-style-text {
+ font-size: 11px;
+ color: @cde-text;
+ font-family: "Averia", sans-serif;
+ font-style: italic;
+}
+
+.cde-kf-techniques {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ border-top: 1px solid fade(@cde-border, 50%);
+}
+
+.cde-kf-tech {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 12px 5px 44px;
+ border-bottom: 1px solid fade(@cde-border, 30%);
+ opacity: 0.5;
+ transition: opacity 0.1s, background 0.1s;
+
+ &:last-child { border-bottom: none; }
+ &--mastered { opacity: 1; }
+ &:hover { background: fade(@cde-border, 20%); opacity: 1; }
+}
+
+.cde-kf-tech-mastered {
+ font-size: 11px;
+ flex-shrink: 0;
+ width: 14px;
+ text-align: center;
+
+ .cde-kf-tech--mastered & { color: @cde-kungfu; }
+ .cde-kf-tech:not(.cde-kf-tech--mastered) & { color: @cde-muted; }
+}
+
+.cde-act-badge {
+ font-size: 9px;
+ font-family: "Averia", sans-serif;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ padding: 2px 5px;
+ border-radius: 3px;
+ flex-shrink: 0;
+ max-width: 110px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border: 1px solid currentColor;
+
+ &.cde-act--action-attack { color: @wu-red; background: fade(@wu-red, 10%); }
+ &.cde-act--action-defense { color: @wu-blue; background: fade(@wu-blue, 10%); }
+ &.cde-act--action-aid { color: @wu-green; background: fade(@wu-green, 10%); }
+ &.cde-act--action-attack-defense { color: @wu-gray; background: fade(@wu-gray, 12%); }
+ &.cde-act--reaction { color: @wu-brown; background: fade(@wu-brown, 12%); }
+ &.cde-act--dice { color: @cde-spell; background: fade(@cde-spell, 10%); }
+ &.cde-act--damage-inflicted { color: @cde-weapon; background: fade(@cde-weapon, 10%); }
+ &.cde-act--damage-received { color: @cde-muted; background: fade(@cde-border, 20%); }
+}
+
+.cde-kf-tech-name {
+ flex: 1;
+ font-size: 12px;
+ color: @cde-text;
+ font-family: "Averia", sans-serif;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ em { color: @cde-muted; }
+}
+
+.cde-kf-empty {
+ padding: 16px;
+ text-align: center;
+ font-size: 12px;
+ color: @cde-muted;
+ font-style: italic;
+}
+
+// Random component chat message
+.cde-chat-random-component {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ padding: 8px 12px;
+}
+
+.cde-chat-component-label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: @cde-muted;
+ font-family: "Averia", sans-serif;
+}
+
+.cde-chat-component-value {
+ font-size: 16px;
+ font-weight: 700;
+ color: @cde-spell;
+ text-shadow: 0 0 8px fade(@cde-spell, 50%);
+}
+
// Legacy tabs (actor sheets still use cde-tabs)
.cde-tabs {
margin-top: @cde-gap;
diff --git a/dist/system.js b/dist/system.js
index 25f4403..6677c1f 100644
--- a/dist/system.js
+++ b/dist/system.js
@@ -449,7 +449,7 @@ var SpellDataModel = class extends foundry.abstract.TypeDataModel {
effects: htmlField(""),
examples: htmlField(""),
notes: htmlField(""),
- discipline: stringField("cinabre"),
+ discipline: stringField("internalcinnabar"),
heiType: stringField("yin"),
heiCost: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 }),
difficulty: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 })
@@ -727,6 +727,55 @@ function registerHandlebarsHelpers() {
Handlebars.registerHelper("getMagicSpecialityLabelElement", function(magic, speciality) {
return game.i18n.localize(MAGICS[magic]?.speciality?.[speciality]?.labelelement ?? "");
});
+ Handlebars.registerHelper("getMagicAspectIcon", function(magic) {
+ const icons = {
+ internalcinnabar: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png",
+ alchemy: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png",
+ masteryoftheway: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png",
+ exorcism: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png",
+ geomancy: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png"
+ };
+ return icons[magic] ?? "";
+ });
+ Handlebars.registerHelper("getElementIcon", function(aspect) {
+ const icons = {
+ metal: "/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png",
+ eau: "/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png",
+ terre: "/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png",
+ feu: "/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png",
+ bois: "/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png"
+ };
+ return icons[aspect] ?? "";
+ });
+ Handlebars.registerHelper("getOrientationIcon", function(orientation) {
+ const icons = {
+ yin: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png",
+ yang: "/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png",
+ yinyang: "/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png"
+ };
+ return icons[orientation] ?? "";
+ });
+ Handlebars.registerHelper("getOrientationLabel", function(orientation) {
+ const keys = {
+ yin: "CDE.OrientationYin",
+ yang: "CDE.OrientationYang",
+ yinyang: "CDE.OrientationYinYang"
+ };
+ return game.i18n.localize(keys[orientation] ?? "CDE.Orientation");
+ });
+ Handlebars.registerHelper("getActivationLabel", function(activation) {
+ const keys = {
+ "action-attack": "CDE.ActivationAttack",
+ "action-defense": "CDE.ActivationDefense",
+ "action-aid": "CDE.ActivationAid",
+ "action-attack-defense": "CDE.ActivationAttackOrDefense",
+ reaction: "CDE.ActivationReaction",
+ dice: "CDE.ActivationDice",
+ "damage-inflicted": "CDE.ActivationDamageInflicted",
+ "damage-received": "CDE.ActivationDamageReceived"
+ };
+ return game.i18n.localize(keys[activation] ?? "CDE.Activation");
+ });
}
// src/ui/templates.js
@@ -1449,15 +1498,19 @@ var CDEBaseActorSheet = class _CDEBaseActorSheet extends HandlebarsApplicationMi
supernatural: "CDE.SupernaturalNew"
};
const name = game.i18n.localize(labels[type] ?? "CDE.ItemNew");
- return cls.create({ name, type }, { parent: this.document });
+ const systemData = {};
+ if (type === "spell" && target.dataset.discipline) {
+ systemData.discipline = target.dataset.discipline;
+ }
+ return cls.create({ name, type, system: systemData }, { parent: this.document });
}
static #onItemEdit(event, target) {
- const itemId = target.closest(".item")?.dataset.itemId;
+ const itemId = target.dataset.itemId ?? target.closest("[data-item-id]")?.dataset.itemId;
const item = this.document.items.get(itemId);
if (item) item.sheet.render(true);
}
static #onItemDelete(event, target) {
- const itemId = target.closest(".item")?.dataset.itemId;
+ const itemId = target.dataset.itemId ?? target.closest("[data-item-id]")?.dataset.itemId;
const item = this.document.items.get(itemId);
if (item) item.delete();
}
@@ -1482,6 +1535,32 @@ var CDECharacterSheet = class extends CDEBaseActorSheet {
context.spells = context.items.filter((item) => item.type === "spell");
context.kungfus = context.items.filter((item) => item.type === "kungfu");
context.CDE = { MAGICS, SUBTYPES };
+ const spellsByDiscipline = {};
+ for (const spell of context.spells) {
+ const disc = spell.system?.discipline ?? "internalcinnabar";
+ if (!spellsByDiscipline[disc]) spellsByDiscipline[disc] = [];
+ spellsByDiscipline[disc].push(spell);
+ }
+ const systemMagics = context.systemData.magics ?? {};
+ context.magicsDisplay = Object.fromEntries(
+ Object.entries(MAGICS).map(([magicKey, magicDef]) => {
+ const magicData = systemMagics[magicKey] ?? {};
+ return [
+ magicKey,
+ {
+ value: magicData.value ?? 0,
+ visible: magicData.visible ?? false,
+ speciality: Object.fromEntries(
+ Object.keys(magicDef.speciality).map((specKey) => [
+ specKey,
+ { check: magicData.speciality?.[specKey]?.check ?? false }
+ ])
+ ),
+ grimoire: spellsByDiscipline[magicKey] ?? []
+ }
+ ];
+ })
+ );
return context;
}
_onRender(context, options) {
@@ -1489,6 +1568,7 @@ var CDECharacterSheet = class extends CDEBaseActorSheet {
this.#bindInitiativeControls();
this.#bindPrefs();
this.#bindRollButtons();
+ this.#bindComponentRandomize();
}
#bindInitiativeControls() {
const buttons = this.element?.querySelectorAll(".click-initiative");
@@ -1563,6 +1643,42 @@ var CDECharacterSheet = class extends CDEBaseActorSheet {
});
});
}
+ #bindComponentRandomize() {
+ const btn = this.element?.querySelector("[data-action='randomize-component']");
+ if (!btn) return;
+ btn.addEventListener("click", async () => {
+ const roll = new Roll("1d10");
+ await roll.evaluate();
+ const face = roll.total === 10 ? 0 : roll.total;
+ const COMPONENT_KEYS = {
+ 1: "one",
+ 2: "two",
+ 3: "three",
+ 4: "four",
+ 5: "five",
+ 6: "six",
+ 7: "seven",
+ 8: "eight",
+ 9: "nine",
+ 0: "zero"
+ };
+ const componentKey = COMPONENT_KEYS[face];
+ const componentValue = this.document.system.component?.[componentKey]?.value ?? "";
+ const label = componentValue ? `${componentValue}` : `${game.i18n.localize("CDE.Component")}${face}`;
+ const content = `
+
+ ${game.i18n.localize("CDE.ChanceThrowResult")}
+ ${label}
+
`;
+ await ChatMessage.create({
+ user: game.user.id,
+ speaker: ChatMessage.getSpeaker({ actor: this.document }),
+ content,
+ rolls: [roll],
+ rollMode: "roll"
+ });
+ });
+ }
};
// src/ui/sheets/actors/npc.js
diff --git a/dist/system.js.map b/dist/system.js.map
index f5bf70a..61dc952 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.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n mind: { label: \"CDE.Mind\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n purification: { label: \"CDE.Purification\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n manipulation: { label: \"CDE.Manipulation\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n aura: { label: \"CDE.Aura\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png\", 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.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n elixirs: { label: \"CDE.Elixirs\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n poisons: { label: \"CDE.Poisons\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n arsenal: { label: \"CDE.Arsenal\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n potions: { label: \"CDE.Potions\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png\", 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.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n transfiguration: { label: \"CDE.Transfiguration\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n necromancy: { label: \"CDE.Necromancy\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n climatecontrol: { label: \"CDE.ClimateControl\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n goldenmagic: { label: \"CDE.GoldenMagic\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png\", 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.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n tracking: { label: \"CDE.Tracking\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n protection: { label: \"CDE.Protection\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n punishment: { label: \"CDE.Punishment\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n domination: { label: \"CDE.Domination\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png\", 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.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png\", labelicon: \"Yin\", labelelement: \"CDE.Metal\" },\n divination: { label: \"CDE.Divination\", classicon: \"icon-yin\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yin.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png\", labelicon: \"Yin\", labelelement: \"CDE.Water\" },\n earthlyprayer: { label: \"CDE.EarthlyPrayer\", classicon: \"icon-yinyang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/yin_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png\", labelicon: \"Yin/Yang\", labelelement: \"CDE.Earth\" },\n heavenlyprayer: { label: \"CDE.HeavenlyPrayer\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png\", labelicon: \"Yang\", labelelement: \"CDE.Fire\" },\n fungseoi: { label: \"CDE.Fungseoi\", classicon: \"icon-yang\", icon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_yang.png\", elementicon: \"/systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png\", 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(\"cinabre\"),\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", "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) || 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\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.png\",\n water: \"systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png\",\n earth: \"systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png\",\n fire: \"systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png\",\n wood: \"systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png\",\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