IMplémentation de la ajorité des remarques de Nepherius
This commit is contained in:
@@ -29,6 +29,7 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
|
||||
trackBox: CelestopolActorSheet.#onTrackBox,
|
||||
skillLevel: CelestopolActorSheet.#onSkillLevel,
|
||||
factionLevel: CelestopolActorSheet.#onFactionLevel,
|
||||
toggleArmure: CelestopolActorSheet.#onToggleArmure,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -173,12 +174,21 @@ export default class CelestopolActorSheet extends HandlebarsApplicationMixin(fou
|
||||
}
|
||||
|
||||
/** Met à jour le score d'une faction par clic sur un point. */
|
||||
static async #onToggleArmure(_event, target) {
|
||||
const uuid = target.closest('[data-item-uuid]')?.dataset.itemUuid
|
||||
if (!uuid) return
|
||||
const item = await fromUuid(uuid)
|
||||
if (item?.type === "armure") await item.update({ "system.equipped": !item.system.equipped })
|
||||
}
|
||||
|
||||
static #onFactionLevel(_event, target) {
|
||||
if (!this.isEditable) return
|
||||
const factionId = target.dataset.faction
|
||||
const index = parseInt(target.dataset.index)
|
||||
const index = parseInt(target.dataset.index) // 0-8
|
||||
const newValue = index - 4 // -4 à +4
|
||||
const current = this.document.system.factions[factionId]?.value ?? 0
|
||||
const newValue = (index <= current) ? index - 1 : index
|
||||
this.document.update({ [`system.factions.${factionId}.value`]: Math.max(0, newValue) })
|
||||
// Cliquer sur le dot actif (sauf neutre) remet à 0
|
||||
const finalValue = (newValue === current && newValue !== 0) ? 0 : newValue
|
||||
this.document.update({ [`system.factions.${factionId}.value`]: finalValue })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses,
|
||||
depenseXp: CelestopolCharacterSheet.#onDepenseXp,
|
||||
supprimerXpLog: CelestopolCharacterSheet.#onSupprimerXpLog,
|
||||
rollMoonDie: CelestopolCharacterSheet.#onRollMoonDie,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -93,15 +94,46 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
|
||||
case "factions":
|
||||
context.tab = context.tabs.factions
|
||||
context.factionRows = Object.entries(SYSTEM.FACTIONS).map(([id, fDef]) => {
|
||||
const val = this.document.system.factions[id]?.value ?? 0
|
||||
return {
|
||||
id,
|
||||
label: fDef.label,
|
||||
value: val,
|
||||
valueStr: val > 0 ? `+${val}` : `${val}`,
|
||||
dots: Array.from({ length: 9 }, (_, i) => ({
|
||||
index: i,
|
||||
filled: i <= val + 4,
|
||||
type: i < 4 ? "neg" : i === 4 ? "neutral" : "pos",
|
||||
})),
|
||||
}
|
||||
})
|
||||
context.factionCustom = ["perso1", "perso2"].map(id => {
|
||||
const f = this.document.system.factions[id]
|
||||
const val = f?.value ?? 0
|
||||
return {
|
||||
id,
|
||||
label: f?.label ?? "",
|
||||
value: val,
|
||||
valueStr: val > 0 ? `+${val}` : `${val}`,
|
||||
dots: Array.from({ length: 9 }, (_, i) => ({
|
||||
index: i,
|
||||
filled: i <= val + 4,
|
||||
type: i < 4 ? "neg" : i === 4 ? "neutral" : "pos",
|
||||
})),
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case "biography":
|
||||
context.tab = context.tabs.biography
|
||||
context.xpLogEmpty = (doc.system.xp?.log?.length ?? 0) === 0
|
||||
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
doc.system.description, { async: true })
|
||||
context.enrichedDescriptionPhysique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
doc.system.descriptionPhysique, { relativeTo: this.document })
|
||||
context.enrichedDescriptionPsychologique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
doc.system.descriptionPsychologique, { relativeTo: this.document })
|
||||
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
doc.system.notes, { async: true })
|
||||
doc.system.notes, { relativeTo: this.document })
|
||||
break
|
||||
|
||||
case "equipement":
|
||||
@@ -235,4 +267,10 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
"system.xp.log": log,
|
||||
})
|
||||
}
|
||||
|
||||
/** Lance le Dé de la Lune de façon autonome depuis le header de la fiche. */
|
||||
static async #onRollMoonDie() {
|
||||
const { CelestopolRoll } = await import("../../documents/roll.mjs")
|
||||
await CelestopolRoll.rollMoonStandalone(this.document)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,34 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["npc"],
|
||||
position: { width: 760, height: 600 },
|
||||
position: { width: 780, height: 640 },
|
||||
window: { contentClasses: ["npc-content"] },
|
||||
actions: {
|
||||
createAspect: CelestopolNPCSheet.#onCreateAspect,
|
||||
createWeapon: CelestopolNPCSheet.#onCreateWeapon,
|
||||
createArmure: CelestopolNPCSheet.#onCreateArmure,
|
||||
rollMoonDie: CelestopolNPCSheet.#onRollMoonDie,
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-celestopol/templates/npc-main.hbs" },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
competences:{ template: "systems/fvtt-celestopol/templates/npc-competences.hbs" },
|
||||
blessures: { template: "systems/fvtt-celestopol/templates/npc-blessures.hbs" },
|
||||
main: { template: "systems/fvtt-celestopol/templates/npc-main.hbs" },
|
||||
tabs: { template: "templates/generic/tab-navigation.hbs" },
|
||||
competences: { template: "systems/fvtt-celestopol/templates/npc-competences.hbs" },
|
||||
blessures: { template: "systems/fvtt-celestopol/templates/npc-blessures.hbs" },
|
||||
equipement: { template: "systems/fvtt-celestopol/templates/npc-equipement.hbs" },
|
||||
biographie: { template: "systems/fvtt-celestopol/templates/npc-biographie.hbs" },
|
||||
}
|
||||
|
||||
tabGroups = { sheet: "competences" }
|
||||
|
||||
#getTabs() {
|
||||
const tabs = {
|
||||
competences:{ id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" },
|
||||
blessures: { id: "blessures", group: "sheet", icon: "fa-solid fa-heart-crack", label: "CELESTOPOL.Tab.blessures" },
|
||||
competences: { id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" },
|
||||
blessures: { id: "blessures", group: "sheet", icon: "fa-solid fa-heart-crack", label: "CELESTOPOL.Tab.blessures" },
|
||||
equipement: { id: "equipement", group: "sheet", icon: "fa-solid fa-shield-halved",label: "CELESTOPOL.Tab.equipement" },
|
||||
biographie: { id: "biographie", group: "sheet", icon: "fa-solid fa-book-open", label: "CELESTOPOL.Tab.biographie" },
|
||||
}
|
||||
for (const v of Object.values(tabs)) {
|
||||
v.active = this.tabGroups[v.group] === v.id
|
||||
@@ -33,12 +43,29 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet {
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
context.stats = SYSTEM.STATS
|
||||
context.skills = SYSTEM.SKILLS
|
||||
context.anomalyTypes = SYSTEM.ANOMALY_TYPES
|
||||
context.woundLevels = SYSTEM.WOUND_LEVELS
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
context.stats = SYSTEM.STATS
|
||||
context.anomalyTypes = SYSTEM.ANOMALY_TYPES
|
||||
context.woundLevels = SYSTEM.WOUND_LEVELS
|
||||
context.npcTypes = SYSTEM.NPC_TYPES
|
||||
context.factions = SYSTEM.FACTIONS
|
||||
context.antagonisteStats = SYSTEM.ANTAGONISTE_STATS
|
||||
|
||||
const sys = this.document.system
|
||||
context.aspects = this.document.itemTypes.aspect ?? []
|
||||
context.weapons = this.document.itemTypes.weapon ?? []
|
||||
context.armures = this.document.itemTypes.armure ?? []
|
||||
context.armorMalus = sys.armorMalus ?? 0
|
||||
|
||||
// Label effectif de chaque domaine selon le type de PNJ
|
||||
const isAntagoniste = sys.npcType === "antagoniste"
|
||||
context.domainLabels = {
|
||||
ame: isAntagoniste ? "CELESTOPOL.NPC.emprise" : "CELESTOPOL.Stat.ame",
|
||||
corps: isAntagoniste ? "CELESTOPOL.NPC.peril" : "CELESTOPOL.Stat.corps",
|
||||
coeur: isAntagoniste ? "CELESTOPOL.NPC.menace" : "CELESTOPOL.Stat.coeur",
|
||||
esprit: isAntagoniste ? "CELESTOPOL.NPC.danger" : "CELESTOPOL.Stat.esprit",
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -48,11 +75,59 @@ export default class CelestopolNPCSheet extends CelestopolActorSheet {
|
||||
switch (partId) {
|
||||
case "competences":
|
||||
context.tab = context.tabs.competences
|
||||
// Enrichissement des aspects
|
||||
context.enrichedAspects = await Promise.all(
|
||||
(context.aspects ?? []).map(async a => ({
|
||||
item: a,
|
||||
enrichedDesc: await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
a.system.description, { relativeTo: this.document }
|
||||
),
|
||||
}))
|
||||
)
|
||||
break
|
||||
case "blessures":
|
||||
context.tab = context.tabs.blessures
|
||||
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
context.system.notes, { relativeTo: this.document }
|
||||
)
|
||||
break
|
||||
case "equipement":
|
||||
context.tab = context.tabs.equipement
|
||||
break
|
||||
case "biographie":
|
||||
context.tab = context.tabs.biographie
|
||||
context.enrichedHistoire = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
context.system.histoire, { relativeTo: this.document }
|
||||
)
|
||||
context.enrichedDescriptionPhysique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
context.system.descriptionPhysique, { relativeTo: this.document }
|
||||
)
|
||||
break
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
static async #onCreateAspect() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect",
|
||||
}])
|
||||
}
|
||||
|
||||
static async #onCreateWeapon() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("TYPES.Item.weapon"), type: "weapon",
|
||||
}])
|
||||
}
|
||||
|
||||
static async #onCreateArmure() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("TYPES.Item.armure"), type: "armure",
|
||||
}])
|
||||
}
|
||||
|
||||
/** Lance le Dé de la Lune de façon autonome depuis le header de la fiche PNJ. */
|
||||
static async #onRollMoonDie() {
|
||||
const { CelestopolRoll } = await import("../../documents/roll.mjs")
|
||||
await CelestopolRoll.rollMoonStandalone(this.document)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,35 @@ export const WEAPON_RANGE_TYPES = {
|
||||
longue: { id: "longue", label: "CELESTOPOL.Weapon.rangeLongue" },
|
||||
}
|
||||
|
||||
/** Types de PNJ : standard (domaines classiques) ou antagoniste (Emprise/Péril/Menace/Danger). */
|
||||
export const NPC_TYPES = {
|
||||
standard: { id: "standard", label: "CELESTOPOL.NPC.typeStandard" },
|
||||
antagoniste: { id: "antagoniste", label: "CELESTOPOL.NPC.typeAntagoniste" },
|
||||
}
|
||||
|
||||
/**
|
||||
* Labels alternatifs des domaines pour les Antagonistes.
|
||||
* Le domaine ame ↔ Emprise, corps ↔ Péril, coeur ↔ Menace, esprit ↔ Danger.
|
||||
*/
|
||||
export const ANTAGONISTE_STATS = {
|
||||
ame: { id: "ame", label: "CELESTOPOL.NPC.emprise" },
|
||||
corps: { id: "corps", label: "CELESTOPOL.NPC.peril" },
|
||||
coeur: { id: "coeur", label: "CELESTOPOL.NPC.menace" },
|
||||
esprit: { id: "esprit", label: "CELESTOPOL.NPC.danger" },
|
||||
}
|
||||
|
||||
/**
|
||||
* Modificateurs de tir à distance (LdB p.XX).
|
||||
* Affiché dans le dialogue de jet uniquement pour les armes de type "distance".
|
||||
*/
|
||||
export const RANGED_MODIFIERS = [
|
||||
{ id: "none", value: 0, label: "CELESTOPOL.Combat.rangedModNone" },
|
||||
{ id: "aim", value: +2, label: "CELESTOPOL.Combat.rangedModAim" },
|
||||
{ id: "moving", value: -2, label: "CELESTOPOL.Combat.rangedModMoving" },
|
||||
{ id: "engaged", value: -4, label: "CELESTOPOL.Combat.rangedModEngaged" },
|
||||
{ id: "longRange", value: -4, label: "CELESTOPOL.Combat.rangedModLongRange" },
|
||||
]
|
||||
|
||||
export const SYSTEM = {
|
||||
id: SYSTEM_ID,
|
||||
ASCII,
|
||||
@@ -180,6 +209,8 @@ export const SYSTEM = {
|
||||
ANOMALY_TYPES,
|
||||
ANOMALY_DEFINITIONS,
|
||||
FACTIONS,
|
||||
NPC_TYPES,
|
||||
ANTAGONISTE_STATS,
|
||||
WOUND_LEVELS,
|
||||
DIFFICULTY_CHOICES,
|
||||
CONTEXT_MODIFIER_CHOICES,
|
||||
@@ -189,4 +220,5 @@ export const SYSTEM = {
|
||||
WEAPON_DAMAGE_TYPES,
|
||||
WEAPON_RANGE_TYPES,
|
||||
WEAPON_COMBAT_TYPES,
|
||||
RANGED_MODIFIERS,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as CelestopolActor } from "./actor.mjs"
|
||||
export { default as CelestopolItem } from "./item.mjs"
|
||||
export { default as CelestopolChatMessage } from "./chat-message.mjs"
|
||||
export { default as CelestopolCombat } from "./combat.mjs"
|
||||
export { CelestopolRoll } from "./roll.mjs"
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
export default class CelestopolActor extends Actor {
|
||||
/** @override */
|
||||
getRollData() {
|
||||
return this.toObject(false).system
|
||||
// Inclure les valeurs dérivées (initiative, résistances…) calculées par prepareDerivedData
|
||||
return { ...this.toObject(false).system, initiative: this.system.initiative ?? 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Override de l'initiative : valeur déterministe (pas de dé).
|
||||
* Personnage : 4 + Mobilité + Inspiration
|
||||
* PNJ : Corps.res
|
||||
* @override
|
||||
*/
|
||||
async rollInitiative() {
|
||||
if (!game.combat) return null
|
||||
const combatant = game.combat.combatants.find(c => c.actorId === this.id)
|
||||
if (!combatant) return null
|
||||
const initiative = this.system.initiative ?? 0
|
||||
await combatant.update({ initiative })
|
||||
return combatant
|
||||
}
|
||||
}
|
||||
|
||||
53
module/documents/combat.mjs
Normal file
53
module/documents/combat.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
const SYSTEM_ID = "fvtt-celestopol"
|
||||
|
||||
export default class CelestopolCombat extends Combat {
|
||||
/** @override — Initiative déterministe, message stylé maison */
|
||||
async rollInitiative(ids, { updateTurn = true } = {}) {
|
||||
ids = typeof ids === "string" ? [ids] : ids
|
||||
const combatants = ids.map(id => this.combatants.get(id)).filter(Boolean)
|
||||
if (!combatants.length) return this
|
||||
|
||||
const updates = []
|
||||
for (const combatant of combatants) {
|
||||
const actor = combatant.actor
|
||||
if (!actor) continue
|
||||
const value = actor.system.initiative ?? 0
|
||||
updates.push({ _id: combatant.id, initiative: value })
|
||||
await CelestopolCombat._postInitiativeMessage(combatant, actor, value)
|
||||
}
|
||||
|
||||
if (updates.length) await this.updateEmbeddedDocuments("Combatant", updates)
|
||||
if (updateTurn && this.turn !== null) await this.update({ turn: this.turn })
|
||||
return this
|
||||
}
|
||||
|
||||
static async _postInitiativeMessage(combatant, actor, value) {
|
||||
const sys = actor.system
|
||||
let detail
|
||||
if (actor.type === "character") {
|
||||
const mob = sys.stats?.corps?.mobilite?.value ?? 0
|
||||
const insp = sys.stats?.coeur?.inspiration?.value ?? 0
|
||||
detail = `4 + ${mob} (${game.i18n.localize("CELESTOPOL.Skill.mobilite")}) + ${insp} (${game.i18n.localize("CELESTOPOL.Skill.inspiration")})`
|
||||
} else {
|
||||
const corps = sys.stats?.corps?.res ?? value
|
||||
detail = `${game.i18n.localize("CELESTOPOL.Stat.corps")} : ${corps}`
|
||||
}
|
||||
|
||||
const content = await renderTemplate(
|
||||
`systems/${SYSTEM_ID}/templates/chat-initiative.hbs`,
|
||||
{
|
||||
actorName: combatant.name ?? actor.name,
|
||||
actorImg: actor.img,
|
||||
value,
|
||||
detail,
|
||||
}
|
||||
)
|
||||
|
||||
await ChatMessage.create({
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content,
|
||||
style: CONST.CHAT_MESSAGE_STYLES.OTHER,
|
||||
flags: { [SYSTEM_ID]: { type: "initiative" } },
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,18 +35,21 @@ export class CelestopolRoll extends Roll {
|
||||
* @returns {Promise<CelestopolRoll|null>}
|
||||
*/
|
||||
static async prompt(options = {}) {
|
||||
const woundMalus = options.woundMalus ?? 0
|
||||
const skillValue = options.skillValue ?? 0
|
||||
const woundLevelId = options.woundLevel ?? 0
|
||||
const destGaugeFull = options.destGaugeFull ?? false
|
||||
const fortuneValue = options.fortuneValue ?? 0
|
||||
const isResistance = options.isResistance ?? false
|
||||
const isCombat = options.isCombat ?? false
|
||||
const woundMalus = options.woundMalus ?? 0
|
||||
const armorMalus = options.armorMalus ?? 0
|
||||
const skillValue = options.skillValue ?? 0
|
||||
const woundLevelId = options.woundLevel ?? 0
|
||||
const destGaugeFull = options.destGaugeFull ?? false
|
||||
const fortuneValue = options.fortuneValue ?? 0
|
||||
const isResistance = options.isResistance ?? false
|
||||
const isCombat = options.isCombat ?? false
|
||||
const isRangedDefense = options.isRangedDefense ?? false
|
||||
const weaponType = options.weaponType ?? "melee"
|
||||
const weaponName = options.weaponName ?? null
|
||||
const weaponDegats = options.weaponDegats ?? "0"
|
||||
const woundLabel = woundLevelId > 0
|
||||
const weaponType = options.weaponType ?? "melee"
|
||||
const weaponName = options.weaponName ?? null
|
||||
const weaponDegats = options.weaponDegats ?? "0"
|
||||
const availableTargets = options.availableTargets ?? []
|
||||
const isRangedAttack = isCombat && !isRangedDefense && weaponType === "distance"
|
||||
const woundLabel = woundLevelId > 0
|
||||
? game.i18n.localize(SYSTEM.WOUND_LEVELS[woundLevelId]?.label ?? "")
|
||||
: null
|
||||
|
||||
@@ -63,6 +66,11 @@ export class CelestopolRoll extends Roll {
|
||||
const v = i - 8
|
||||
return { value: v, label: v > 0 ? `+${v}` : `${v}` }
|
||||
})
|
||||
const rangedModChoices = SYSTEM.RANGED_MODIFIERS.map(m => ({
|
||||
id: m.id,
|
||||
value: m.value,
|
||||
label: game.i18n.localize(m.label),
|
||||
}))
|
||||
|
||||
const dialogContext = {
|
||||
actorName: options.actorName,
|
||||
@@ -71,20 +79,22 @@ export class CelestopolRoll extends Roll {
|
||||
skillValue,
|
||||
woundMalus,
|
||||
woundLabel,
|
||||
difficultyChoices: SYSTEM.DIFFICULTY_CHOICES,
|
||||
defaultDifficulty: options.difficulty ?? "normal",
|
||||
destGaugeFull,
|
||||
defaultRollMoonDie: options.rollMoonDie ?? false,
|
||||
isResistance,
|
||||
isCombat,
|
||||
isRangedDefense,
|
||||
isRangedAttack,
|
||||
weaponType,
|
||||
weaponName,
|
||||
weaponDegats,
|
||||
modifierChoices,
|
||||
aspectChoices,
|
||||
situationChoices,
|
||||
rangedModChoices,
|
||||
availableTargets,
|
||||
fortuneValue,
|
||||
armorMalus,
|
||||
destGaugeFull,
|
||||
defaultRollMoonDie: options.rollMoonDie ?? false,
|
||||
isResistance,
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
@@ -104,8 +114,40 @@ export class CelestopolRoll extends Roll {
|
||||
const wrap = dialog.element.querySelector('.roll-dialog-content')
|
||||
if (!wrap) return
|
||||
|
||||
function hasMalus(mod, asp, sit) {
|
||||
return woundMalus < 0 || mod < 0 || asp < 0 || sit < 0
|
||||
// Sélection de cible PNJ : masque le champ Corps PNJ (valeur cachée)
|
||||
const targetSelect = wrap.querySelector('#targetSelect')
|
||||
const corpsPnjRow = wrap.querySelector('#corps-pnj-row')
|
||||
const targetConfirmedRow = wrap.querySelector('#target-confirmed-row')
|
||||
const targetConfirmedName = wrap.querySelector('#target-confirmed-name')
|
||||
|
||||
function applyTargetSelection() {
|
||||
if (!targetSelect) return
|
||||
const selectedOption = targetSelect.options[targetSelect.selectedIndex]
|
||||
const val = parseFloat(targetSelect.value)
|
||||
const corpsPnjInput = wrap.querySelector('#corpsPnj')
|
||||
if (targetSelect.value && !isNaN(val)) {
|
||||
// Cible sélectionnée : masquer la valeur, afficher le nom
|
||||
if (corpsPnjRow) corpsPnjRow.style.display = 'none'
|
||||
if (targetConfirmedRow) targetConfirmedRow.style.display = ''
|
||||
if (targetConfirmedName) targetConfirmedName.textContent = selectedOption?.text ?? ''
|
||||
if (corpsPnjInput) {
|
||||
corpsPnjInput.value = val
|
||||
corpsPnjInput.dispatchEvent(new Event('input'))
|
||||
}
|
||||
} else {
|
||||
// Saisie manuelle
|
||||
if (corpsPnjRow) corpsPnjRow.style.display = ''
|
||||
if (targetConfirmedRow) targetConfirmedRow.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
if (targetSelect) {
|
||||
targetSelect.addEventListener('change', applyTargetSelection)
|
||||
applyTargetSelection()
|
||||
}
|
||||
|
||||
function hasMalus(mod, asp, sit, ranged) {
|
||||
return woundMalus < 0 || armorMalus < 0 || mod < 0 || asp < 0 || sit < 0 || ranged < 0
|
||||
}
|
||||
|
||||
function update() {
|
||||
@@ -114,6 +156,7 @@ export class CelestopolRoll extends Roll {
|
||||
const modifier = autoSucc ? 0 : (parseInt(rawMod ?? 0) || 0)
|
||||
const aspectMod = parseInt(wrap.querySelector('#aspectModifier')?.value ?? 0) || 0
|
||||
const situMod = parseInt(wrap.querySelector('#situationMod')?.value ?? 0) || 0
|
||||
const rangedMod = parseInt(wrap.querySelector('#rangedMod')?.value ?? 0) || 0
|
||||
const useDestin = wrap.querySelector('#useDestin')?.checked
|
||||
const useFort = wrap.querySelector('#useFortune')?.checked
|
||||
const puiser = wrap.querySelector('#puiserRessources')?.checked
|
||||
@@ -122,7 +165,7 @@ export class CelestopolRoll extends Roll {
|
||||
// En résistance : pas de "Puiser" possible
|
||||
const puiserRow = wrap.querySelector('#puiser-row')
|
||||
if (puiserRow) {
|
||||
if (!isResistance && hasMalus(modifier, aspectMod, situMod)) {
|
||||
if (!isResistance && hasMalus(modifier, aspectMod, situMod, rangedMod)) {
|
||||
puiserRow.style.display = ''
|
||||
} else {
|
||||
puiserRow.style.display = 'none'
|
||||
@@ -135,7 +178,9 @@ export class CelestopolRoll extends Roll {
|
||||
const effMod = puiser ? Math.max(0, modifier) : modifier
|
||||
const effAspect = puiser ? Math.max(0, aspectMod) : aspectMod
|
||||
const effSit = puiser ? Math.max(0, situMod) : situMod
|
||||
const totalMod = skillValue + effWound + effMod + effAspect + effSit
|
||||
const effArmor = puiser ? 0 : armorMalus
|
||||
const effRanged = puiser ? Math.max(0, rangedMod) : rangedMod
|
||||
const totalMod = skillValue + effWound + effMod + effAspect + effSit + effArmor + effRanged
|
||||
|
||||
let formula
|
||||
if (autoSucc) {
|
||||
@@ -153,7 +198,7 @@ export class CelestopolRoll extends Roll {
|
||||
if (previewEl) previewEl.textContent = formula
|
||||
}
|
||||
|
||||
wrap.querySelectorAll('#modifier, #aspectModifier, #situationMod, #useDestin, #useFortune, #puiserRessources, #corpsPnj')
|
||||
wrap.querySelectorAll('#modifier, #aspectModifier, #situationMod, #rangedMod, #useDestin, #useFortune, #puiserRessources, #corpsPnj')
|
||||
.forEach(el => {
|
||||
el.addEventListener('change', update)
|
||||
el.addEventListener('input', update)
|
||||
@@ -179,16 +224,18 @@ export class CelestopolRoll extends Roll {
|
||||
|
||||
if (!rollContext) return null
|
||||
|
||||
// En combat : Corps PNJ = seuil direct (pas le sélect difficulté)
|
||||
// En combat : Corps PNJ = seuil direct ; sinon seuil fixe = 11
|
||||
const corpsPnj = isCombat ? (parseInt(rollContext.corpsPnj ?? 7) || 7) : null
|
||||
const difficulty = isCombat ? "combat" : (rollContext.difficulty ?? "normal")
|
||||
const difficulty = isCombat ? "combat" : "standard"
|
||||
const diffConfig = isCombat
|
||||
? { value: corpsPnj, label: "CELESTOPOL.Combat.corpsPnj" }
|
||||
: (SYSTEM.DIFFICULTY_CHOICES[difficulty] ?? SYSTEM.DIFFICULTY_CHOICES.normal)
|
||||
: { value: 11, label: "CELESTOPOL.Roll.threshold" }
|
||||
const autoSuccess = rollContext.modifier === "auto"
|
||||
const modifier = autoSuccess ? 0 : (parseInt(rollContext.modifier ?? 0) || 0)
|
||||
const aspectMod = parseInt(rollContext.aspectModifier ?? 0) || 0
|
||||
const situationMod = parseInt(rollContext.situationMod ?? 0) || 0
|
||||
const rangedMod = isRangedAttack ? (parseInt(rollContext.rangedMod ?? 0) || 0) : 0
|
||||
const isOpposition = !isCombat && !isResistance && (rollContext.isOpposition === true || rollContext.isOpposition === "true")
|
||||
const useDestin = destGaugeFull && (rollContext.useDestin === true || rollContext.useDestin === "true")
|
||||
const useFortune = fortuneValue > 0 && (rollContext.useFortune === true || rollContext.useFortune === "true")
|
||||
const puiserRessources = rollContext.puiserRessources === true || rollContext.puiserRessources === "true"
|
||||
@@ -200,13 +247,15 @@ export class CelestopolRoll extends Roll {
|
||||
|
||||
// Puiser dans ses ressources → ignorer tous les malus
|
||||
const effectiveWoundMalus = effectivePuiser ? 0 : woundMalus
|
||||
const effectiveArmorMalus = effectivePuiser ? 0 : armorMalus
|
||||
const effectiveModifier = effectivePuiser ? Math.max(0, modifier) : modifier
|
||||
const effectiveAspectMod = effectivePuiser ? Math.max(0, aspectMod) : aspectMod
|
||||
const effectiveSituationMod = effectivePuiser ? Math.max(0, situationMod) : situationMod
|
||||
const effectiveRangedMod = effectivePuiser ? Math.max(0, rangedMod) : rangedMod
|
||||
|
||||
// Fortune : 1d8 + 8 ; Destin : 3d8 ; sinon : 2d8
|
||||
const nbDice = (!isResistance && useDestin) ? 3 : 2
|
||||
const totalModifier = skillValue + effectiveWoundMalus + effectiveAspectMod + effectiveModifier + effectiveSituationMod
|
||||
const totalModifier = skillValue + effectiveWoundMalus + effectiveAspectMod + effectiveModifier + effectiveSituationMod + effectiveArmorMalus + effectiveRangedMod
|
||||
const formula = (!isResistance && useFortune)
|
||||
? buildFormula(1, totalModifier + 8)
|
||||
: buildFormula(nbDice, totalModifier)
|
||||
@@ -232,11 +281,13 @@ export class CelestopolRoll extends Roll {
|
||||
woundMalus: effectiveWoundMalus,
|
||||
autoSuccess,
|
||||
isResistance,
|
||||
isOpposition,
|
||||
isCombat,
|
||||
isRangedDefense,
|
||||
weaponType,
|
||||
weaponName,
|
||||
weaponDegats,
|
||||
rangedMod: effectiveRangedMod,
|
||||
useDestin: !isResistance && useDestin,
|
||||
useFortune: !isResistance && useFortune,
|
||||
puiserRessources: effectivePuiser,
|
||||
@@ -294,9 +345,11 @@ export class CelestopolRoll extends Roll {
|
||||
}
|
||||
}
|
||||
|
||||
// Mémoriser les préférences
|
||||
updateData["system.prefs.rollMoonDie"] = rollData.rollMoonDie
|
||||
updateData["system.prefs.difficulty"] = difficulty
|
||||
// Mémoriser les préférences (protagonistes uniquement — le modèle NPC n'a pas de champ prefs)
|
||||
if (actor.type === "character") {
|
||||
updateData["system.prefs.rollMoonDie"] = rollData.rollMoonDie
|
||||
updateData["system.prefs.difficulty"] = difficulty
|
||||
}
|
||||
|
||||
await actor.update(updateData)
|
||||
}
|
||||
@@ -306,11 +359,8 @@ export class CelestopolRoll extends Roll {
|
||||
|
||||
/**
|
||||
* Détermine succès/échec selon la marge (total − seuil).
|
||||
* - Marge ≥ 5 → succès critique
|
||||
* - Marge > 0 → succès
|
||||
* - Marge = 0 → succès (ou égalité en combat)
|
||||
* - Marge ≤ −5 → échec critique
|
||||
* - Marge < 0 → échec
|
||||
* Seuil : 11 pour les tests normaux, Corps PNJ pour le combat.
|
||||
* Pas de succès/échec critique — seul le Dé de la Lune produit des résultats exceptionnels.
|
||||
*/
|
||||
computeResult() {
|
||||
if (this.options.autoSuccess) {
|
||||
@@ -318,9 +368,15 @@ export class CelestopolRoll extends Roll {
|
||||
this.options.margin = null
|
||||
return
|
||||
}
|
||||
// En test d'opposition : pas de résultat calculé — le MJ décide
|
||||
if (this.options.isOpposition) {
|
||||
this.options.resultType = "opposition"
|
||||
this.options.margin = null
|
||||
return
|
||||
}
|
||||
const threshold = this.options.isCombat
|
||||
? (this.options.difficultyValue ?? 0)
|
||||
: (SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0)
|
||||
: 11
|
||||
if (threshold === 0) {
|
||||
this.options.resultType = "unknown"
|
||||
this.options.margin = null
|
||||
@@ -330,10 +386,11 @@ export class CelestopolRoll extends Roll {
|
||||
this.options.margin = margin
|
||||
if (this.options.isCombat && margin === 0) {
|
||||
this.options.resultType = "tie"
|
||||
} else if (margin >= 5) this.options.resultType = "critical-success"
|
||||
else if (margin >= 0) this.options.resultType = "success"
|
||||
else if (margin <= -5) this.options.resultType = "critical-failure"
|
||||
else this.options.resultType = "failure"
|
||||
} else if (margin >= 0) {
|
||||
this.options.resultType = "success"
|
||||
} else {
|
||||
this.options.resultType = "failure"
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
@@ -350,7 +407,7 @@ export class CelestopolRoll extends Roll {
|
||||
const diceSum = diceResults.reduce((a, b) => a + b, 0)
|
||||
const threshold = this.options.isCombat
|
||||
? (this.options.difficultyValue ?? 0)
|
||||
: (SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.value ?? 0)
|
||||
: 11
|
||||
const margin = this.options.margin
|
||||
const woundMalus = this.options.woundMalus ?? 0
|
||||
const skillValue = this.options.skillValue ?? 0
|
||||
@@ -365,18 +422,21 @@ export class CelestopolRoll extends Roll {
|
||||
const moonResultType = this.options.moonResultType ?? null
|
||||
|
||||
const resultClassMap = {
|
||||
"critical-success": "critical-success",
|
||||
"success": "success",
|
||||
"tie": "tie",
|
||||
"failure": "failure",
|
||||
"critical-failure": "critical-failure",
|
||||
"unknown": "",
|
||||
"success": "success",
|
||||
"tie": "tie",
|
||||
"failure": "failure",
|
||||
"opposition": "opposition",
|
||||
"unknown": "",
|
||||
}
|
||||
|
||||
// Libellé de difficulté : en combat, afficher "Corps PNJ : N"
|
||||
const isOpposition = this.options.isOpposition ?? false
|
||||
|
||||
// Libellé de difficulté : en combat "Corps PNJ : N", en opposition "vs ?", sinon "Seuil : 11"
|
||||
const difficultyLabel = this.options.isCombat
|
||||
? `${game.i18n.localize("CELESTOPOL.Combat.corpsPnj")} : ${threshold}`
|
||||
: game.i18n.localize(SYSTEM.DIFFICULTY_CHOICES[this.options.difficulty]?.label ?? "")
|
||||
: isOpposition
|
||||
? `${game.i18n.localize("CELESTOPOL.Roll.oppositionVs")}`
|
||||
: `${game.i18n.localize("CELESTOPOL.Roll.threshold")} : 11`
|
||||
|
||||
return {
|
||||
cssClass: [SYSTEM.id, "dice-roll"].join(" "),
|
||||
@@ -394,11 +454,10 @@ export class CelestopolRoll extends Roll {
|
||||
isSuccess: this.isSuccess,
|
||||
isFailure: this.isFailure,
|
||||
isTie: this.isTie,
|
||||
isCriticalSuccess: this.isCriticalSuccess,
|
||||
isCriticalFailure: this.isCriticalFailure,
|
||||
isOpposition,
|
||||
difficulty: this.options.difficulty,
|
||||
difficultyLabel,
|
||||
difficultyValue: threshold,
|
||||
difficultyValue: isOpposition ? null : threshold,
|
||||
margin,
|
||||
marginAbs: margin !== null ? Math.abs(margin) : null,
|
||||
marginAbove: margin !== null && margin >= 0,
|
||||
@@ -419,6 +478,8 @@ export class CelestopolRoll extends Roll {
|
||||
weaponType: this.options.weaponType ?? null,
|
||||
isRangedDefense: this.options.isRangedDefense ?? false,
|
||||
woundTaken: this.options.woundTaken ?? null,
|
||||
situationMod: this.options.situationMod ?? 0,
|
||||
rangedMod: this.options.rangedMod ?? 0,
|
||||
// Dé de lune
|
||||
hasMoonDie: moonDieResult !== null,
|
||||
moonDieResult,
|
||||
@@ -442,4 +503,44 @@ export class CelestopolRoll extends Roll {
|
||||
: `<strong>${skillLocalized}</strong>`
|
||||
return super.toMessage({ flavor, ...messageData }, { rollMode })
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance le dé de la Lune de façon autonome (hors test de spécialisation).
|
||||
* Affiche un carte de chat avec le résultat narratif ET l'interprétation chance.
|
||||
* @param {Actor|null} actor Acteur initiateur (pour le speaker du message)
|
||||
*/
|
||||
static async rollMoonStandalone(actor = null) {
|
||||
const roll = await new Roll("1d8").evaluate()
|
||||
const result = roll.total
|
||||
const face = SYSTEM.MOON_DIE_FACES[result] ?? null
|
||||
const resultType = face ? SYSTEM.MOON_RESULT_TYPES[face.result] ?? null : null
|
||||
const isGoodFortune = result <= 4
|
||||
|
||||
const templateData = {
|
||||
result,
|
||||
moonFaceSymbol: face?.symbol ?? "",
|
||||
moonFaceLabel: face ? game.i18n.localize(face.label) : "",
|
||||
moonResultLabel: resultType ? game.i18n.localize(resultType.label) : "",
|
||||
moonResultDesc: resultType ? game.i18n.localize(resultType.desc) : "",
|
||||
moonResultClass: resultType?.cssClass ?? "",
|
||||
isGoodFortune,
|
||||
actorName: actor?.name ?? null,
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-celestopol/templates/moon-standalone.hbs",
|
||||
templateData
|
||||
)
|
||||
|
||||
const speaker = actor
|
||||
? ChatMessage.getSpeaker({ actor })
|
||||
: { alias: game.i18n.localize("CELESTOPOL.Moon.standalone") }
|
||||
|
||||
await ChatMessage.create({
|
||||
content,
|
||||
speaker,
|
||||
rolls: [roll],
|
||||
style: CONST.CHAT_MESSAGE_STYLES?.ROLL ?? 5,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
vision: persoAttrField(),
|
||||
})
|
||||
|
||||
// Factions — score entier direct (0-9)
|
||||
// Factions — niveau de relation -4 (hostile) à +4 (allié), 0 = neutre
|
||||
const factionField = () => new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 9 }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: -4, max: 4 }),
|
||||
})
|
||||
schema.factions = new fields.SchemaField({
|
||||
pinkerton: factionField(),
|
||||
@@ -89,11 +89,11 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
cour: factionField(),
|
||||
perso1: new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: -4, max: 4 }),
|
||||
}),
|
||||
perso2: new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: -4, max: 4 }),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -114,8 +114,9 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
})
|
||||
|
||||
// Description & notes
|
||||
schema.description = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.descriptionPhysique = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.descriptionPsychologique = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
|
||||
|
||||
// Données biographiques
|
||||
schema.biodata = new fields.SchemaField({
|
||||
@@ -152,6 +153,20 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
|
||||
// XP dépensée = somme des montants du log
|
||||
this.xp.depense = this.xp.log.reduce((sum, entry) => sum + entry.montant, 0)
|
||||
|
||||
// Malus d'armure(s) équipée(s)
|
||||
this.armorMalus = this.getArmorMalus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le malus total des armures équipées portées par le protagoniste.
|
||||
* @returns {number}
|
||||
*/
|
||||
getArmorMalus() {
|
||||
if (!this.parent) return 0
|
||||
return -(this.parent.itemTypes.armure
|
||||
.filter(a => a.system.equipped && a.system.malus > 0)
|
||||
.reduce((sum, a) => sum + a.system.malus, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,6 +198,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
skillLabel: skill.label,
|
||||
skillValue: skill.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
armorMalus: this.getArmorMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
difficulty: this.prefs.difficulty,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
@@ -213,6 +229,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
skillLabel: "CELESTOPOL.Roll.resistanceTest",
|
||||
skillValue: statData.res,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
armorMalus: this.getArmorMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
isResistance: true,
|
||||
rollMoonDie: false,
|
||||
@@ -222,6 +239,38 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Collecte les tokens PNJs disponibles comme cibles de combat.
|
||||
* Priorise le combat tracker, sinon les tokens ciblés par l'utilisateur.
|
||||
* @returns {Array<{id:string, name:string, corps:number}>}
|
||||
*/
|
||||
_getCombatTargets() {
|
||||
const toEntry = actor => ({
|
||||
id: actor.id,
|
||||
name: actor.name,
|
||||
corps: actor.system.stats?.corps?.res ?? 0,
|
||||
})
|
||||
// Priorité 1 : PNJs dans le combat actif
|
||||
if (game.combat?.active) {
|
||||
const list = game.combat.combatants
|
||||
.filter(c => c.actor?.type === "npc" && c.actorId !== this.parent.id)
|
||||
.map(c => toEntry(c.actor))
|
||||
if (list.length) return list
|
||||
}
|
||||
// Priorité 2 : Tokens ciblés par le joueur
|
||||
const targeted = [...(game.user?.targets ?? [])]
|
||||
.filter(t => t.actor?.type === "npc")
|
||||
.map(t => toEntry(t.actor))
|
||||
if (targeted.length) return targeted
|
||||
// Priorité 3 : Tous les tokens NPC de la scène active
|
||||
if (canvas?.tokens?.placeables) {
|
||||
return canvas.tokens.placeables
|
||||
.filter(t => t.actor?.type === "npc" && t.actor.id !== this.parent.id)
|
||||
.map(t => toEntry(t.actor))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance une attaque avec une arme.
|
||||
* Mêlée : test Échauffourée vs Corps PNJ ; échec → blessure joueur.
|
||||
@@ -238,24 +287,26 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
if (!echauffouree) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId: "corps",
|
||||
skillId: "echauffouree",
|
||||
statLabel: SYSTEM.STATS.corps.label,
|
||||
skillLabel: SYSTEM.SKILLS.corps.echauffouree.label,
|
||||
skillValue: echauffouree.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
isRangedDefense: false,
|
||||
weaponType: item.system.type,
|
||||
weaponName: item.name,
|
||||
weaponDegats: item.system.degats,
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId: "corps",
|
||||
skillId: "echauffouree",
|
||||
statLabel: SYSTEM.STATS.corps.label,
|
||||
skillLabel: SYSTEM.SKILLS.corps.echauffouree.label,
|
||||
skillValue: echauffouree.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
armorMalus: this.getArmorMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
isRangedDefense: false,
|
||||
weaponType: item.system.type,
|
||||
weaponName: item.name,
|
||||
weaponDegats: item.system.degats,
|
||||
availableTargets: this._getCombatTargets(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -274,24 +325,26 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
if (!mobilite) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId: "corps",
|
||||
skillId: "mobilite",
|
||||
statLabel: SYSTEM.STATS.corps.label,
|
||||
skillLabel: SYSTEM.SKILLS.corps.mobilite.label,
|
||||
skillValue: mobilite.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
isRangedDefense: true,
|
||||
weaponType: "distance",
|
||||
weaponName: item.name,
|
||||
weaponDegats: "0",
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId: "corps",
|
||||
skillId: "mobilite",
|
||||
statLabel: SYSTEM.STATS.corps.label,
|
||||
skillLabel: SYSTEM.SKILLS.corps.mobilite.label,
|
||||
skillValue: mobilite.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
armorMalus: this.getArmorMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
isRangedDefense: true,
|
||||
weaponType: "distance",
|
||||
weaponName: item.name,
|
||||
weaponDegats: "0",
|
||||
availableTargets: this._getCombatTargets(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export class CelestopolArmure extends foundry.abstract.TypeDataModel {
|
||||
return {
|
||||
protection: new fields.NumberField({ ...reqInt, initial: 1, min: 1, max: 2 }),
|
||||
malus: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 2 }),
|
||||
equipped: new fields.BooleanField({ initial: false }),
|
||||
description: new fields.HTMLField({ required: true, textSearch: true }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
const schema = {}
|
||||
|
||||
schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
schema.npcType = new fields.StringField({ required: true, nullable: false, initial: "standard",
|
||||
choices: Object.keys(SYSTEM.NPC_TYPES) })
|
||||
schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
schema.faction = new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
schema.initiative = new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||
|
||||
schema.anomaly = new fields.SchemaField({
|
||||
@@ -15,43 +18,27 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
const skillField = (label) => new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: label }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
// PNJs : 4 domaines uniquement (pas de sous-compétences)
|
||||
const domainField = (statId) => new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }),
|
||||
res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
})
|
||||
|
||||
const statField = (statId) => {
|
||||
const skills = SYSTEM.SKILLS[statId]
|
||||
const skillSchema = {}
|
||||
for (const [key, skill] of Object.entries(skills)) {
|
||||
skillSchema[key] = skillField(skill.label)
|
||||
}
|
||||
return new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }),
|
||||
res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }), // res + wound malus
|
||||
...skillSchema,
|
||||
})
|
||||
}
|
||||
|
||||
schema.stats = new fields.SchemaField({
|
||||
ame: statField("ame"),
|
||||
corps: statField("corps"),
|
||||
coeur: statField("coeur"),
|
||||
esprit: statField("esprit"),
|
||||
ame: domainField("ame"),
|
||||
corps: domainField("corps"),
|
||||
coeur: domainField("coeur"),
|
||||
esprit: domainField("esprit"),
|
||||
})
|
||||
|
||||
schema.blessures = new fields.SchemaField({
|
||||
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
schema.prefs = new fields.SchemaField({
|
||||
rollMoonDie: new fields.BooleanField({ required: true, initial: false }),
|
||||
difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
|
||||
})
|
||||
|
||||
schema.description = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.histoire = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.descriptionPhysique = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
|
||||
|
||||
return schema
|
||||
}
|
||||
@@ -61,11 +48,12 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
const malus = this.getWoundMalus()
|
||||
// Initiative PNJ : valeur du Domaine Corps
|
||||
// Initiative PNJ : valeur du Domaine Corps (avec malus blessures)
|
||||
this.initiative = Math.max(0, this.stats.corps.res + malus)
|
||||
for (const stat of Object.values(this.stats)) {
|
||||
stat.actuel = Math.max(0, stat.res + malus)
|
||||
}
|
||||
this.armorMalus = this.getArmorMalus()
|
||||
}
|
||||
|
||||
getWoundMalus() {
|
||||
@@ -73,22 +61,43 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
|
||||
return SYSTEM.WOUND_LEVELS[lvl]?.malus ?? 0
|
||||
}
|
||||
|
||||
async roll(statId, skillId) {
|
||||
/** Somme des malus des armures équipées (valeur négative ou 0). */
|
||||
getArmorMalus() {
|
||||
const armures = this.parent?.itemTypes?.armure ?? []
|
||||
return armures
|
||||
.filter(a => a.system.equipped)
|
||||
.reduce((sum, a) => sum + (a.system.malus ? -Math.abs(a.system.malus) : 0), 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance un jet sur un domaine (Âme/Corps/Cœur/Esprit).
|
||||
* Le label affiché tient compte du type de PNJ (standard vs antagoniste).
|
||||
*/
|
||||
async roll(statId) {
|
||||
const { CelestopolRoll } = await import("../documents/roll.mjs")
|
||||
const skill = this.stats[statId][skillId]
|
||||
if (!skill) return null
|
||||
const statData = this.stats[statId]
|
||||
if (!statData) return null
|
||||
|
||||
const isAntagoniste = this.npcType === "antagoniste"
|
||||
const skillLabel = isAntagoniste
|
||||
? SYSTEM.ANTAGONISTE_STATS[statId]?.label
|
||||
: SYSTEM.STATS[statId]?.label
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId,
|
||||
skillId,
|
||||
skillLabel: skill.label,
|
||||
skillValue: skill.value,
|
||||
skillLabel,
|
||||
skillValue: statData.res,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
difficulty: this.prefs.difficulty,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
armorMalus: this.getArmorMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
})
|
||||
}
|
||||
|
||||
/** Alias pour compatibilité avec le handler _onRoll (clic sans skillId). */
|
||||
async rollResistance(statId) {
|
||||
return this.roll(statId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user