feat: capacités de créature, blessures séquentielles et refonte fiche Groupe
- 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.
This commit is contained in:
@@ -11,6 +11,7 @@ export {
|
||||
VermineWeaponSheetV2,
|
||||
VermineDefenseSheetV2,
|
||||
VermineAbilitySheetV2,
|
||||
VermineCreatureCapacitySheetV2,
|
||||
VermineSpecialtySheetV2,
|
||||
VermineBackgroundSheetV2,
|
||||
VermineTraumaSheetV2,
|
||||
|
||||
@@ -38,6 +38,7 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
||||
roll: VermineBaseActorSheet.#onRollItem,
|
||||
attack: VermineBaseActorSheet.#onAttack,
|
||||
clickRadio: VermineBaseActorSheet.#onClickRadioHexa,
|
||||
clickWound: VermineBaseActorSheet.#onClickWound,
|
||||
effectControl: VermineBaseActorSheet.#onEffectControl,
|
||||
chooseTotem: VermineBaseActorSheet.#onChooseTotem
|
||||
}
|
||||
@@ -264,9 +265,10 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
||||
if (!id) return
|
||||
const item = this.document.items.get(id)
|
||||
if (!item) return
|
||||
// Armes : PNJ → dialogue de combat (valeur d'Attaque) ; personnage →
|
||||
// dialogue de compétence. Les PNJ n'ont ni caractéristiques ni compétences.
|
||||
if (item.type === "weapon" && this.document.type === 'npc') {
|
||||
// Armes : PNJ et créatures → dialogue de combat (valeur d'Attaque) ;
|
||||
// personnage → dialogue de compétence. PNJ/créatures n'ont ni
|
||||
// caractéristiques ni compétences.
|
||||
if (item.type === "weapon" && (this.document.type === 'npc' || this.document.type === 'creature')) {
|
||||
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||
const dialog = await CombatDialog.create({ actorId: this.document.id, weapon: item })
|
||||
if (dialog) dialog.render(true)
|
||||
@@ -297,6 +299,8 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
||||
}
|
||||
|
||||
static async #onToggleEquip(event, target) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const id = target.closest("[data-item-id]")?.dataset?.itemId
|
||||
if (!id) return
|
||||
const item = this.document.items.get(id)
|
||||
@@ -322,6 +326,36 @@ export default class VermineBaseActorSheet extends HandlebarsApplicationMixin(fo
|
||||
this.document.update(update)
|
||||
}
|
||||
|
||||
/**
|
||||
* Coche/décoche les cases de Blessure en respectant l'ordre de gravité
|
||||
* (règles p.39) : on ne peut cocher que la case suivante dans l'ordre
|
||||
* (une blessure s'applique toujours au Seuil égal ou inférieur le plus
|
||||
* élevé), et on ne peut pas en sauter. Décocher une case retire toutes
|
||||
* celles situées après elle.
|
||||
*/
|
||||
static #onClickWound(event, target) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const input = target
|
||||
const box = parseInt(input.value, 10) || 0 // numéro de case (1-based)
|
||||
let current = this.document
|
||||
const propTree = input.name.split(".")
|
||||
for (const prop of propTree) {
|
||||
current = current[prop]
|
||||
}
|
||||
const currentValue = parseInt(current, 10) || 0
|
||||
let next = currentValue
|
||||
if (box === currentValue + 1) {
|
||||
// Cocher la case suivante dans l'ordre.
|
||||
next = box
|
||||
} else if (box <= currentValue) {
|
||||
// Décocher : retire la case cliquée et toutes celles après.
|
||||
next = box - 1
|
||||
}
|
||||
// Si box > currentValue + 1 : on ne permet pas de sauter des cases.
|
||||
this.document.update({ [input.name]: next })
|
||||
}
|
||||
|
||||
static #onEffectControl(event, target) {
|
||||
onManageActiveEffect(event, this.document)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export default class VermineCharacterSheetV2 extends VermineBaseActorSheet {
|
||||
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["character"],
|
||||
position: { width: 860, height: 720 },
|
||||
position: { width: 920, height: 720 },
|
||||
window: { contentClasses: ["character-content"] },
|
||||
actions: {
|
||||
addSpecialty: VermineCharacterSheetV2.#onAddSpecialty,
|
||||
|
||||
@@ -59,16 +59,25 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
||||
context.roleOptions = CONFIG.VERMINE.creatureRoleLevels
|
||||
context.sizeOptions = CONFIG.VERMINE.creatureSizeLevels
|
||||
context.packOptions = CONFIG.VERMINE.creaturePackLevels
|
||||
context.sizeLabel = doc.system.size?.value !== undefined
|
||||
? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "")
|
||||
: ""
|
||||
context.packLabel = doc.system.pack?.value
|
||||
? game.i18n.localize(CONFIG.VERMINE.creaturePackLevels[doc.system.pack.value]?.label ?? "")
|
||||
: ""
|
||||
break
|
||||
case "info":
|
||||
context.tab = context.tabs.info
|
||||
context.capacities = doc.itemTypes.creaturecapacity
|
||||
break
|
||||
case "stats":
|
||||
context.tab = context.tabs.stats
|
||||
context.patternLabel = doc.system.pattern?.value ? game.i18n.localize(CONFIG.VERMINE.creaturePatternLevels[doc.system.pattern.value]?.label ?? "") : ""
|
||||
context.sizeLabel = doc.system.size?.value !== undefined ? game.i18n.localize(CONFIG.VERMINE.creatureSizeLevels[doc.system.size.value]?.label ?? "") : ""
|
||||
context.roleLabel = doc.system.role?.value ? game.i18n.localize(CONFIG.VERMINE.creatureRoleLevels[doc.system.role.value]?.label ?? "") : ""
|
||||
context.packLabel = doc.system.pack?.value || game.i18n.localize("VERMINE.none")
|
||||
context.packLabel = doc.system.pack?.value
|
||||
? `${doc.system.pack.value} (${game.i18n.localize(CONFIG.VERMINE.creaturePackLevels[doc.system.pack.value]?.label ?? "")})`
|
||||
: game.i18n.localize("VERMINE.none")
|
||||
// Contribution cumulée de la Taille (les modificateurs se cumulent : Taille N = somme 1..N)
|
||||
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
|
||||
const sizeLevel = doc.system.size?.value ?? 0
|
||||
@@ -97,11 +106,17 @@ export default class VermineCreatureSheetV2 extends VermineBaseActorSheet {
|
||||
return context
|
||||
}
|
||||
|
||||
/** @override - Les créatures n'ont pas de caractéristiques/compétences : on ouvre le dialogue d'attaque (pool calculé). */
|
||||
/** @override - Les créatures n'ont pas de caractéristiques/compétences : on ouvre le dialogue d'attaque (pool calculé) ou de Réaction. */
|
||||
async _onRoll(event) {
|
||||
event.preventDefault()
|
||||
const el = event.currentTarget
|
||||
const type = el.dataset.type
|
||||
if (type === "creature-reaction") {
|
||||
const { default: NpcRollDialog } = await import("../../system/dialogs/npcRollDialog.mjs")
|
||||
const dialog = await NpcRollDialog.create({ actorId: this.document.id, rolltype: "reaction" })
|
||||
if (dialog) dialog.render(true)
|
||||
return
|
||||
}
|
||||
if (type !== "creature-attack") return super._onRoll(event)
|
||||
const { default: CombatDialog } = await import("../../system/dialogs/combatDialog.mjs")
|
||||
const dialog = await CombatDialog.create({ actorId: this.document.id })
|
||||
|
||||
@@ -7,6 +7,7 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
position: { width: 700, height: 600 },
|
||||
window: { contentClasses: ["group-content"] },
|
||||
actions: {
|
||||
chooseTotem: VermineGroupSheetV2.#onChooseTotem,
|
||||
chooseActor: VermineGroupSheetV2.#onChooseActor,
|
||||
deleteMember: VermineGroupSheetV2.#onDeleteMember,
|
||||
deleteEncounter: VermineGroupSheetV2.#onDeleteEncounter,
|
||||
@@ -56,9 +57,22 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
if (this.document.system.encounters?.length > 0) {
|
||||
for (const encId of this.document.system.encounters) {
|
||||
const a = game.actors.get(encId)
|
||||
if (a) context.resolvedEncounters[encId] = { name: a.name, id: a.id }
|
||||
if (a) {
|
||||
context.resolvedEncounters[encId] = {
|
||||
name: a.name,
|
||||
id: a.id,
|
||||
type: a.type,
|
||||
profile: a.system?.identity?.profile || "",
|
||||
totem: a.system?.identity?.totem || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Notes de voyage (riche texte)
|
||||
const roadNotes = this.document.system?.roadNotes
|
||||
context.enrichedRoadNotes = roadNotes
|
||||
? await foundry.applications.ux.TextEditor.implementation.enrichHTML(roadNotes, { async: true })
|
||||
: ""
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -70,10 +84,6 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
context.tab = context.tabs.info
|
||||
context.abilities = doc.itemTypes.ability.filter(i => i.system.type !== "totem")
|
||||
context.totem_abilities = doc.itemTypes.ability.filter(i => i.system.type === "totem")
|
||||
context.specialties = doc.itemTypes.specialty
|
||||
context.backgrounds = doc.itemTypes.background
|
||||
context.traumas = doc.itemTypes.trauma
|
||||
context.evolutions = doc.itemTypes.evolution
|
||||
break
|
||||
case "gear":
|
||||
context.tab = context.tabs.gear
|
||||
@@ -84,7 +94,6 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
break
|
||||
case "road":
|
||||
context.tab = context.tabs.road
|
||||
context.vehicles = this.#getOwnedVehicles()
|
||||
break
|
||||
case "reserve":
|
||||
context.tab = context.tabs.reserve
|
||||
@@ -101,7 +110,8 @@ export default class VermineGroupSheetV2 extends VermineBaseActorSheet {
|
||||
// Actions : délégation aux applications AppV1 existantes pour TotemPicker/ActorPicker
|
||||
static async #onChooseTotem(event, target) {
|
||||
const { TotemPicker } = await import("../../system/applications.mjs")
|
||||
new TotemPicker(target, this.document).render(true)
|
||||
// Le Totem du Groupe remplit automatiquement Instincts/Interdits.
|
||||
new TotemPicker(target, this.document, { fillInstincts: true }).render(true)
|
||||
}
|
||||
static async #onChooseActor(event, target) {
|
||||
const { ActorPicker } = await import("../../system/applications.mjs")
|
||||
|
||||
@@ -24,6 +24,12 @@ export class VermineAbilitySheetV2 extends VermineBaseItemSheet {
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-ability-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Capacité de créature ────────────────────────────────────────────────
|
||||
export class VermineCreatureCapacitySheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["creaturecapacity"], position: { width: 560 } }
|
||||
static PARTS = { main: { template: "systems/vermine2047/templates/item/item-creaturecapacity-sheet.hbs", scrollable: [""] } }
|
||||
}
|
||||
|
||||
// ── Spécialité ────────────────────────────────────────────────────────
|
||||
export class VermineSpecialtySheetV2 extends VermineBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = { classes: ["specialty"], position: { width: 400 } }
|
||||
|
||||
@@ -8,6 +8,7 @@ export { default as VermineWeaponData } from "./weapon.mjs"
|
||||
export { default as VermineDefenseData } from "./defense.mjs"
|
||||
export { default as VermineVehicleData } from "./vehicle.mjs"
|
||||
export { default as VermineAbilityData } from "./ability.mjs"
|
||||
export { default as VermineCreatureCapacityData } from "./creaturecapacity.mjs"
|
||||
export { default as VermineSpecialtyData } from "./specialty.mjs"
|
||||
export { default as VermineBackgroundData } from "./background.mjs"
|
||||
export { default as VermineTraumaData } from "./trauma.mjs"
|
||||
|
||||
+26
-23
@@ -130,8 +130,9 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
// Vigueur : taille + meute
|
||||
this.computed.vigor = sizeVigor + (packConfig.damage || 0)
|
||||
|
||||
// Réaction : rôle
|
||||
this.computed.reaction = (roleConfig.reaction || 0) + (roleConfig.reaction_bonus || 0)
|
||||
// Réaction : rôle (le bonus de rôle est compté séparément et ajouté
|
||||
// comme réussites automatiques par le NpcRollDialog — cf. modèle PNJ).
|
||||
this.computed.reaction = roleConfig.reaction || 0
|
||||
this.computed.reactionBonus = roleConfig.reaction_bonus || 0
|
||||
|
||||
// Réserves
|
||||
@@ -146,8 +147,13 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les seuils de blessures à partir du patron, de la taille et de la meute.
|
||||
* Les seuils sont la somme des valeurs correspondantes des trois sources.
|
||||
* Calcule les seuils et les cercles de blessures à partir du patron, de la
|
||||
* taille et de la meute.
|
||||
*
|
||||
* Règles (regles_creatures.txt) :
|
||||
* - Seuil = contribution du Gabarit + contributions de Taille (cumulées).
|
||||
* Le Groupe n'ajoute PAS de seuil (tableau « Groupe » : cercles seuls).
|
||||
* - Cercles = cercles du Gabarit + cercles de Taille (cumulés) + cercles du Groupe.
|
||||
*/
|
||||
_calculateCreatureWoundThresholds() {
|
||||
const patternLevel = this.pattern?.value || 1
|
||||
@@ -159,30 +165,27 @@ export default class VermineCreatureData extends foundry.abstract.TypeDataModel
|
||||
|
||||
// Les modificateurs de Taille se cumulent (Taille N = somme des tailles 1..N).
|
||||
const sizeLevels = CONFIG.VERMINE.creatureSizeLevels || {}
|
||||
let sizeMinor = 0
|
||||
let sizeMajor = 0
|
||||
let sizeDeadly = 0
|
||||
const sizeThresholds = { minor: 0, major: 0, deadly: 0 }
|
||||
const sizeWounds = { minor: 0, major: 0, deadly: 0 }
|
||||
for (let i = 1; i <= sizeLevel; i++) {
|
||||
const cfg = sizeLevels[i] || {}
|
||||
sizeMinor += cfg.minorWound || 0
|
||||
sizeMajor += cfg.majorWound || 0
|
||||
sizeDeadly += cfg.deadlyWound || 0
|
||||
sizeThresholds.minor += cfg.minorThreshold || 0
|
||||
sizeThresholds.major += cfg.majorThreshold || 0
|
||||
sizeThresholds.deadly += cfg.deadlyThreshold || 0
|
||||
sizeWounds.minor += cfg.minorWound || 0
|
||||
sizeWounds.major += cfg.majorWound || 0
|
||||
sizeWounds.deadly += cfg.deadlyWound || 0
|
||||
}
|
||||
|
||||
this.minorWound.threshold = (patternConfig.minorWound || 0)
|
||||
+ sizeMinor
|
||||
+ (packConfig.minorWound || 0)
|
||||
this.majorWound.threshold = (patternConfig.majorWound || 0)
|
||||
+ sizeMajor
|
||||
+ (packConfig.majorWound || 0)
|
||||
this.deadlyWound.threshold = (patternConfig.deadlyWound || 0)
|
||||
+ sizeDeadly
|
||||
+ (packConfig.deadlyWound || 0)
|
||||
// Seuils : gabarit + taille cumulée (pas de contribution du groupe).
|
||||
this.minorWound.threshold = (patternConfig.minorThreshold ?? 0) + sizeThresholds.minor
|
||||
this.majorWound.threshold = (patternConfig.majorThreshold ?? 0) + sizeThresholds.major
|
||||
this.deadlyWound.threshold = (patternConfig.deadlyThreshold ?? 0) + sizeThresholds.deadly
|
||||
|
||||
// Max de blessures
|
||||
this.minorWound.max = Math.min(5, this.minorWound.threshold + 2)
|
||||
this.majorWound.max = Math.min(4, this.majorWound.threshold + 1)
|
||||
this.deadlyWound.max = Math.min(2, this.deadlyWound.threshold)
|
||||
// Cercles : gabarit + taille cumulée + groupe.
|
||||
this.minorWound.max = (patternConfig.minorWound ?? 0) + sizeWounds.minor + (packConfig.minorWound ?? 0)
|
||||
this.majorWound.max = (patternConfig.majorWound ?? 0) + sizeWounds.major + (packConfig.majorWound ?? 0)
|
||||
this.deadlyWound.max = (patternConfig.deadlyWound ?? 0) + sizeWounds.deadly + (packConfig.deadlyWound ?? 0)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { listItemSchema } from "./_shared.mjs"
|
||||
|
||||
/**
|
||||
* DataModel pour les items de type "creaturecapacity" (capacités de créature).
|
||||
* @augments {foundry.abstract.TypeDataModel}
|
||||
*/
|
||||
export default class VermineCreatureCapacityData extends foundry.abstract.TypeDataModel {
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["VERMINE.item.creaturecapacity"]
|
||||
|
||||
/** @override */
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
return {
|
||||
...listItemSchema(),
|
||||
type: new fields.StringField({ required: true, initial: "base" })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,9 @@ export default class VermineGroupData extends foundry.abstract.TypeDataModel {
|
||||
// Équipement
|
||||
equipment: equipmentSchema(),
|
||||
|
||||
// Notes de voyage (lieux, dangers, routes…)
|
||||
roadNotes: new fields.HTMLField({ required: true, initial: "" }),
|
||||
|
||||
// Niveau du groupe (1-10)
|
||||
level: levelSchema(1, 1, 10),
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
|
||||
|
||||
export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
|
||||
constructor(linkEl, actor) {
|
||||
constructor(linkEl, actor, options = {}) {
|
||||
super();
|
||||
this.linkEl = linkEl;
|
||||
this.actor = actor;
|
||||
this.fillInstincts = options.fillInstincts === true;
|
||||
}
|
||||
|
||||
static get DEFAULT_OPTIONS() {
|
||||
@@ -32,7 +33,17 @@ export class TotemPicker extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||
el.addEventListener('click', event => {
|
||||
const link = event.target.closest('a');
|
||||
if (!link?.dataset.totem) return;
|
||||
this.actor.update({ 'system.identity.totem': link.dataset.totem });
|
||||
const update = { 'system.identity.totem': link.dataset.totem };
|
||||
// Remplit automatiquement Instincts/Interdits depuis la config du totem
|
||||
// (utilisé pour le Totem du Groupe).
|
||||
if (this.fillInstincts && this.actor.system?.identity) {
|
||||
const totemKey = link.dataset.totem;
|
||||
const instincts = game.i18n.localize(`TOTEMS.${totemKey}.instincts`);
|
||||
const bans = game.i18n.localize(`TOTEMS.${totemKey}.bans`);
|
||||
if (!instincts.startsWith("TOTEMS.")) update['system.identity.instincts'] = instincts;
|
||||
if (!bans.startsWith("TOTEMS.")) update['system.identity.prohibits'] = bans;
|
||||
}
|
||||
this.actor.update(update);
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
|
||||
+31
-12
@@ -129,30 +129,30 @@ VERMINE.npcRoleLevels = {
|
||||
* Creature Pattern Levels configuration
|
||||
*/
|
||||
VERMINE.creaturePatternLevels = {
|
||||
1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
1: { "label": "PATTERN_LEVELS.insect", "attack": 2, "damage": 0, "minorThreshold": 0, "majorThreshold": 0, "deadlyThreshold": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PATTERN_LEVELS.rat", "attack": 3, "damage": 1, "minorThreshold": 0, "majorThreshold": 1, "deadlyThreshold": 3, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "PATTERN_LEVELS.dog", "attack": 4, "damage": 3, "minorThreshold": 1, "majorThreshold": 3, "deadlyThreshold": 5, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
4: { "label": "PATTERN_LEVELS.bear", "attack": 6, "damage": 6, "minorThreshold": 3, "majorThreshold": 5, "deadlyThreshold": 7, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creature Size Levels configuration
|
||||
*/
|
||||
VERMINE.creatureSizeLevels = {
|
||||
0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
|
||||
3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||
0: { "label": "SIZE_LEVELS.tiny", "attack": 0, "vigor": 0, "minorThreshold": 0, "majorThreshold": 0, "deadlyThreshold": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "label": "SIZE_LEVELS.small", "attack": 1, "vigor": 1, "minorThreshold": 0, "majorThreshold": 1, "deadlyThreshold": 1, "minorWound": 0, "majorWound": 1, "deadlyWound": 1 },
|
||||
2: { "label": "SIZE_LEVELS.medium", "attack": 2, "vigor": 2, "minorThreshold": 2, "majorThreshold": 2, "deadlyThreshold": 2, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 },
|
||||
3: { "label": "SIZE_LEVELS.large", "attack": 3, "vigor": 3, "minorThreshold": 3, "majorThreshold": 3, "deadlyThreshold": 3, "minorWound": 1, "majorWound": 1, "deadlyWound": 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creature Pack Levels configuration
|
||||
*/
|
||||
VERMINE.creaturePackLevels = {
|
||||
0: { "attack": 0, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
|
||||
3: { "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||
0: { "label": "VERMINE.none", "attack": 0, "damage": 0, "minorWound": 0, "majorWound": 0, "deadlyWound": 0 },
|
||||
1: { "label": "PACK_LEVELS.small", "attack": 1, "damage": 1, "minorWound": 0, "majorWound": 0, "deadlyWound": 1 },
|
||||
2: { "label": "PACK_LEVELS.large", "attack": 2, "damage": 2, "minorWound": 2, "majorWound": 2, "deadlyWound": 2 },
|
||||
3: { "label": "PACK_LEVELS.giant", "attack": 5, "damage": 5, "minorWound": 3, "majorWound": 3, "deadlyWound": 3 }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,6 +180,25 @@ VERMINE.abilityCategories = {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Types de Capacités de créature (mode d'activation/disponibilité).
|
||||
* Correspond aux Modes de jeu + une catégorie "base".
|
||||
*/
|
||||
VERMINE.creatureCapacityTypes = {
|
||||
"base": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.base"
|
||||
},
|
||||
"survival": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.survival"
|
||||
},
|
||||
"nightmare": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.nightmare"
|
||||
},
|
||||
"apocalypse": {
|
||||
"label": "CREATURE_CAPACITY_TYPES.apocalypse"
|
||||
}
|
||||
}
|
||||
|
||||
VERMINE.abilities = {
|
||||
"vigor": "ABILITIES.vigor.name",
|
||||
"health": "ABILITIES.health.name",
|
||||
|
||||
@@ -260,8 +260,9 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
const toolsChecked = this.#el.querySelector("input[name='usingTools']:checked");
|
||||
const tooling = toolsChecked && toolsChecked.value !== "0" ? 1 : 0;
|
||||
const group = parseInt(this.#el.querySelector("#group")?.value, 10) || 0;
|
||||
const total = this.basePool + selfControl + pools + specialty + help + tooling + group;
|
||||
return { ability, skill, selfControl, pools, specialty, help, tooling, group, total };
|
||||
const malus = VermineExchange.woundMalus(this.actor);
|
||||
const total = Math.max(0, this.basePool + selfControl + pools + specialty + help + tooling + group - malus);
|
||||
return { ability, skill, selfControl, pools, specialty, help, tooling, group, malus, total };
|
||||
}
|
||||
|
||||
getDicePool() {
|
||||
@@ -297,7 +298,7 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
|
||||
const b = this.getPoolBreakdown();
|
||||
const bonusEl = this.#el.querySelector("#total-bonus");
|
||||
if (bonusEl) bonusEl.textContent = b.selfControl + b.pools + b.specialty + b.help + b.tooling + b.group;
|
||||
if (bonusEl) bonusEl.textContent = b.selfControl + b.pools + b.specialty + b.help + b.tooling + b.group - b.malus;
|
||||
|
||||
const handSel = this.#el.querySelector("#handicap");
|
||||
const handEl = this.#el.querySelector("#current-handicap");
|
||||
@@ -367,7 +368,7 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
: null
|
||||
};
|
||||
|
||||
await VermineUtils.roll({
|
||||
const rollParams = {
|
||||
actor: this.actor,
|
||||
NoD: this.getDicePool(),
|
||||
Reroll: this.getReroll(),
|
||||
@@ -387,9 +388,13 @@ export default class CombatDialog extends HandlebarsApplicationMixin(foundry.app
|
||||
targets: targets.map(t => t.name),
|
||||
attack,
|
||||
messageFlags: { "vermine-exchange": attack }
|
||||
});
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
#getRangeKey() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VermineUtils } from "../roll.mjs"
|
||||
import { VermineExchange } from "../exchange.mjs"
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||
|
||||
@@ -161,7 +162,8 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
|
||||
getPoolBreakdown() {
|
||||
const help = this.#getHelped()
|
||||
const group = this.#getGroup()
|
||||
return { pool: this.pool, help, group, total: this.pool + help + group }
|
||||
const malus = VermineExchange.woundMalus(this.#actor)
|
||||
return { pool: this.pool, help, group, malus, total: Math.max(0, this.pool + help + group - malus) }
|
||||
}
|
||||
|
||||
#updateUI() {
|
||||
@@ -169,7 +171,7 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
|
||||
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
|
||||
if (bonusEl) bonusEl.textContent = b.help + b.group - b.malus
|
||||
const freeEl = this.#el.querySelector("#free-successes")
|
||||
if (freeEl) freeEl.textContent = String(this.freeSuccesses)
|
||||
}
|
||||
@@ -191,7 +193,7 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
|
||||
: game.i18n.localize("VERMINE.defense_esquive"))
|
||||
: this.label
|
||||
|
||||
await VermineUtils.roll({
|
||||
const rollParams = {
|
||||
actor: this.#actor,
|
||||
NoD: this.getPoolBreakdown().total,
|
||||
Reroll: this.rerolls,
|
||||
@@ -201,8 +203,12 @@ export default class NpcRollDialog extends HandlebarsApplicationMixin(foundry.ap
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VermineUtils } from "../roll.mjs";
|
||||
import { VermineExchange } from "../exchange.mjs";
|
||||
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||
|
||||
@@ -164,8 +165,9 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
const specialty = specChecked ? 1 : 0;
|
||||
const help = helped ? 1 : 0;
|
||||
const tooling = tools ? 1 : 0;
|
||||
const total = ability + skillPool + selfControl + specialty + help + tooling + group + experience;
|
||||
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, total };
|
||||
const malus = VermineExchange.woundMalus(this.#actor);
|
||||
const total = Math.max(0, ability + skillPool + selfControl + specialty + help + tooling + group + experience - malus);
|
||||
return { ability, skill: skillPool, selfControl, specialty, help, tooling, group, experience, malus, total };
|
||||
}
|
||||
|
||||
getDicePool() {
|
||||
@@ -293,7 +295,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
|
||||
#calculateBonusCount() {
|
||||
const b = this.getPoolBreakdown();
|
||||
return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience;
|
||||
return b.specialty + b.help + b.tooling + b.group + b.selfControl + b.experience - b.malus;
|
||||
}
|
||||
|
||||
#refreshExperienceUI() {
|
||||
@@ -448,7 +450,7 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
await group.update({ "system.experience.dice": groupPool - pulled });
|
||||
}
|
||||
|
||||
await VermineUtils.roll({
|
||||
const rollParams = {
|
||||
actor: this.#actor,
|
||||
NoD: this.getDicePool(),
|
||||
Reroll: this.getReroll(),
|
||||
@@ -467,9 +469,13 @@ export default class RollDialog extends HandlebarsApplicationMixin(foundry.appli
|
||||
weapon: this.weapon?.toObject() ?? null,
|
||||
targets: [...game.user.targets].map(t => t.name),
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+50
-10
@@ -204,20 +204,52 @@ export class VermineExchange {
|
||||
return Boolean(actor) && (game.user.isGM || actor.isOwner)
|
||||
}
|
||||
|
||||
/** Ordre de gravité des blessures (croissant). */
|
||||
static WOUND_ORDER = ['minorWound', 'majorWound', 'deadlyWound']
|
||||
|
||||
/** Malus de la pire blessure (règles p.42) : -1D Légère, -2D Grave, -3D Mortelle. */
|
||||
static WOUND_MALUS = { minorWound: 1, majorWound: 2, deadlyWound: 3 }
|
||||
|
||||
/**
|
||||
* Applique +1 dans la catégorie de blessure indiquée (plafonné au max).
|
||||
* Malus de blessure applicable à toutes les actions : seul le Malus de la
|
||||
* Blessure la plus grave compte (non cumulatif).
|
||||
* @param {Actor} actor
|
||||
* @returns {number} 0, 1, 2 ou 3
|
||||
*/
|
||||
static woundMalus(actor) {
|
||||
if (!actor?.system) return 0
|
||||
if (actor.system.deadlyWound?.value > 0) return 3
|
||||
if (actor.system.majorWound?.value > 0) return 2
|
||||
if (actor.system.minorWound?.value > 0) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique +1 cercle de blessure, en montant en gravité si les cercles du
|
||||
* niveau correspondant sont déjà pleins (règles p.42 : si tous les cercles
|
||||
* d'une Blessure sont cochés, la suivante est de gravité supérieure).
|
||||
* @param {Actor} actor
|
||||
* @param {string} woundCategory 'minorWound' | 'majorWound' | 'deadlyWound'
|
||||
* @returns {Promise<number|boolean>} nouvelle valeur, ou false si refusé
|
||||
* @returns {Promise<Object|boolean|null>}
|
||||
* - false si refusé (permissions) ou null si invalide
|
||||
* - { category, value, escalated, death } si appliqué (death si tout est plein)
|
||||
*/
|
||||
static async applyWound(actor, woundCategory) {
|
||||
if (!actor || !woundCategory) return null
|
||||
if (!this.canApplyWound(actor)) return false
|
||||
const current = actor.system?.[woundCategory]?.value || 0
|
||||
const max = actor.system?.[woundCategory]?.max ?? Number.POSITIVE_INFINITY
|
||||
const next = Math.min(current + 1, max)
|
||||
await actor.update({ [`system.${woundCategory}.value`]: next })
|
||||
return next
|
||||
const startIdx = this.WOUND_ORDER.indexOf(woundCategory)
|
||||
if (startIdx === -1) return null
|
||||
for (let i = startIdx; i < this.WOUND_ORDER.length; i++) {
|
||||
const cat = this.WOUND_ORDER[i]
|
||||
const sys = actor.system?.[cat]
|
||||
const max = sys?.max ?? Number.POSITIVE_INFINITY
|
||||
const value = sys?.value || 0
|
||||
if (value < max) {
|
||||
await actor.update({ [`system.${cat}.value`]: value + 1 })
|
||||
return { category: cat, value: value + 1, escalated: cat !== woundCategory, death: false }
|
||||
}
|
||||
}
|
||||
return { category: woundCategory, value: null, escalated: false, death: true }
|
||||
}
|
||||
|
||||
// ── Résolution ──────────────────────────────────────────────────────
|
||||
@@ -457,10 +489,18 @@ export class VermineExchange {
|
||||
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_permission'))
|
||||
return
|
||||
}
|
||||
const next = await this.applyWound(defender, wound)
|
||||
if (next === false) return
|
||||
const result = await this.applyWound(defender, wound)
|
||||
if (result === false) return
|
||||
btn.disabled = true
|
||||
btn.innerHTML = `<i class="fas fa-check"></i> ${game.i18n.localize('VERMINE.wound_applied')}`
|
||||
let label = game.i18n.localize('VERMINE.wound_applied')
|
||||
if (result?.death) {
|
||||
label = game.i18n.localize('VERMINE.wound_death')
|
||||
} else if (result?.escalated) {
|
||||
const from = game.i18n.localize(this.woundLabelKey(wound))
|
||||
const to = game.i18n.localize(this.woundLabelKey(result.category))
|
||||
label = game.i18n.format('VERMINE.wound_escalated', { from, to })
|
||||
}
|
||||
btn.innerHTML = `<i class="fas fa-check"></i> ${label}`
|
||||
// Persiste l'état du bouton seulement si l'utilisateur peut modifier le message
|
||||
if (game.user.isGM || attackMessage.isOwner) {
|
||||
const msgEl = btn.closest('.vermine-roll-message')
|
||||
|
||||
@@ -367,6 +367,17 @@ export const registerHandlebarsHelpers = function () {
|
||||
Handlebars.registerHelper('ifStartsWith', function (arg1, arg2, options) {
|
||||
return (typeof arg1 === 'string' && arg1.startsWith(arg2)) ? options.fn(this) : options.inverse(this);
|
||||
});
|
||||
// Chiffre → chiffre romain (handicaps, entraves). 0 → chaîne vide.
|
||||
Handlebars.registerHelper('roman', function (n) {
|
||||
const table = [[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']]
|
||||
let num = Number(n)
|
||||
if (!Number.isFinite(num) || num <= 0) return ''
|
||||
let out = ''
|
||||
for (const [v, s] of table) {
|
||||
while (num >= v) { out += s; num -= v }
|
||||
}
|
||||
return out
|
||||
});
|
||||
|
||||
|
||||
//math operations
|
||||
|
||||
@@ -53,6 +53,7 @@ Hooks.once('init', async function () {
|
||||
CONFIG.Item.dataModels.weapon = models.VermineWeaponData;
|
||||
CONFIG.Item.dataModels.defense = models.VermineDefenseData;
|
||||
CONFIG.Item.dataModels.ability = models.VermineAbilityData;
|
||||
CONFIG.Item.dataModels.creaturecapacity = models.VermineCreatureCapacityData;
|
||||
CONFIG.Item.dataModels.specialty = models.VermineSpecialtyData;
|
||||
CONFIG.Item.dataModels.background = models.VermineBackgroundData;
|
||||
CONFIG.Item.dataModels.trauma = models.VermineTraumaData;
|
||||
@@ -119,6 +120,9 @@ Hooks.once('init', async function () {
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineAbilitySheetV2, {
|
||||
types: ["ability"], makeDefault: true
|
||||
})
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineCreatureCapacitySheetV2, {
|
||||
types: ["creaturecapacity"], makeDefault: true
|
||||
})
|
||||
foundry.documents.collections.Items.registerSheet("vermine2047", sheets.VermineSpecialtySheetV2, {
|
||||
types: ["specialty"], makeDefault: true
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user