System development, WIP

This commit is contained in:
2026-05-05 13:55:42 +02:00
commit c0223977d2
250 changed files with 10362 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
export { default as MGNEActorSheet } from "./sheets/base-actor-sheet.mjs"
export { default as MGNEItemSheet } from "./sheets/base-item-sheet.mjs"
export { default as MGNECharacterSheet } from "./sheets/character-sheet.mjs"
export { default as MGNECreatureSheet } from "./sheets/creature-sheet.mjs"
export { default as MGNECompanionSheet } from "./sheets/companion-sheet.mjs"
export { default as MGNEWeaponSheet } from "./sheets/weapon-sheet.mjs"
export { default as MGNEArmorSheet } from "./sheets/armor-sheet.mjs"
export { default as MGNEShieldSheet } from "./sheets/shield-sheet.mjs"
export { default as MGNEEquipmentSheet } from "./sheets/equipment-sheet.mjs"
export { default as MGNEResonanceCoreSheet } from "./sheets/resonance-core-sheet.mjs"
export { default as MGNEArtifactSheet } from "./sheets/artifact-sheet.mjs"
export { default as MGNEFeatureSheet } from "./sheets/feature-sheet.mjs"
@@ -0,0 +1,7 @@
import MGNEItemSheet from "./base-item-sheet.mjs"
export default class MGNEArmorSheet extends MGNEItemSheet {
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/armor.hbs" },
}
}
@@ -0,0 +1,7 @@
import MGNEItemSheet from "./base-item-sheet.mjs"
export default class MGNEArtifactSheet extends MGNEItemSheet {
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/artifact.hbs" },
}
}
@@ -0,0 +1,156 @@
import { SYSTEM } from "../../config/system.mjs"
import { buildSharedSelectOptions } from "./select-options.mjs"
import { enrichHTMLFields } from "./rich-text.mjs"
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class MGNEActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
static DEFAULT_OPTIONS = {
classes: ["mgne", "actor-sheet"],
position: {
width: 900,
height: "auto",
},
window: {
resizable: true,
},
form: {
submitOnChange: true,
},
actions: {
editImage: MGNEActorSheet.onEditImage,
changeTab: MGNEActorSheet.onChangeTab,
createItem: MGNEActorSheet.onCreateItem,
editItem: MGNEActorSheet.onEditItem,
deleteItem: MGNEActorSheet.onDeleteItem,
toggleEquipped: MGNEActorSheet.onToggleEquipped,
syncArtifact: MGNEActorSheet.onSyncArtifact,
resetDaily: MGNEActorSheet.onResetDaily,
rollResonancePerDay: MGNEActorSheet.onRollResonancePerDay,
quickRest: MGNEActorSheet.onQuickRest,
fullRest: MGNEActorSheet.onFullRest,
},
}
async _prepareContext() {
const systemFields = this.document.system.schema.fields
return {
fields: this.document.schema.fields,
systemFields,
actor: this.document,
system: this.document.system,
source: this.document.toObject(),
config: SYSTEM,
enrichedFields: await enrichHTMLFields(this.document.system, systemFields),
isEditable: this.isEditable,
selectOptions: buildSharedSelectOptions(),
}
}
_onRender(context, options) {
super._onRender?.(context, options)
this.element.querySelectorAll(".rollable").forEach(element => {
element.addEventListener("click", this._onRoll.bind(this))
})
}
async _onRoll(event) {
const target = event.currentTarget
const itemId = target.closest("[data-item-id]")?.dataset.itemId
const rollType = target.dataset.rollType
switch (rollType) {
case "ability":
return this.document.rollAbility(target.dataset.abilityId)
case "defense":
return this.document.rollDefense()
case "weapon":
return this.document.rollWeapon(itemId)
case "profile-attack":
return this.document.rollProfileAttack()
case "profile-damage":
return this.document.rollProfileDamage()
case "damage":
return this.document.rollDamage(itemId)
case "resonation":
return this.document.rollResonation(itemId)
case "morale":
return this.document.rollMorale()
case "usage":
return this.document.rollUsage(itemId)
default:
return null
}
}
static async onEditImage(_event, target) {
const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr)
const picker = new FilePicker({
current,
type: "image",
callback: path => this.document.update({ [attr]: path }),
top: this.position.top + 40,
left: this.position.left + 10,
})
return picker.browse()
}
static onChangeTab(_event, target) {
const group = target.dataset.group
const tab = target.dataset.tab
this.tabGroups[group] = tab
this.render()
}
static async onCreateItem(_event, target) {
const itemType = target.dataset.itemType
const typeLabel = SYSTEM.itemTypes[itemType]?.label ?? itemType
const [created] = await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.format("MGNE.Common.NewItem", { type: typeLabel }),
type: itemType,
img: SYSTEM.itemTypes[itemType]?.icon,
}])
created?.sheet?.render(true)
}
static async onEditItem(_event, target) {
const itemId = target.closest("[data-item-id]")?.dataset.itemId
const item = this.document.items.get(itemId)
return item?.sheet?.render(true)
}
static async onDeleteItem(_event, target) {
const itemId = target.closest("[data-item-id]")?.dataset.itemId
const item = this.document.items.get(itemId)
if (!item) return null
return item.delete()
}
static async onToggleEquipped(_event, target) {
const itemId = target.closest("[data-item-id]")?.dataset.itemId
return this.document.toggleItemEquipped(itemId)
}
static async onSyncArtifact(_event, target) {
const itemId = target.closest("[data-item-id]")?.dataset.itemId
return this.document.syncArtifact(itemId)
}
static async onResetDaily() {
return this.document.resetDaily()
}
static async onRollResonancePerDay() {
return this.document.rollResonancePerDay()
}
static async onQuickRest() {
return this.document.quickRest()
}
static async onFullRest() {
return this.document.fullRest()
}
}
@@ -0,0 +1,56 @@
import { SYSTEM } from "../../config/system.mjs"
import { buildSharedSelectOptions, numericOptions } from "./select-options.mjs"
import { enrichHTMLFields } from "./rich-text.mjs"
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class MGNEItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
static DEFAULT_OPTIONS = {
classes: ["mgne", "item-sheet"],
position: {
width: 720,
height: "auto",
},
window: {
resizable: true,
},
form: {
submitOnChange: true,
},
actions: {
editImage: MGNEItemSheet.onEditImage,
},
}
async _prepareContext() {
const selectOptions = buildSharedSelectOptions()
const systemFields = this.document.system.schema.fields
if (this.document.type === "armor") selectOptions.penalties = numericOptions(0, 6, this.document.system.penalty)
if (this.document.type === "shield") selectOptions.penalties = numericOptions(0, 4, this.document.system.penalty)
return {
fields: this.document.schema.fields,
systemFields,
item: this.document,
system: this.document.system,
source: this.document.toObject(),
config: SYSTEM,
enrichedFields: await enrichHTMLFields(this.document.system, systemFields),
isEditable: this.isEditable,
selectOptions,
}
}
static async onEditImage(_event, target) {
const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr)
const picker = new FilePicker({
current,
type: "image",
callback: path => this.document.update({ [attr]: path }),
top: this.position.top + 40,
left: this.position.left + 10,
})
return picker.browse()
}
}
@@ -0,0 +1,99 @@
import MGNEActorSheet from "./base-actor-sheet.mjs"
import { SYSTEM } from "../../config/system.mjs"
import { buildCharacterSelectOptions } from "./select-options.mjs"
export default class MGNECharacterSheet extends MGNEActorSheet {
static DEFAULT_OPTIONS = {
classes: ["character"],
position: {
width: 1040,
height: 760,
},
}
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-main.hbs" },
tabs: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-tabs.hbs" },
overview: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-overview.hbs" },
daily: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-daily.hbs" },
equipment: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-equipment.hbs" },
features: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-features.hbs" },
notes: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/character-notes.hbs" },
}
tabGroups = { sheet: "overview" }
getTabs() {
const tabs = {
overview: { id: "overview", group: "sheet", label: game.i18n.localize("MGNE.Tabs.overview") },
daily: { id: "daily", group: "sheet", label: game.i18n.localize("MGNE.Tabs.daily") },
equipment: { id: "equipment", group: "sheet", label: game.i18n.localize("MGNE.Tabs.equipment") },
features: { id: "features", group: "sheet", label: game.i18n.localize("MGNE.Tabs.features") },
notes: { id: "notes", group: "sheet", label: game.i18n.localize("MGNE.Tabs.notes") },
}
for (const tab of Object.values(tabs)) {
tab.active = this.tabGroups[tab.group] === tab.id
tab.cssClass = tab.active ? "active" : ""
}
return tabs
}
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.getTabs()
context.abilityList = SYSTEM.abilityOrder.map(id => ({
id,
...SYSTEM.abilities[id],
value: context.source.system.abilities?.[id]?.value ?? 0,
}))
context.selectOptions = {
...context.selectOptions,
...buildCharacterSelectOptions(context.system),
}
return context
}
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "overview":
context.tab = context.tabs.overview
context.valueConditions = Object.entries(doc.system.conditions ?? {})
.filter(([id]) => SYSTEM.conditions[id]?.hasValue)
.map(([id, cond]) => ({
id,
label: SYSTEM.conditions[id].label,
value: cond.value,
options: context.selectOptions.conditionValues,
}))
context.flagConditions = Object.entries(doc.system.conditions ?? {})
.filter(([id]) => !SYSTEM.conditions[id]?.hasValue)
.map(([id, cond]) => ({
id,
label: SYSTEM.conditions[id].label,
active: cond.active,
}))
break
case "daily":
context.tab = context.tabs.daily
break
case "equipment":
context.tab = context.tabs.equipment
context.weapons = doc.itemTypes.weapon
context.armors = doc.itemTypes.armor
context.shields = doc.itemTypes.shield
context.equipmentItems = doc.itemTypes.equipment
context.cores = doc.itemTypes["resonance-core"]
context.artifacts = doc.itemTypes.artifact
break
case "features":
context.tab = context.tabs.features
context.features = doc.itemTypes.feature
break
case "notes":
context.tab = context.tabs.notes
break
}
return context
}
}
@@ -0,0 +1,26 @@
import MGNEActorSheet from "./base-actor-sheet.mjs"
import { SYSTEM } from "../../config/system.mjs"
export default class MGNECompanionSheet extends MGNEActorSheet {
static DEFAULT_OPTIONS = {
classes: ["companion"],
position: {
width: 820,
height: 700,
},
}
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/companion-main.hbs" },
}
async _prepareContext() {
const context = await super._prepareContext()
context.abilityList = SYSTEM.abilityOrder.map(id => ({
id,
...SYSTEM.abilities[id],
value: context.source.system.abilities?.[id]?.value ?? 0,
}))
return context
}
}
@@ -0,0 +1,26 @@
import MGNEActorSheet from "./base-actor-sheet.mjs"
import { SYSTEM } from "../../config/system.mjs"
export default class MGNECreatureSheet extends MGNEActorSheet {
static DEFAULT_OPTIONS = {
classes: ["creature"],
position: {
width: 760,
height: 640,
},
}
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/creature-main.hbs" },
}
async _prepareContext() {
const context = await super._prepareContext()
context.abilityList = SYSTEM.abilityOrder.map(id => ({
id,
...SYSTEM.abilities[id],
value: context.source.system.abilities?.[id]?.value ?? 0,
}))
return context
}
}
@@ -0,0 +1,7 @@
import MGNEItemSheet from "./base-item-sheet.mjs"
export default class MGNEEquipmentSheet extends MGNEItemSheet {
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/equipment.hbs" },
}
}
@@ -0,0 +1,7 @@
import MGNEItemSheet from "./base-item-sheet.mjs"
export default class MGNEFeatureSheet extends MGNEItemSheet {
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/feature.hbs" },
}
}
@@ -0,0 +1,7 @@
import MGNEItemSheet from "./base-item-sheet.mjs"
export default class MGNEResonanceCoreSheet extends MGNEItemSheet {
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/resonance-core.hbs" },
}
}
+17
View File
@@ -0,0 +1,17 @@
export async function enrichHTMLFields(data, schemaFields) {
const enrichedFields = {}
for (const [key, field] of Object.entries(schemaFields ?? {})) {
if (field instanceof foundry.data.fields.HTMLField) {
enrichedFields[key] = await foundry.applications.ux.TextEditor.implementation.enrichHTML(data?.[key] ?? "", { async: true })
continue
}
if (field instanceof foundry.data.fields.SchemaField) {
const nested = await enrichHTMLFields(data?.[key], field.fields)
if (Object.keys(nested).length) enrichedFields[key] = nested
}
}
return enrichedFields
}
@@ -0,0 +1,50 @@
import { SYSTEM } from "../../config/system.mjs"
function normalizeMax(max, current) {
return Math.max(max ?? 0, Number.isFinite(current) ? current : 0)
}
export function numericOptions(min, max, current = null) {
const resolvedMin = Math.min(min, Number.isFinite(current) ? current : min)
const resolvedMax = normalizeMax(max, current)
return Array.from({ length: Math.max(0, resolvedMax - resolvedMin) + 1 }, (_, index) => {
const value = resolvedMin + index
return { value, label: String(value) }
})
}
export function objectOptions(choices) {
return Object.entries(choices).map(([value, label]) => ({ value, label }))
}
export function dieMax(die) {
if (typeof die !== "string" || !die.startsWith("d")) return 0
const faces = Number.parseInt(die.slice(1), 10)
return Number.isFinite(faces) ? faces : 0
}
export function buildSharedSelectOptions() {
return {
abilityValues: numericOptions(-3, 6),
conditionValues: numericOptions(0, 12),
moraleValues: numericOptions(2, 12),
armorPenalties: numericOptions(0, 6),
shieldPenalties: numericOptions(0, 4),
weaponCategories: objectOptions(SYSTEM.weaponCategories),
usageDice: objectOptions(SYSTEM.usageDieChoices),
armorDice: objectOptions(SYSTEM.armorDieChoices),
omenDice: objectOptions(SYSTEM.omenDieChoices),
resonanceList: objectOptions(SYSTEM.resonanceList),
equipmentSubtypes: objectOptions(SYSTEM.equipmentSubtypes),
artifactIds: objectOptions(SYSTEM.artifactChoices),
featureIds: objectOptions(SYSTEM.featureChoices),
}
}
export function buildCharacterSelectOptions(system) {
return {
omenCurrent: numericOptions(0, dieMax(system.omens?.die), system.omens?.current),
resonanceUsed: numericOptions(0, system.resonance?.max ?? 0, system.resonance?.used),
artifactSyncUsed: numericOptions(0, system.syncLimit ?? 0, system.artifactSync?.used),
}
}
@@ -0,0 +1,7 @@
import MGNEItemSheet from "./base-item-sheet.mjs"
export default class MGNEShieldSheet extends MGNEItemSheet {
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/shield.hbs" },
}
}
@@ -0,0 +1,7 @@
import MGNEItemSheet from "./base-item-sheet.mjs"
export default class MGNEWeaponSheet extends MGNEItemSheet {
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/weapon.hbs" },
}
}