IMplémentation de la ajorité des remarques de Nepherius

This commit is contained in:
2026-04-06 17:48:30 +02:00
parent a3f7b11f82
commit 1022597bf8
51 changed files with 1900 additions and 443 deletions

View File

@@ -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(),
})
}
}

View File

@@ -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 }),
}
}

View File

@@ -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)
}
}