feat: fiche PNJ (profil + niveaux) et passage du véhicule en acteur
Release Creation / build (release) Failing after 1m17s
Release Creation / build (release) Failing after 1m17s
This commit is contained in:
@@ -99,10 +99,10 @@ VERMINE.totemDomains = {
|
||||
* NPC Threat Levels configuration
|
||||
*/
|
||||
VERMINE.npcThreatLevels = {
|
||||
1: { "label": "THREAT_LEVELS.minor", "attack": 3, "vigor": 1, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
2: { "label": "THREAT_LEVELS.serious", "attack": 4, "vigor": 2, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "THREAT_LEVELS.major", "attack": 5, "vigor": 3, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "THREAT_LEVELS.deadly", "attack": 6, "vigor": 4, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
1: { "label": "THREAT_LEVELS.minor", "attack": 3, "vigor": 1, "minorThreshold": 1, "majorThreshold": 4, "deadlyThreshold": 6, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
2: { "label": "THREAT_LEVELS.serious", "attack": 4, "vigor": 2, "minorThreshold": 2, "majorThreshold": 5, "deadlyThreshold": 8, "minorWound": 2, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "THREAT_LEVELS.major", "attack": 5, "vigor": 3, "minorThreshold": 2, "majorThreshold": 5, "deadlyThreshold": 9, "minorWound": 2, "majorWound": 2, "deadlyWound": 1 },
|
||||
4: { "label": "THREAT_LEVELS.deadly", "attack": 6, "vigor": 4, "minorThreshold": 3, "majorThreshold": 6, "deadlyThreshold": 9, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { VermineUtils } from "../roll.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()
|
||||
return { pool: this.pool, help, group, total: this.pool + help + group }
|
||||
}
|
||||
|
||||
#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
|
||||
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
|
||||
|
||||
await VermineUtils.roll({
|
||||
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
|
||||
})
|
||||
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
@@ -359,6 +359,28 @@ export class VermineExchange {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_creature_no_defense'))
|
||||
return
|
||||
}
|
||||
const defense = {
|
||||
exchangeId: exchange.id,
|
||||
type,
|
||||
actorId: actor.id,
|
||||
actorUuid: actor.uuid,
|
||||
actorName: actor.name,
|
||||
difficulty: exchange.difficulty,
|
||||
parryLevel: type === 'parade' ? this.#parryLevel(actor) : null
|
||||
}
|
||||
// PNJ : pas de caractéristiques/compétences — la défense utilise la valeur
|
||||
// d'Action (Expérience), difficulté verrouillée.
|
||||
if (actor.type === 'npc') {
|
||||
const { default: NpcRollDialog } = await import('./dialogs/npcRollDialog.mjs')
|
||||
const dialog = await NpcRollDialog.create({
|
||||
actor,
|
||||
rolltype: 'action',
|
||||
locks: { difficulty: exchange.difficulty },
|
||||
defense
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
const { default: RollDialog } = await import('./dialogs/rollDialog.mjs')
|
||||
const locks = this.#defenseLocks(actor, exchange, type)
|
||||
const dialog = await RollDialog.create({
|
||||
@@ -366,15 +388,7 @@ export class VermineExchange {
|
||||
rolltype: 'skill',
|
||||
label: locks.skill,
|
||||
locks,
|
||||
defense: {
|
||||
exchangeId: exchange.id,
|
||||
type,
|
||||
actorId: actor.id,
|
||||
actorUuid: actor.uuid,
|
||||
actorName: actor.name,
|
||||
difficulty: exchange.difficulty,
|
||||
parryLevel: type === 'parade' ? this.#parryLevel(actor) : null
|
||||
}
|
||||
defense
|
||||
})
|
||||
if (dialog) dialog.render(true)
|
||||
}
|
||||
|
||||
+11
-4
@@ -30,6 +30,7 @@ export class VermineUtils {
|
||||
skillLevel = null,
|
||||
hasSpecialty = false,
|
||||
handicap = 0,
|
||||
bonusSuccesses = 0,
|
||||
poolBreakdown = null,
|
||||
specialtyName = null,
|
||||
weapon = null,
|
||||
@@ -147,9 +148,9 @@ export class VermineUtils {
|
||||
totemBonuses: { ...totemBonus },
|
||||
baseNoD: NoD,
|
||||
rerolls: Reroll,
|
||||
selfControl: self_control
|
||||
selfControl: self_control,
|
||||
bonusSuccesses: bonusSuccesses
|
||||
};
|
||||
|
||||
// Evaluate the roll
|
||||
await roll.evaluate();
|
||||
|
||||
@@ -171,6 +172,7 @@ export class VermineUtils {
|
||||
skillLevel,
|
||||
hasSpecialty,
|
||||
handicap,
|
||||
bonusSuccesses,
|
||||
poolBreakdown,
|
||||
specialtyName,
|
||||
weapon,
|
||||
@@ -321,6 +323,9 @@ export class VermineUtils {
|
||||
const isTotem = die.classList.contains('human') || die.classList.contains('adapted');
|
||||
total += isTotem ? 2 : 1;
|
||||
});
|
||||
// Réussites automatiques (ex. bonus de Réaction) : ajoutées au total.
|
||||
const freeSuccesses = parseInt(rollMessage.dataset?.freeSuccesses ?? '0', 10) || 0;
|
||||
total += freeSuccesses;
|
||||
totalEl.innerText = total;
|
||||
}
|
||||
|
||||
@@ -488,11 +493,13 @@ export class VermineUtils {
|
||||
}
|
||||
// Verdict
|
||||
const required = 1 + (param.handicap ?? 0);
|
||||
const total = roll._total ?? 0;
|
||||
const bonusSuccesses = param.bonusSuccesses ?? 0;
|
||||
const total = (roll._total ?? 0) + bonusSuccesses;
|
||||
const verdict = {
|
||||
success: total >= required,
|
||||
total,
|
||||
required
|
||||
required,
|
||||
bonusSuccesses
|
||||
};
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param, diceStats, verdict });
|
||||
|
||||
Reference in New Issue
Block a user