feat(traits): tâche 0 + phases 2-4 — automatisation complète des Traits
Tâche 0 : - item.mjs : champ equipped sur items génériques + toggle fiche - nouveau module/system/traits.mjs (helpers partagés : equippedItems, lourdMalus, mobilityHandicap, feticheItem…) Phase 2 — Combat : - Lourd : malus cumulés auto (Vigueur < n) dans le dialogue + pool - Rapide : handicap (n) pré-rempli dans la défense (character + PNJ) + note carte - Rafale : case tir en rafale (+2 difficulté), +n réussites si réussi (bonusOnSuccess) - Portée : portée effective n × Vigueur affichée - Maniable/Mobilité : note de rappel (dialogue + carte) - Incapacitant : bouton « Jet de Santé (n) » sur la carte d'échange - getLabel() : libellés caractéristiques/compétences localisés Phase 3 — Jets d'action : - Ponctuel : annule jusqu'à n Handicaps, consommé au jet (Traits supprimé à 0) - Pratique : outil sélectionné +2D au lieu de +1D - Intimidant : case « Brandit » → -1D Psychologie ou Relance 1D - Malus : champ « Malus temporaires » manuel déduit du pool - Durée : note « n tours » sur la carte de jet d'arme - roll() : transmission effectiveRange/mobilityHandicap/ponctuelUsed/intimidant (corrige l'affichage Portée/Mobilité) Phase 4 — Acteur : - Fétiche : +1D Effort ET Sang-Froid (max dérivé, config corrigé), alerte 2+ fétiches, action « Perdre le fétiche » (-2D, max -1D persistant feticheLoss) - Zone : note + bouton « Appliquer aux cibles de la zone » (#onApplyZone) CSS : styles LESS pour les nouvelles classes (fetiche-block, exchange-reminder/zone, traits-reminder, tool-bonus/ponctuel, duree-note) i18n : 29 clés fr/en TRAITS_PLAN.md : plan complet cochée
This commit is contained in:
@@ -369,6 +369,12 @@ export class VermineExchange {
|
||||
html.querySelectorAll('[data-action="exchange-apply"]').forEach(btn => {
|
||||
btn.addEventListener('click', ev => this.#onApply(ev, message))
|
||||
})
|
||||
html.querySelectorAll('.exchange-incapacitant').forEach(btn => {
|
||||
btn.addEventListener('click', ev => this.#onIncapacitant(ev, message))
|
||||
})
|
||||
html.querySelectorAll('.exchange-zone').forEach(btn => {
|
||||
btn.addEventListener('click', ev => this.#onApplyZone(ev, message))
|
||||
})
|
||||
}
|
||||
|
||||
static async #onDefense(ev, attackMessage) {
|
||||
@@ -407,7 +413,7 @@ export class VermineExchange {
|
||||
const dialog = await NpcRollDialog.create({
|
||||
actor,
|
||||
rolltype: 'action',
|
||||
locks: { difficulty: exchange.difficulty },
|
||||
locks: { difficulty: exchange.difficulty, handicap: exchange.rapideHandicap ?? null },
|
||||
defense
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
@@ -426,14 +432,16 @@ export class VermineExchange {
|
||||
}
|
||||
|
||||
static #defenseLocks(actor, exchange, type) {
|
||||
if (type === 'parade') {
|
||||
return { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
|
||||
}
|
||||
return {
|
||||
ability: 'reflexes',
|
||||
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
|
||||
difficulty: exchange.difficulty
|
||||
}
|
||||
const locks = type === 'parade'
|
||||
? { ability: 'vigor', skill: this.#parrySkill(actor), difficulty: exchange.difficulty }
|
||||
: {
|
||||
ability: 'reflexes',
|
||||
skill: exchange.mode === 'ranged' ? 'alertness' : 'close',
|
||||
difficulty: exchange.difficulty
|
||||
}
|
||||
// Trait Rapide (n) : le défenseur subit (n) Handicaps sur sa défense.
|
||||
if (exchange.rapideHandicap) locks.handicap = exchange.rapideHandicap
|
||||
return locks
|
||||
}
|
||||
|
||||
static #parrySkill(actor) {
|
||||
@@ -479,6 +487,75 @@ export class VermineExchange {
|
||||
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||
}
|
||||
|
||||
static async #onIncapacitant(ev) {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
const btn = ev.currentTarget
|
||||
const actor = await this.#resolveDefender({ id: btn.dataset.targetId, uuid: btn.dataset.targetUuid })
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_actor_selected'))
|
||||
return
|
||||
}
|
||||
if (!game.user.isGM && !actor.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||
return
|
||||
}
|
||||
const value = parseInt(btn.dataset.value, 10) || 7
|
||||
if (actor.type === 'character') {
|
||||
const { default: RollDialog } = await import('./dialogs/rollDialog.mjs')
|
||||
const dialog = await RollDialog.create({
|
||||
actor,
|
||||
rolltype: 'ability',
|
||||
label: 'health',
|
||||
locks: { ability: 'health', difficulty: value }
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
const { default: NpcRollDialog } = await import('./dialogs/npcRollDialog.mjs')
|
||||
const dialog = await NpcRollDialog.create({
|
||||
actor,
|
||||
rolltype: 'action',
|
||||
locks: { difficulty: value }
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trait Zone (n) : applique la blessure résolue à toutes les cibles de
|
||||
* l'attaque présentes dans la zone (confort pour le MJ).
|
||||
*/
|
||||
static async #onApplyZone(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
const btn = ev.currentTarget
|
||||
const exchange = attackMessage.getFlag('world', FLAG_EXCHANGE)
|
||||
if (!exchange) return
|
||||
if (!game.user.isGM && !attackMessage.isOwner) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||
return
|
||||
}
|
||||
if (!exchange.targets?.length) {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_target'))
|
||||
return
|
||||
}
|
||||
let applied = 0
|
||||
for (const target of exchange.targets) {
|
||||
const r = await this.resolve(attackMessage, target?.id ?? null)
|
||||
if (!r?.hit || !r.woundCategory) continue
|
||||
const defender = await this.#resolveDefender({ id: r.defenderId, uuid: r.defenderUuid })
|
||||
if (!defender || !this.canApplyWound(defender)) continue
|
||||
const result = await this.applyWound(defender, r.woundCategory)
|
||||
if (result && result !== false) applied += 1
|
||||
}
|
||||
btn.disabled = true
|
||||
btn.innerHTML = `<i class="fas fa-check"></i> ${game.i18n.format('VERMINE.zone_applied', { count: applied })}`
|
||||
if (game.user.isGM || attackMessage.isOwner) {
|
||||
const msgEl = btn.closest('.vermine-roll-message')
|
||||
if (msgEl) await attackMessage.update({ content: msgEl.outerHTML })
|
||||
}
|
||||
}
|
||||
|
||||
static async #onApply(ev, attackMessage) {
|
||||
ev.preventDefault()
|
||||
const btn = ev.currentTarget
|
||||
|
||||
Reference in New Issue
Block a user