DIvers rework de CSS/LESS et améliorations de messages/layout
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user