feat: portée armes, récupération 3T, socket loksyu, import batch
Portée des armes - Suppression des malus de portée (mediane/longue/extreme) - Sélecteur filtré : seules les portées <= portée max de l'arme Récupération (3 Trésors) - Nouveau bouton dans l'onglet Trésors - Jet basé sur l'aspect du Gardien Céleste - Dé pair : +1 Hei Yang, +1 San - Dé impair : +1 Hei Yin, +1 Zing - Fenêtre de bonus dice pour les pouvoirs (Acupuncture...) - Ignore les malus de palier San/Zing Socket Loksyu/TinJi - Les joueurs peuvent mettre à jour Loksyu/TinJi via socket - Le MJ proxy les écritures (game.settings world = GM only) - Opérations atomiques pour éviter les races conditions (roll simultanés) Import batch - 4 nouveaux migrators item (weapon/armor/sanhei/ingredient) - magicOrder dans la migration character - Confirmation en 2 clics avant l'import - Détection des doublons avec le monde - Progression en direct pendant l'import - Dédoublonnage non-silencieux + flag _duplicate calculé en direct UI - Noms d'items en Signika (plus lisible qu'Averia) - ASCII art au lancement dans la console - Clés i18n manquantes ajoutées (BonusMalus, EnablePrompt, Material, Roll, ThrowType) Fix divers - Calcul puissance sort : utilise l'élément de la spécialité, pas celui de l'école - preLocalizeConfig préserve labelelementkey pour la résolution d'aspect - combat.js: guard undefined ids dans rollInitiative
This commit is contained in:
@@ -2780,6 +2780,82 @@ section.npc .cde-neon-tabs .item.active {
|
||||
color: #4a9eff;
|
||||
text-shadow: 0 0 8px rgba(74, 158, 255, 0.5);
|
||||
}
|
||||
/* Recovery roll chat message */
|
||||
.cde-chat-recovery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.cde-chat-recovery-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.cde-chat-recovery-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.cde-chat-recovery-title {
|
||||
font-family: "Averia", sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #e2e8f4;
|
||||
}
|
||||
.cde-chat-recovery-dice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.cde-chat-recovery-dice-label {
|
||||
font-weight: 700;
|
||||
color: #00d4d4;
|
||||
}
|
||||
.cde-chat-recovery-dice-results {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: #e2e8f4;
|
||||
}
|
||||
.cde-chat-recovery-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.cde-chat-recovery-even {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #4a9eff;
|
||||
}
|
||||
.cde-chat-recovery-odd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #cc44ff;
|
||||
}
|
||||
.cde-chat-recovery-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cde-chat-recovery-even .cde-chat-recovery-count {
|
||||
background: #4a9eff;
|
||||
}
|
||||
.cde-chat-recovery-odd .cde-chat-recovery-count {
|
||||
background: #cc44ff;
|
||||
}
|
||||
.cde-tabs {
|
||||
margin-top: 12px;
|
||||
border-bottom: 1px solid #1a2436;
|
||||
@@ -2864,6 +2940,30 @@ strong.ellipsis {
|
||||
#interface .chat-message .message-header .message-sender {
|
||||
width: 100px;
|
||||
}
|
||||
.cde-recovery-bar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cde-recovery-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #4a9eff;
|
||||
border-radius: 6px;
|
||||
background: rgba(74, 158, 255, 0.15);
|
||||
color: #4a9eff;
|
||||
font-family: "Averia", sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, filter 0.15s;
|
||||
}
|
||||
.cde-recovery-btn:hover {
|
||||
background: rgba(74, 158, 255, 0.3);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.cde-flex-center-text {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -2805,6 +2805,95 @@ section.npc .cde-neon-tabs .item.active { color: @cde-supernatural; borde
|
||||
text-shadow: 0 0 8px fade(@cde-spell, 50%);
|
||||
}
|
||||
|
||||
/* Recovery roll chat message */
|
||||
.cde-chat-recovery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-title {
|
||||
font-family: "Averia", sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: @cde-text;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-dice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-dice-label {
|
||||
font-weight: 700;
|
||||
color: @cde-item;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-dice-results {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: @cde-text;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-even {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: @cde-spell;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-odd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: @cde-supernatural;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-even .cde-chat-recovery-count {
|
||||
background: @cde-spell;
|
||||
}
|
||||
|
||||
.cde-chat-recovery-odd .cde-chat-recovery-count {
|
||||
background: @cde-supernatural;
|
||||
}
|
||||
|
||||
// Legacy tabs (actor sheets still use cde-tabs)
|
||||
.cde-tabs {
|
||||
margin-top: @cde-gap;
|
||||
@@ -2910,6 +2999,37 @@ strong.ellipsis {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Recovery button
|
||||
// ============================================================
|
||||
|
||||
.cde-recovery-bar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cde-recovery-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid @cde-spell;
|
||||
border-radius: 6px;
|
||||
background: fadeout(@cde-spell, 85%);
|
||||
color: @cde-spell;
|
||||
font-family: "Averia", sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, filter 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: fadeout(@cde-spell, 70%);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Three Treasures partial layout
|
||||
// ============================================================
|
||||
|
||||
Vendored
+114
-19
@@ -804,15 +804,6 @@ function preLocalizeConfig() {
|
||||
|
||||
// src/config/runtime.js
|
||||
function configureRuntime() {
|
||||
CONFIG.Actor.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/actor-banner.webp";
|
||||
CONFIG.Adventure.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/adventure-banner.webp";
|
||||
CONFIG.Cards.compendiumBanner = "ui/banners/cards-banner.webp";
|
||||
CONFIG.Item.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/item-banner.webp";
|
||||
CONFIG.JournalEntry.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/journalentry-banner.webp";
|
||||
CONFIG.Macro.compendiumBanner = "ui/banners/macro-banner.webp";
|
||||
CONFIG.Playlist.compendiumBanner = "ui/banners/playlist-banner.webp";
|
||||
CONFIG.RollTable.compendiumBanner = "ui/banners/rolltable-banner.webp";
|
||||
CONFIG.Scene.compendiumBanner = "/systems/fvtt-chroniques-de-l-etrange/images/banners/scene-banner.webp";
|
||||
}
|
||||
|
||||
// src/data/actors/character.js
|
||||
@@ -1682,12 +1673,13 @@ var LABELELEMENT_TO_ASPECT = {
|
||||
"CDE.Fire": "fire",
|
||||
"CDE.Wood": "wood"
|
||||
};
|
||||
var RANGE_MALUS = {
|
||||
contact: 0,
|
||||
courte: 0,
|
||||
mediane: -1,
|
||||
longue: -2,
|
||||
extreme: -3
|
||||
var RANGE_ORDER = ["contact", "courte", "mediane", "longue", "extreme"];
|
||||
var RANGE_LABELS = {
|
||||
contact: "CDE.RangeContact",
|
||||
courte: "CDE.RangeCourte",
|
||||
mediane: "CDE.RangeMediane",
|
||||
longue: "CDE.RangeLongue",
|
||||
extreme: "CDE.RangeExtreme"
|
||||
};
|
||||
var WEAPON_TYPE_SKILL = {
|
||||
melee: "kungfu",
|
||||
@@ -1787,6 +1779,13 @@ async function showMagicPrompt(params) {
|
||||
});
|
||||
}
|
||||
async function showWeaponPrompt(params) {
|
||||
const maxRange = params.effectiverange ?? "contact";
|
||||
const maxIdx = RANGE_ORDER.indexOf(maxRange);
|
||||
const validRanges = RANGE_ORDER.slice(0, maxIdx + 1).map((r) => ({
|
||||
value: r,
|
||||
label: RANGE_LABELS[r],
|
||||
selected: r === maxRange
|
||||
}));
|
||||
return showRollPrompt({
|
||||
title: params.title,
|
||||
template: WEAPON_PROMPT_TEMPLATE,
|
||||
@@ -1800,6 +1799,7 @@ async function showWeaponPrompt(params) {
|
||||
weaponskill: params.weaponskill ?? "kungfu",
|
||||
aspect: Number(params.aspect ?? 0),
|
||||
effectiverange: params.effectiverange ?? "contact",
|
||||
validRanges,
|
||||
bonusmalus: params.bonusmalus ?? 0,
|
||||
woundmalus: params.woundmalus ?? 0,
|
||||
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
|
||||
@@ -1956,14 +1956,13 @@ async function rollForActor(actor, rollKey) {
|
||||
const wpSkillDice = sys.skills?.[wpChosenSkill]?.value ?? 0;
|
||||
const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx);
|
||||
const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0;
|
||||
const wpRangeMalus = RANGE_MALUS[wParams.effectiverange ?? "contact"] ?? 0;
|
||||
const wpBonusMalus = Number(wParams.bonusmalus ?? 0);
|
||||
const wpWoundMalus = Number(wParams.woundmalus ?? 0);
|
||||
const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 0);
|
||||
const wpThrowMode = Number(wParams.typeofthrow ?? 0);
|
||||
const wpDamageBase = wpItem.system.damageBase ?? 0;
|
||||
const wpSpecialtyBonus = wpItem.system.hasSpeciality ? 1 : 0;
|
||||
const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus;
|
||||
const wpTotalDice = wpSkillDice + wpAspectDice + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus;
|
||||
if (wpTotalDice <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error0"));
|
||||
return;
|
||||
@@ -1975,7 +1974,6 @@ async function rollForActor(actor, rollKey) {
|
||||
const wpResults = computeWuXingResults(wpFaces, wpAspectName, wpBonusAusp);
|
||||
if (!wpResults) return;
|
||||
const wpModParts = [];
|
||||
if (wpRangeMalus !== 0) wpModParts.push(`${wpRangeMalus} ${game.i18n.localize("CDE.RangePenalty")}`);
|
||||
if (wpBonusMalus !== 0) wpModParts.push(`${wpBonusMalus > 0 ? "+" : ""}${wpBonusMalus} ${game.i18n.localize("CDE.BonusMalus")}`);
|
||||
if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`);
|
||||
if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`);
|
||||
@@ -2277,7 +2275,8 @@ var CDECharacterSheet = class _CDECharacterSheet extends CDEBaseActorSheet {
|
||||
classes: ["character"],
|
||||
actions: {
|
||||
moveMagicUp: _CDECharacterSheet.#onMoveMagicUp,
|
||||
moveMagicDown: _CDECharacterSheet.#onMoveMagicDown
|
||||
moveMagicDown: _CDECharacterSheet.#onMoveMagicDown,
|
||||
recovery: _CDECharacterSheet.#onRecovery
|
||||
}
|
||||
};
|
||||
static PARTS = {
|
||||
@@ -2447,6 +2446,102 @@ var CDECharacterSheet = class _CDECharacterSheet extends CDEBaseActorSheet {
|
||||
[order[idx], order[idx + 1]] = [order[idx + 1], order[idx]];
|
||||
await this.document.update({ "system.magicOrder": order });
|
||||
}
|
||||
static async #onRecovery(event, target) {
|
||||
const actor = this.document;
|
||||
const sys = actor.system;
|
||||
const guardian = Number(sys.guardian ?? 0);
|
||||
if (guardian < 1 || guardian > 5) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.RecoveryNoGuardian"));
|
||||
return;
|
||||
}
|
||||
const aspectName = ASPECT_NAMES[guardian - 1];
|
||||
const aspectLabel = game.i18n.localize(ASPECT_LABELS[aspectName]);
|
||||
const aspectValue = sys.aspect?.[aspectName]?.value ?? 0;
|
||||
if (aspectValue <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.RecoveryNoAspect"));
|
||||
return;
|
||||
}
|
||||
const RECOVERY_PROMPT = `systems/${SYSTEM_ID}/templates/form/cde-recovery-prompt.html`;
|
||||
const content = await foundry.applications.handlebars.renderTemplate(RECOVERY_PROMPT, {
|
||||
basedice: aspectValue
|
||||
});
|
||||
const params = await foundry.applications.api.DialogV2.prompt({
|
||||
window: { title: game.i18n.localize("CDE.Recovery") },
|
||||
content,
|
||||
rejectClose: false,
|
||||
ok: {
|
||||
label: game.i18n.localize("CDE.Validate"),
|
||||
callback: (event2, button, dialog) => {
|
||||
const root = dialog.element ?? dialog;
|
||||
return { bonusdice: Number(root.querySelector("[name=bonusdice]")?.value ?? 0) };
|
||||
}
|
||||
}
|
||||
});
|
||||
if (params === void 0) return;
|
||||
const totalDice = aspectValue + Math.max(0, Number(params.bonusdice ?? 0));
|
||||
const roll = new Roll(`${totalDice}d10`);
|
||||
await roll.evaluate();
|
||||
const results = roll.dice[0].results.map((r) => r.result);
|
||||
const evenCount = results.filter((r) => r % 2 === 0).length;
|
||||
const oddCount = results.filter((r) => r % 2 !== 0).length;
|
||||
const cap = (max) => max > 0 ? max : Infinity;
|
||||
const heiYang = Math.min(
|
||||
(sys.threetreasures?.heiyang?.value ?? 0) + evenCount,
|
||||
cap(sys.threetreasures?.heiyang?.max)
|
||||
);
|
||||
const heiYin = Math.min(
|
||||
(sys.threetreasures?.heiyin?.value ?? 0) + oddCount,
|
||||
cap(sys.threetreasures?.heiyin?.max)
|
||||
);
|
||||
const san = Math.min(
|
||||
(sys.threetreasures?.dicelevel?.level0d?.san?.value ?? 0) + evenCount,
|
||||
cap(sys.threetreasures?.dicelevel?.level0d?.san?.max)
|
||||
);
|
||||
const zing = Math.min(
|
||||
(sys.threetreasures?.dicelevel?.level0d?.zing?.value ?? 0) + oddCount,
|
||||
cap(sys.threetreasures?.dicelevel?.level0d?.zing?.max)
|
||||
);
|
||||
await actor.update({
|
||||
"system.threetreasures.heiyang.value": heiYang,
|
||||
"system.threetreasures.heiyin.value": heiYin,
|
||||
"system.threetreasures.dicelevel.level0d.san.value": san,
|
||||
"system.threetreasures.dicelevel.level0d.zing.value": zing
|
||||
});
|
||||
const rollLabel = `${game.i18n.localize("CDE.Recovery")} \u2014 ${aspectLabel}`;
|
||||
const diceStr = results.join(" \xB7 ");
|
||||
const typeOfThrow = Number(sys.prefs?.typeofthrow?.choice ?? 0);
|
||||
const ROLL_MODES2 = ["roll", "gmroll", "blindroll", "selfroll"];
|
||||
const rollMode = ROLL_MODES2[typeOfThrow] ?? "roll";
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content: `
|
||||
<div class="cde-chat-recovery">
|
||||
<div class="cde-chat-recovery-header">
|
||||
<img src="${ASPECT_ICONS[aspectName]}" class="cde-chat-recovery-icon" alt="${aspectLabel}"/>
|
||||
<span class="cde-chat-recovery-title">${rollLabel}</span>
|
||||
</div>
|
||||
<div class="cde-chat-recovery-dice">
|
||||
<span class="cde-chat-recovery-dice-label">${totalDice}d10</span>
|
||||
<span class="cde-chat-recovery-dice-results">${diceStr}</span>
|
||||
</div>
|
||||
<div class="cde-chat-recovery-results">
|
||||
<div class="cde-chat-recovery-even">
|
||||
<span class="cde-chat-recovery-count">${evenCount}</span>
|
||||
<span><strong>${game.i18n.localize("CDE.RecoveryEvenLabel")}</strong></span>
|
||||
<span>\u2192 +${evenCount} Hei Yang, +${evenCount} San</span>
|
||||
</div>
|
||||
<div class="cde-chat-recovery-odd">
|
||||
<span class="cde-chat-recovery-count">${oddCount}</span>
|
||||
<span><strong>${game.i18n.localize("CDE.RecoveryOddLabel")}</strong></span>
|
||||
<span>\u2192 +${oddCount} Hei Yin, +${oddCount} Zing</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
rolls: [roll],
|
||||
rollMode
|
||||
});
|
||||
}
|
||||
#bindComponentRandomize() {
|
||||
const btn = this.element?.querySelector("[data-action='randomize-component']");
|
||||
if (!btn) return;
|
||||
|
||||
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -285,6 +285,12 @@
|
||||
"CDE.RealizationTimeAccelerated": "Temps de Réalisation (Accéléré)",
|
||||
"CDE.RealizationTimeRitual": "Temps de Réalisation (Rituel)",
|
||||
"CDE.Reference": "Référence",
|
||||
"CDE.Recovery": "Récupération",
|
||||
"CDE.RecoveryBaseDice": "Dés de base",
|
||||
"CDE.RecoveryEvenLabel": "Pairs",
|
||||
"CDE.RecoveryNoAspect": "Votre aspect est à 0, la Récupération est impossible.",
|
||||
"CDE.RecoveryNoGuardian": "Vous devez choisir un Gardien Céleste pour utiliser la Récupération.",
|
||||
"CDE.RecoveryOddLabel": "Impairs",
|
||||
"CDE.Renowned": "Renommé",
|
||||
"CDE.ResourceMax": "Max",
|
||||
"CDE.ResourceMin": "Min",
|
||||
|
||||
+20
-10
@@ -44,13 +44,16 @@ const LABELELEMENT_TO_ASPECT = {
|
||||
"CDE.Wood": "wood",
|
||||
}
|
||||
|
||||
/** Maps weapon range string → dice malus applied to the attack pool */
|
||||
const RANGE_MALUS = {
|
||||
contact: 0,
|
||||
courte: 0,
|
||||
mediane: -1,
|
||||
longue: -2,
|
||||
extreme: -3,
|
||||
/** Range order from closest to furthest. Weapons can be used at any
|
||||
* range ≤ the weapon's listed range, with no dice malus. */
|
||||
const RANGE_ORDER = ["contact", "courte", "mediane", "longue", "extreme"]
|
||||
|
||||
const RANGE_LABELS = {
|
||||
contact: "CDE.RangeContact",
|
||||
courte: "CDE.RangeCourte",
|
||||
mediane: "CDE.RangeMediane",
|
||||
longue: "CDE.RangeLongue",
|
||||
extreme: "CDE.RangeExtreme",
|
||||
}
|
||||
|
||||
/** Maps weapon type string → default skill key */
|
||||
@@ -181,6 +184,14 @@ async function showMagicPrompt(params) {
|
||||
* Open the weapon attack roll prompt and return user-confirmed parameters.
|
||||
*/
|
||||
async function showWeaponPrompt(params) {
|
||||
const maxRange = params.effectiverange ?? "contact"
|
||||
const maxIdx = RANGE_ORDER.indexOf(maxRange)
|
||||
const validRanges = RANGE_ORDER.slice(0, maxIdx + 1).map(r => ({
|
||||
value: r,
|
||||
label: RANGE_LABELS[r],
|
||||
selected: r === maxRange,
|
||||
}))
|
||||
|
||||
return showRollPrompt({
|
||||
title: params.title,
|
||||
template: WEAPON_PROMPT_TEMPLATE,
|
||||
@@ -194,6 +205,7 @@ async function showWeaponPrompt(params) {
|
||||
weaponskill: params.weaponskill ?? "kungfu",
|
||||
aspect: Number(params.aspect ?? 0),
|
||||
effectiverange: params.effectiverange ?? "contact",
|
||||
validRanges,
|
||||
bonusmalus: params.bonusmalus ?? 0,
|
||||
woundmalus: params.woundmalus ?? 0,
|
||||
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
|
||||
@@ -366,7 +378,6 @@ export async function rollForActor(actor, rollKey) {
|
||||
const wpSkillDice = sys.skills?.[wpChosenSkill]?.value ?? 0
|
||||
const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx)
|
||||
const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0
|
||||
const wpRangeMalus = RANGE_MALUS[wParams.effectiverange ?? "contact"] ?? 0
|
||||
const wpBonusMalus = Number(wParams.bonusmalus ?? 0)
|
||||
const wpWoundMalus = Number(wParams.woundmalus ?? 0)
|
||||
const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 0)
|
||||
@@ -375,7 +386,7 @@ export async function rollForActor(actor, rollKey) {
|
||||
|
||||
const wpSpecialtyBonus = wpItem.system.hasSpeciality ? 1 : 0
|
||||
|
||||
const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus
|
||||
const wpTotalDice = wpSkillDice + wpAspectDice + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus
|
||||
if (wpTotalDice <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error0"))
|
||||
return
|
||||
@@ -390,7 +401,6 @@ export async function rollForActor(actor, rollKey) {
|
||||
if (!wpResults) return
|
||||
|
||||
const wpModParts = []
|
||||
if (wpRangeMalus !== 0) wpModParts.push(`${wpRangeMalus} ${game.i18n.localize("CDE.RangePenalty")}`)
|
||||
if (wpBonusMalus !== 0) wpModParts.push(`${wpBonusMalus > 0 ? "+" : ""}${wpBonusMalus} ${game.i18n.localize("CDE.BonusMalus")}`)
|
||||
if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`)
|
||||
if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*/
|
||||
|
||||
import { MAGICS, SUBTYPES } from "../../../config/constants.js"
|
||||
import { MAGICS, SUBTYPES, ASPECT_NAMES, ASPECT_LABELS, ASPECT_ICONS, SYSTEM_ID } from "../../../config/constants.js"
|
||||
import { rollInitiativePC } from "../../initiative.js"
|
||||
import { rollForActor } from "../../rolling.js"
|
||||
import { CDEBaseActorSheet } from "./base.js"
|
||||
@@ -22,6 +22,7 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
actions: {
|
||||
moveMagicUp: CDECharacterSheet.#onMoveMagicUp,
|
||||
moveMagicDown: CDECharacterSheet.#onMoveMagicDown,
|
||||
recovery: CDECharacterSheet.#onRecovery,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -207,6 +208,115 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
await this.document.update({ "system.magicOrder": order })
|
||||
}
|
||||
|
||||
static async #onRecovery(event, target) {
|
||||
const actor = this.document
|
||||
const sys = actor.system
|
||||
|
||||
const guardian = Number(sys.guardian ?? 0)
|
||||
if (guardian < 1 || guardian > 5) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.RecoveryNoGuardian"))
|
||||
return
|
||||
}
|
||||
|
||||
const aspectName = ASPECT_NAMES[guardian - 1]
|
||||
const aspectLabel = game.i18n.localize(ASPECT_LABELS[aspectName])
|
||||
const aspectValue = sys.aspect?.[aspectName]?.value ?? 0
|
||||
if (aspectValue <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.RecoveryNoAspect"))
|
||||
return
|
||||
}
|
||||
|
||||
// Show prompt for bonus dice
|
||||
const RECOVERY_PROMPT = `systems/${SYSTEM_ID}/templates/form/cde-recovery-prompt.html`
|
||||
const content = await foundry.applications.handlebars.renderTemplate(RECOVERY_PROMPT, {
|
||||
basedice: aspectValue,
|
||||
})
|
||||
const params = await foundry.applications.api.DialogV2.prompt({
|
||||
window: { title: game.i18n.localize("CDE.Recovery") },
|
||||
content,
|
||||
rejectClose: false,
|
||||
ok: {
|
||||
label: game.i18n.localize("CDE.Validate"),
|
||||
callback: (event, button, dialog) => {
|
||||
const root = dialog.element ?? dialog
|
||||
return { bonusdice: Number(root.querySelector("[name=bonusdice]")?.value ?? 0) }
|
||||
},
|
||||
},
|
||||
})
|
||||
if (params === undefined) return
|
||||
|
||||
const totalDice = aspectValue + Math.max(0, Number(params.bonusdice ?? 0))
|
||||
const roll = new Roll(`${totalDice}d10`)
|
||||
await roll.evaluate()
|
||||
|
||||
// Count even (pair) and odd (impair) results
|
||||
const results = roll.dice[0].results.map(r => r.result)
|
||||
const evenCount = results.filter(r => r % 2 === 0).length
|
||||
const oddCount = results.filter(r => r % 2 !== 0).length
|
||||
|
||||
// Clamp gains to configured max values. If max is 0 (unconfigured),
|
||||
// allow free accumulation (no clamp).
|
||||
const cap = (max) => (max > 0 ? max : Infinity)
|
||||
const heiYang = Math.min(
|
||||
(sys.threetreasures?.heiyang?.value ?? 0) + evenCount,
|
||||
cap(sys.threetreasures?.heiyang?.max)
|
||||
)
|
||||
const heiYin = Math.min(
|
||||
(sys.threetreasures?.heiyin?.value ?? 0) + oddCount,
|
||||
cap(sys.threetreasures?.heiyin?.max)
|
||||
)
|
||||
const san = Math.min(
|
||||
(sys.threetreasures?.dicelevel?.level0d?.san?.value ?? 0) + evenCount,
|
||||
cap(sys.threetreasures?.dicelevel?.level0d?.san?.max)
|
||||
)
|
||||
const zing = Math.min(
|
||||
(sys.threetreasures?.dicelevel?.level0d?.zing?.value ?? 0) + oddCount,
|
||||
cap(sys.threetreasures?.dicelevel?.level0d?.zing?.max)
|
||||
)
|
||||
|
||||
await actor.update({
|
||||
"system.threetreasures.heiyang.value": heiYang,
|
||||
"system.threetreasures.heiyin.value": heiYin,
|
||||
"system.threetreasures.dicelevel.level0d.san.value": san,
|
||||
"system.threetreasures.dicelevel.level0d.zing.value": zing,
|
||||
})
|
||||
|
||||
const rollLabel = `${game.i18n.localize("CDE.Recovery")} — ${aspectLabel}`
|
||||
const diceStr = results.join(" · ")
|
||||
const typeOfThrow = Number(sys.prefs?.typeofthrow?.choice ?? 0)
|
||||
const ROLL_MODES = ["roll", "gmroll", "blindroll", "selfroll"]
|
||||
const rollMode = ROLL_MODES[typeOfThrow] ?? "roll"
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content: `
|
||||
<div class="cde-chat-recovery">
|
||||
<div class="cde-chat-recovery-header">
|
||||
<img src="${ASPECT_ICONS[aspectName]}" class="cde-chat-recovery-icon" alt="${aspectLabel}"/>
|
||||
<span class="cde-chat-recovery-title">${rollLabel}</span>
|
||||
</div>
|
||||
<div class="cde-chat-recovery-dice">
|
||||
<span class="cde-chat-recovery-dice-label">${totalDice}d10</span>
|
||||
<span class="cde-chat-recovery-dice-results">${diceStr}</span>
|
||||
</div>
|
||||
<div class="cde-chat-recovery-results">
|
||||
<div class="cde-chat-recovery-even">
|
||||
<span class="cde-chat-recovery-count">${evenCount}</span>
|
||||
<span><strong>${game.i18n.localize("CDE.RecoveryEvenLabel")}</strong></span>
|
||||
<span>→ +${evenCount} Hei Yang, +${evenCount} San</span>
|
||||
</div>
|
||||
<div class="cde-chat-recovery-odd">
|
||||
<span class="cde-chat-recovery-count">${oddCount}</span>
|
||||
<span><strong>${game.i18n.localize("CDE.RecoveryOddLabel")}</strong></span>
|
||||
<span>→ +${oddCount} Hei Yin, +${oddCount} Zing</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
rolls: [roll],
|
||||
rollMode,
|
||||
})
|
||||
}
|
||||
|
||||
#bindComponentRandomize() {
|
||||
const btn = this.element?.querySelector("[data-action='randomize-component']")
|
||||
if (!btn) return
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
{{!-- Recovery button --}}
|
||||
<div class="cde-recovery-bar">
|
||||
<button type="button" class="cde-recovery-btn" data-action="recovery">
|
||||
<i class="fas fa-heartbeat"></i> {{ localize "CDE.Recovery" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{!-- HEI — Essence --}}
|
||||
<div class="tab-sous-titre">{{ localize "CDE.HEI" }} — {{ localize "CDE.Essence2" }}</div>
|
||||
<div class="cde-tt-block">
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="cde-roll-prompt cde-recovery-prompt {{cssClass}}">
|
||||
<div class="cde-roll-header">
|
||||
<span class="cde-roll-title">{{ localize "CDE.Recovery" }}</span>
|
||||
<span class="cde-roll-dice-count">{{basedice}}<span class="cde-roll-dice-label">d10</span></span>
|
||||
</div>
|
||||
|
||||
<div class="cde-roll-section">
|
||||
<div class="cde-roll-field">
|
||||
<label>{{ localize "CDE.RecoveryBaseDice" }}</label>
|
||||
<input type="number" name="bonusdice" value="0" min="0" max="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,11 +41,9 @@
|
||||
<div class="cde-roll-field">
|
||||
<label>{{ localize "CDE.EffectiveRange" }}</label>
|
||||
<select name="effectiverange">
|
||||
<option value="contact" {{#if (eq effectiverange "contact")}}selected{{/if}}>{{ localize "CDE.RangeContact" }} (±0)</option>
|
||||
<option value="courte" {{#if (eq effectiverange "courte") }}selected{{/if}}>{{ localize "CDE.RangeCourte" }} (±0)</option>
|
||||
<option value="mediane" {{#if (eq effectiverange "mediane")}}selected{{/if}}>{{ localize "CDE.RangeMediane" }} (−1)</option>
|
||||
<option value="longue" {{#if (eq effectiverange "longue") }}selected{{/if}}>{{ localize "CDE.RangeLongue" }} (−2)</option>
|
||||
<option value="extreme" {{#if (eq effectiverange "extreme")}}selected{{/if}}>{{ localize "CDE.RangeExtreme" }} (−3)</option>
|
||||
{{#each validRanges}}
|
||||
<option value="{{value}}" {{#if selected}}selected{{/if}}>{{ localize label }}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="cde-roll-field">
|
||||
|
||||
Reference in New Issue
Block a user