DIvers rework de CSS/LESS et améliorations de messages/layout

This commit is contained in:
2026-05-03 20:20:30 +02:00
parent 4f8735f86f
commit 267f992874
113 changed files with 11565 additions and 843 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ import LesOubliesItemSheet from "./base-item-sheet.mjs"
export default class LesOubliesArmeSheet extends LesOubliesItemSheet {
static PARTS = {
sheet: {
template: "systems/fvtt-les-oublies/templates/item-arme-sheet.hbs",
template: "systems/fvtt-les-oublies/templates/item-arme-sheet-v2.hbs",
},
}
}
@@ -32,9 +32,11 @@ export default class LesOubliesActorSheet extends HandlebarsApplicationMixin(fou
rollSkill: LesOubliesActorSheet.#onRollSkill,
useWeapon: LesOubliesActorSheet.#onUseWeapon,
resolveWeaponDamage: LesOubliesActorSheet.#onResolveWeaponDamage,
toggleEquipped: LesOubliesActorSheet.#onToggleEquipped,
useSpell: LesOubliesActorSheet.#onUseSpell,
openCombatPreset: LesOubliesActorSheet.#onOpenCombatPreset,
openThreadHarvest: LesOubliesActorSheet.#onOpenThreadHarvest,
openLinkedActor: LesOubliesActorSheet.#onOpenLinkedActor,
},
}
@@ -49,6 +51,14 @@ export default class LesOubliesActorSheet extends HandlebarsApplicationMixin(fou
}
async _prepareContext() {
const config = CONFIG.LESOUBLIES
const enriched = await LesOubliesUtility.prepareEnrichedHtml("Actor", this.document.type, this.document.system)
const choiceSets = {
profileOptions: config.profiles.map((profile) => ({ value: profile.id, label: profile.label })),
personnageSizeOptions: LesOubliesUtility.createRangeChoices(1, 4, config.sizes),
creatureSizeOptions: LesOubliesUtility.createRangeChoices(1, 8, config.sizes),
}
return {
actor: this.document,
system: this.document.system,
@@ -59,8 +69,10 @@ export default class LesOubliesActorSheet extends HandlebarsApplicationMixin(fou
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isGM: game.user.isGM,
config: CONFIG.LESOUBLIES,
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.biodata?.description ?? this.document.system.description ?? "", { async: true }),
config,
choiceSets,
enriched,
enrichedDescription: foundry.utils.getProperty(enriched, "biodata.description") ?? foundry.utils.getProperty(enriched, "description") ?? "",
}
}
@@ -99,6 +111,7 @@ export default class LesOubliesActorSheet extends HandlebarsApplicationMixin(fou
static async #onEditImage(event, target) {
const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr)
const FilePicker = foundry.applications.apps.FilePicker.implementation
const fp = new FilePicker({
current,
type: "image",
@@ -113,11 +126,17 @@ export default class LesOubliesActorSheet extends HandlebarsApplicationMixin(fou
const type = target.dataset.type
if (!type) return
const label = game.i18n.localize(`TYPES.Item.${type}`)
return this.document.createEmbeddedDocuments("Item", [{
const itemData = {
name: label,
type,
img: LesOubliesUtility.getDefaultItemImage(type),
}])
}
if (type === "competence") {
itemData.system = {
profileKey: CONFIG.LESOUBLIES.profiles[0]?.id ?? "",
}
}
return this.document.createEmbeddedDocuments("Item", [itemData])
}
static async #onEditItem(event, target) {
@@ -168,6 +187,14 @@ export default class LesOubliesActorSheet extends HandlebarsApplicationMixin(fou
await this.document.openDamageDialog({ itemId })
}
static async #onToggleEquipped(event, target) {
const itemId = target.dataset.itemId
if (!itemId) return
const item = this.document.items.get(itemId)
if (!item || !("equipped" in (item.system ?? {}))) return
await item.update({ "system.equipped": !item.system.equipped })
}
static async #onUseSpell(event, target) {
const itemId = target.dataset.itemId
if (!itemId) return
@@ -183,4 +210,11 @@ export default class LesOubliesActorSheet extends HandlebarsApplicationMixin(fou
static async #onOpenThreadHarvest() {
await this.document.openThreadHarvestDialog()
}
static async #onOpenLinkedActor(event, target) {
const actorId = target.dataset.actorId
if (!actorId) return
const actor = game.actors.get(actorId)
if (actor) actor.sheet.render(true)
}
}
@@ -1,5 +1,7 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
import { LesOubliesUtility } from "../../les-oublies-utility.js"
export default class LesOubliesItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
@@ -33,6 +35,62 @@ export default class LesOubliesItemSheet extends HandlebarsApplicationMixin(foun
}
async _prepareContext() {
const config = CONFIG.LESOUBLIES
const enriched = await LesOubliesUtility.prepareEnrichedHtml("Item", this.document.type, this.document.system)
const skillLabels = Object.fromEntries(Object.entries(config.skills).map(([key, skill]) => [key, skill.label]))
const choiceSets = {
profileOptions: config.profiles.map((profile) => ({ value: profile.id, label: profile.label })),
skillOptions: LesOubliesUtility.createChoices(Object.keys(config.skills), skillLabels),
spellSkillOptions: LesOubliesUtility.createChoices(["magie", "onirologie", "chimerisme"], skillLabels),
weaponCategoryOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createChoices(Object.keys(config.weaponCategoryLabels), config.weaponCategoryLabels),
this.document.system.category,
),
weaponOriginOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createChoices(Object.keys(config.weaponOriginLabels), config.weaponOriginLabels),
this.document.system.origin,
),
armorStateOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createChoices(Object.keys(config.armorStateLabels), config.armorStateLabels),
this.document.system.state,
),
equipmentCategoryOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createChoices(Object.keys(config.equipmentCategoryLabels), config.equipmentCategoryLabels),
this.document.system.category,
),
spellTraditionOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createChoices(Object.keys(config.spellTraditionLabels), config.spellTraditionLabels),
this.document.system.tradition,
),
polarityOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createChoices(Object.keys(config.polarityLabels), config.polarityLabels),
this.document.system.polarity,
),
stackingOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createChoices(Object.keys(config.stackingLabels), config.stackingLabels),
this.document.system.stacking,
),
companyPowerScopeOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createChoices(Object.keys(config.companyPowerScopeLabels), config.companyPowerScopeLabels),
this.document.system.scope,
),
companyPowerModeOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createChoices(Object.keys(config.companyPowerModeLabels), config.companyPowerModeLabels),
this.document.system.effectMode,
),
raceSizeOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.createRangeChoices(1, 4, config.sizes),
this.document.system.size,
),
mainRaceOptions: LesOubliesUtility.ensureChoice(
LesOubliesUtility.sortByName(game.items.filter((item) => item.type === "race")).map((item) => ({
value: item.name,
label: item.name,
})),
this.document.system.mainRace,
),
}
return {
item: this.document,
system: this.document.system,
@@ -43,8 +101,10 @@ export default class LesOubliesItemSheet extends HandlebarsApplicationMixin(foun
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isGM: game.user.isGM,
config: CONFIG.LESOUBLIES,
enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description ?? "", { async: true }),
config,
choiceSets,
enriched,
enrichedDescription: foundry.utils.getProperty(enriched, "description") ?? "",
}
}
@@ -1,9 +1,14 @@
import LesOubliesActorSheet from "./base-actor-sheet.mjs"
import { LesOubliesUtility } from "../../les-oublies-utility.js"
export default class LesOubliesCompagnieSheet extends LesOubliesActorSheet {
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
classes: [...super.DEFAULT_OPTIONS.classes, "compagnie"],
actions: {
...super.DEFAULT_OPTIONS.actions,
switchTab: LesOubliesCompagnieSheet.#onSwitchTab,
},
window: {
...super.DEFAULT_OPTIONS.window,
title: "TYPES.Actor.compagnie",
@@ -12,12 +17,30 @@ export default class LesOubliesCompagnieSheet extends LesOubliesActorSheet {
static PARTS = {
sheet: {
template: "systems/fvtt-les-oublies/templates/actor-compagnie-sheet.hbs",
template: "systems/fvtt-les-oublies/templates/actor-compagnie-sheet-v4.hbs",
},
}
_activeTab = "power"
#getTabs() {
const tabs = {
power: { id: "power", label: "Pouvoir", icon: "fa-solid fa-burst" },
members: { id: "members", label: "Membres & liens", icon: "fa-solid fa-people-group" },
notes: { id: "notes", label: "Notes", icon: "fa-solid fa-feather-pointed" },
}
for (const tab of Object.values(tabs)) {
tab.active = this._activeTab === tab.id
tab.cssClass = tab.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.members = (this.document.system.memberIds ?? []).map((id) => game.actors.get(id)).filter(Boolean)
context.captain = this.document.system.captainId ? game.actors.get(this.document.system.captainId) : null
context.shadow = this.document.system.ombreDuTourmentId ? game.actors.get(this.document.system.ombreDuTourmentId) : null
@@ -28,6 +51,19 @@ export default class LesOubliesCompagnieSheet extends LesOubliesActorSheet {
sourceLabel: game.actors.get(link.sourceId)?.name ?? link.sourceId,
targetLabel: game.actors.get(link.targetId)?.name ?? link.targetId,
}))
const actorChoices = LesOubliesUtility.sortByName(game.actors.filter((actor) => actor.type === "personnage")).map((actor) => ({
value: actor.id,
label: actor.name,
}))
context.choiceSets.captainOptions = LesOubliesUtility.ensureChoice(actorChoices, this.document.system.captainId, context.captain?.name)
context.choiceSets.shadowOptions = LesOubliesUtility.ensureChoice(actorChoices, this.document.system.ombreDuTourmentId, context.shadow?.name)
return context
}
static #onSwitchTab(event, target) {
const tab = target.dataset.tab
if (!tab || this._activeTab === tab) return
this._activeTab = tab
this.render()
}
}
+31 -1
View File
@@ -4,6 +4,10 @@ export default class LesOubliesCreatureSheet extends LesOubliesActorSheet {
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
classes: [...super.DEFAULT_OPTIONS.classes, "creature"],
actions: {
...super.DEFAULT_OPTIONS.actions,
switchTab: LesOubliesCreatureSheet.#onSwitchTab,
},
window: {
...super.DEFAULT_OPTIONS.window,
title: "TYPES.Actor.creature",
@@ -12,12 +16,31 @@ export default class LesOubliesCreatureSheet extends LesOubliesActorSheet {
static PARTS = {
sheet: {
template: "systems/fvtt-les-oublies/templates/actor-creature-sheet.hbs",
template: "systems/fvtt-les-oublies/templates/actor-creature-sheet-v5.hbs",
},
}
_activeTab = "overview"
#getTabs() {
const tabs = {
overview: { id: "overview", label: "Aperçu", icon: "fa-solid fa-dragon" },
aptitudes: { id: "aptitudes", label: "Aptitudes", icon: "fa-solid fa-book-open" },
combat: { id: "combat", label: "Combat & équipement", icon: "fa-solid fa-shield-halved" },
notes: { id: "notes", label: "Notes", icon: "fa-solid fa-feather-pointed" },
}
for (const tab of Object.values(tabs)) {
tab.active = this._activeTab === tab.id
tab.cssClass = tab.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.derived = this.document.getDerivedOverview()
context.skillGroups = this.document.getGroupedCompetences()
context.spells = this.document.getEmbeddedItems("sortilege")
@@ -26,4 +49,11 @@ export default class LesOubliesCreatureSheet extends LesOubliesActorSheet {
context.equipment = this.document.getEmbeddedItems("equipement")
return context
}
static #onSwitchTab(event, target) {
const tab = target.dataset.tab
if (!tab || this._activeTab === tab) return
this._activeTab = tab
this.render()
}
}
@@ -1,9 +1,17 @@
import LesOubliesActorSheet from "./base-actor-sheet.mjs"
import { LesOubliesUtility } from "../../les-oublies-utility.js"
export default class LesOubliesPersonnageSheet extends LesOubliesActorSheet {
static CREATION_DROP_TYPES = new Set(["race", "tribu", "metier"])
static DEFAULT_OPTIONS = {
...super.DEFAULT_OPTIONS,
classes: [...super.DEFAULT_OPTIONS.classes, "personnage"],
actions: {
...super.DEFAULT_OPTIONS.actions,
switchTab: LesOubliesPersonnageSheet.#onSwitchTab,
removeCreationItem: LesOubliesPersonnageSheet.#onRemoveCreationItem,
},
window: {
...super.DEFAULT_OPTIONS.window,
title: "TYPES.Actor.personnage",
@@ -12,26 +20,121 @@ export default class LesOubliesPersonnageSheet extends LesOubliesActorSheet {
static PARTS = {
sheet: {
template: "systems/fvtt-les-oublies/templates/actor-personnage-sheet.hbs",
template: "systems/fvtt-les-oublies/templates/actor-personnage-sheet-v14.hbs",
},
}
_activeTab = "overview"
#getTabs() {
const tabs = {
overview: { id: "overview", label: "Portrait", icon: "fa-solid fa-id-card" },
skills: { id: "skills", label: "Compétences", icon: "fa-solid fa-book-open" },
actions: { id: "actions", label: "Combat & magie", icon: "fa-solid fa-wand-sparkles" },
equipment: { id: "equipment", label: "Équipement", icon: "fa-solid fa-suitcase" },
notes: { id: "notes", label: "Notes", icon: "fa-solid fa-feather-pointed" },
}
for (const tab of Object.values(tabs)) {
tab.active = this._activeTab === tab.id
tab.cssClass = tab.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.derived = this.document.getDerivedOverview()
context.creation = {
race: this.document.getCreationItem("race"),
tribu: this.document.getCreationItem("tribu"),
metier: this.document.getCreationItem("metier"),
}
context.profileEntries = this.document.system.profils
context.creationSlots = [
this.#buildCreationSlot("race", context.creation.race, "Glissez une race ici depuis un compendium ou le répertoire des objets."),
this.#buildCreationSlot("tribu", context.creation.tribu, "Glissez une tribu ici depuis un compendium ou le répertoire des objets."),
this.#buildCreationSlot("metier", context.creation.metier, "Glissez un métier ici depuis un compendium ou le répertoire des objets."),
]
context.skillGroups = this.document.getGroupedCompetences()
const splitIndex = Math.ceil(context.skillGroups.length / 2)
context.skillColumns = [
context.skillGroups.slice(0, splitIndex),
context.skillGroups.slice(splitIndex),
]
context.spells = this.document.getEmbeddedItems("sortilege")
context.weapons = this.document.getEmbeddedItems("arme")
context.equippedWeapons = context.weapons.filter((item) => item.system.equipped)
context.armors = this.document.getEmbeddedItems("armure")
context.equipment = this.document.getEmbeddedItems("equipement")
context.companyPowers = this.document.getEmbeddedItems("pouvoircompagnie")
context.activeCompanyPower = context.derived.compagnie?.getEmbeddedItems?.("pouvoircompagnie")?.[0] ?? null
context.choiceSets.companyOptions = LesOubliesUtility.ensureChoice(
LesOubliesUtility.sortByName(game.actors.filter((actor) => actor.type === "compagnie")).map((actor) => ({
value: actor.id,
label: actor.name,
})),
this.document.system.references?.compagnieId,
context.derived.compagnie?.name,
)
return context
}
#buildCreationSlot(type, item, emptyHint) {
return {
type,
label: game.i18n.localize(`TYPES.Item.${type}`),
item,
emptyHint,
filledHint: "Déposez un autre élément du même type pour le remplacer.",
}
}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data.type !== "Item" || !data.uuid) return super._onDrop(event)
const item = await fromUuid(data.uuid)
if (!item) return
const slot = event.target?.closest?.("[data-drop-creation-type]")
const slotType = slot?.dataset?.dropCreationType ?? ""
const inCreationZone = Boolean(event.target?.closest?.("[data-creation-drop-zone]"))
if (!LesOubliesPersonnageSheet.CREATION_DROP_TYPES.has(item.type)) {
if (slot || inCreationZone) {
ui.notifications.warn("Seules les races, tribus et métiers peuvent être déposés dans cette zone.")
return
}
return super._onDrop(event)
}
if (slotType && slotType !== item.type) {
ui.notifications.warn(`Déposez ici un élément de type ${game.i18n.localize(`TYPES.Item.${slotType}`)}.`)
return
}
if (slot || inCreationZone) {
await this.document.assignCreationItem(item)
this.render()
return
}
return super._onDrop(event)
}
static #onSwitchTab(event, target) {
const tab = target.dataset.tab
if (!tab || this._activeTab === tab) return
this._activeTab = tab
this.render()
}
static async #onRemoveCreationItem(event, target) {
const type = target.dataset.type
if (!LesOubliesPersonnageSheet.CREATION_DROP_TYPES.has(type)) return
await this.document.clearCreationItem(type)
this.render()
}
}
+43
View File
@@ -3,6 +3,8 @@ import { LesOubliesUtility } from "./les-oublies-utility.js"
import { LesOubliesRolls } from "./les-oublies-rolls.js"
export class LesOubliesActor extends Actor {
static CREATION_ITEM_TYPES = new Set(["race", "tribu", "metier"])
prepareDerivedData() {
super.prepareDerivedData()
@@ -43,6 +45,12 @@ export class LesOubliesActor extends Actor {
}
getCreationItem(type) {
if (!LesOubliesActor.CREATION_ITEM_TYPES.has(type)) return this.items.find((item) => item.type === type) ?? null
const referenceId = this.system.references?.[`${type}Id`] ?? ""
if (referenceId) {
const referencedItem = this.items.get(referenceId)
if (referencedItem?.type === type) return referencedItem
}
return this.items.find((item) => item.type === type) ?? null
}
@@ -51,6 +59,40 @@ export class LesOubliesActor extends Actor {
return LesOubliesUtility.sortByName(items)
}
async assignCreationItem(sourceItem) {
if (!sourceItem || !LesOubliesActor.CREATION_ITEM_TYPES.has(sourceItem.type)) return null
const itemData = sourceItem.toObject()
delete itemData._id
const existingIds = this.getEmbeddedItems(sourceItem.type).map((item) => item.id)
if (existingIds.length) {
await this.deleteEmbeddedDocuments("Item", existingIds, { renderSheet: false })
}
const [createdItem] = await this.createEmbeddedDocuments("Item", [itemData], { renderSheet: false })
if (!createdItem) return null
await this.update({
[`system.references.${sourceItem.type}Id`]: createdItem.id,
})
return createdItem
}
async clearCreationItem(type) {
if (!LesOubliesActor.CREATION_ITEM_TYPES.has(type)) return
const existingIds = this.getEmbeddedItems(type).map((item) => item.id)
if (existingIds.length) {
await this.deleteEmbeddedDocuments("Item", existingIds, { renderSheet: false })
}
await this.update({
[`system.references.${type}Id`]: "",
})
}
getCompagnie() {
const compagnieId = this.system.references?.compagnieId
return compagnieId ? game.actors.get(compagnieId) ?? null : null
@@ -83,6 +125,7 @@ export class LesOubliesActor extends Actor {
getGroupedCompetences() {
return LESOUBLIES_CONFIG.profiles.map((profile) => ({
...profile,
profileValue: this.getProfileValue(profile.id),
items: this.getCompetences().filter((entry) => entry.item.system.profileKey === profile.id),
}))
}
+71
View File
@@ -59,6 +59,67 @@ export const SIZE_LABELS = {
4: "Grande",
}
export const WEAPON_CATEGORY_LABELS = {
melee: "Mêlée",
tir: "Tir",
jet: "Jet",
}
export const WEAPON_ORIGIN_LABELS = {
geant: "Géant",
petitPeuple: "Petit Peuple",
}
export const WEAPON_SIZE_MODE_LABELS = {
fixe: "Fixe",
plage: "Plage",
variable: "Variable",
}
export const ARMOR_STATE_LABELS = {
protege: "Protégé",
harnache: "Harnaché",
barde: "Bardé",
}
export const EQUIPMENT_CATEGORY_LABELS = {
butin: "Butin",
ecriture: "Écriture",
monture: "Monture",
soin: "Soin",
survie: "Survie",
voyage: "Voyage",
}
export const SPELL_TRADITION_LABELS = {
chimerisme: "Chimérisme",
farfadet: "Farfadet",
magie: "Magie",
onirologie: "Onirologie",
}
export const POLARITY_LABELS = {
cauchemar: "Cauchemar",
songes: "Songes",
}
export const STACKING_LABELS = {
"-": "—",
non: "Non",
oui: "Oui",
}
export const COMPANY_POWER_SCOPE_LABELS = {
compagnie: "Compagnie",
}
export const COMPANY_POWER_MODE_LABELS = {
passif: "Passif",
"préparation": "Préparation",
"réaction": "Réaction",
ressource: "Ressource",
}
export const ACTOR_IMAGES = {
personnage: "icons/svg/mystery-man.svg",
compagnie: "icons/svg/book.svg",
@@ -84,6 +145,16 @@ export const LESOUBLIES_CONFIG = {
profileLabels: PROFILE_LABELS,
skills: SKILLS,
sizes: SIZE_LABELS,
weaponCategoryLabels: WEAPON_CATEGORY_LABELS,
weaponOriginLabels: WEAPON_ORIGIN_LABELS,
weaponSizeModeLabels: WEAPON_SIZE_MODE_LABELS,
armorStateLabels: ARMOR_STATE_LABELS,
equipmentCategoryLabels: EQUIPMENT_CATEGORY_LABELS,
spellTraditionLabels: SPELL_TRADITION_LABELS,
polarityLabels: POLARITY_LABELS,
stackingLabels: STACKING_LABELS,
companyPowerScopeLabels: COMPANY_POWER_SCOPE_LABELS,
companyPowerModeLabels: COMPANY_POWER_MODE_LABELS,
actorImages: ACTOR_IMAGES,
itemImages: ITEM_IMAGES,
}
+14
View File
@@ -6,8 +6,22 @@ import { LesOubliesRolls } from "./les-oublies-rolls.js"
import * as models from "./models/index.mjs"
import * as sheets from "./applications/sheets/_module.mjs"
function ensureSystemStyles() {
const href = `systems/${game.system.id}/css/les-oublies.css`
const existingLink = document.querySelector(`link[href$="${href}"]`)
if (existingLink) return
const link = document.createElement("link")
link.rel = "stylesheet"
link.type = "text/css"
link.href = href
link.dataset.systemStyle = game.system.id
document.head.append(link)
}
Hooks.once("init", function () {
console.info("Les Oubliés | Initialisation du système")
ensureSystemStyles()
CONFIG.Actor.documentClass = LesOubliesActor
CONFIG.Actor.dataModels = {
+333 -60
View File
@@ -1,3 +1,5 @@
import { LesOubliesUtility } from "./les-oublies-utility.js"
const PRIME_DEFINITIONS = [
{
id: "none",
@@ -187,6 +189,18 @@ const MOVEMENT_DIFFICULTIES = [
{ value: -3, label: "Faire un détour (-3)" },
]
const TEST_DIFFICULTIES = [
{ value: 12, label: "Exceptionnellement facile (+12)" },
{ value: 9, label: "Très facile (+9)" },
{ value: 6, label: "Facile (+6)" },
{ value: 3, label: "Avantageuse (+3)" },
{ value: 0, label: "Normale (+0)" },
{ value: -3, label: "Difficile (-3)" },
{ value: -6, label: "Très difficile (-6)" },
{ value: -9, label: "Extrêmement difficile (-9)" },
{ value: -12, label: "Presque impossible (-12)" },
]
const HARVEST_SIDE_EFFECTS = {
1: "La main du personnage tremble plus ou moins violemment.",
2: "Le personnage n'arrive à trouver ni repos ni sommeil.",
@@ -250,6 +264,8 @@ const PRESET_ACTIONS = {
}
export class LesOubliesRolls {
static #actorLocks = new Map()
static async openTestDialog(actor, preset = {}) {
const data = await this.#promptTestOptions(actor, preset)
if (!data || typeof data !== "object") return null
@@ -284,7 +300,18 @@ export class LesOubliesRolls {
static async openConfrontationDialog(actor, preset = {}) {
const data = await this.#promptConfrontationOptions(actor, preset)
if (!data || typeof data !== "object") return null
return this.#createConfrontationMessage(actor, data, preset.actionData ?? null)
const defenderActor = this.#resolveDialogTargetActor(data.defenderActorId, preset.targetActor ?? this.#getTargetActor())
return this.#createConfrontationMessage(actor, {
...data,
defenderLabel: defenderActor?.name ?? data.defenderLabel,
defenderScore: defenderActor
? this.#getSkillScoreWithAlternatives(defenderActor, data.defenderSkill)
: Number(data.defenderScore ?? 0),
defenderSongesValue: defenderActor ? Number(defenderActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
defenderSongesPoints: defenderActor ? Number(defenderActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
defenderCauchemarValue: defenderActor ? Number(defenderActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
defenderCauchemarPoints: defenderActor ? Number(defenderActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
}, preset.actionData ?? null)
}
static async openAttackDialog(actor, { itemId = null, mode = null } = {}) {
@@ -297,6 +324,11 @@ export class LesOubliesRolls {
targetActor,
})
if (!data) return null
const defenderActor = this.#resolveDialogTargetActor(data.defenderActorId, targetActor)
if (!defenderActor) {
ui.notifications.info("Aucune cible sélectionnée : choisissez un adversaire avant de lancer une attaque.")
return null
}
const modifiers = this.#resolveModifierSelection(data.primeId, data.penaltyId, attackMode === "ranged" ? "rangedAttack" : "meleeAttack")
const reactionOptions = this.#getAttackReactionOptions(data.attackerSkill)
@@ -311,17 +343,17 @@ export class LesOubliesRolls {
modifiers,
targetLabel: data.defenderLabel,
notes: data.notes?.trim() || "",
targetActor,
applyToTarget: Boolean(data.applyToTarget && targetActor),
targetActor: defenderActor,
applyToTarget: Boolean(data.applyToTarget && defenderActor),
damageRequest: {
actor,
weapon,
baseDamage: Number(data.baseDamage ?? 0),
baseLabel: String(data.baseDamageLabel || weapon?.system?.damage || data.baseDamage || "0"),
targetProtection: Number(data.targetProtection ?? 0),
targetLabel: String(data.defenderLabel || targetActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")),
targetActor,
applyToTarget: Boolean(data.applyToTarget && targetActor),
targetLabel: String(data.defenderLabel || defenderActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")),
targetActor: defenderActor,
applyToTarget: Boolean(data.applyToTarget && defenderActor),
modifiers,
},
extraContext: {
@@ -339,17 +371,17 @@ export class LesOubliesRolls {
attackerExtraDie: data.attackerExtraDie,
attackerFinalModifier: modifiers.summary.finalModifier,
defenderLabel: data.defenderLabel,
defenderScore: targetActor
? this.#getSkillScoreWithAlternatives(targetActor, data.defenderSkill)
defenderScore: defenderActor
? this.#getSkillScoreWithAlternatives(defenderActor, data.defenderSkill)
: Number(data.defenderScore ?? 0),
defenderDifficulty: Number(data.defenderDifficulty ?? 0),
defenderRollMode: data.defenderRollMode,
defenderExtraDie: data.defenderExtraDie,
defenderFinalModifier: modifiers.summary.opponentFinalModifier,
defenderSongesValue: targetActor ? Number(targetActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
defenderSongesPoints: targetActor ? Number(targetActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
defenderCauchemarValue: targetActor ? Number(targetActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
defenderCauchemarPoints: targetActor ? Number(targetActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
defenderSongesValue: defenderActor ? Number(defenderActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
defenderSongesPoints: defenderActor ? Number(defenderActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
defenderCauchemarValue: defenderActor ? Number(defenderActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
defenderCauchemarPoints: defenderActor ? Number(defenderActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
}, actionData)
}
@@ -407,33 +439,40 @@ export class LesOubliesRolls {
const data = await this.#promptSpellOptions(actor, spell)
if (!data) return null
const skill = actor.getCompetenceByKey?.(spell.system.skillKey) ?? null
const skillBase = Number(skill?.system?.base ?? 0)
if (skillBase < 1) {
ui.notifications.warn(`Il faut au moins une base de 1 en ${this.#getSkillLabel(spell.system.skillKey)} pour activer ce sortilège.`)
return null
}
const métierMatch = this.#actorMatchesSpellGrant(actor, spell)
const surcharge = !métierMatch && data.applyMetierSurcharge
const effectiveCost = Number(data.actualCost ?? 0) * (surcharge ? 2 : 1)
const paymentMode = String(data.paymentMode || "points")
if (paymentMode === "points") {
const resource = spell.system.polarity || "songes"
if (Number(actor.system?.[resource]?.points ?? 0) < effectiveCost) {
ui.notifications.warn(game.i18n.format("LESOUBLIES.rolls.notEnoughResource", {
resource: resource === "songes" ? game.i18n.localize("LESOUBLIES.ui.songes") : game.i18n.localize("LESOUBLIES.ui.cauchemar"),
actor: actor.name,
}))
const activation = await this.#withActorLock(`spell:${actor.id}`, async () => {
const skill = actor.getCompetenceByKey?.(spell.system.skillKey) ?? null
const skillBase = Number(skill?.system?.base ?? 0)
if (skillBase < 1) {
ui.notifications.warn(`Il faut au moins une base de 1 en ${this.#getSkillLabel(spell.system.skillKey)} pour activer ce sortilège.`)
return null
}
if (effectiveCost > 0) {
await actor.update({
[`system.${resource}.points`]: Math.max(Number(actor.system?.[resource]?.points ?? 0) - effectiveCost, 0),
})
const métierMatch = this.#actorMatchesSpellGrant(actor, spell)
const surcharge = !métierMatch
const effectiveCost = Number(data.actualCost ?? 0) * (surcharge ? 2 : 1)
const paymentMode = String(data.paymentMode || "points")
if (paymentMode === "points") {
const resource = spell.system.polarity || "songes"
const available = Number(actor.system?.[resource]?.points ?? 0)
if (available < effectiveCost) {
ui.notifications.warn(game.i18n.format("LESOUBLIES.rolls.notEnoughResourceDetailed", {
resource: resource === "songes" ? game.i18n.localize("LESOUBLIES.ui.songes") : game.i18n.localize("LESOUBLIES.ui.cauchemar"),
actor: actor.name,
required: effectiveCost,
available,
}))
return null
}
if (effectiveCost > 0) {
await actor.update({
[`system.${resource}.points`]: Math.max(available - effectiveCost, 0),
})
}
}
}
return { métierMatch, surcharge, effectiveCost, paymentMode }
})
if (!activation) return null
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-les-oublies/templates/chat-spell-activation.hbs",
@@ -442,14 +481,14 @@ export class LesOubliesRolls {
spell,
activation: {
targetLabel: data.targetLabel?.trim() || "Sans cible précisée",
paymentMode,
paymentMode: activation.paymentMode,
actualCost: Number(data.actualCost ?? 0),
effectiveCost,
costLabel: paymentMode === "points"
? `${effectiveCost} point${effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`
: `${effectiveCost} fil${effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`,
métierMatch,
surcharge,
effectiveCost: activation.effectiveCost,
costLabel: activation.paymentMode === "points"
? `${activation.effectiveCost} point${activation.effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`
: `${activation.effectiveCost} fil${activation.effectiveCost > 1 ? "s" : ""} de ${spell.system.polarity === "cauchemar" ? "Cauchemar" : "Songes"}`,
métierMatch: activation.métierMatch,
surcharge: activation.surcharge,
notes: data.notes?.trim() || "",
},
},
@@ -495,6 +534,7 @@ export class LesOubliesRolls {
const targetActor = this.#getTargetActor()
const data = await this.#promptPresetConfrontationOptions(actor, preset, targetActor)
if (!data) return null
const defenderActor = this.#resolveDialogTargetActor(data.defenderActorId, targetActor)
const modifiers = this.#resolveModifierSelection(data.primeId, data.penaltyId, actionKey)
const actionData = {
@@ -504,8 +544,8 @@ export class LesOubliesRolls {
hint: preset.hint,
modifiers,
notes: data.notes?.trim() || "",
targetLabel: data.defenderLabel,
targetActor,
targetLabel: defenderActor?.name ?? data.defenderLabel,
targetActor: defenderActor,
applyToTarget: false,
outcome: this.#buildPresetOutcome(actionKey, data),
}
@@ -518,18 +558,18 @@ export class LesOubliesRolls {
attackerRollMode: data.attackerRollMode,
attackerExtraDie: data.attackerExtraDie,
attackerFinalModifier: modifiers.summary.finalModifier,
defenderLabel: data.defenderLabel,
defenderScore: targetActor
? this.#getSkillScoreWithAlternatives(targetActor, preset.defenderSkillKey)
defenderLabel: defenderActor?.name ?? data.defenderLabel,
defenderScore: defenderActor
? this.#getSkillScoreWithAlternatives(defenderActor, preset.defenderSkillKey)
: Number(data.defenderScore ?? 0),
defenderDifficulty: Number(data.defenderDifficulty ?? 0),
defenderRollMode: data.defenderRollMode,
defenderExtraDie: data.defenderExtraDie,
defenderFinalModifier: modifiers.summary.opponentFinalModifier,
defenderSongesValue: targetActor ? Number(targetActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
defenderSongesPoints: targetActor ? Number(targetActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
defenderCauchemarValue: targetActor ? Number(targetActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
defenderCauchemarPoints: targetActor ? Number(targetActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
defenderSongesValue: defenderActor ? Number(defenderActor.system.songes?.value ?? 0) : Number(data.defenderSongesValue ?? 0),
defenderSongesPoints: defenderActor ? Number(defenderActor.system.songes?.points ?? 0) : Number(data.defenderSongesPoints ?? 0),
defenderCauchemarValue: defenderActor ? Number(defenderActor.system.cauchemar?.value ?? 0) : Number(data.defenderCauchemarValue ?? 0),
defenderCauchemarPoints: defenderActor ? Number(defenderActor.system.cauchemar?.points ?? 0) : Number(data.defenderCauchemarPoints ?? 0),
}, actionData)
}
@@ -761,17 +801,19 @@ export class LesOubliesRolls {
}
static async #promptTestOptions(actor, preset = {}) {
const difficulty = Number(preset.difficulty ?? 0)
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-les-oublies/templates/dialog-roll-test.hbs",
{
actor,
rollModes: this.getRollModes(),
extraDieModes: this.getExtraDieModes(),
difficultyOptions: this.#getDifficultyOptions(TEST_DIFFICULTIES, difficulty),
resources: this.#getDialogResources(actor),
values: {
label: preset.label ?? "",
score: Number(preset.score ?? 0),
difficulty: Number(preset.difficulty ?? 0),
difficulty,
rollMode: preset.rollMode ?? this.getDefaultRollMode(actor),
extraDie: preset.extraDie ?? "",
},
@@ -816,10 +858,15 @@ export class LesOubliesRolls {
static async #promptConfrontationOptions(actor, preset = {}) {
const targetActor = preset.targetActor ?? this.#getTargetActor()
const defenderSkill = preset.defenderSkill ?? "esquive"
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-les-oublies/templates/dialog-roll-confrontation.hbs",
{
actor,
targetActor,
targetStatus: this.#getConfrontationTargetStatus(targetActor),
targetOptions: this.#getConfrontationTargetOptions(actor, targetActor),
defenderSkillOptions: this.#getConfrontationSkillOptions(),
rollModes: this.getRollModes(),
extraDieModes: this.getExtraDieModes(),
defaultRollMode: this.getDefaultRollMode(actor),
@@ -837,7 +884,11 @@ export class LesOubliesRolls {
attackerRollMode: preset.attackerRollMode ?? this.getDefaultRollMode(actor),
attackerExtraDie: preset.attackerExtraDie ?? "",
defenderLabel: targetActor?.name ?? preset.defenderLabel ?? game.i18n.localize("LESOUBLIES.rolls.defender"),
defenderScore: Number(preset.defenderScore ?? 0),
defenderActorId: targetActor?.id ?? "",
defenderSkill,
defenderScore: targetActor
? this.#getSkillScoreWithAlternatives(targetActor, defenderSkill)
: Number(preset.defenderScore ?? 0),
defenderDifficulty: Number(preset.defenderDifficulty ?? 0),
defenderRollMode: preset.defenderRollMode ?? this.getDefaultRollMode(targetActor ?? actor),
defenderExtraDie: preset.defenderExtraDie ?? "",
@@ -851,6 +902,13 @@ export class LesOubliesRolls {
title: game.i18n.localize("LESOUBLIES.rolls.dialogs.confrontationTitle"),
},
content,
render: (_event, dialog) => {
this.#bindConfrontationTargetSelection(dialog, {
actor,
fallbackTargetActor: targetActor,
skillFieldName: "defenderSkill",
})
},
buttons: [
{
action: "roll",
@@ -867,6 +925,8 @@ export class LesOubliesRolls {
attackerDifficulty: Number(data.attackerDifficulty ?? 0),
attackerRollMode: String(data.attackerRollMode || this.getDefaultRollMode(actor)),
attackerExtraDie: String(data.attackerExtraDie || ""),
defenderActorId: String(data.defenderActorId || ""),
defenderSkill: String(data.defenderSkill || defenderSkill),
defenderLabel: String(data.defenderLabel || targetActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")).trim(),
defenderScore: Number(data.defenderScore ?? 0),
defenderDifficulty: Number(data.defenderDifficulty ?? 0),
@@ -892,7 +952,7 @@ export class LesOubliesRolls {
const baseDamage = this.#getWeaponBaseDamage(actor, weapon)
const baseDamageLabel = weapon?.system?.damage || String(baseDamage)
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-les-oublies/templates/dialog-roll-attack.hbs",
"systems/fvtt-les-oublies/templates/dialog-roll-attack-v2.hbs",
{
actor,
weapon,
@@ -905,6 +965,10 @@ export class LesOubliesRolls {
cauchemarPoints: 0,
},
targetActor,
targetStatus: this.#getConfrontationTargetStatus(targetActor, { requireTarget: true }),
targetOptions: this.#getConfrontationTargetOptions(actor, targetActor).map((entry, index) => (
index === 0 ? { ...entry, label: "— Sélectionner un adversaire —" } : entry
)),
rollModes: this.getRollModes(),
extraDieModes: this.getExtraDieModes(),
attackSkills: this.#getAttackSkillOptions(attackMode),
@@ -917,6 +981,7 @@ export class LesOubliesRolls {
attackerRollMode: this.getDefaultRollMode(actor),
attackerExtraDie: "",
defenderLabel: targetActor?.name ?? game.i18n.localize("LESOUBLIES.rolls.defender"),
defenderActorId: targetActor?.id ?? "",
defenderSkill: attackMode === "ranged" ? "esquive" : "esquive",
defenderScore: 0,
defenderDifficulty: 0,
@@ -940,6 +1005,14 @@ export class LesOubliesRolls {
title: attackMode === "ranged" ? "Attaque à distance" : "Attaque de mêlée",
},
content,
render: (_event, dialog) => {
this.#bindConfrontationTargetSelection(dialog, {
actor,
fallbackTargetActor: targetActor,
skillFieldName: "defenderSkill",
requireTarget: true,
})
},
buttons: [
{
action: "roll",
@@ -949,12 +1022,19 @@ export class LesOubliesRolls {
const form = this.#getDialogElement(dialog)?.querySelector("form")
if (!form) return null
const data = this.#formToObject(form)
const defenderActor = this.#resolveDialogTargetActor(data.defenderActorId, targetActor)
if (!defenderActor) {
ui.notifications.info("Aucune cible sélectionnée : choisissez un adversaire avant de lancer une attaque.")
dialog.close()
return null
}
const difficultyPreset = Number(data.difficultyPreset ?? 0)
const customDifficulty = Number(data.customDifficulty ?? 0)
return {
attackerSkill: String(data.attackerSkill || (attackMode === "ranged" ? "tir" : "melee")),
attackerRollMode: String(data.attackerRollMode || this.getDefaultRollMode(actor)),
attackerExtraDie: String(data.attackerExtraDie || ""),
defenderActorId: String(data.defenderActorId || ""),
defenderLabel: String(data.defenderLabel || targetActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")).trim(),
defenderSkill: String(data.defenderSkill || "esquive"),
defenderScore: Number(data.defenderScore ?? 0),
@@ -1044,17 +1124,22 @@ export class LesOubliesRolls {
}
static async #promptSpellOptions(actor, spell) {
const isMetierMatch = this.#actorMatchesSpellGrant(actor, spell)
const effectiveCost = Number(spell.system.cost ?? 0) * (isMetierMatch ? 1 : 2)
const polarityLabel = spell.system.polarity === "cauchemar"
? game.i18n.localize("LESOUBLIES.ui.cauchemar")
: game.i18n.localize("LESOUBLIES.ui.songes")
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-les-oublies/templates/dialog-spell-activation.hbs",
{
actor,
spell,
resources: this.#getDialogResources(actor),
isMetierMatch: this.#actorMatchesSpellGrant(actor, spell),
isMetierMatch,
effectiveCostLabel: `${effectiveCost} point${effectiveCost > 1 ? "s" : ""} de ${polarityLabel}`,
values: {
actualCost: Number(spell.system.cost ?? 0),
paymentMode: "points",
applyMetierSurcharge: true,
targetLabel: "",
notes: "",
},
@@ -1078,7 +1163,6 @@ export class LesOubliesRolls {
return {
actualCost: Number(data.actualCost ?? spell.system.cost ?? 0),
paymentMode: String(data.paymentMode || "points"),
applyMetierSurcharge: data.applyMetierSurcharge === "on",
targetLabel: String(data.targetLabel || ""),
notes: String(data.notes || ""),
}
@@ -1164,6 +1248,9 @@ export class LesOubliesRolls {
title: preset.title,
hint: preset.hint,
targetActor,
targetStatus: this.#getConfrontationTargetStatus(targetActor),
defenderSkillLabel: this.#getSkillLabel(preset.defenderSkillKey),
targetOptions: this.#getConfrontationTargetOptions(actor, targetActor),
rollModes: this.getRollModes(),
extraDieModes: this.getExtraDieModes(),
attackerResources: this.#getDialogResources(actor),
@@ -1178,12 +1265,15 @@ export class LesOubliesRolls {
values: {
attackerDifficulty: Number(preset.difficulty ?? 0),
defenderLabel: targetActor?.name ?? game.i18n.localize("LESOUBLIES.rolls.defender"),
defenderActorId: targetActor?.id ?? "",
defenderDifficulty: 0,
attackerRollMode: this.getDefaultRollMode(actor),
attackerExtraDie: "",
defenderRollMode: this.getDefaultRollMode(targetActor ?? actor),
defenderExtraDie: "",
defenderScore: 0,
defenderScore: targetActor
? this.#getSkillScoreWithAlternatives(targetActor, preset.defenderSkillKey)
: 0,
primeId: "none",
penaltyId: "none",
outcomeChoice: "",
@@ -1201,6 +1291,13 @@ export class LesOubliesRolls {
title: preset.title,
},
content,
render: (_event, dialog) => {
this.#bindConfrontationTargetSelection(dialog, {
actor,
fallbackTargetActor: targetActor,
skillKey: preset.defenderSkillKey,
})
},
buttons: [
{
action: "roll",
@@ -1212,6 +1309,7 @@ export class LesOubliesRolls {
const data = this.#formToObject(form)
return {
attackerDifficulty: Number(data.attackerDifficulty ?? preset.difficulty ?? 0),
defenderActorId: String(data.defenderActorId || ""),
defenderLabel: String(data.defenderLabel || targetActor?.name || game.i18n.localize("LESOUBLIES.rolls.defender")).trim(),
defenderDifficulty: Number(data.defenderDifficulty ?? 0),
attackerRollMode: String(data.attackerRollMode || this.getDefaultRollMode(actor)),
@@ -1498,7 +1596,7 @@ export class LesOubliesRolls {
static #getWeaponAttackMode(weapon) {
const category = String(weapon?.system?.category || "").toLowerCase()
if (["distance", "ranged", "tir", "projectile"].some((keyword) => category.includes(keyword))) return "ranged"
if (["distance", "ranged", "tir", "projectile", "jet"].some((keyword) => category.includes(keyword))) return "ranged"
return "melee"
}
@@ -1528,6 +1626,166 @@ export class LesOubliesRolls {
]
}
static #getConfrontationTargetOptions(actor, selectedActor = null) {
const choices = LesOubliesUtility.sortByName(
game.actors.filter((candidate) => (
["creature", "personnage"].includes(candidate.type)
&& candidate.id !== actor?.id
)),
).map((candidate) => ({
value: candidate.id,
label: `${candidate.name}${game.i18n.localize(`TYPES.Actor.${candidate.type}`)}`,
}))
return [
{ value: "", label: "Saisie manuelle" },
...LesOubliesUtility.ensureChoice(
choices,
selectedActor?.id,
selectedActor ? `${selectedActor.name}${game.i18n.localize(`TYPES.Actor.${selectedActor.type}`)}` : null,
),
]
}
static #getConfrontationSkillOptions() {
const skills = CONFIG.LESOUBLIES?.config?.skills ?? CONFIG.LESOUBLIES?.skills ?? {}
return Object.entries(skills)
.map(([value, data]) => ({
value,
label: data.label ?? value,
}))
.sort((left, right) => left.label.localeCompare(right.label, "fr"))
}
static #resolveDialogTargetActor(actorId, fallbackTargetActor = null) {
if (actorId !== undefined && actorId !== null && actorId !== "") {
return game.actors.get(String(actorId)) ?? null
}
if (actorId === "") return null
return fallbackTargetActor ?? null
}
static #getConfrontationTargetStatus(targetActor = null, { requireTarget = false } = {}) {
if (!targetActor) {
return {
message: requireTarget
? "Aucune cible n'est actuellement sélectionnée. Sélectionnez un adversaire dans la liste ci-dessous pour lancer l'attaque."
: "Aucune cible n'est actuellement sélectionnée. Choisissez un adversaire dans la liste ci-dessous ou conservez la saisie manuelle.",
state: "empty",
}
}
return {
message: `Adversaire sélectionné : ${targetActor.name}. Ses valeurs de confrontation sont utilisées automatiquement.`,
state: "selected",
}
}
static #bindConfrontationTargetSelection(dialog, {
actor,
fallbackTargetActor = null,
skillFieldName = null,
skillKey = null,
requireTarget = false,
} = {}) {
const root = this.#getDialogElement(dialog)
const form = root?.querySelector("form")
if (!form) return
const actorField = form.elements.namedItem("defenderActorId")
if (!(actorField instanceof HTMLSelectElement)) return
const labelField = form.elements.namedItem("defenderLabel")
const scoreField = form.elements.namedItem("defenderScore")
const rollModeField = form.elements.namedItem("defenderRollMode")
const songesValueField = form.elements.namedItem("defenderSongesValue")
const songesPointsField = form.elements.namedItem("defenderSongesPoints")
const cauchemarValueField = form.elements.namedItem("defenderCauchemarValue")
const cauchemarPointsField = form.elements.namedItem("defenderCauchemarPoints")
const skillField = skillFieldName ? form.elements.namedItem(skillFieldName) : null
const targetStatusField = root.querySelector("[data-target-status]")
const defaultLabel = game.i18n.localize("LESOUBLIES.rolls.defender")
const getSelectedSkill = () => {
if (skillKey) return skillKey
if (skillField instanceof HTMLSelectElement) return String(skillField.value || "melee")
return "melee"
}
const updateTargetFields = ({ preserveRollMode = false } = {}) => {
const targetActor = this.#resolveDialogTargetActor(actorField.value, fallbackTargetActor)
const hasActor = Boolean(targetActor)
const currentSkill = getSelectedSkill()
if (targetStatusField instanceof HTMLElement) {
const targetStatus = this.#getConfrontationTargetStatus(targetActor, { requireTarget })
targetStatusField.textContent = targetStatus.message
targetStatusField.dataset.state = targetStatus.state
}
if (labelField instanceof HTMLInputElement) {
labelField.value = hasActor ? targetActor.name : (labelField.value || defaultLabel)
labelField.readOnly = hasActor
}
if (scoreField instanceof HTMLInputElement) {
if (hasActor) {
scoreField.value = String(this.#getSkillScoreWithAlternatives(targetActor, currentSkill))
}
scoreField.readOnly = hasActor
}
if (rollModeField instanceof HTMLSelectElement && hasActor && !preserveRollMode) {
rollModeField.value = this.getDefaultRollMode(targetActor)
}
const resourceValues = hasActor
? {
songesValue: Number(targetActor.system.songes?.value ?? 0),
songesPoints: Number(targetActor.system.songes?.points ?? 0),
cauchemarValue: Number(targetActor.system.cauchemar?.value ?? 0),
cauchemarPoints: Number(targetActor.system.cauchemar?.points ?? 0),
}
: null
const bindNumericField = (field, value) => {
if (!(field instanceof HTMLInputElement)) return
if (resourceValues) field.value = String(value)
field.readOnly = hasActor
}
bindNumericField(songesValueField, resourceValues?.songesValue ?? 0)
bindNumericField(songesPointsField, resourceValues?.songesPoints ?? 0)
bindNumericField(cauchemarValueField, resourceValues?.cauchemarValue ?? 0)
bindNumericField(cauchemarPointsField, resourceValues?.cauchemarPoints ?? 0)
}
actorField.addEventListener("change", () => updateTargetFields())
if (skillField instanceof HTMLSelectElement) {
skillField.addEventListener("change", () => updateTargetFields({ preserveRollMode: true }))
}
updateTargetFields()
}
static async #withActorLock(lockKey, callback) {
const previous = this.#actorLocks.get(lockKey) ?? Promise.resolve()
let release
const current = new Promise((resolve) => {
release = resolve
})
const queued = previous.finally(() => current)
this.#actorLocks.set(lockKey, queued)
await previous
try {
return await callback()
} finally {
release()
if (this.#actorLocks.get(lockKey) === queued) {
this.#actorLocks.delete(lockKey)
}
}
}
static #getModifierOptions(type, actionType) {
const source = type === "prime" ? PRIME_DEFINITIONS : PENALTY_DEFINITIONS
return source
@@ -1677,6 +1935,21 @@ export class LesOubliesRolls {
return parts.length ? parts.join(" ") : "0"
}
static #getDifficultyOptions(options, selectedValue = 0) {
const normalizedValue = Number(selectedValue ?? 0)
const entries = options.map((entry) => ({
value: Number(entry.value ?? 0),
label: entry.label,
}))
if (!entries.some((entry) => entry.value === normalizedValue)) {
entries.push({
value: normalizedValue,
label: `Personnalisée (${normalizedValue > 0 ? "+" : ""}${normalizedValue})`,
})
}
return entries
}
static #getConfrontationOutcome(attacker, defender) {
const attackerSuccess = attacker.success
const defenderSuccess = defender.success
+36
View File
@@ -44,6 +44,29 @@ export class LesOubliesUtility {
return ITEM_IMAGES[type] ?? "icons/svg/item-bag.svg"
}
static createChoices(values = [], labels = {}) {
return values.map((value) => ({
value,
label: labels[value] ?? String(value),
}))
}
static createRangeChoices(min, max, labels = {}) {
return Array.from({ length: Math.max(max - min + 1, 0) }, (_, index) => min + index).map((value) => ({
value,
label: labels[value] ?? String(value),
}))
}
static ensureChoice(choices = [], value, label = null) {
if (value === undefined || value === null || value === "") return choices
if (choices.some((choice) => String(choice.value) === String(value))) return choices
return [{
value,
label: label ?? `${value} (personnalisé)`,
}, ...choices]
}
static createEmptyProfiles() {
return PROFILE_KEYS.reduce((profiles, key) => {
profiles[key] = 0
@@ -73,4 +96,17 @@ export class LesOubliesUtility {
static sortByName(documents = []) {
return [...documents].sort((left, right) => left.name.localeCompare(right.name, "fr"))
}
static async prepareEnrichedHtml(documentName, type, systemData) {
const htmlFields = game.system.documentTypes?.[documentName]?.[type]?.htmlFields ?? []
const enriched = {}
for (const path of htmlFields) {
const value = foundry.utils.getProperty(systemData, path) ?? ""
const html = await foundry.applications.ux.TextEditor.implementation.enrichHTML(value, { async: true })
foundry.utils.setProperty(enriched, path, html)
}
return enriched
}
}