Import initial du système
This commit is contained in:
@@ -12,6 +12,10 @@ export const ITEM_TYPES = {
|
||||
kungfu: "kungfu",
|
||||
spell: "spell",
|
||||
supernatural: "supernatural",
|
||||
weapon: "weapon",
|
||||
armor: "armor",
|
||||
sanhei: "sanhei",
|
||||
ingredient: "ingredient",
|
||||
}
|
||||
|
||||
export const SUBTYPES = {
|
||||
|
||||
@@ -86,7 +86,7 @@ export default class CharacterDataModel extends foundry.abstract.TypeDataModel {
|
||||
|
||||
const schema = {
|
||||
concept: stringField(""),
|
||||
guardian: stringField("0"),
|
||||
guardian: numberField(0, { min: 0, max: 5 }),
|
||||
initiative: numberField(1, { min: 0 }),
|
||||
anti_initiative: numberField(24, { min: 0 }),
|
||||
description: htmlField(""),
|
||||
@@ -158,8 +158,8 @@ export default class CharacterDataModel extends foundry.abstract.TypeDataModel {
|
||||
geomancy: magicField(),
|
||||
}),
|
||||
threetreasures: new fields.SchemaField({
|
||||
heiyang: new fields.SchemaField({ value: numberField(0, { min: 0 }) }),
|
||||
heiyin: new fields.SchemaField({ value: numberField(0, { min: 0 }) }),
|
||||
heiyang: new fields.SchemaField({ value: numberField(0, { min: 0 }), max: numberField(0, { min: 0 }) }),
|
||||
heiyin: new fields.SchemaField({ value: numberField(0, { min: 0 }), max: numberField(0, { min: 0 }) }),
|
||||
dicelevel: new fields.SchemaField({
|
||||
level0d: treasureLevel(),
|
||||
level1d: treasureLevel(),
|
||||
|
||||
@@ -21,8 +21,8 @@ export default class NpcDataModel extends foundry.abstract.TypeDataModel {
|
||||
|
||||
return {
|
||||
type: stringField(""),
|
||||
levelofthreat: numberField(0, { min: 0 }),
|
||||
powerofnuisance: numberField(0, { min: 0 }),
|
||||
threat: numberField(0, { min: 0, max: 4 }), // profane(0) | apprentice(1) | initiate(2) | accomplished(3) | renowned(4)
|
||||
nuisance: numberField(0, { min: 0, max: 5 }), // figurant(0) | minion(1) | adversary(2) | ally(3) | boss(4) | divinity(5)
|
||||
initiative: numberField(1, { min: 0 }),
|
||||
anti_initiative: numberField(24, { min: 0 }),
|
||||
aptitudes: new fields.SchemaField({
|
||||
|
||||
19
src/data/items/armor.js
Normal file
19
src/data/items/armor.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default class ArmorDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const { fields } = foundry.data
|
||||
const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial })
|
||||
const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })
|
||||
const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })
|
||||
|
||||
return {
|
||||
reference: stringField(""),
|
||||
description: htmlField(""),
|
||||
protectionValue: intField(0),
|
||||
domain: stringField(""),
|
||||
obtainLevel: intField(0, { min: 0, max: 5 }),
|
||||
obtainDifficulty: intField(0, { min: 0, max: 3 }),
|
||||
quantity: intField(1),
|
||||
notes: htmlField(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,7 @@ export { default as EquipmentDataModel } from "./item.js"
|
||||
export { default as KungfuDataModel } from "./kungfu.js"
|
||||
export { default as SpellDataModel } from "./spell.js"
|
||||
export { default as SupernaturalDataModel } from "./supernatural.js"
|
||||
export { default as WeaponDataModel } from "./weapon.js"
|
||||
export { default as ArmorDataModel } from "./armor.js"
|
||||
export { default as SanheiDataModel } from "./sanhei.js"
|
||||
export { default as IngredientDataModel } from "./ingredient.js"
|
||||
|
||||
18
src/data/items/ingredient.js
Normal file
18
src/data/items/ingredient.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export default class IngredientDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const { fields } = foundry.data
|
||||
const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial })
|
||||
const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })
|
||||
const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })
|
||||
|
||||
return {
|
||||
reference: stringField(""),
|
||||
description: htmlField(""),
|
||||
school: stringField("all"),
|
||||
obtainLevel: intField(0, { min: 0, max: 5 }),
|
||||
obtainDifficulty: intField(0, { min: 0, max: 3 }),
|
||||
quantity: intField(1),
|
||||
notes: htmlField(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,10 @@ export default class EquipmentDataModel extends foundry.abstract.TypeDataModel {
|
||||
const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })
|
||||
|
||||
return {
|
||||
subtype: stringField(""),
|
||||
reference: stringField(""),
|
||||
description: htmlField(""),
|
||||
quantity: numberField(1, { min: 0 }),
|
||||
weight: numberField(0, { min: 0 }),
|
||||
protection: stringField(""),
|
||||
damage: stringField(""),
|
||||
range: stringField(""),
|
||||
notes: htmlField(""),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,16 @@ export default class KungfuDataModel extends foundry.abstract.TypeDataModel {
|
||||
new fields.SchemaField({
|
||||
check: boolField(false),
|
||||
name: stringField(""),
|
||||
activation: stringField(""),
|
||||
activation: stringField("action-attack"), // action-attack | action-defense | action-aid | action-attack-defense | reaction | dice | damage-inflicted | damage-received
|
||||
technique: htmlField(""),
|
||||
})
|
||||
|
||||
return {
|
||||
reference: stringField(""),
|
||||
description: htmlField(""),
|
||||
orientation: stringField(""),
|
||||
aspect: stringField(""),
|
||||
skill: stringField(""),
|
||||
orientation: stringField("yin"), // yin | yang | yinyang
|
||||
aspect: stringField("metal"), // metal | eau | terre | feu | bois
|
||||
skill: stringField("kungfu"), // kungfu | rangedcombat
|
||||
speciality: stringField(""),
|
||||
style: stringField(""),
|
||||
techniques: new fields.SchemaField({
|
||||
|
||||
27
src/data/items/sanhei.js
Normal file
27
src/data/items/sanhei.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export default class SanheiDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const { fields } = foundry.data
|
||||
const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial })
|
||||
const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })
|
||||
const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })
|
||||
|
||||
const propertySchema = () => new fields.SchemaField({
|
||||
name: stringField(""),
|
||||
heiCost: intField(0),
|
||||
heiType: stringField("yin"),
|
||||
description: htmlField(""),
|
||||
})
|
||||
|
||||
return {
|
||||
reference: stringField(""),
|
||||
description: htmlField(""),
|
||||
heiType: stringField("yin"),
|
||||
properties: new fields.SchemaField({
|
||||
prop1: propertySchema(),
|
||||
prop2: propertySchema(),
|
||||
prop3: propertySchema(),
|
||||
}),
|
||||
notes: htmlField(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export default class SpellDataModel extends foundry.abstract.TypeDataModel {
|
||||
reference: stringField(""),
|
||||
description: htmlField(""),
|
||||
specialityname: stringField(""),
|
||||
associatedelement: stringField(""),
|
||||
associatedelement: stringField("metal"), // metal | eau | terre | feu | bois
|
||||
hei: stringField(""),
|
||||
realizationtimeritual: stringField(""),
|
||||
realizationtimeaccelerated: stringField(""),
|
||||
@@ -17,6 +17,10 @@ export default class SpellDataModel extends foundry.abstract.TypeDataModel {
|
||||
effects: htmlField(""),
|
||||
examples: htmlField(""),
|
||||
notes: htmlField(""),
|
||||
discipline: stringField("cinabre"),
|
||||
heiType: stringField("yin"),
|
||||
heiCost: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 }),
|
||||
difficulty: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 1 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ export default class SupernaturalDataModel extends foundry.abstract.TypeDataMode
|
||||
reference: stringField(""),
|
||||
description: htmlField(""),
|
||||
notes: htmlField(""),
|
||||
heiType: stringField("yin"),
|
||||
heiCost: new fields.NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 0 }),
|
||||
trigger: stringField(""),
|
||||
effects: htmlField(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
src/data/items/weapon.js
Normal file
22
src/data/items/weapon.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default class WeaponDataModel extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const { fields } = foundry.data
|
||||
const stringField = (initial = "") => new fields.StringField({ required: true, nullable: false, initial })
|
||||
const htmlField = (initial = "") => new fields.HTMLField({ required: true, nullable: false, initial, textSearch: true })
|
||||
const intField = (initial = 0, opts = {}) => new fields.NumberField({ required: true, nullable: false, integer: true, initial, ...opts })
|
||||
|
||||
return {
|
||||
reference: stringField(""),
|
||||
description: htmlField(""),
|
||||
weaponType: stringField("melee"),
|
||||
material: stringField(""),
|
||||
damageAspect: stringField("metal"),
|
||||
damageBase: intField(1),
|
||||
range: stringField("contact"), // contact | courte | mediane | longue | extreme
|
||||
obtainLevel: intField(0, { min: 0, max: 5 }),
|
||||
obtainDifficulty: intField(0, { min: 0, max: 3 }),
|
||||
quantity: intField(1),
|
||||
notes: htmlField(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export function registerSettings() {
|
||||
export async function migrateIfNeeded() {
|
||||
const current = game.system.version ?? MIGRATION_VERSION
|
||||
const stored = game.settings.get(SYSTEM_ID, "migrationVersion") ?? "0.0.0"
|
||||
if (!isNewerVersion(current, stored)) return
|
||||
if (!foundry.utils.isNewerVersion(current, stored)) return
|
||||
|
||||
ui.notifications.info(`CHRONIQUESDELETRANGE | Migration vers ${current} en cours...`, { permanent: true })
|
||||
await migrateActors()
|
||||
@@ -92,6 +92,26 @@ function migrateActorData(actor) {
|
||||
updateData["system.prefs.typeofthrow"] = { check: defaultCheck, choice: "0" }
|
||||
}
|
||||
|
||||
// Migrate NPC field renames: levelofthreat → threat, powerofnuisance → nuisance
|
||||
if (actorType === "npc") {
|
||||
if (system.levelofthreat !== undefined && system.threat === undefined) {
|
||||
updateData["system.threat"] = system.levelofthreat
|
||||
updateData["system.-=levelofthreat"] = null
|
||||
}
|
||||
if (system.powerofnuisance !== undefined && system.nuisance === undefined) {
|
||||
updateData["system.nuisance"] = system.powerofnuisance
|
||||
updateData["system.-=powerofnuisance"] = null
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate character guardian from string to number
|
||||
if (actorType === "character" && typeof system.guardian === "string") {
|
||||
const guardianNum = parseInt(system.guardian, 10)
|
||||
if (!isNaN(guardianNum)) {
|
||||
updateData["system.guardian"] = guardianNum
|
||||
}
|
||||
}
|
||||
|
||||
return updateData
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ACTOR_TYPES, ITEM_TYPES, MAGICS, SUBTYPES, SYSTEM_ID } from "./config/c
|
||||
import { preLocalizeConfig } from "./config/localize.js"
|
||||
import { configureRuntime } from "./config/runtime.js"
|
||||
import { CharacterDataModel, LoksyuDataModel, NpcDataModel, TinjiDataModel } from "./data/actors/index.js"
|
||||
import { EquipmentDataModel, KungfuDataModel, SpellDataModel, SupernaturalDataModel } from "./data/items/index.js"
|
||||
import { EquipmentDataModel, KungfuDataModel, SpellDataModel, SupernaturalDataModel, WeaponDataModel, ArmorDataModel, SanheiDataModel, IngredientDataModel } from "./data/items/index.js"
|
||||
import { CDEMessage } from "./documents/chat-message.js"
|
||||
import { CDEActor } from "./documents/actor.js"
|
||||
import { CDEItem } from "./documents/item.js"
|
||||
@@ -10,7 +10,7 @@ import { registerDice } from "./ui/dice.js"
|
||||
import { registerHandlebarsHelpers } from "./ui/helpers.js"
|
||||
import { preloadPartials } from "./ui/templates.js"
|
||||
import { CDELoksyuSheet, CDECharacterSheet, CDENpcSheet, CDETinjiSheet } from "./ui/sheets/actors/index.js"
|
||||
import { CDEItemSheet, CDEKungfuSheet, CDESpellSheet, CDESupernaturalSheet } from "./ui/sheets/items/index.js"
|
||||
import { CDEItemSheet, CDEKungfuSheet, CDESpellSheet, CDESupernaturalSheet, CDEWeaponSheet, CDEArmorSheet, CDESanheiSheet, CDEIngredientSheet } from "./ui/sheets/items/index.js"
|
||||
import { migrateIfNeeded, registerSettings } from "./migration.js"
|
||||
|
||||
Hooks.once("i18nInit", preLocalizeConfig)
|
||||
@@ -22,17 +22,21 @@ Hooks.once("init", async () => {
|
||||
|
||||
game.system.CONST = { MAGICS, SUBTYPES }
|
||||
|
||||
CONFIG.Actor.systemDataModels = {
|
||||
CONFIG.Actor.dataModels = {
|
||||
[ACTOR_TYPES.character]: CharacterDataModel,
|
||||
[ACTOR_TYPES.npc]: NpcDataModel,
|
||||
[ACTOR_TYPES.tinji]: TinjiDataModel,
|
||||
[ACTOR_TYPES.loksyu]: LoksyuDataModel,
|
||||
}
|
||||
CONFIG.Item.systemDataModels = {
|
||||
CONFIG.Item.dataModels = {
|
||||
[ITEM_TYPES.item]: EquipmentDataModel,
|
||||
[ITEM_TYPES.kungfu]: KungfuDataModel,
|
||||
[ITEM_TYPES.spell]: SpellDataModel,
|
||||
[ITEM_TYPES.supernatural]: SupernaturalDataModel,
|
||||
[ITEM_TYPES.weapon]: WeaponDataModel,
|
||||
[ITEM_TYPES.armor]: ArmorDataModel,
|
||||
[ITEM_TYPES.sanhei]: SanheiDataModel,
|
||||
[ITEM_TYPES.ingredient]: IngredientDataModel,
|
||||
}
|
||||
|
||||
CONFIG.Actor.documentClass = CDEActor
|
||||
@@ -41,50 +45,70 @@ Hooks.once("init", async () => {
|
||||
|
||||
configureRuntime()
|
||||
|
||||
DocumentSheetConfig.unregisterSheet(Actor, "core", ActorSheet)
|
||||
DocumentSheetConfig.unregisterSheet(Item, "core", ItemSheet)
|
||||
foundry.applications.apps.DocumentSheetConfig.unregisterSheet(Actor, "core", ActorSheet)
|
||||
foundry.applications.apps.DocumentSheetConfig.unregisterSheet(Item, "core", ItemSheet)
|
||||
|
||||
DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDECharacterSheet, {
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDECharacterSheet, {
|
||||
types: [ACTOR_TYPES.character],
|
||||
makeDefault: true,
|
||||
label: "CDE Character Sheet (V2)",
|
||||
})
|
||||
DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDENpcSheet, {
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDENpcSheet, {
|
||||
types: [ACTOR_TYPES.npc],
|
||||
makeDefault: true,
|
||||
label: "CDE NPC Sheet (V2)",
|
||||
})
|
||||
DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDETinjiSheet, {
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDETinjiSheet, {
|
||||
types: [ACTOR_TYPES.tinji],
|
||||
makeDefault: true,
|
||||
label: "CDE Tinji Sheet (V2)",
|
||||
})
|
||||
DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDELoksyuSheet, {
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Actor, SYSTEM_ID, CDELoksyuSheet, {
|
||||
types: [ACTOR_TYPES.loksyu],
|
||||
makeDefault: true,
|
||||
label: "CDE Loksyu Sheet (V2)",
|
||||
})
|
||||
|
||||
DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEItemSheet, {
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEItemSheet, {
|
||||
types: [ITEM_TYPES.item],
|
||||
makeDefault: true,
|
||||
label: "CDE Item Sheet (V2)",
|
||||
})
|
||||
DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEKungfuSheet, {
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEKungfuSheet, {
|
||||
types: [ITEM_TYPES.kungfu],
|
||||
makeDefault: true,
|
||||
label: "CDE KungFu Sheet (V2)",
|
||||
})
|
||||
DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESpellSheet, {
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESpellSheet, {
|
||||
types: [ITEM_TYPES.spell],
|
||||
makeDefault: true,
|
||||
label: "CDE Spell Sheet (V2)",
|
||||
})
|
||||
DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESupernaturalSheet, {
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESupernaturalSheet, {
|
||||
types: [ITEM_TYPES.supernatural],
|
||||
makeDefault: true,
|
||||
label: "CDE Supernatural Sheet (V2)",
|
||||
})
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEWeaponSheet, {
|
||||
types: [ITEM_TYPES.weapon],
|
||||
makeDefault: true,
|
||||
label: "CDE Weapon Sheet (V2)",
|
||||
})
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEArmorSheet, {
|
||||
types: [ITEM_TYPES.armor],
|
||||
makeDefault: true,
|
||||
label: "CDE Armor Sheet (V2)",
|
||||
})
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDESanheiSheet, {
|
||||
types: [ITEM_TYPES.sanhei],
|
||||
makeDefault: true,
|
||||
label: "CDE Sanhei Sheet (V2)",
|
||||
})
|
||||
foundry.applications.apps.DocumentSheetConfig.registerSheet(Item, SYSTEM_ID, CDEIngredientSheet, {
|
||||
types: [ITEM_TYPES.ingredient],
|
||||
makeDefault: true,
|
||||
label: "CDE Ingredient Sheet (V2)",
|
||||
})
|
||||
|
||||
await preloadPartials()
|
||||
registerHandlebarsHelpers()
|
||||
|
||||
173
src/ui/initiative.js
Normal file
173
src/ui/initiative.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Initiative determination system for Chroniques de l'Étrange.
|
||||
*
|
||||
* PJ formula: Initiative = Prouesse + Première action (compétence/ressource/magie)
|
||||
* PNJ formula: Initiative = Aptitude physique + Première action (aptitude)
|
||||
*
|
||||
* Range 1-24 ; anti-initiative = 25 − initiative.
|
||||
* Combat order is ascending (low initiative acts first).
|
||||
*/
|
||||
|
||||
const PC_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-prompt.html"
|
||||
const NPC_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-prompt-npc.html"
|
||||
const RESULT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-initiative-result.html"
|
||||
|
||||
/** Skills, resources and magics available as "première action" for a PC. */
|
||||
function buildPCOptions(sys) {
|
||||
const sk = sys.skills ?? {}
|
||||
const rs = sys.resources ?? {}
|
||||
const mg = sys.magics ?? {}
|
||||
return [
|
||||
{ key: "art", label: game.i18n.localize("CDE.Art"), value: sk.art?.value ?? 0 },
|
||||
{ key: "investigation", label: game.i18n.localize("CDE.Investigation"), value: sk.investigation?.value ?? 0 },
|
||||
{ key: "erudition", label: game.i18n.localize("CDE.Erudition"), value: sk.erudition?.value ?? 0 },
|
||||
{ key: "knavery", label: game.i18n.localize("CDE.Knavery"), value: sk.knavery?.value ?? 0 },
|
||||
{ key: "wordliness", label: game.i18n.localize("CDE.Wordliness"), value: sk.wordliness?.value ?? 0 },
|
||||
{ key: "prowess", label: game.i18n.localize("CDE.Prowess"), value: sk.prowess?.value ?? 0 },
|
||||
{ key: "sciences", label: game.i18n.localize("CDE.Sciences"), value: sk.sciences?.value ?? 0 },
|
||||
{ key: "technologies", label: game.i18n.localize("CDE.Technologies"), value: sk.technologies?.value ?? 0 },
|
||||
{ key: "kungfu", label: game.i18n.localize("CDE.KungFu"), value: sk.kungfu?.value ?? 0 },
|
||||
{ key: "rangedcombat", label: game.i18n.localize("CDE.RangedCombat"), value: sk.rangedcombat?.value ?? 0 },
|
||||
{ key: "supply", label: game.i18n.localize("CDE.Supply"), value: rs.supply?.value ?? 0 },
|
||||
{ key: "inquiry", label: game.i18n.localize("CDE.Inquiry"), value: rs.inquiry?.value ?? 0 },
|
||||
{ key: "influence", label: game.i18n.localize("CDE.Influence"), value: rs.influence?.value ?? 0 },
|
||||
{ key: "internalcinnabar", label: game.i18n.localize("CDE.InternalCinnabar"), value: mg.internalcinnabar?.value ?? 0 },
|
||||
{ key: "alchemy", label: game.i18n.localize("CDE.Alchemy"), value: mg.alchemy?.value ?? 0 },
|
||||
{ key: "masteryoftheway", label: game.i18n.localize("CDE.MasteryOfTheWay"), value: mg.masteryoftheway?.value ?? 0 },
|
||||
{ key: "exorcism", label: game.i18n.localize("CDE.Exorcism"), value: mg.exorcism?.value ?? 0 },
|
||||
{ key: "geomancy", label: game.i18n.localize("CDE.Geomancy"), value: mg.geomancy?.value ?? 0 },
|
||||
]
|
||||
}
|
||||
|
||||
/** Aptitudes available as "première action" for an NPC. */
|
||||
function buildNPCOptions(sys) {
|
||||
const ap = sys.aptitudes ?? {}
|
||||
return [
|
||||
{ key: "physical", label: game.i18n.localize("CDE.Physical"), value: ap.physical?.value ?? 0 },
|
||||
{ key: "martial", label: game.i18n.localize("CDE.Martial"), value: ap.martial?.value ?? 0 },
|
||||
{ key: "mental", label: game.i18n.localize("CDE.Mental"), value: ap.mental?.value ?? 0 },
|
||||
{ key: "social", label: game.i18n.localize("CDE.Social"), value: ap.social?.value ?? 0 },
|
||||
{ key: "spiritual", label: game.i18n.localize("CDE.Spiritual"), value: ap.spiritual?.value ?? 0 },
|
||||
]
|
||||
}
|
||||
|
||||
/** Parse the dialog element and extract firstaction + modifier. */
|
||||
function readInitFields(dialog) {
|
||||
const root = dialog.element ?? dialog
|
||||
const selectedKey = root.querySelector("select[name='firstaction']")?.value ?? ""
|
||||
const modifier = parseInt(root.querySelector("input[name='modifier']")?.value ?? 0) || 0
|
||||
return { selectedKey, modifier }
|
||||
}
|
||||
|
||||
/** Post a styled initiative chat message. */
|
||||
async function sendInitChatMessage({ actor, baseName, baseValue, actionName, actionValue, modifier, initiative, antiInitiative }) {
|
||||
const html = await foundry.applications.handlebars.renderTemplate(RESULT_TEMPLATE, {
|
||||
actorName: actor.name,
|
||||
actorImg: actor.img,
|
||||
baseName,
|
||||
baseValue,
|
||||
actionName,
|
||||
actionValue,
|
||||
modifier,
|
||||
hasModifier: modifier !== 0,
|
||||
initiative,
|
||||
antiInitiative,
|
||||
})
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content: html,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the PC initiative dialog, compute initiative (Prouesse + Première action + modificateur)
|
||||
* and update the actor, then post a chat card.
|
||||
*/
|
||||
export async function rollInitiativePC(actor) {
|
||||
const sys = actor.system
|
||||
const prowess = sys.skills?.prowess?.value ?? 0
|
||||
const options = buildPCOptions(sys)
|
||||
const baseName = game.i18n.localize("CDE.Prowess")
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(PC_PROMPT_TEMPLATE, {
|
||||
prowessValue: prowess,
|
||||
options,
|
||||
modifier: 0,
|
||||
})
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.prompt({
|
||||
window: { title: game.i18n.localize("CDE.InitiativeRoll") },
|
||||
content,
|
||||
rejectClose: false,
|
||||
ok: {
|
||||
label: game.i18n.localize("CDE.Validate"),
|
||||
callback: (_ev, _btn, dialog) => readInitFields(dialog),
|
||||
},
|
||||
})
|
||||
if (!result) return
|
||||
|
||||
const { selectedKey, modifier } = result
|
||||
const selected = options.find((o) => o.key === selectedKey) ?? options[0]
|
||||
const rawValue = prowess + selected.value + modifier
|
||||
const initiative = Math.max(1, Math.min(24, rawValue))
|
||||
const antiInit = 25 - initiative
|
||||
|
||||
await actor.update({ "system.initiative": initiative })
|
||||
await sendInitChatMessage({
|
||||
actor,
|
||||
baseName,
|
||||
baseValue: prowess,
|
||||
actionName: selected.label,
|
||||
actionValue: selected.value,
|
||||
modifier,
|
||||
initiative,
|
||||
antiInitiative: antiInit,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the NPC initiative dialog, compute initiative (Aptitude physique + Première action + modificateur)
|
||||
* and update the actor, then post a chat card.
|
||||
*/
|
||||
export async function rollInitiativeNPC(actor) {
|
||||
const sys = actor.system
|
||||
const physical = sys.aptitudes?.physical?.value ?? 0
|
||||
const options = buildNPCOptions(sys)
|
||||
const baseName = game.i18n.localize("CDE.Physical")
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(NPC_PROMPT_TEMPLATE, {
|
||||
physicalValue: physical,
|
||||
options,
|
||||
modifier: 0,
|
||||
})
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.prompt({
|
||||
window: { title: game.i18n.localize("CDE.InitiativeRoll") },
|
||||
content,
|
||||
rejectClose: false,
|
||||
ok: {
|
||||
label: game.i18n.localize("CDE.Validate"),
|
||||
callback: (_ev, _btn, dialog) => readInitFields(dialog),
|
||||
},
|
||||
})
|
||||
if (!result) return
|
||||
|
||||
const { selectedKey, modifier } = result
|
||||
const selected = options.find((o) => o.key === selectedKey) ?? options[0]
|
||||
const rawValue = physical + selected.value + modifier
|
||||
const initiative = Math.max(1, Math.min(24, rawValue))
|
||||
const antiInit = 25 - initiative
|
||||
|
||||
await actor.update({ "system.initiative": initiative })
|
||||
await sendInitChatMessage({
|
||||
actor,
|
||||
baseName,
|
||||
baseValue: physical,
|
||||
actionName: selected.label,
|
||||
actionValue: selected.value,
|
||||
modifier,
|
||||
initiative,
|
||||
antiInitiative: antiInit,
|
||||
})
|
||||
}
|
||||
621
src/ui/rolling.js
Normal file
621
src/ui/rolling.js
Normal file
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* Wu Xing rolling system for Chroniques de l'Étrange.
|
||||
*
|
||||
* The Wu Xing cycle maps each aspect (by index 0-4) to die face groups:
|
||||
* - metal=0 : faces 3,8
|
||||
* - water=1 : faces 1,6
|
||||
* - earth=2 : faces 0/10,5
|
||||
* - fire=3 : faces 2,7
|
||||
* - wood=4 : faces 4,9
|
||||
*
|
||||
* For a given active aspect the five result categories are:
|
||||
* successes / auspicious / noxious / loksyu (yin face, yang face) / tinji
|
||||
* Each category is associated with one of the five aspects in Wu Xing cycle order.
|
||||
*/
|
||||
|
||||
const RESULT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-dice-result.html"
|
||||
const SKILL_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-skill-dice-prompt.html"
|
||||
const SKILL_SPECIAL_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-skill-special-dice-prompt.html"
|
||||
const MAGIC_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-magic-dice-prompt.html"
|
||||
const WEAPON_PROMPT_TEMPLATE = "systems/fvtt-chroniques-de-l-etrange/templates/form/cde-weapon-dice-prompt.html"
|
||||
|
||||
/** Maps i18n element label → aspect name (for speciality default aspect lookup) */
|
||||
const LABELELEMENT_TO_ASPECT = {
|
||||
"CDE.Metal": "metal",
|
||||
"CDE.Water": "water",
|
||||
"CDE.Earth": "earth",
|
||||
"CDE.Fire": "fire",
|
||||
"CDE.Wood": "wood",
|
||||
}
|
||||
|
||||
/** Map aspect index → string name used in result template */
|
||||
const ASPECT_NAMES = ["metal", "water", "earth", "fire", "wood"]
|
||||
|
||||
/** Map aspect name → i18n label key */
|
||||
const ASPECT_LABELS = {
|
||||
metal: "CDE.Metal",
|
||||
water: "CDE.Water",
|
||||
earth: "CDE.Earth",
|
||||
fire: "CDE.Fire",
|
||||
wood: "CDE.Wood",
|
||||
}
|
||||
|
||||
/** Map aspect name → image path */
|
||||
const ASPECT_ICONS = {
|
||||
metal: "systems/fvtt-chroniques-de-l-etrange/images/cde_metal.png",
|
||||
water: "systems/fvtt-chroniques-de-l-etrange/images/cde_eau.png",
|
||||
earth: "systems/fvtt-chroniques-de-l-etrange/images/cde_terre.png",
|
||||
fire: "systems/fvtt-chroniques-de-l-etrange/images/cde_feu.png",
|
||||
wood: "systems/fvtt-chroniques-de-l-etrange/images/cde_bois.png",
|
||||
}
|
||||
|
||||
/** Map aspect index → die face pair [yin, yang] (face=10 stored as 0) */
|
||||
const ASPECT_FACES = {
|
||||
metal: [3, 8],
|
||||
water: [1, 6],
|
||||
earth: [0, 5], // 0 = face "10"
|
||||
fire: [2, 7],
|
||||
wood: [4, 9],
|
||||
}
|
||||
|
||||
/**
|
||||
* Wu Xing generating/overcoming cycle:
|
||||
* wood → fire → earth → metal → water → wood (generating)
|
||||
* For each active aspect, the five categories in order:
|
||||
* [successes, auspicious, noxious, loksyu, tinji]
|
||||
*/
|
||||
const WU_XING_CYCLE = {
|
||||
wood: ["wood", "fire", "water", "earth", "metal"],
|
||||
fire: ["fire", "earth", "wood", "metal", "water"],
|
||||
earth: ["earth", "metal", "fire", "water", "wood"],
|
||||
metal: ["metal", "water", "earth", "wood", "fire"],
|
||||
water: ["water", "wood", "metal", "fire", "earth"],
|
||||
}
|
||||
|
||||
/** Maps weapon range string → dice malus applied to the attack pool */
|
||||
const RANGE_MALUS = {
|
||||
contact: 0,
|
||||
courte: 0,
|
||||
mediane: -1,
|
||||
longue: -2,
|
||||
extreme: -3,
|
||||
}
|
||||
|
||||
/** Maps weapon type string → default skill key */
|
||||
const WEAPON_TYPE_SKILL = {
|
||||
melee: "kungfu",
|
||||
thrown: "rangedcombat",
|
||||
ranged: "rangedcombat",
|
||||
firearm: "rangedcombat",
|
||||
}
|
||||
|
||||
/** Maps weapon damageAspect name → ASPECT_NAMES index */
|
||||
const WEAPON_ASPECT_INDEX = { metal: 0, eau: 1, water: 1, terre: 2, earth: 2, feu: 3, fire: 3, bois: 4, wood: 4 }
|
||||
|
||||
/** Count how many times each die face appeared in the roll results */
|
||||
function countFaces(rollResults) {
|
||||
const counts = { 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 0:0 }
|
||||
for (const die of rollResults) {
|
||||
const face = die.result === 10 ? 0 : die.result
|
||||
counts[face]++
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute Wu Xing result categories from face counts and active aspect.
|
||||
* Returns { successesdice, auspiciousdice, noxiousdice, loksyudice, tinjidice, loksyurepartition }
|
||||
*/
|
||||
function computeWuXingResults(faces, aspectName, bonusAuspicious = 0) {
|
||||
const cycle = WU_XING_CYCLE[aspectName]
|
||||
if (!cycle) return null
|
||||
|
||||
const [succAspect, ausAspect, noxAspect, lokAspect, tinAspect] = cycle
|
||||
const [succYin, succYang] = ASPECT_FACES[succAspect]
|
||||
const [ausYin, ausYang] = ASPECT_FACES[ausAspect]
|
||||
const [noxYin, noxYang] = ASPECT_FACES[noxAspect]
|
||||
const [lokYin, lokYang] = ASPECT_FACES[lokAspect]
|
||||
const [tinYin, tinYang] = ASPECT_FACES[tinAspect]
|
||||
|
||||
const yin = game.i18n.localize("CDE.Yin")
|
||||
const yang = game.i18n.localize("CDE.Yang")
|
||||
|
||||
return {
|
||||
successesdice: faces[succYin] + faces[succYang],
|
||||
auspiciousdice: faces[ausYin] + faces[ausYang] + bonusAuspicious,
|
||||
noxiousdice: faces[noxYin] + faces[noxYang],
|
||||
loksyudice: faces[lokYin] + faces[lokYang],
|
||||
loksyurepartition: `[${yin}(${faces[lokYin]}) ${yang}(${faces[lokYang]})]`,
|
||||
tinjidice: faces[tinYin] + faces[tinYang],
|
||||
}
|
||||
}
|
||||
|
||||
/** Read a named field from a dialog DOM element */
|
||||
function readField(dlg, name) {
|
||||
const el = dlg.querySelector(`[name="${name}"]`)
|
||||
if (!el) return null
|
||||
return el.type === "checkbox" ? el.checked : el.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a DialogV2.prompt with the given template + data and return the resolved form values.
|
||||
* The callback receives the DialogV2 application instance; fields are read from its .element.
|
||||
* @returns {Promise<Record<string,string>|null>}
|
||||
*/
|
||||
async function showRollPrompt({ title, template, data, fields }) {
|
||||
const content = await foundry.applications.handlebars.renderTemplate(template, data)
|
||||
return foundry.applications.api.DialogV2.prompt({
|
||||
window: { title },
|
||||
content,
|
||||
rejectClose: false,
|
||||
ok: {
|
||||
label: game.i18n.localize("CDE.Validate"),
|
||||
callback: (event, button, dialog) => {
|
||||
// In AppV2, dialog is the application instance; .element is the root HTMLElement
|
||||
const root = dialog.element ?? dialog
|
||||
const result = {}
|
||||
for (const field of fields) {
|
||||
result[field] = readField(root, field)
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the skill roll prompt and return the user-confirmed parameters.
|
||||
* @param {object} params - Initial values
|
||||
* @returns {Promise<object|null>}
|
||||
*/
|
||||
export async function showSkillPrompt(params) {
|
||||
return showRollPrompt({
|
||||
title: params.title,
|
||||
template: params.isSpecial ? SKILL_SPECIAL_PROMPT_TEMPLATE : SKILL_PROMPT_TEMPLATE,
|
||||
data: {
|
||||
numberofdice: params.numberofdice,
|
||||
aspect: Number(params.aspect ?? 0),
|
||||
bonusmalus: params.bonusmalus ?? 0,
|
||||
woundmalus: params.woundmalus ?? 0,
|
||||
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
|
||||
typeofthrow: Number(params.typeofthrow ?? 0),
|
||||
},
|
||||
fields: ["aspect", "bonusmalus", "woundmalus", "bonusauspiciousdice", "typeofthrow"],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the magic roll prompt and return the user-confirmed parameters.
|
||||
*/
|
||||
export async function showMagicPrompt(params) {
|
||||
return showRollPrompt({
|
||||
title: params.title,
|
||||
template: MAGIC_PROMPT_TEMPLATE,
|
||||
data: {
|
||||
numberofdice: params.numberofdice ?? 0,
|
||||
aspectskill: Number(params.aspectskill ?? 0),
|
||||
bonusmalusskill: params.bonusmalusskill ?? 0,
|
||||
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
|
||||
aspectspeciality: Number(params.aspectspeciality ?? 0),
|
||||
rolldifficulty: params.rolldifficulty ?? 1,
|
||||
bonusmalusspeciality: params.bonusmalusspeciality ?? 0,
|
||||
heispend: params.heispend ?? 0,
|
||||
typeofthrow: Number(params.typeofthrow ?? 0),
|
||||
},
|
||||
fields: ["aspectskill", "bonusmalusskill", "bonusauspiciousdice",
|
||||
"aspectspeciality", "rolldifficulty", "bonusmalusspeciality",
|
||||
"heispend", "typeofthrow"],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the weapon attack roll prompt and return user-confirmed parameters.
|
||||
*/
|
||||
export async function showWeaponPrompt(params) {
|
||||
return showRollPrompt({
|
||||
title: params.title,
|
||||
template: WEAPON_PROMPT_TEMPLATE,
|
||||
data: {
|
||||
numberofdice: params.numberofdice ?? 0,
|
||||
weaponName: params.weaponName ?? "",
|
||||
weaponTypeLabel: params.weaponTypeLabel ?? "CDE.Weapon",
|
||||
weaponAspectIcon: params.weaponAspectIcon ?? "",
|
||||
weaponAspectLabel: params.weaponAspectLabel ?? "",
|
||||
damageBase: params.damageBase ?? 1,
|
||||
weaponskill: params.weaponskill ?? "kungfu",
|
||||
aspect: Number(params.aspect ?? 0),
|
||||
effectiverange: params.effectiverange ?? "contact",
|
||||
bonusmalus: params.bonusmalus ?? 0,
|
||||
woundmalus: params.woundmalus ?? 0,
|
||||
bonusauspiciousdice: params.bonusauspiciousdice ?? 0,
|
||||
typeofthrow: Number(params.typeofthrow ?? 0),
|
||||
},
|
||||
fields: ["weaponskill", "aspect", "effectiverange", "bonusmalus", "woundmalus",
|
||||
"bonusauspiciousdice", "typeofthrow"],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and send a single enriched ChatMessage containing both the roll
|
||||
* (for Dice So Nice) and the Wu Xing result card.
|
||||
*/
|
||||
async function sendResultMessage(actor, resultData, roll, rollMode) {
|
||||
const html = await foundry.applications.handlebars.renderTemplate(RESULT_TEMPLATE, resultData)
|
||||
return ChatMessage.create({
|
||||
user: game.user.id,
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content: html,
|
||||
rolls: [roll],
|
||||
rollMode,
|
||||
})
|
||||
}
|
||||
|
||||
const ROLL_MODES = ["roll", "gmroll", "blindroll", "selfroll"]
|
||||
|
||||
/**
|
||||
* Main entry point: roll dice for a given actor.
|
||||
*
|
||||
* @param {Actor} actor
|
||||
* @param {string} rollKey - e.g. "prowess-skill", "fire-aspect", "alchemy-magic-elixirs"
|
||||
*/
|
||||
export async function rollForActor(actor, rollKey) {
|
||||
const parts = rollKey.split("-")
|
||||
const skillLibel = parts[0]
|
||||
const typeLibel = parts[1]
|
||||
const specialLibel = parts[2] ?? null
|
||||
|
||||
const sys = actor.system
|
||||
const typeOfThrow = Number(sys.prefs?.typeofthrow?.choice ?? 0)
|
||||
|
||||
let numberofdice = 0
|
||||
let title = ""
|
||||
let isSpecial = false
|
||||
let isMagic = false
|
||||
let isMagicSpecial = false
|
||||
let kfDefaultAspect = -1 // set in "itemkungfu" case, used when computing defaultAspect
|
||||
|
||||
// ---- Determine dice count + title ----
|
||||
const MAGIC_I18N_KEYS = {
|
||||
internalcinnabar: "CDE.InternalCinnabar",
|
||||
alchemy: "CDE.Alchemy",
|
||||
masteryoftheway: "CDE.MasteryOfTheWay",
|
||||
exorcism: "CDE.Exorcism",
|
||||
geomancy: "CDE.Geomancy",
|
||||
}
|
||||
|
||||
switch (typeLibel) {
|
||||
case "aspect":
|
||||
numberofdice = sys.aspect[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.aspect[skillLibel]?.label ?? "CDE.Roll")
|
||||
break
|
||||
case "skill":
|
||||
numberofdice = sys.skills[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.skills[skillLibel]?.label ?? "CDE.Roll")
|
||||
break
|
||||
case "special":
|
||||
numberofdice = sys.skills[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.skills[skillLibel]?.label ?? "CDE.Roll")
|
||||
title += ` [${game.i18n.localize("CDE.Speciality")}]`
|
||||
isSpecial = true
|
||||
if (!sys.skills[skillLibel]?.specialities) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error2"))
|
||||
return
|
||||
}
|
||||
break
|
||||
case "resource":
|
||||
numberofdice = sys.resources[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.resources[skillLibel]?.label ?? "CDE.Roll")
|
||||
break
|
||||
case "field":
|
||||
numberofdice = sys.resources[skillLibel]?.value ?? 0
|
||||
title = game.i18n.localize(sys.resources[skillLibel]?.label ?? "CDE.Roll")
|
||||
title += ` [${game.i18n.localize("CDE.Field")}]`
|
||||
isSpecial = true
|
||||
if (!sys.resources[skillLibel]?.specialities) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error4"))
|
||||
return
|
||||
}
|
||||
break
|
||||
case "magic":
|
||||
numberofdice = sys.magics[skillLibel]?.value ?? 0
|
||||
isMagic = true
|
||||
title = game.i18n.localize(MAGIC_I18N_KEYS[skillLibel] ?? "CDE.Magics")
|
||||
break
|
||||
case "magicspecial":
|
||||
numberofdice = sys.magics[skillLibel]?.value ?? 0
|
||||
isMagicSpecial = true
|
||||
isMagic = true
|
||||
if (!sys.magics[skillLibel]?.speciality?.[specialLibel]?.check) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error6"))
|
||||
return
|
||||
}
|
||||
title = `${game.i18n.localize(MAGIC_I18N_KEYS[skillLibel] ?? "CDE.Magics")} [${game.i18n.localize(game.system.CONST?.MAGICS?.[skillLibel]?.speciality?.[specialLibel]?.label ?? "")}]`
|
||||
break
|
||||
case "itemkungfu": {
|
||||
// skillLibel = item._id — look up the kungfu item to find which skill + aspect to use
|
||||
const kfItem = actor.items.get(skillLibel)
|
||||
if (!kfItem) { ui.notifications.warn(game.i18n.localize("CDE.Error0")); return }
|
||||
const kfSkill = kfItem.system.skill ?? "kungfu"
|
||||
numberofdice = sys.skills[kfSkill]?.value ?? 0
|
||||
title = `${kfItem.name} [${game.i18n.localize(sys.skills[kfSkill]?.label ?? "CDE.KungFu")}]`
|
||||
kfDefaultAspect = ASPECT_NAMES.indexOf(kfItem.system.aspect ?? "metal")
|
||||
if (kfDefaultAspect < 0) kfDefaultAspect = 0
|
||||
break
|
||||
}
|
||||
case "itemweapon": {
|
||||
// skillLibel = item._id — look up the weapon item to find type + aspect + damage
|
||||
const wpItem = actor.items.get(skillLibel)
|
||||
if (!wpItem) { ui.notifications.warn(game.i18n.localize("CDE.Error0")); return }
|
||||
|
||||
const wpType = wpItem.system.weaponType ?? "melee"
|
||||
const wpSkill = WEAPON_TYPE_SKILL[wpType] ?? "kungfu"
|
||||
numberofdice = sys.skills[wpSkill]?.value ?? 0
|
||||
|
||||
const wpAspectRaw = wpItem.system.damageAspect ?? "metal"
|
||||
const wpAspectIdx = WEAPON_ASPECT_INDEX[wpAspectRaw] ?? 0
|
||||
const wpRange = wpItem.system.range ?? "contact"
|
||||
|
||||
const WEAPON_TYPE_LABELS = {
|
||||
melee: "CDE.WeaponMelee",
|
||||
thrown: "CDE.WeaponThrown",
|
||||
ranged: "CDE.WeaponRanged",
|
||||
firearm: "CDE.WeaponFirearm",
|
||||
}
|
||||
|
||||
// Show weapon-specific prompt
|
||||
const wParams = await showWeaponPrompt({
|
||||
title: `${wpItem.name} [${game.i18n.localize(sys.skills[wpSkill]?.label ?? "CDE.WeaponRoll")}]`,
|
||||
numberofdice,
|
||||
weaponName: wpItem.name,
|
||||
weaponTypeLabel: WEAPON_TYPE_LABELS[wpType] ?? "CDE.Weapon",
|
||||
weaponAspectIcon: ASPECT_ICONS[ASPECT_NAMES[wpAspectIdx]] ?? "",
|
||||
weaponAspectLabel: game.i18n.localize(ASPECT_LABELS[ASPECT_NAMES[wpAspectIdx]] ?? ""),
|
||||
damageBase: wpItem.system.damageBase ?? 1,
|
||||
weaponskill: wpSkill,
|
||||
aspect: wpAspectIdx,
|
||||
effectiverange: wpRange,
|
||||
bonusmalus: 0,
|
||||
woundmalus: 0,
|
||||
bonusauspiciousdice: 0,
|
||||
typeofthrow: typeOfThrow,
|
||||
})
|
||||
|
||||
if (!wParams) return // cancelled
|
||||
|
||||
// Resolve final pool from weapon prompt values
|
||||
const wpChosenSkill = wParams.weaponskill ?? wpSkill
|
||||
const wpSkillDice = sys.skills[wpChosenSkill]?.value ?? 0
|
||||
const wpAspFinal = Number(wParams.aspect ?? wpAspectIdx)
|
||||
const wpAspectDice = sys.aspect[ASPECT_NAMES[wpAspFinal]]?.value ?? 0
|
||||
const wpRangeMalus = RANGE_MALUS[wParams.effectiverange ?? "contact"] ?? 0
|
||||
const wpBonusMalus = Number(wParams.bonusmalus ?? 0)
|
||||
const wpWoundMalus = Number(wParams.woundmalus ?? 0)
|
||||
const wpBonusAusp = Number(wParams.bonusauspiciousdice ?? 0)
|
||||
const wpThrowMode = Number(wParams.typeofthrow ?? 0)
|
||||
const wpDamageBase = wpItem.system.damageBase ?? 1
|
||||
|
||||
const wpTotalDice = wpSkillDice + wpAspectDice + wpRangeMalus + wpBonusMalus - wpWoundMalus
|
||||
if (wpTotalDice <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error0"))
|
||||
return
|
||||
}
|
||||
|
||||
const wpRoll = new Roll(`${wpTotalDice}d10`)
|
||||
await wpRoll.evaluate()
|
||||
|
||||
const wpAspectName = ASPECT_NAMES[wpAspFinal] ?? "metal"
|
||||
const wpFaces = countFaces(wpRoll.dice[0]?.results ?? [])
|
||||
const wpResults = computeWuXingResults(wpFaces, wpAspectName, wpBonusAusp)
|
||||
if (!wpResults) return
|
||||
|
||||
const wpModParts = []
|
||||
if (wpRangeMalus !== 0) wpModParts.push(`${wpRangeMalus} ${game.i18n.localize("CDE.RangePenalty")}`)
|
||||
if (wpBonusMalus !== 0) wpModParts.push(`${wpBonusMalus > 0 ? "+" : ""}${wpBonusMalus} ${game.i18n.localize("CDE.BonusMalus")}`)
|
||||
if (wpWoundMalus !== 0) wpModParts.push(`-${wpWoundMalus} ${game.i18n.localize("CDE.WoundMalus")}`)
|
||||
if (wpBonusAusp !== 0) wpModParts.push(`+${wpBonusAusp} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`)
|
||||
|
||||
const wpMsg = await sendResultMessage(actor, {
|
||||
rollLabel: `${wpItem.name}`,
|
||||
aspectName: wpAspectName,
|
||||
aspectLabel: game.i18n.localize(ASPECT_LABELS[wpAspectName] ?? ""),
|
||||
aspectIcon: ASPECT_ICONS[wpAspectName] ?? "",
|
||||
totalDice: wpTotalDice,
|
||||
modifiersText: wpModParts.length ? wpModParts.join(" · ") : "",
|
||||
spellPower: null,
|
||||
rollDifficulty: null,
|
||||
actorName: actor.name ?? "",
|
||||
actorImg: actor.img ?? "",
|
||||
// weapon-specific
|
||||
weaponName: wpItem.name,
|
||||
damageBase: wpDamageBase,
|
||||
totalDamage: wpResults.successesdice * wpDamageBase,
|
||||
...wpResults,
|
||||
aspect: wpAspectName,
|
||||
d1: wpFaces[1], d2: wpFaces[2], d3: wpFaces[3], d4: wpFaces[4], d5: wpFaces[5],
|
||||
d6: wpFaces[6], d7: wpFaces[7], d8: wpFaces[8], d9: wpFaces[9], d0: wpFaces[0],
|
||||
}, wpRoll, ROLL_MODES[wpThrowMode] ?? "roll")
|
||||
|
||||
if (game.modules.get("dice-so-nice")?.active && wpMsg?.id) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(wpMsg.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
default:
|
||||
ui.notifications.warn(`Unknown roll type: ${typeLibel}`)
|
||||
return
|
||||
}
|
||||
|
||||
// For magic rolls the prompt allows adding HEI dice, so don't block early.
|
||||
// For itemkungfu, allow 0 base dice (user can add bonus dice in the prompt).
|
||||
if (numberofdice <= 0 && typeLibel !== "aspect" && typeLibel !== "itemkungfu" && !isMagic) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error0"))
|
||||
return
|
||||
}
|
||||
|
||||
// ---- Pre-compute default aspect for magic based on skill ----
|
||||
const MAGIC_ASPECTS = {
|
||||
internalcinnabar: 0, // metal
|
||||
alchemy: 1, // water
|
||||
masteryoftheway: 2, // earth
|
||||
exorcism: 3, // fire
|
||||
geomancy: 4, // wood
|
||||
}
|
||||
|
||||
let defaultAspect = typeLibel === "aspect"
|
||||
? ["metal","water","earth","fire","wood"].indexOf(skillLibel)
|
||||
: 0
|
||||
if (isMagic && MAGIC_ASPECTS[skillLibel] !== undefined) {
|
||||
defaultAspect = MAGIC_ASPECTS[skillLibel]
|
||||
}
|
||||
if (kfDefaultAspect >= 0) {
|
||||
defaultAspect = kfDefaultAspect
|
||||
}
|
||||
|
||||
let defaultSpecialAspect = 0
|
||||
if (isMagicSpecial && specialLibel) {
|
||||
// Look up the speciality's element from the MAGICS config constant
|
||||
const specialCfg = game.system.CONST?.MAGICS?.[skillLibel]?.speciality?.[specialLibel]
|
||||
const aspectName = LABELELEMENT_TO_ASPECT[specialCfg?.labelelement]
|
||||
if (aspectName) {
|
||||
defaultSpecialAspect = ASPECT_NAMES.indexOf(aspectName)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Show roll prompt ----
|
||||
let params
|
||||
|
||||
if (isMagic) {
|
||||
params = await showMagicPrompt({
|
||||
title,
|
||||
numberofdice,
|
||||
aspectskill: defaultAspect,
|
||||
bonusmalusskill: 0,
|
||||
bonusauspiciousdice: 0,
|
||||
aspectspeciality: defaultSpecialAspect,
|
||||
rolldifficulty: 1,
|
||||
bonusmalusspeciality: 0,
|
||||
heispend: 0,
|
||||
typeofthrow: typeOfThrow,
|
||||
})
|
||||
} else {
|
||||
params = await showSkillPrompt({
|
||||
title,
|
||||
numberofdice,
|
||||
aspect: defaultAspect,
|
||||
bonusmalus: 0,
|
||||
woundmalus: 0,
|
||||
bonusauspiciousdice: 0,
|
||||
typeofthrow: typeOfThrow,
|
||||
isSpecial,
|
||||
})
|
||||
}
|
||||
|
||||
if (!params) return // cancelled
|
||||
|
||||
// ---- Compute total dice and roll ----
|
||||
let aspectIndex, bonusMalus, bonusAuspicious, throwMode
|
||||
let spellAspectIndex = null // magic only: aspect of the speciality for Wu Xing
|
||||
let rollDifficulty = 1 // magic only: multiplier applied to successes
|
||||
|
||||
if (isMagic) {
|
||||
const skillAspectIndex = Number(params.aspectskill ?? 0)
|
||||
spellAspectIndex = Number(params.aspectspeciality ?? skillAspectIndex)
|
||||
aspectIndex = skillAspectIndex // used only for skill dice pool
|
||||
bonusMalus = Number(params.bonusmalusskill ?? 0)
|
||||
bonusAuspicious = Number(params.bonusauspiciousdice ?? 0)
|
||||
rollDifficulty = Math.max(1, Number(params.rolldifficulty ?? 1))
|
||||
throwMode = Number(params.typeofthrow ?? 0)
|
||||
// magic: magic skill + aspect + bonuses + 1 (speciality base) + HEI spent
|
||||
const aspectDice = sys.aspect[ASPECT_NAMES[aspectIndex]]?.value ?? 0
|
||||
const bonusSpec = Number(params.bonusmalusspeciality ?? 0)
|
||||
const heiDice = Number(params.heispend ?? 0)
|
||||
numberofdice = numberofdice + aspectDice + bonusMalus + 1 + bonusSpec + heiDice
|
||||
} else {
|
||||
aspectIndex = Number(params.aspect ?? 0)
|
||||
bonusMalus = Number(params.bonusmalus ?? 0)
|
||||
const woundMalus = Number(params.woundmalus ?? 0)
|
||||
bonusAuspicious = Number(params.bonusauspiciousdice ?? 0)
|
||||
throwMode = Number(params.typeofthrow ?? 0)
|
||||
|
||||
const aspectDice = (typeLibel !== "aspect")
|
||||
? (sys.aspect[ASPECT_NAMES[aspectIndex]]?.value ?? 0)
|
||||
: 0
|
||||
|
||||
numberofdice = numberofdice + aspectDice + bonusMalus - woundMalus
|
||||
if (isSpecial) numberofdice += 1 // +1d for speciality
|
||||
}
|
||||
|
||||
if (numberofdice <= 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CDE.Error0"))
|
||||
return
|
||||
}
|
||||
|
||||
// ---- Roll ----
|
||||
const roll = new Roll(`${numberofdice}d10`)
|
||||
await roll.evaluate()
|
||||
|
||||
const rollModeKey = ROLL_MODES[throwMode] ?? "roll"
|
||||
|
||||
// ---- Compute Wu Xing results ----
|
||||
// For magic rolls, the spell's aspect (aspectspeciality) governs the Wu Xing
|
||||
// cycle (which faces count as successes/auspicious/etc.), not the skill aspect.
|
||||
const wuXingAspectName = spellAspectIndex !== null
|
||||
? ASPECT_NAMES[spellAspectIndex]
|
||||
: ASPECT_NAMES[aspectIndex]
|
||||
const allResults = roll.dice[0]?.results ?? []
|
||||
const faces = countFaces(allResults)
|
||||
const results = computeWuXingResults(faces, wuXingAspectName, bonusAuspicious)
|
||||
if (!results) return
|
||||
|
||||
// For magic, successesdice × rollDifficulty = spell power
|
||||
const spellPower = isMagic ? results.successesdice * rollDifficulty : null
|
||||
|
||||
// ---- Build modifier summary text ----
|
||||
const modParts = []
|
||||
if (isMagic) {
|
||||
const bm = Number(params.bonusmalusskill ?? 0)
|
||||
const bs = Number(params.bonusmalusspeciality ?? 0)
|
||||
const hs = Number(params.heispend ?? 0)
|
||||
const ba = Number(params.bonusauspiciousdice ?? 0)
|
||||
if (bm !== 0) modParts.push(`${bm > 0 ? "+" : ""}${bm} ${game.i18n.localize("CDE.BonusMalus")}`)
|
||||
if (bs !== 0) modParts.push(`${bs > 0 ? "+" : ""}${bs} ${game.i18n.localize("CDE.SpellBonus")}`)
|
||||
if (ba !== 0) modParts.push(`+${ba} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`)
|
||||
if (hs !== 0) modParts.push(`${hs} ${game.i18n.localize("CDE.HeiSpend")}`)
|
||||
if (rollDifficulty !== 1) modParts.push(`×${rollDifficulty} ${game.i18n.localize("CDE.RollDifficulty")}`)
|
||||
} else {
|
||||
const bm = Number(params.bonusmalus ?? 0)
|
||||
const wm = Number(params.woundmalus ?? 0)
|
||||
const ba = Number(params.bonusauspiciousdice ?? 0)
|
||||
if (bm !== 0) modParts.push(`${bm > 0 ? "+" : ""}${bm} ${game.i18n.localize("CDE.BonusMalus")}`)
|
||||
if (wm !== 0) modParts.push(`-${wm} ${game.i18n.localize("CDE.WoundMalus")}`)
|
||||
if (ba !== 0) modParts.push(`+${ba} ${game.i18n.localize("CDE.BonusAuspiciousDice")}`)
|
||||
}
|
||||
|
||||
// ---- Send single enriched ChatMessage (roll + result card) ----
|
||||
const msg = await sendResultMessage(actor, {
|
||||
// Roll identity
|
||||
rollLabel: title,
|
||||
aspectName: wuXingAspectName,
|
||||
aspectLabel: game.i18n.localize(ASPECT_LABELS[wuXingAspectName] ?? ""),
|
||||
aspectIcon: ASPECT_ICONS[wuXingAspectName] ?? "",
|
||||
totalDice: numberofdice,
|
||||
modifiersText: modParts.length ? modParts.join(" · ") : "",
|
||||
// Spell power (magic only)
|
||||
spellPower,
|
||||
rollDifficulty: isMagic ? rollDifficulty : null,
|
||||
// Actor info
|
||||
actorName: actor.name ?? "",
|
||||
actorImg: actor.img ?? "",
|
||||
// Wu Xing results
|
||||
aspect: wuXingAspectName,
|
||||
...results,
|
||||
// Die face counts
|
||||
d1: faces[1], d2: faces[2], d3: faces[3], d4: faces[4], d5: faces[5],
|
||||
d6: faces[6], d7: faces[7], d8: faces[8], d9: faces[9], d0: faces[0],
|
||||
}, roll, rollModeKey)
|
||||
|
||||
// ---- Wait for Dice So Nice animation ----
|
||||
if (game.modules.get("dice-so-nice")?.active && msg?.id) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(msg.id)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||
export class CDEBaseActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["fvtt-chroniques-de-l-etrange", "actor"],
|
||||
position: { width: 920, height: "auto" },
|
||||
position: { width: 920, height: 800 },
|
||||
window: { resizable: true },
|
||||
form: { submitOnChange: true },
|
||||
dragDrop: [{ dragSelector: ".item, [data-drag='true']", dropSelector: null }],
|
||||
@@ -16,6 +16,10 @@ export class CDEBaseActorSheet extends HandlebarsApplicationMixin(foundry.applic
|
||||
|
||||
tabGroups = { primary: "description" }
|
||||
|
||||
get title() {
|
||||
return this.document.name
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const descriptionHTML = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description ?? "", { async: true })
|
||||
const cssClass = this.options.classes?.join(" ") ?? ""
|
||||
@@ -23,6 +27,7 @@ export class CDEBaseActorSheet extends HandlebarsApplicationMixin(foundry.applic
|
||||
actor: this.document,
|
||||
system: this.document.system,
|
||||
systemData: this.document.system,
|
||||
systemFields: this.document.system.schema.fields,
|
||||
items: this.document.items.contents,
|
||||
descriptionHTML,
|
||||
editable: this.isEditable,
|
||||
@@ -30,14 +35,11 @@ export class CDEBaseActorSheet extends HandlebarsApplicationMixin(foundry.applic
|
||||
}
|
||||
}
|
||||
|
||||
async _onFirstRender(context, options) {
|
||||
await super._onFirstRender(context, options)
|
||||
for (const [group, tab] of Object.entries(this.tabGroups)) {
|
||||
this.changeTab(tab, group, { force: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the active tab after every render (including re-renders from submitOnChange).
|
||||
// AppV2 does NOT preserve tab state natively — we must re-apply it from this.tabGroups,
|
||||
// which is dynamically updated by changeTab() when the user clicks a tab.
|
||||
_onRender(context, options) {
|
||||
super._onRender?.(context, options)
|
||||
for (const [group, tab] of Object.entries(this.tabGroups)) {
|
||||
this.changeTab(tab, group, { force: true })
|
||||
}
|
||||
@@ -48,6 +50,10 @@ export class CDEBaseActorSheet extends HandlebarsApplicationMixin(foundry.applic
|
||||
const cls = getDocumentClass("Item")
|
||||
const labels = {
|
||||
item: "CDE.ItemNew",
|
||||
weapon: "CDE.WeaponNew",
|
||||
armor: "CDE.ArmorNew",
|
||||
sanhei: "CDE.SanheiNew",
|
||||
ingredient: "CDE.IngredientNew",
|
||||
kungfu: "CDE.KFNew",
|
||||
spell: "CDE.SpellNew",
|
||||
supernatural: "CDE.SupernaturalNew",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { MAGICS, SUBTYPES } from "../../../config/constants.js"
|
||||
import { rollInitiativePC } from "../../initiative.js"
|
||||
import { rollForActor } from "../../rolling.js"
|
||||
import { CDEBaseActorSheet } from "./base.js"
|
||||
|
||||
export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
@@ -15,6 +17,10 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.equipments = context.items.filter((item) => item.type === "item")
|
||||
context.weapons = context.items.filter((item) => item.type === "weapon")
|
||||
context.armors = context.items.filter((item) => item.type === "armor")
|
||||
context.sanheis = context.items.filter((item) => item.type === "sanhei")
|
||||
context.ingredients = context.items.filter((item) => item.type === "ingredient")
|
||||
context.spells = context.items.filter((item) => item.type === "spell")
|
||||
context.kungfus = context.items.filter((item) => item.type === "kungfu")
|
||||
context.CDE = { MAGICS, SUBTYPES }
|
||||
@@ -25,6 +31,7 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
super._onRender?.(context, options)
|
||||
this.#bindInitiativeControls()
|
||||
this.#bindPrefs()
|
||||
this.#bindRollButtons()
|
||||
}
|
||||
|
||||
#bindInitiativeControls() {
|
||||
@@ -45,26 +52,7 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
return
|
||||
}
|
||||
if (action === "create") {
|
||||
const html = `
|
||||
<form class="flexcol">
|
||||
<div class="form-group">
|
||||
<label>${game.i18n.localize("CDE.TurnOrder")}</label>
|
||||
<input type="number" name="initiative" value="${initiative}" min="1" max="24" />
|
||||
</div>
|
||||
</form>`
|
||||
const value = await Dialog.prompt({
|
||||
title: game.i18n.localize("CDE.TurnOrder"),
|
||||
content: html,
|
||||
label: game.i18n.localize("CDE.Validate"),
|
||||
callback: (dlg) => {
|
||||
const input = dlg.querySelector("input[name='initiative']")
|
||||
return Number(input?.value ?? initiative)
|
||||
},
|
||||
})
|
||||
if (Number.isFinite(value)) {
|
||||
const sanitized = foundry.utils.clamp(Number(value), 1, 24)
|
||||
await this.document.update({ "system.initiative": sanitized })
|
||||
}
|
||||
await rollInitiativePC(this.document)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -109,4 +97,16 @@ export class CDECharacterSheet extends CDEBaseActorSheet {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#bindRollButtons() {
|
||||
const cells = this.element?.querySelectorAll("td.click[data-libel-id], td.click2[data-libel-id], .cde-roll-trigger[data-libel-id]")
|
||||
if (!cells?.length) return
|
||||
cells.forEach((cell) => {
|
||||
cell.addEventListener("click", (event) => {
|
||||
event.preventDefault()
|
||||
const rollKey = cell.dataset.libelId
|
||||
if (rollKey) rollForActor(this.document, rollKey)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { rollInitiativeNPC } from "../../initiative.js"
|
||||
import { CDEBaseActorSheet } from "./base.js"
|
||||
|
||||
export class CDENpcSheet extends CDEBaseActorSheet {
|
||||
@@ -43,23 +44,7 @@ export class CDENpcSheet extends CDEBaseActorSheet {
|
||||
return
|
||||
}
|
||||
if (action === "create") {
|
||||
const html = `
|
||||
<form class="flexcol">
|
||||
<div class="form-group">
|
||||
<label>${game.i18n.localize("CDE.TurnOrder")}</label>
|
||||
<input type="number" name="initiative" value="${initiative}" min="1" max="24" />
|
||||
</div>
|
||||
</form>`
|
||||
const value = await Dialog.prompt({
|
||||
title: game.i18n.localize("CDE.TurnOrder"),
|
||||
content: html,
|
||||
label: game.i18n.localize("CDE.Validate"),
|
||||
callback: (dlg) => Number(dlg.querySelector("input[name='initiative']")?.value ?? initiative),
|
||||
})
|
||||
if (Number.isFinite(value)) {
|
||||
const sanitized = foundry.utils.clamp(Number(value), 1, 24)
|
||||
await this.document.update({ "system.initiative": sanitized })
|
||||
}
|
||||
await rollInitiativeNPC(this.document)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
12
src/ui/sheets/items/armor.js
Normal file
12
src/ui/sheets/items/armor.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { CDEBaseItemSheet } from "./base.js"
|
||||
|
||||
export class CDEArmorSheet extends CDEBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["armor"],
|
||||
position: { width: 520, height: 460 },
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-armor-sheet.html" },
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,11 @@ export class CDEBaseItemSheet extends HandlebarsApplicationMixin(foundry.applica
|
||||
actions: {},
|
||||
}
|
||||
|
||||
tabGroups = { primary: "description" }
|
||||
tabGroups = { primary: "details" }
|
||||
|
||||
get title() {
|
||||
return this.document.name
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const cssClass = this.options.classes?.join(" ") ?? ""
|
||||
@@ -19,6 +23,7 @@ export class CDEBaseItemSheet extends HandlebarsApplicationMixin(foundry.applica
|
||||
item: this.document,
|
||||
system: this.document.system,
|
||||
systemData: this.document.system,
|
||||
systemFields: this.document.system.schema.fields,
|
||||
editable: this.isEditable,
|
||||
cssClass,
|
||||
enrichedDescription,
|
||||
@@ -28,14 +33,9 @@ export class CDEBaseItemSheet extends HandlebarsApplicationMixin(foundry.applica
|
||||
}
|
||||
}
|
||||
|
||||
async _onFirstRender(context, options) {
|
||||
await super._onFirstRender(context, options)
|
||||
for (const [group, tab] of Object.entries(this.tabGroups)) {
|
||||
this.changeTab(tab, group, { force: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the active tab after every render (including re-renders from submitOnChange).
|
||||
_onRender(context, options) {
|
||||
super._onRender?.(context, options)
|
||||
for (const [group, tab] of Object.entries(this.tabGroups)) {
|
||||
this.changeTab(tab, group, { force: true })
|
||||
}
|
||||
|
||||
@@ -3,3 +3,7 @@ export { CDEItemSheet } from "./item.js"
|
||||
export { CDEKungfuSheet } from "./kungfu.js"
|
||||
export { CDESpellSheet } from "./spell.js"
|
||||
export { CDESupernaturalSheet } from "./supernatural.js"
|
||||
export { CDEWeaponSheet } from "./weapon.js"
|
||||
export { CDEArmorSheet } from "./armor.js"
|
||||
export { CDESanheiSheet } from "./sanhei.js"
|
||||
export { CDEIngredientSheet } from "./ingredient.js"
|
||||
|
||||
12
src/ui/sheets/items/ingredient.js
Normal file
12
src/ui/sheets/items/ingredient.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { CDEBaseItemSheet } from "./base.js"
|
||||
|
||||
export class CDEIngredientSheet extends CDEBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["ingredient"],
|
||||
position: { width: 520, height: 460 },
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-ingredient-sheet.html" },
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,12 @@
|
||||
import { SUBTYPES } from "../../../config/constants.js"
|
||||
import { CDEBaseItemSheet } from "./base.js"
|
||||
|
||||
export class CDEItemSheet extends CDEBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["equipment"],
|
||||
position: { width: 620, height: 580 },
|
||||
position: { width: 560, height: 460 },
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-item-sheet.html" },
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.subtypes = SUBTYPES
|
||||
context.isWeapon = this.document.isWeapon
|
||||
context.isArmor = this.document.isArmor
|
||||
context.isSanhei = this.document.isSanhei
|
||||
context.isOther = this.document.isOther
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
23
src/ui/sheets/items/sanhei.js
Normal file
23
src/ui/sheets/items/sanhei.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { CDEBaseItemSheet } from "./base.js"
|
||||
|
||||
export class CDESanheiSheet extends CDEBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["sanhei"],
|
||||
position: { width: 580, height: 620 },
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-sanhei-sheet.html" },
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true })
|
||||
const props = this.document.system.properties
|
||||
context.prop1DescriptionHTML = await enrich(props.prop1.description)
|
||||
context.prop2DescriptionHTML = await enrich(props.prop2.description)
|
||||
context.prop3DescriptionHTML = await enrich(props.prop3.description)
|
||||
context.propFields = this.document.system.schema.fields.properties.fields
|
||||
return context
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,11 @@ export class CDESupernaturalSheet extends CDEBaseItemSheet {
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-supernatural-sheet.html" },
|
||||
}
|
||||
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
const enrich = (content) => foundry.applications.ux.TextEditor.implementation.enrichHTML(content ?? "", { async: true })
|
||||
context.effectsHTML = await enrich(this.document.system.effects)
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
12
src/ui/sheets/items/weapon.js
Normal file
12
src/ui/sheets/items/weapon.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { CDEBaseItemSheet } from "./base.js"
|
||||
|
||||
export class CDEWeaponSheet extends CDEBaseItemSheet {
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["weapon"],
|
||||
position: { width: 580, height: 520 },
|
||||
}
|
||||
|
||||
static PARTS = {
|
||||
main: { template: "systems/fvtt-chroniques-de-l-etrange/templates/item/cde-weapon-sheet.html" },
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user