feat: actions complexes avec suivi de résistance
- Nouvelle fenêtre Actions complexes (GM-only) - Création d'actions avec nom, symbole, résistance (nb de succès requis) - Barre de progression + compteurs succès/échecs - Boutons Réussite/Échec pour suivre l'avancement - Notification et message chat quand l'objectif est atteint - Persistance dans un setting world-scope - Bouton dans les contrôles de scène (icône tasks) - Accessible via game.system.api.openActionComplexe() Conforme aux règles p.157 : actions nécessitant 2-5 victoires
This commit is contained in:
+18
-2
@@ -29,6 +29,7 @@ Hooks.once("init", function () {
|
|||||||
models,
|
models,
|
||||||
documents,
|
documents,
|
||||||
openTarotDeckManager: () => applications.HamalronTarotDeckManager.openManager(),
|
openTarotDeckManager: () => applications.HamalronTarotDeckManager.openManager(),
|
||||||
|
openActionComplexe: () => applications.HamalronActionComplexe.open(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global reference needed by initiative override
|
// Global reference needed by initiative override
|
||||||
@@ -124,6 +125,15 @@ Hooks.once("init", function () {
|
|||||||
default: { deck: [], discard: [] },
|
default: { deck: [], discard: [] },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
game.settings.register("fvtt-hamalron", "actionComplexeState", {
|
||||||
|
name: "Action Complexe State",
|
||||||
|
hint: "Stores ongoing complex actions",
|
||||||
|
scope: "world",
|
||||||
|
config: false,
|
||||||
|
type: Object,
|
||||||
|
default: { actions: [] },
|
||||||
|
})
|
||||||
|
|
||||||
// Activate socket handler
|
// Activate socket handler
|
||||||
game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent)
|
game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent)
|
||||||
|
|
||||||
@@ -156,10 +166,9 @@ Hooks.once("ready", function () {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add button to scene controls for Tarot Deck Manager
|
// Add button to scene controls for Tarot Deck Manager and Actions Complexes
|
||||||
Hooks.on("getSceneControlButtons", (controls) => {
|
Hooks.on("getSceneControlButtons", (controls) => {
|
||||||
if (game.user.isGM) {
|
if (game.user.isGM) {
|
||||||
// Handle both old and new Foundry API
|
|
||||||
const controlsArray = Array.isArray(controls) ? controls : controls.controls || [];
|
const controlsArray = Array.isArray(controls) ? controls : controls.controls || [];
|
||||||
const tokenControls = controlsArray.find(c => c.name === "token")
|
const tokenControls = controlsArray.find(c => c.name === "token")
|
||||||
if (tokenControls) {
|
if (tokenControls) {
|
||||||
@@ -170,6 +179,13 @@ Hooks.on("getSceneControlButtons", (controls) => {
|
|||||||
button: true,
|
button: true,
|
||||||
onClick: () => applications.HamalronTarotDeckManager.openManager(),
|
onClick: () => applications.HamalronTarotDeckManager.openManager(),
|
||||||
})
|
})
|
||||||
|
tokenControls.tools.push({
|
||||||
|
name: "action-complexe",
|
||||||
|
title: "HAMALRON.ActionComplexe.Title",
|
||||||
|
icon: "fas fa-tasks",
|
||||||
|
button: true,
|
||||||
|
onClick: () => applications.HamalronActionComplexe.open(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -810,6 +810,24 @@
|
|||||||
"label": "Seuil"
|
"label": "Seuil"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ActionComplexe": {
|
||||||
|
"Title": "Actions complexes",
|
||||||
|
"CreateAction": "Nouvelle action",
|
||||||
|
"CreateTitle": "Créer une action complexe",
|
||||||
|
"Create": "Créer",
|
||||||
|
"Name": "Nom",
|
||||||
|
"Symbole": "Symbole",
|
||||||
|
"Resistance": "Résistance",
|
||||||
|
"Notes": "Notes",
|
||||||
|
"Success": "Réussite",
|
||||||
|
"Failure": "Échec",
|
||||||
|
"Successes": "succès",
|
||||||
|
"NoActions": "Aucune action complexe en cours",
|
||||||
|
"Completed": "{name} — Action accomplie !",
|
||||||
|
"Progress": "{name} — {current}/{total} succès",
|
||||||
|
"DeleteTitle": "Supprimer l'action",
|
||||||
|
"DeleteWarning": "Voulez-vous vraiment supprimer cette action complexe ? Les données seront perdues."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,3 +14,4 @@ export { default as HamalronItemSheet } from "./sheets/base-item-sheet.mjs"
|
|||||||
export { default as HamalronActorSheet } from "./sheets/base-actor-sheet.mjs"
|
export { default as HamalronActorSheet } from "./sheets/base-actor-sheet.mjs"
|
||||||
export { default as HamalronTarotDeckManager } from "./tarot-deck-manager.mjs"
|
export { default as HamalronTarotDeckManager } from "./tarot-deck-manager.mjs"
|
||||||
export { default as HamalronSortilegeCasting } from "./sortilege-casting.mjs"
|
export { default as HamalronSortilegeCasting } from "./sortilege-casting.mjs"
|
||||||
|
export { default as HamalronActionComplexe } from "./action-complexe.mjs"
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||||
|
|
||||||
|
export default class HamalronActionComplexe extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
||||||
|
constructor(options = {}) {
|
||||||
|
super(options)
|
||||||
|
HamalronActionComplexe.#instance = this
|
||||||
|
this.actions = []
|
||||||
|
this.#loadState()
|
||||||
|
}
|
||||||
|
|
||||||
|
static #instance
|
||||||
|
|
||||||
|
static get instance() {
|
||||||
|
if (!HamalronActionComplexe.#instance) {
|
||||||
|
HamalronActionComplexe.#instance = new HamalronActionComplexe()
|
||||||
|
}
|
||||||
|
return HamalronActionComplexe.#instance
|
||||||
|
}
|
||||||
|
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
id: "action-complexe",
|
||||||
|
classes: ["fvtt-hamalron", "action-complexe"],
|
||||||
|
tag: "div",
|
||||||
|
window: {
|
||||||
|
title: "HAMALRON.ActionComplexe.Title",
|
||||||
|
icon: "fas fa-tasks",
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 500,
|
||||||
|
height: "auto",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
createAction: HamalronActionComplexe.#onCreateAction,
|
||||||
|
recordSuccess: HamalronActionComplexe.#onRecordSuccess,
|
||||||
|
recordFailure: HamalronActionComplexe.#onRecordFailure,
|
||||||
|
deleteAction: HamalronActionComplexe.#onDeleteAction,
|
||||||
|
resetAction: HamalronActionComplexe.#onResetAction,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
main: {
|
||||||
|
template: "systems/fvtt-hamalron/templates/dialog-action-complexe.hbs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareContext(options) {
|
||||||
|
const context = await super._prepareContext(options)
|
||||||
|
context.actions = this.actions.map(a => ({
|
||||||
|
...a,
|
||||||
|
pct: a.resistance > 0 ? Math.round((a.currentSuccesses / a.resistance) * 100) : 0,
|
||||||
|
isComplete: a.currentSuccesses >= a.resistance,
|
||||||
|
symboleIcon: HamalronActionComplexe.#symboleIcon(a.symbole),
|
||||||
|
}))
|
||||||
|
context.hasActions = context.actions.length > 0
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
static #symboleIcon(symbole) {
|
||||||
|
const icons = { coupe: "fa-heart", epee: "fa-crossed-swords", denier: "fa-coins", baton: "fa-staff" }
|
||||||
|
return icons[symbole] || "fa-circle"
|
||||||
|
}
|
||||||
|
|
||||||
|
#loadState() {
|
||||||
|
const state = game.settings.get("fvtt-hamalron", "actionComplexeState")
|
||||||
|
if (state?.actions?.length) {
|
||||||
|
this.actions = state.actions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #saveState() {
|
||||||
|
await game.settings.set("fvtt-hamalron", "actionComplexeState", { actions: this.actions })
|
||||||
|
}
|
||||||
|
|
||||||
|
static #onCreateAction(event, target) {
|
||||||
|
const self = this
|
||||||
|
const content = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${game.i18n.localize("HAMALRON.ActionComplexe.Name")}</label>
|
||||||
|
<input type="text" id="ac-name" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${game.i18n.localize("HAMALRON.ActionComplexe.Symbole")}</label>
|
||||||
|
<select id="ac-symbole">
|
||||||
|
<option value="coupe">${game.i18n.localize("HAMALRON.Tarot.FIELDS.symbole.coupe")}</option>
|
||||||
|
<option value="epee">${game.i18n.localize("HAMALRON.Tarot.FIELDS.symbole.epee")}</option>
|
||||||
|
<option value="denier">${game.i18n.localize("HAMALRON.Tarot.FIELDS.symbole.denier")}</option>
|
||||||
|
<option value="baton">${game.i18n.localize("HAMALRON.Tarot.FIELDS.symbole.baton")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${game.i18n.localize("HAMALRON.ActionComplexe.Resistance")}</label>
|
||||||
|
<input type="number" id="ac-resistance" value="3" min="1" max="10" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${game.i18n.localize("HAMALRON.ActionComplexe.Notes")}</label>
|
||||||
|
<textarea id="ac-notes" rows="2"></textarea>
|
||||||
|
</div>`
|
||||||
|
foundry.applications.api.DialogV2.wait({
|
||||||
|
window: { title: game.i18n.localize("HAMALRON.ActionComplexe.CreateTitle") },
|
||||||
|
content,
|
||||||
|
rejectClose: false,
|
||||||
|
buttons: [{
|
||||||
|
action: "create",
|
||||||
|
label: game.i18n.localize("HAMALRON.ActionComplexe.Create"),
|
||||||
|
default: true,
|
||||||
|
callback: (event, button) => {
|
||||||
|
const name = button.form.elements["ac-name"].value.trim()
|
||||||
|
if (!name) return null
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
symbole: button.form.elements["ac-symbole"].value,
|
||||||
|
resistance: Number(button.form.elements["ac-resistance"].value) || 3,
|
||||||
|
notes: button.form.elements["ac-notes"].value.trim(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
action: "cancel",
|
||||||
|
label: game.i18n.localize("Cancel"),
|
||||||
|
}],
|
||||||
|
}).then(result => {
|
||||||
|
if (!result) return
|
||||||
|
self.actions.push({
|
||||||
|
id: foundry.utils.randomID(8),
|
||||||
|
name: result.name,
|
||||||
|
symbole: result.symbole,
|
||||||
|
resistance: result.resistance,
|
||||||
|
currentSuccesses: 0,
|
||||||
|
failures: 0,
|
||||||
|
notes: result.notes,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
})
|
||||||
|
self.#saveState()
|
||||||
|
self.render()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onRecordSuccess(event, target) {
|
||||||
|
const id = target.dataset.actionId
|
||||||
|
const action = this.actions.find(a => a.id === id)
|
||||||
|
if (!action || action.isComplete) return
|
||||||
|
|
||||||
|
action.currentSuccesses++
|
||||||
|
await this.#saveState()
|
||||||
|
|
||||||
|
if (action.currentSuccesses >= action.resistance) {
|
||||||
|
ui.notifications.info(
|
||||||
|
game.i18n.format("HAMALRON.ActionComplexe.Completed", { name: action.name })
|
||||||
|
)
|
||||||
|
ChatMessage.create({
|
||||||
|
content: `<div class="fvtt-hamalron"><h3><i class="fas fa-trophy"></i> ${game.i18n.format("HAMALRON.ActionComplexe.Completed", { name: action.name })}</h3><p>${action.currentSuccesses}/${action.resistance} ${game.i18n.localize("HAMALRON.ActionComplexe.Successes")}</p></div>`,
|
||||||
|
style: CONST.CHAT_MESSAGE_STYLES.OTHER,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onRecordFailure(event, target) {
|
||||||
|
const id = target.dataset.actionId
|
||||||
|
const action = this.actions.find(a => a.id === id)
|
||||||
|
if (!action) return
|
||||||
|
action.failures++
|
||||||
|
await this.#saveState()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async #onDeleteAction(event, target) {
|
||||||
|
const id = target.dataset.actionId
|
||||||
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||||||
|
window: { title: game.i18n.localize("HAMALRON.ActionComplexe.DeleteTitle") },
|
||||||
|
content: `<p>${game.i18n.localize("HAMALRON.ActionComplexe.DeleteWarning")}</p>`,
|
||||||
|
rejectClose: false,
|
||||||
|
modal: true,
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
this.actions = this.actions.filter(a => a.id !== id)
|
||||||
|
await this.#saveState()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
static #onResetAction(event, target) {
|
||||||
|
const id = target.dataset.actionId
|
||||||
|
const action = this.actions.find(a => a.id === id)
|
||||||
|
if (!action) return
|
||||||
|
action.currentSuccesses = 0
|
||||||
|
action.failures = 0
|
||||||
|
this.#saveState()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
static open() {
|
||||||
|
if (!game.user.isGM) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("HAMALRON.Notifications.GMOnly"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
HamalronActionComplexe.instance.render(true, { focus: true })
|
||||||
|
return HamalronActionComplexe.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<div class="action-complexe-container">
|
||||||
|
<div class="action-complexe-header">
|
||||||
|
<button type="button" data-action="createAction">
|
||||||
|
<i class="fas fa-plus"></i> {{localize "HAMALRON.ActionComplexe.CreateAction"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if hasActions}}
|
||||||
|
<div class="action-complexe-list">
|
||||||
|
{{#each actions}}
|
||||||
|
<div class="action-card {{#if isComplete}}action-complete{{/if}}">
|
||||||
|
<div class="action-header">
|
||||||
|
<span class="action-name">{{name}}</span>
|
||||||
|
<span class="action-symbole"><i class="fas {{symboleIcon}}"></i></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-progress">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: {{pct}}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-text">{{currentSuccesses}} / {{resistance}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-stats">
|
||||||
|
<span class="stat-success"><i class="fas fa-check-circle"></i> {{currentSuccesses}}</span>
|
||||||
|
<span class="stat-failure"><i class="fas fa-times-circle"></i> {{failures}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if notes}}
|
||||||
|
<div class="action-notes">{{notes}}</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="action-controls">
|
||||||
|
{{#unless isComplete}}
|
||||||
|
<button type="button" data-action="recordSuccess" data-action-id="{{id}}" class="btn-success">
|
||||||
|
<i class="fas fa-check"></i> {{localize "HAMALRON.ActionComplexe.Success"}}
|
||||||
|
</button>
|
||||||
|
<button type="button" data-action="recordFailure" data-action-id="{{id}}" class="btn-failure">
|
||||||
|
<i class="fas fa-times"></i> {{localize "HAMALRON.ActionComplexe.Failure"}}
|
||||||
|
</button>
|
||||||
|
{{/unless}}
|
||||||
|
<button type="button" data-action="resetAction" data-action-id="{{id}}" class="btn-reset">
|
||||||
|
<i class="fas fa-undo"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" data-action="deleteAction" data-action-id="{{id}}" class="btn-delete">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="action-empty">
|
||||||
|
<i class="fas fa-tasks fa-3x"></i>
|
||||||
|
<p>{{localize "HAMALRON.ActionComplexe.NoActions"}}</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user