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;
|
color: #4a9eff;
|
||||||
text-shadow: 0 0 8px rgba(74, 158, 255, 0.5);
|
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 {
|
.cde-tabs {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
border-bottom: 1px solid #1a2436;
|
border-bottom: 1px solid #1a2436;
|
||||||
@@ -2864,6 +2940,30 @@ strong.ellipsis {
|
|||||||
#interface .chat-message .message-header .message-sender {
|
#interface .chat-message .message-header .message-sender {
|
||||||
width: 100px;
|
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 {
|
.cde-flex-center-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
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%);
|
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)
|
// Legacy tabs (actor sheets still use cde-tabs)
|
||||||
.cde-tabs {
|
.cde-tabs {
|
||||||
margin-top: @cde-gap;
|
margin-top: @cde-gap;
|
||||||
@@ -2910,6 +2999,37 @@ strong.ellipsis {
|
|||||||
width: 100px;
|
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
|
// Three Treasures partial layout
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
Vendored
+114
-19
@@ -804,15 +804,6 @@ function preLocalizeConfig() {
|
|||||||
|
|
||||||
// src/config/runtime.js
|
// src/config/runtime.js
|
||||||
function configureRuntime() {
|
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
|
// src/data/actors/character.js
|
||||||
@@ -1682,12 +1673,13 @@ var LABELELEMENT_TO_ASPECT = {
|
|||||||
"CDE.Fire": "fire",
|
"CDE.Fire": "fire",
|
||||||
"CDE.Wood": "wood"
|
"CDE.Wood": "wood"
|
||||||
};
|
};
|
||||||
var RANGE_MALUS = {
|
var RANGE_ORDER = ["contact", "courte", "mediane", "longue", "extreme"];
|
||||||
contact: 0,
|
var RANGE_LABELS = {
|
||||||
courte: 0,
|
contact: "CDE.RangeContact",
|
||||||
mediane: -1,
|
courte: "CDE.RangeCourte",
|
||||||
longue: -2,
|
mediane: "CDE.RangeMediane",
|
||||||
extreme: -3
|
longue: "CDE.RangeLongue",
|
||||||
|
extreme: "CDE.RangeExtreme"
|
||||||
};
|
};
|
||||||
var WEAPON_TYPE_SKILL = {
|
var WEAPON_TYPE_SKILL = {
|
||||||
melee: "kungfu",
|
melee: "kungfu",
|
||||||
@@ -1787,6 +1779,13 @@ async function showMagicPrompt(params) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function showWeaponPrompt(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({
|
return showRollPrompt({
|
||||||
title: params.title,
|
title: params.title,
|
||||||
template: WEAPON_PROMPT_TEMPLATE,
|
template: WEAPON_PROMPT_TEMPLATE,
|
||||||
@@ -1800,6 +1799,7 @@ async function showWeaponPrompt(params) {
|
|||||||
weaponskill: params.weaponskill ?? "kungfu",
|
weaponskill: params.weaponskill ?? "kungfu",
|
||||||
aspect: Number(params.aspect ?? 0),
|
aspect: Number(params.aspect ?? 0),
|
||||||
effectiverange: params.effectiverange ?? "contact",
|
effectiverange: params.effectiverange ?? "contact",
|
||||||
|
validRanges,
|
||||||
bonusmalus: params.bonusmalus ?? 0,
|
bonusmalus: params.bonusmalus ?? 0,
|
||||||
woundmalus: params.woundmalus ?? 0,
|
woundmalus: params.woundmalus ?? 0,
|
||||||
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
|
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
|
||||||
@@ -1956,14 +1956,13 @@ async function rollForActor(actor, rollKey) {
|
|||||||
const wpSkillDice = sys.skills?.[wpChosenSkill]?.value ?? 0;
|
const wpSkillDice = sys.skills?.[wpChosenSkill]?.value ?? 0;
|
||||||
const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx);
|
const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx);
|
||||||
const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0;
|
const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0;
|
||||||
const wpRangeMalus = RANGE_MALUS[wParams.effectiverange ?? "contact"] ?? 0;
|
|
||||||
const wpBonusMalus = Number(wParams.bonusmalus ?? 0);
|
const wpBonusMalus = Number(wParams.bonusmalus ?? 0);
|
||||||
const wpWoundMalus = Number(wParams.woundmalus ?? 0);
|
const wpWoundMalus = Number(wParams.woundmalus ?? 0);
|
||||||
const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 0);
|
const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 0);
|
||||||
const wpThrowMode = Number(wParams.typeofthrow ?? 0);
|
const wpThrowMode = Number(wParams.typeofthrow ?? 0);
|
||||||
const wpDamageBase = wpItem.system.damageBase ?? 0;
|
const wpDamageBase = wpItem.system.damageBase ?? 0;
|
||||||
const wpSpecialtyBonus = wpItem.system.hasSpeciality ? 1 : 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) {
|
if (wpTotalDice <= 0) {
|
||||||
ui.notifications.warn(game.i18n.localize("CDE.Error0"));
|
ui.notifications.warn(game.i18n.localize("CDE.Error0"));
|
||||||
return;
|
return;
|
||||||
@@ -1975,7 +1974,6 @@ async function rollForActor(actor, rollKey) {
|
|||||||
const wpResults = computeWuXingResults(wpFaces, wpAspectName, wpBonusAusp);
|
const wpResults = computeWuXingResults(wpFaces, wpAspectName, wpBonusAusp);
|
||||||
if (!wpResults) return;
|
if (!wpResults) return;
|
||||||
const wpModParts = [];
|
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 (wpBonusMalus !== 0) wpModParts.push(`${wpBonusMalus > 0 ? "+" : ""}${wpBonusMalus} ${game.i18n.localize("CDE.BonusMalus")}`);
|
||||||
if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`);
|
if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`);
|
||||||
if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`);
|
if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`);
|
||||||
@@ -2277,7 +2275,8 @@ var CDECharacterSheet = class _CDECharacterSheet extends CDEBaseActorSheet {
|
|||||||
classes: ["character"],
|
classes: ["character"],
|
||||||
actions: {
|
actions: {
|
||||||
moveMagicUp: _CDECharacterSheet.#onMoveMagicUp,
|
moveMagicUp: _CDECharacterSheet.#onMoveMagicUp,
|
||||||
moveMagicDown: _CDECharacterSheet.#onMoveMagicDown
|
moveMagicDown: _CDECharacterSheet.#onMoveMagicDown,
|
||||||
|
recovery: _CDECharacterSheet.#onRecovery
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
@@ -2447,6 +2446,102 @@ var CDECharacterSheet = class _CDECharacterSheet extends CDEBaseActorSheet {
|
|||||||
[order[idx], order[idx + 1]] = [order[idx + 1], order[idx]];
|
[order[idx], order[idx + 1]] = [order[idx + 1], order[idx]];
|
||||||
await this.document.update({ "system.magicOrder": order });
|
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() {
|
#bindComponentRandomize() {
|
||||||
const btn = this.element?.querySelector("[data-action='randomize-component']");
|
const btn = this.element?.querySelector("[data-action='randomize-component']");
|
||||||
if (!btn) return;
|
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.RealizationTimeAccelerated": "Temps de Réalisation (Accéléré)",
|
||||||
"CDE.RealizationTimeRitual": "Temps de Réalisation (Rituel)",
|
"CDE.RealizationTimeRitual": "Temps de Réalisation (Rituel)",
|
||||||
"CDE.Reference": "Référence",
|
"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.Renowned": "Renommé",
|
||||||
"CDE.ResourceMax": "Max",
|
"CDE.ResourceMax": "Max",
|
||||||
"CDE.ResourceMin": "Min",
|
"CDE.ResourceMin": "Min",
|
||||||
|
|||||||
+20
-10
@@ -44,13 +44,16 @@ const LABELELEMENT_TO_ASPECT = {
|
|||||||
"CDE.Wood": "wood",
|
"CDE.Wood": "wood",
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Maps weapon range string → dice malus applied to the attack pool */
|
/** Range order from closest to furthest. Weapons can be used at any
|
||||||
const RANGE_MALUS = {
|
* range ≤ the weapon's listed range, with no dice malus. */
|
||||||
contact: 0,
|
const RANGE_ORDER = ["contact", "courte", "mediane", "longue", "extreme"]
|
||||||
courte: 0,
|
|
||||||
mediane: -1,
|
const RANGE_LABELS = {
|
||||||
longue: -2,
|
contact: "CDE.RangeContact",
|
||||||
extreme: -3,
|
courte: "CDE.RangeCourte",
|
||||||
|
mediane: "CDE.RangeMediane",
|
||||||
|
longue: "CDE.RangeLongue",
|
||||||
|
extreme: "CDE.RangeExtreme",
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Maps weapon type string → default skill key */
|
/** 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.
|
* Open the weapon attack roll prompt and return user-confirmed parameters.
|
||||||
*/
|
*/
|
||||||
async function showWeaponPrompt(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({
|
return showRollPrompt({
|
||||||
title: params.title,
|
title: params.title,
|
||||||
template: WEAPON_PROMPT_TEMPLATE,
|
template: WEAPON_PROMPT_TEMPLATE,
|
||||||
@@ -194,6 +205,7 @@ async function showWeaponPrompt(params) {
|
|||||||
weaponskill: params.weaponskill ?? "kungfu",
|
weaponskill: params.weaponskill ?? "kungfu",
|
||||||
aspect: Number(params.aspect ?? 0),
|
aspect: Number(params.aspect ?? 0),
|
||||||
effectiverange: params.effectiverange ?? "contact",
|
effectiverange: params.effectiverange ?? "contact",
|
||||||
|
validRanges,
|
||||||
bonusmalus: params.bonusmalus ?? 0,
|
bonusmalus: params.bonusmalus ?? 0,
|
||||||
woundmalus: params.woundmalus ?? 0,
|
woundmalus: params.woundmalus ?? 0,
|
||||||
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
|
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
|
||||||
@@ -366,7 +378,6 @@ export async function rollForActor(actor, rollKey) {
|
|||||||
const wpSkillDice = sys.skills?.[wpChosenSkill]?.value ?? 0
|
const wpSkillDice = sys.skills?.[wpChosenSkill]?.value ?? 0
|
||||||
const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx)
|
const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx)
|
||||||
const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0
|
const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0
|
||||||
const wpRangeMalus = RANGE_MALUS[wParams.effectiverange ?? "contact"] ?? 0
|
|
||||||
const wpBonusMalus = Number(wParams.bonusmalus ?? 0)
|
const wpBonusMalus = Number(wParams.bonusmalus ?? 0)
|
||||||
const wpWoundMalus = Number(wParams.woundmalus ?? 0)
|
const wpWoundMalus = Number(wParams.woundmalus ?? 0)
|
||||||
const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 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 wpSpecialtyBonus = wpItem.system.hasSpeciality ? 1 : 0
|
||||||
|
|
||||||
const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus
|
const wpTotalDice = wpSkillDice + wpAspectDice + wpBonusMalus - wpWoundMalus + wpSpecialtyBonus
|
||||||
if (wpTotalDice <= 0) {
|
if (wpTotalDice <= 0) {
|
||||||
ui.notifications.warn(game.i18n.localize("CDE.Error0"))
|
ui.notifications.warn(game.i18n.localize("CDE.Error0"))
|
||||||
return
|
return
|
||||||
@@ -390,7 +401,6 @@ export async function rollForActor(actor, rollKey) {
|
|||||||
if (!wpResults) return
|
if (!wpResults) return
|
||||||
|
|
||||||
const wpModParts = []
|
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 (wpBonusMalus !== 0) wpModParts.push(`${wpBonusMalus > 0 ? "+" : ""}${wpBonusMalus} ${game.i18n.localize("CDE.BonusMalus")}`)
|
||||||
if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`)
|
if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`)
|
||||||
if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`)
|
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/
|
* @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 { rollInitiativePC } from "../../initiative.js"
|
||||||
import { rollForActor } from "../../rolling.js"
|
import { rollForActor } from "../../rolling.js"
|
||||||
import { CDEBaseActorSheet } from "./base.js"
|
import { CDEBaseActorSheet } from "./base.js"
|
||||||
@@ -22,6 +22,7 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
|||||||
actions: {
|
actions: {
|
||||||
moveMagicUp: CDECharacterSheet.#onMoveMagicUp,
|
moveMagicUp: CDECharacterSheet.#onMoveMagicUp,
|
||||||
moveMagicDown: CDECharacterSheet.#onMoveMagicDown,
|
moveMagicDown: CDECharacterSheet.#onMoveMagicDown,
|
||||||
|
recovery: CDECharacterSheet.#onRecovery,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +208,115 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
|||||||
await this.document.update({ "system.magicOrder": order })
|
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() {
|
#bindComponentRandomize() {
|
||||||
const btn = this.element?.querySelector("[data-action='randomize-component']")
|
const btn = this.element?.querySelector("[data-action='randomize-component']")
|
||||||
if (!btn) return
|
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 --}}
|
{{!-- HEI — Essence --}}
|
||||||
<div class="tab-sous-titre">{{ localize "CDE.HEI" }} — {{ localize "CDE.Essence2" }}</div>
|
<div class="tab-sous-titre">{{ localize "CDE.HEI" }} — {{ localize "CDE.Essence2" }}</div>
|
||||||
<div class="cde-tt-block">
|
<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">
|
<div class="cde-roll-field">
|
||||||
<label>{{ localize "CDE.EffectiveRange" }}</label>
|
<label>{{ localize "CDE.EffectiveRange" }}</label>
|
||||||
<select name="effectiverange">
|
<select name="effectiverange">
|
||||||
<option value="contact" {{#if (eq effectiverange "contact")}}selected{{/if}}>{{ localize "CDE.RangeContact" }} (±0)</option>
|
{{#each validRanges}}
|
||||||
<option value="courte" {{#if (eq effectiverange "courte") }}selected{{/if}}>{{ localize "CDE.RangeCourte" }} (±0)</option>
|
<option value="{{value}}" {{#if selected}}selected{{/if}}>{{ localize label }}</option>
|
||||||
<option value="mediane" {{#if (eq effectiverange "mediane")}}selected{{/if}}>{{ localize "CDE.RangeMediane" }} (−1)</option>
|
{{/each}}
|
||||||
<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>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="cde-roll-field">
|
<div class="cde-roll-field">
|
||||||
|
|||||||
Reference in New Issue
Block a user