Correction compendiums
This commit is contained in:
@@ -14,3 +14,4 @@
|
||||
export { CDELoksyuApp } from "./loksyu-app.js"
|
||||
export { CDETinjiApp } from "./tinji-app.js"
|
||||
export { updateLoksyuFromRoll, updateTinjiFromRoll } from "./singletons.js"
|
||||
export { CDEWheelApp } from "./wheel-app.js"
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Chroniques de l'Étrange — Système FoundryVTT
|
||||
*
|
||||
* Chroniques de l'Étrange est un jeu de rôle édité par Antre-Monde Éditions.
|
||||
* Ce système FoundryVTT est une implémentation indépendante et n'est pas
|
||||
* affilié à Antre-Monde Éditions,
|
||||
* mais a été réalisé avec l'autorisation d'Antre-Monde Éditions.
|
||||
*
|
||||
* @author LeRatierBretonnien
|
||||
* @copyright 2024–2026 LeRatierBretonnien
|
||||
* @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||
*/
|
||||
|
||||
import { advanceCombatantPosition } from "../../documents/combat.js"
|
||||
|
||||
const WHEEL_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/apps/cde-wheel-app.html"
|
||||
|
||||
/**
|
||||
* Action costs for common combat actions (in crans).
|
||||
* Listed in order from cheapest to most expensive.
|
||||
*/
|
||||
export const ACTION_COSTS = [
|
||||
{ key: "draw", label: "CDE.ActionCostDraw", cost: 1 },
|
||||
{ key: "changestyle", label: "CDE.ActionCostChangeStyle", cost: 1 },
|
||||
{ key: "defense", label: "CDE.ActionCostDefense", cost: 1 },
|
||||
{ key: "aim", label: "CDE.ActionCostAim", cost: 2 },
|
||||
{ key: "help", label: "CDE.ActionCostHelp", cost: 2 },
|
||||
{ key: "defally", label: "CDE.ActionCostDefendAlly", cost: 2 },
|
||||
{ key: "move", label: "CDE.ActionCostMove", cost: 2 },
|
||||
{ key: "attack", label: "CDE.ActionCostAttack", cost: 3 },
|
||||
{ key: "delay", label: "CDE.ActionCostDelay", cost: 6 },
|
||||
]
|
||||
|
||||
/**
|
||||
* Wu Xing color segments for the 24-cran wheel.
|
||||
* 6 colors × 4 crans = 24. Each colour covers crans [start..start+3].
|
||||
* An effect lasting 6 crans returns to the same colour.
|
||||
*/
|
||||
const WHEEL_SEGMENTS = [
|
||||
{ label: "Métal", color: "#b8c4cc", textColor: "#1a1a1a", crans: [1, 2, 3, 4] },
|
||||
{ label: "Eau", color: "#3a7bd5", textColor: "#ffffff", crans: [5, 6, 7, 8] },
|
||||
{ label: "Terre", color: "#c8a84b", textColor: "#1a1a1a", crans: [9, 10, 11, 12] },
|
||||
{ label: "Feu", color: "#d94f3d", textColor: "#ffffff", crans: [13, 14, 15, 16] },
|
||||
{ label: "Bois", color: "#4a9b5a", textColor: "#ffffff", crans: [17, 18, 19, 20] },
|
||||
{ label: "Repère", color: "#1a1a2e", textColor: "#aaaaaa", crans: [21, 22, 23, 24] },
|
||||
]
|
||||
|
||||
/** Return the segment data for a given cran (1–24). */
|
||||
function segmentForCran(cran) {
|
||||
return WHEEL_SEGMENTS.find(s => s.crans.includes(cran)) ?? WHEEL_SEGMENTS[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Roue d'Initiative — visual initiative wheel for CDE combat.
|
||||
*
|
||||
* Shows all combatants in the current combat scene on a 24-cran wheel.
|
||||
* Provides action-cost buttons to advance a combatant's position.
|
||||
*
|
||||
* Singleton: open via CDEWheelApp.open().
|
||||
*/
|
||||
export class CDEWheelApp extends foundry.applications.api.ApplicationV2 {
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: "cde-wheel-app",
|
||||
classes: ["cde-wheel-app"],
|
||||
tag: "div",
|
||||
window: {
|
||||
title: "CDE.InitiativeWheel",
|
||||
icon: "fas fa-circle-notch",
|
||||
resizable: true,
|
||||
},
|
||||
position: { width: 820, height: 620 },
|
||||
actions: {
|
||||
advanceCran: CDEWheelApp.#advanceCran,
|
||||
setSurprised: CDEWheelApp.#setSurprised,
|
||||
rollInitiative: CDEWheelApp.#rollInitiative,
|
||||
},
|
||||
}
|
||||
|
||||
/** @type {CDEWheelApp|null} */
|
||||
static #instance = null
|
||||
|
||||
/** Open (or bring to front) the singleton instance. */
|
||||
static open() {
|
||||
if (!CDEWheelApp.#instance || CDEWheelApp.#instance.rendered === false) {
|
||||
CDEWheelApp.#instance = new CDEWheelApp()
|
||||
CDEWheelApp.#instance.render(true)
|
||||
} else {
|
||||
CDEWheelApp.#instance.bringToFront()
|
||||
}
|
||||
return CDEWheelApp.#instance
|
||||
}
|
||||
|
||||
/** Currently selected combatant id (for action panel). */
|
||||
#selectedId = null
|
||||
|
||||
async _prepareContext(options) {
|
||||
const combat = game.combat
|
||||
const combatants = combat ? [...combat.combatants.values()] : []
|
||||
const sorted = [...combatants].sort((a, b) => (b.initiative ?? 0) - (a.initiative ?? 0))
|
||||
const cranData = this.#buildCranData(combatants)
|
||||
const selected = this.#selectedId
|
||||
? combatants.find(c => c.id === this.#selectedId)
|
||||
: null
|
||||
const actionCosts = ACTION_COSTS.map(a => ({
|
||||
...a,
|
||||
label: game.i18n.localize(a.label),
|
||||
}))
|
||||
|
||||
return {
|
||||
hasCombat: !!combat,
|
||||
combatants: sorted.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
img: c.token?.texture?.src ?? c.actor?.img ?? "icons/svg/mystery-man.svg",
|
||||
initiative: c.initiative ?? "—",
|
||||
segment: segmentForCran(c.initiative ?? 1),
|
||||
isActive: combat?.current?.combatantId === c.id,
|
||||
isSelected: c.id === this.#selectedId,
|
||||
hasInitiative: c.initiative != null,
|
||||
})),
|
||||
cranData,
|
||||
selected,
|
||||
selectedName: selected?.name ?? null,
|
||||
actionCosts,
|
||||
}
|
||||
}
|
||||
|
||||
async _renderHTML(context, options) {
|
||||
return foundry.applications.handlebars.renderTemplate(WHEEL_TEMPLATE, context)
|
||||
}
|
||||
|
||||
_replaceHTML(result, content, options) {
|
||||
content.innerHTML = result
|
||||
this.#bindEvents(content)
|
||||
}
|
||||
|
||||
/** Build per-cran data for the SVG wheel. */
|
||||
#buildCranData(combatants) {
|
||||
const data = []
|
||||
for (let cran = 1; cran <= 24; cran++) {
|
||||
const segment = segmentForCran(cran)
|
||||
const fighters = combatants.filter(c => Math.round(c.initiative) === cran)
|
||||
data.push({ cran, segment, fighters })
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/** Bind click events for combatant selection. */
|
||||
#bindEvents(content) {
|
||||
content.querySelectorAll("[data-select-combatant]").forEach(el => {
|
||||
el.addEventListener("click", () => {
|
||||
this.#selectedId = el.dataset.selectCombatant
|
||||
this.render()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** Action: advance selected combatant by given cran cost. */
|
||||
static async #advanceCran(event, element) {
|
||||
const app = CDEWheelApp.#instance
|
||||
if (!app?.#selectedId) return
|
||||
const cost = parseInt(element.dataset.cost, 10)
|
||||
if (!cost || isNaN(cost)) return
|
||||
const combatant = game.combat?.combatants.get(app.#selectedId)
|
||||
if (!combatant) return
|
||||
await advanceCombatantPosition(combatant, cost)
|
||||
}
|
||||
|
||||
/** Action: set selected combatant to surprised (position 1 = reference). */
|
||||
static async #setSurprised(event, element) {
|
||||
const app = CDEWheelApp.#instance
|
||||
if (!app?.#selectedId) return
|
||||
const combatant = game.combat?.combatants.get(app.#selectedId)
|
||||
if (!combatant) return
|
||||
// Update combatant only — updateCombatant hook in system.js syncs actor
|
||||
await combatant.update({ initiative: 1 })
|
||||
}
|
||||
|
||||
/** Action: open the initiative dialog for the selected combatant. */
|
||||
static async #rollInitiative(event, element) {
|
||||
const app = CDEWheelApp.#instance
|
||||
if (!app?.#selectedId) return
|
||||
const combatant = game.combat?.combatants.get(app.#selectedId)
|
||||
if (!combatant) return
|
||||
await game.combat.rollInitiative([app.#selectedId])
|
||||
}
|
||||
|
||||
/** Re-render when combat state changes. */
|
||||
static registerHooks() {
|
||||
const refresh = () => {
|
||||
if (CDEWheelApp.#instance?.rendered) CDEWheelApp.#instance.render()
|
||||
}
|
||||
Hooks.on("updateCombat", refresh)
|
||||
Hooks.on("updateCombatant", refresh)
|
||||
Hooks.on("createCombatant", refresh)
|
||||
Hooks.on("deleteCombatant", refresh)
|
||||
Hooks.on("updateActor", (_actor, diff) => {
|
||||
if (foundry.utils.hasProperty(diff, "system.initiative")) refresh()
|
||||
})
|
||||
Hooks.on("deleteCombat", () => {
|
||||
if (CDEWheelApp.#instance?.rendered) CDEWheelApp.#instance.render()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -118,4 +118,29 @@ export function registerHandlebarsHelpers() {
|
||||
}
|
||||
return game.i18n.localize(keys[activation] ?? "CDE.Activation")
|
||||
})
|
||||
|
||||
/**
|
||||
* Compute the SVG x,y coordinates for a cran on the initiative wheel.
|
||||
* Cran 1–24 are arranged counter-clockwise from the bottom (reference at 6 o'clock).
|
||||
* angle = 90° + cran * 15° (counter-clockwise = positive in standard math, negative in SVG).
|
||||
* In SVG coords: x = cx + r*cos(a), y = cy - r*sin(a) [y-axis is flipped in SVG].
|
||||
*/
|
||||
Handlebars.registerHelper("cranPosition", function (cran, cx, cy, r) {
|
||||
const angleDeg = 90 + cran * 15 // counter-clockwise from bottom
|
||||
const angleRad = (angleDeg * Math.PI) / 180
|
||||
const x = Math.round(cx + r * Math.cos(angleRad))
|
||||
const y = Math.round(cy - r * Math.sin(angleRad))
|
||||
return { x, y }
|
||||
})
|
||||
|
||||
/** X offset for overlapping fighters on the same cran. Centres a 30px image on the cran cx. */
|
||||
Handlebars.registerHelper("fighterX", function (cx, index, total) {
|
||||
const offset = total > 1 ? (index - (total - 1) / 2) * 34 : 0
|
||||
return Math.round(cx - 15 + offset)
|
||||
})
|
||||
|
||||
/** Y offset for fighters — positions image just above the cran circle. */
|
||||
Handlebars.registerHelper("fighterY", function (cy, index, total) {
|
||||
return Math.round(cy - 50)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user