- Nouvel item Capacité de créature (creaturecapacity) : DataModel, fiche AppV2, chat card, types Base/Survie/Cauchemar/Apocalypse, liste sur l'onglet Informations de la créature avec drag&drop et post au chat. - Blessures : coches séquentielles respectant la gravité (onClickWound), malus de blessure -1D/-2D/-3D appliqué aux jets (Règles p.42), montée en gravité automatique si les cercles sont pleins. - Fiche Groupe : objectifs majeurs/mineurs éditables, liste de membres simplifiée, notes de voyage (roadNotes), totem qui remplit instinct/interdits, refonte visuelle des onglets. - Créature : Réaction lançable, seuils/cercles de blessures corrigés (Taille cumulée, Groupe sans seuil), libellés Taille/Meute. - Corrections : icône de carte de chat plafonnée à 64px, slash orphelin d'entrave à 0, notes de matériel du Groupe éditable (formInput), bonus de Réaction non double-compté. - Divers : suppression des captures de débogage, licence README.
215 lines
6.8 KiB
JavaScript
215 lines
6.8 KiB
JavaScript
import { VermineUtils } from "../roll.mjs"
|
|
import { VermineExchange } from "../exchange.mjs"
|
|
|
|
const { HandlebarsApplicationMixin } = foundry.applications.api
|
|
|
|
/**
|
|
* Dialogue de jet simplifié pour les PNJ.
|
|
*
|
|
* Un PNJ n'a ni caractéristiques ni compétences : toutes ses actions sont
|
|
* résolues par une valeur unique dérivée de son Profil (Menace / Expérience /
|
|
* Rôle), exposée dans system.computed. Ce dialogue fixe le pool selon le type
|
|
* de jet (Action, Spécialité, Attaque ou Réaction) et n'offre que la difficulté,
|
|
* le handicap et les bonus simples.
|
|
*
|
|
* Utilisé par :
|
|
* - la fiche PNJ (boutons de jet rapide Action / Spécialité / Réaction) ;
|
|
* - la défense des PNJ dans les échanges de coups (mode verrouillé).
|
|
*/
|
|
export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) {
|
|
|
|
#actor
|
|
|
|
get title() {
|
|
return game.i18n.localize("VERMINE.roll")
|
|
}
|
|
|
|
static DEFAULT_OPTIONS = {
|
|
classes: ["vermine-roll"],
|
|
tag: "form",
|
|
window: {
|
|
icon: "fas fa-dice-d10",
|
|
resizable: false
|
|
},
|
|
position: {
|
|
width: 520,
|
|
height: 600
|
|
},
|
|
actions: {
|
|
roll: NpcRollDialog.#onRoll,
|
|
cancel: NpcRollDialog.#onCancel
|
|
}
|
|
}
|
|
|
|
static PARTS = {
|
|
main: { template: "systems/vermine2047/templates/dialogs/npc-roll-dialog.hbs" }
|
|
}
|
|
|
|
static async create(data = {}) {
|
|
const actor = data.actor instanceof Actor ? data.actor : await game.actors.get(data.actorId)
|
|
if (!actor) {
|
|
ui.notifications.warn(game.i18n.localize("VERMINE.error_no_actor_selected"))
|
|
return null
|
|
}
|
|
return new NpcRollDialog({
|
|
actor,
|
|
rolltype: data.rolltype ?? "action",
|
|
locks: data.locks ?? null,
|
|
defense: data.defense ?? null
|
|
})
|
|
}
|
|
|
|
constructor(options = {}) {
|
|
super(options)
|
|
this.#actor = options.actor
|
|
this.rolltype = options.rolltype ?? "action"
|
|
this.locks = options.locks ?? null
|
|
this.defense = options.defense ?? null
|
|
}
|
|
|
|
// ── Getters ──────────────────────────────────────────────────────────
|
|
|
|
get computed() { return this.#actor.system?.computed || {} }
|
|
|
|
/** Pool de dés selon le type de jet. */
|
|
get pool() {
|
|
const c = this.computed
|
|
switch (this.rolltype) {
|
|
case "specialty": return c.specialties || 0
|
|
case "attack": return c.attack || 0
|
|
case "reaction": return c.reaction || 0
|
|
default: return c.action || 3
|
|
}
|
|
}
|
|
|
|
/** Réussites automatiques (bonus de Réaction du rôle). */
|
|
get freeSuccesses() {
|
|
return this.rolltype === "reaction" ? (this.computed.reactionBonus || 0) : 0
|
|
}
|
|
|
|
/** Relances : utilisables uniquement sur les attaques ou les Spécialités. */
|
|
get rerolls() {
|
|
if (this.rolltype !== "specialty" && this.rolltype !== "attack") return 0
|
|
return this.computed.rerolls || 0
|
|
}
|
|
|
|
get label() {
|
|
const keys = {
|
|
action: "ADVERSITY.action",
|
|
specialty: "ADVERSITY.specialties",
|
|
attack: "ADVERSITY.attack",
|
|
reaction: "ADVERSITY.reaction"
|
|
}
|
|
return game.i18n.localize(keys[this.rolltype] || "VERMINE.roll")
|
|
}
|
|
|
|
// ── Rendu ────────────────────────────────────────────────────────────
|
|
|
|
async _prepareContext() {
|
|
const difficultyOptions = []
|
|
for (let d = 3; d <= 10; d++) {
|
|
difficultyOptions.push({ difficulty: d, label: String(d) })
|
|
}
|
|
let lockedDifficulty = null
|
|
let defaultDifficulty = 7
|
|
if (this.locks?.difficulty !== undefined && this.locks?.difficulty !== null) {
|
|
lockedDifficulty = parseInt(this.locks.difficulty, 10)
|
|
defaultDifficulty = lockedDifficulty
|
|
}
|
|
return {
|
|
actor: this.#actor,
|
|
system: this.#actor.system,
|
|
config: CONFIG.VERMINE,
|
|
rollLabel: this.label,
|
|
rolltype: this.rolltype,
|
|
pool: this.pool,
|
|
freeSuccesses: this.freeSuccesses,
|
|
rerolls: this.rerolls,
|
|
isDefense: Boolean(this.defense),
|
|
lockedDifficulty,
|
|
difficultyOptions,
|
|
defaultDifficulty,
|
|
speakerId: this.#actor.id,
|
|
availableItems: this.#actor.items.filter(i => i.type === "item")
|
|
}
|
|
}
|
|
|
|
async _onRender(context, options) {
|
|
this.element.dataset.actorId = this.#actor.id
|
|
for (const inp of this.element.querySelectorAll("[data-roll]")) {
|
|
inp.addEventListener("change", this.#onInputChange.bind(this))
|
|
}
|
|
this.element.querySelector("#handicap")?.addEventListener("change", () => this.#updateUI())
|
|
this.#updateUI()
|
|
}
|
|
|
|
get #el() { return this.element }
|
|
|
|
#getHandicap() {
|
|
const sel = this.#el.querySelector("#handicap")
|
|
return Math.max(0, (parseInt(sel?.value, 10) || 1) - 1)
|
|
}
|
|
|
|
#getDifficulty() {
|
|
const sel = this.#el.querySelector("#difficulty")
|
|
return parseInt(sel?.value, 10) || 7
|
|
}
|
|
|
|
#getHelped() { return this.#el.querySelector("#helped")?.checked ? 1 : 0 }
|
|
|
|
#getGroup() { return parseInt(this.#el.querySelector("#group")?.value, 10) || 0 }
|
|
|
|
getPoolBreakdown() {
|
|
const help = this.#getHelped()
|
|
const group = this.#getGroup()
|
|
const malus = VermineExchange.woundMalus(this.#actor)
|
|
return { pool: this.pool, help, group, malus, total: Math.max(0, this.pool + help + group - malus) }
|
|
}
|
|
|
|
#updateUI() {
|
|
const b = this.getPoolBreakdown()
|
|
const totalEl = this.#el.querySelector("#dice-pool-total")
|
|
if (totalEl) totalEl.textContent = `${b.total}D`
|
|
const bonusEl = this.#el.querySelector("#total-bonus")
|
|
if (bonusEl) bonusEl.textContent = b.help + b.group - b.malus
|
|
const freeEl = this.#el.querySelector("#free-successes")
|
|
if (freeEl) freeEl.textContent = String(this.freeSuccesses)
|
|
}
|
|
|
|
#onInputChange() {
|
|
this.#updateUI()
|
|
}
|
|
|
|
// ── Actions ──────────────────────────────────────────────────────────
|
|
|
|
static async #onCancel(event, target) {
|
|
this.close()
|
|
}
|
|
|
|
static async #onRoll(event, target) {
|
|
const label = this.defense
|
|
? (this.defense.type === "parade"
|
|
? game.i18n.localize("VERMINE.defense_parade")
|
|
: game.i18n.localize("VERMINE.defense_esquive"))
|
|
: this.label
|
|
|
|
const rollParams = {
|
|
actor: this.#actor,
|
|
NoD: this.getPoolBreakdown().total,
|
|
Reroll: this.rerolls,
|
|
difficulty: this.#getDifficulty(),
|
|
handicap: this.#getHandicap(),
|
|
rollLabel: label,
|
|
bonusSuccesses: this.freeSuccesses,
|
|
poolBreakdown: this.getPoolBreakdown(),
|
|
messageFlags: this.defense ? { "vermine-defense": this.defense } : null
|
|
}
|
|
|
|
// Fermeture immédiate du dialogue : le jet (et l'animation 3D) continue
|
|
// en arrière-plan sans bloquer la fermeture.
|
|
this.close()
|
|
|
|
await VermineUtils.roll(rollParams)
|
|
}
|
|
}
|