Fix as per CSV sheet tracking + creature explanation

This commit is contained in:
2026-05-17 17:43:33 +02:00
parent a572c66678
commit 374854cc8b
99 changed files with 2716 additions and 464 deletions
+2
View File
@@ -3,6 +3,7 @@ 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 MGNEPartySheet } from "./sheets/party-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"
@@ -10,3 +11,4 @@ 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"
export { default as MGNECreatureTraitSheet } from "./sheets/creature-trait-sheet.mjs"
@@ -64,6 +64,8 @@ export default class MGNEActorSheet extends HandlebarsApplicationMixin(foundry.a
switch (rollType) {
case "ability":
return this.document.rollAbility(target.dataset.abilityId)
case "armor":
return this.document.rollArmorSave()
case "defense":
return this.document.rollDefense()
case "weapon":
@@ -78,6 +80,8 @@ export default class MGNEActorSheet extends HandlebarsApplicationMixin(foundry.a
return this.document.rollResonation(itemId)
case "morale":
return this.document.rollMorale()
case "durability":
return this.document.rollDurability(itemId)
case "usage":
return this.document.rollUsage(itemId)
default:
+11 -7
View File
@@ -2,6 +2,10 @@ import MGNEActorSheet from "./base-actor-sheet.mjs"
import { SYSTEM } from "../../config/system.mjs"
import { buildCharacterSelectOptions } from "./select-options.mjs"
export function stripHtml(html) {
return (html ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
}
export default class MGNECharacterSheet extends MGNEActorSheet {
static DEFAULT_OPTIONS = {
classes: ["character"],
@@ -79,16 +83,16 @@ export default class MGNECharacterSheet extends MGNEActorSheet {
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
context.weapons = doc.itemTypes.weapon.map(i => ({ ...i, tooltip: stripHtml(i.system.description) }))
context.armors = doc.itemTypes.armor.map(i => ({ ...i, tooltip: stripHtml(i.system.description) }))
context.shields = doc.itemTypes.shield.map(i => ({ ...i, tooltip: stripHtml(i.system.description) }))
context.equipmentItems = doc.itemTypes.equipment.map(i => ({ ...i, tooltip: stripHtml(i.system.description) }))
context.cores = doc.itemTypes["resonance-core"].map(i => ({ ...i, tooltip: stripHtml(i.system.description) }))
context.artifacts = doc.itemTypes.artifact.map(i => ({ ...i, tooltip: stripHtml(i.system.description) }))
break
case "features":
context.tab = context.tabs.features
context.features = doc.itemTypes.feature
context.features = doc.itemTypes.feature.map(i => ({ ...i, tooltip: stripHtml(i.system.description) }))
break
case "notes":
context.tab = context.tabs.notes
@@ -1,5 +1,4 @@
import MGNEActorSheet from "./base-actor-sheet.mjs"
import { SYSTEM } from "../../config/system.mjs"
export default class MGNECompanionSheet extends MGNEActorSheet {
static DEFAULT_OPTIONS = {
@@ -13,14 +12,4 @@ export default class MGNECompanionSheet extends MGNEActorSheet {
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
}
}
+65 -6
View File
@@ -1,12 +1,17 @@
import MGNEActorSheet from "./base-actor-sheet.mjs"
import { SYSTEM } from "../../config/system.mjs"
import { stripHtml } from "./character-sheet.mjs"
export default class MGNECreatureSheet extends MGNEActorSheet {
static DEFAULT_OPTIONS = {
classes: ["creature"],
position: {
width: 760,
height: 640,
height: 680,
},
actions: {
rollActionTable: MGNECreatureSheet.prototype._rollActionTable,
clearActionTable: MGNECreatureSheet.prototype._clearActionTable,
openActionTable: MGNECreatureSheet.prototype._openActionTable,
},
}
@@ -14,13 +19,67 @@ export default class MGNECreatureSheet extends MGNEActorSheet {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/creature-main.hbs" },
}
_processSubmitData(event, form, submitData) {
// Foundry sends null for unchecked checkboxes in a SetField array — strip them
if (Array.isArray(submitData.system?.creatureType)) {
submitData.system.creatureType = submitData.system.creatureType.filter(v => v != null && v !== "")
}
return super._processSubmitData(event, form, submitData)
}
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,
context.traits = (this.document.itemTypes["creature-trait"] ?? [])
.map(i => ({ ...i, tooltip: stripHtml(i.system.description) }))
// Resolve linked action table
const uuid = this.document.system.actionTableUuid
if (uuid) {
const table = await fromUuid(uuid).catch(() => null)
context.actionTable = table ? { name: table.name, uuid } : null
} else {
context.actionTable = null
}
// Build creature type checkboxes
const typeSet = this.document.system.creatureType ?? new Set()
context.creatureTypes = ["human", "construct", "animal"].map(key => ({
key,
label: game.i18n.localize(`MGNE.Creature.Types.${key.charAt(0).toUpperCase() + key.slice(1)}`),
checked: typeSet.has(key),
}))
return context
}
async _onDrop(event) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data?.type === "RollTable") {
const table = await fromUuid(data.uuid)
if (table) {
await this.document.update({ "system.actionTableUuid": data.uuid })
return
}
}
return super._onDrop(event)
}
async _rollActionTable() {
const uuid = this.document.system.actionTableUuid
if (!uuid) return ui.notifications.warn(game.i18n.localize("MGNE.Creature.NoTableLinked"))
const table = await fromUuid(uuid).catch(() => null)
if (!table) return ui.notifications.warn(game.i18n.localize("MGNE.Creature.TableNotFound"))
await table.draw()
}
async _clearActionTable() {
await this.document.update({ "system.actionTableUuid": "" })
}
async _openActionTable() {
const uuid = this.document.system.actionTableUuid
if (!uuid) return
const table = await fromUuid(uuid).catch(() => null)
if (table) table.sheet.render(true)
}
}
@@ -0,0 +1,7 @@
import MGNEItemSheet from "./base-item-sheet.mjs"
export default class MGNECreatureTraitSheet extends MGNEItemSheet {
static PARTS = {
main: { template: "systems/fvtt-machine-gods-noxian-expanse/templates/creature-trait.hbs" },
}
}
+162
View File
@@ -0,0 +1,162 @@
import MGNEActorSheet from "./base-actor-sheet.mjs"
const SYSTEM_ID = "fvtt-machine-gods-noxian-expanse"
const PARTY_LOOT_TYPES = new Set(["weapon", "armor", "shield", "equipment", "resonance-core", "artifact"])
export default class MGNEPartySheet extends MGNEActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["party"],
position: { width: 820, height: 640 },
actions: {
openMember: MGNEPartySheet.#onOpenMember,
removeMember: MGNEPartySheet.#onRemoveMember,
moveMemberUp: MGNEPartySheet.#onMoveMemberUp,
moveMemberDown: MGNEPartySheet.#onMoveMemberDown,
adjustCredits: MGNEPartySheet.#onAdjustCredits,
},
}
/** @override */
static PARTS = {
main: { template: `systems/${SYSTEM_ID}/templates/party-main.hbs` },
tabs: { template: `systems/${SYSTEM_ID}/templates/party-tabs.hbs` },
members: { template: `systems/${SYSTEM_ID}/templates/party-members.hbs` },
loot: { template: `systems/${SYSTEM_ID}/templates/party-loot.hbs` },
notes: { template: `systems/${SYSTEM_ID}/templates/party-notes.hbs` },
}
tabGroups = { sheet: "members" }
#getTabs() {
const tabs = {
members: { id: "members", group: "sheet", label: game.i18n.localize("MGNE.Tabs.members") },
loot: { id: "loot", group: "sheet", label: game.i18n.localize("MGNE.Tabs.loot") },
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
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "members": {
context.tab = context.tabs.members
// Build member list using actorId for moves; store refIdx (position in
// memberRefs) so move actions always operate on the correct slot even
// when some refs point to deleted actors.
const refs = doc.system.memberRefs ?? []
const members = []
for (let refIdx = 0; refIdx < refs.length; refIdx++) {
const actor = game.actors?.get(refs[refIdx].id)
if (!actor) continue
members.push({
id: actor.id,
refIdx,
name: actor.name,
img: actor.img,
type: actor.type,
typeLabel: game.i18n.localize(`TYPES.Actor.${actor.type}`),
hp: actor.system.hp
? `${actor.system.hp.value ?? "—"}/${actor.system.hp.max ?? "—"}`
: "—",
})
}
// isFirst/isLast based on visible list, but swap uses refIdx
for (let vi = 0; vi < members.length; vi++) {
members[vi].isFirst = vi === 0
members[vi].isLast = vi === members.length - 1
}
context.members = members
break
}
case "loot": {
context.tab = context.tabs.loot
context.lootItems = doc.items.contents
.filter(i => PARTY_LOOT_TYPES.has(i.type))
.map(i => ({
id: i.id,
img: i.img,
name: i.name,
typeLabel: game.i18n.localize(`TYPES.Item.${i.type}`),
}))
break
}
case "notes":
context.tab = context.tabs.notes
break
}
return context
}
/** @override */
async _onDrop(event) {
if (!this.isEditable) return
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data.type === "Actor") {
const actor = await fromUuid(data.uuid)
if (!actor || !["character", "companion"].includes(actor.type)) return
const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? [])
if (refs.some(r => r.id === actor.id)) return
refs.push({ id: actor.id })
return this.document.update({ "system.memberRefs": refs })
}
if (data.type === "Item") {
const item = await fromUuid(data.uuid)
if (!item || !PARTY_LOOT_TYPES.has(item.type)) return
return this.document.createEmbeddedDocuments("Item", [item.toObject()])
}
}
// ── Actions ─────────────────────────────────────────────────────────────────
static async #onOpenMember(_event, target) {
const actor = game.actors?.get(target.dataset.actorId)
if (actor) actor.sheet.render(true)
}
static async #onRemoveMember(_event, target) {
const id = target.dataset.actorId
const refs = (this.document.system.memberRefs ?? []).filter(r => r.id !== id)
await this.document.update({ "system.memberRefs": refs })
}
static async #onMoveMemberUp(_event, target) {
const refIdx = parseInt(target.dataset.refIdx, 10)
if (refIdx <= 0) return
const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? []);
[refs[refIdx - 1], refs[refIdx]] = [refs[refIdx], refs[refIdx - 1]]
await this.document.update({ "system.memberRefs": refs })
}
static async #onMoveMemberDown(_event, target) {
const refIdx = parseInt(target.dataset.refIdx, 10)
const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? [])
if (refIdx >= refs.length - 1) return;
[refs[refIdx], refs[refIdx + 1]] = [refs[refIdx + 1], refs[refIdx]]
await this.document.update({ "system.memberRefs": refs })
}
static async #onAdjustCredits(_event, target) {
const delta = parseInt(target.dataset.delta, 10)
const current = this.document.system.credits ?? 0
await this.document.update({ "system.credits": Math.max(0, current + delta) })
}
}
@@ -31,12 +31,13 @@ export function buildSharedSelectOptions() {
armorPenalties: numericOptions(0, 6),
shieldPenalties: numericOptions(0, 4),
weaponCategories: objectOptions(SYSTEM.weaponCategories),
weaponProperties: Object.entries(SYSTEM.weaponProperties).map(([key, p]) => ({ value: key, label: p.label, hint: p.hint })),
weightCategories: objectOptions(SYSTEM.weightCategories),
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),
}
}
+29
View File
@@ -28,6 +28,7 @@ export const SYSTEM = {
character: { id: "character", label: "Character" },
creature: { id: "creature", label: "Creature" },
companion: { id: "companion", label: "Companion" },
party: { id: "party", label: "Party" },
},
itemTypes: {
weapon: { id: "weapon", label: "Weapon", icon: itemIcon("weapon") },
@@ -37,6 +38,7 @@ export const SYSTEM = {
"resonance-core": { id: "resonance-core", label: "Resonance Core", icon: itemIcon("resonance-core") },
artifact: { id: "artifact", label: "Artifact", icon: itemIcon("artifact") },
feature: { id: "feature", label: "Feature", icon: itemIcon("feature") },
"creature-trait": { id: "creature-trait", label: "Creature Trait", icon: itemIcon("creature-trait") },
},
abilities: {
agility: { id: "agility", label: "Agility" },
@@ -63,10 +65,37 @@ export const SYSTEM = {
armorDieChoices: dieChoiceLabels(["d12", "d10", "d8", "d6", "d4", "d2", "0"]),
omenDice: ["d2", "d4", "d6", "d8"],
omenDieChoices: dieChoiceLabels(["d2", "d4", "d6", "d8"]),
weightCategories: {
trivial: "Trivial",
light: "Light",
normal: "Normal",
heavy: "Heavy",
},
weaponCategories: {
melee: "Melee",
ranged: "Ranged",
},
weaponProperties: {
ammo: { label: "Ammo", hint: "Requires ammunition. Improvised in melee or without ammo." },
awkward: { label: "Awkward", hint: "+1 DR to attacks." },
binding: { label: "Binding", hint: "Beat DR by 4+ on same-size or smaller target: inflict Restrained." },
durant: { label: "Durant", hint: "Durability die is D8." },
finesse: { label: "Finesse", hint: "Attacks can use either Presence or Strength." },
fling: { label: "Fling", hint: "Can make ranged attacks. Roll D4 Usage Die to avoid losing it." },
fragile: { label: "Fragile", hint: "Durability die is D4." },
glinting: { label: "Glinting", hint: "After attacking, next attack vs same target this combat gains -1 DR." },
overbearing:{ label: "Overbearing",hint: "+2 damage." },
parrying: { label: "Parrying", hint: "-1 DR to avoiding melee attacks while wielding." },
precise: { label: "Precise", hint: "-1 DR to hit." },
razored: { label: "Razored", hint: "Beat DR by 4+: inflict Bleeding (1)." },
ringing: { label: "Ringing", hint: "Beat DR by 4+ and target not Stunned: inflict Stunned (1)." },
"two-handed":{ label: "Two-Handed",hint: "Requires both hands." },
unwieldy: { label: "Unwieldy", hint: "Cannot attack more than once per Round." },
versatile: { label: "Versatile", hint: "Two-handed: -1 DR to hit and +1 damage." },
},
get weaponPropertyLabels() {
return Object.fromEntries(Object.entries(this.weaponProperties).map(([k, v]) => [k, v.label]))
},
resonanceList: {
accelerate: "Accelerate",
blast: "Blast",
+14 -1
View File
@@ -93,6 +93,10 @@ export default class MGNEActor extends Actor {
return result
}
async rollArmorSave() {
return MGNERoll.rollArmorSave(this)
}
async rollWeapon(itemId) {
const item = this.items.get(itemId)
if (!item) return null
@@ -129,7 +133,10 @@ export default class MGNEActor extends Actor {
async rollProfileAttack() {
const attackBaseLabel = normalizeGenericActionLabel(this.system.attack?.label ?? t("MGNE.Common.Attack"), t("MGNE.Common.Attack"))
const attackLabel = formatActionLabel(attackBaseLabel, t("MGNE.Common.Attack"))
const result = await this.rollAbility("strength", {
// Creatures have no ability scores; ability value defaults to 0 via roll.mjs
const result = await MGNERoll.promptCheck({
actor: this,
abilityId: "strength",
label: attackLabel,
rollType: "attack",
})
@@ -220,6 +227,12 @@ export default class MGNEActor extends Actor {
return item.rollUsage()
}
async rollDurability(itemId) {
const item = this.items.get(itemId)
if (!item) return null
return MGNERoll.rollDurability(item)
}
async quickRest() {
const roll = await (new Roll("1d4")).evaluate()
const hp = this.system.hp?.value ?? 0
+86
View File
@@ -57,11 +57,15 @@ async function renderCard(context) {
const normalizedEyebrow = `${eyebrow}`.trim().toLowerCase()
const normalizedLabel = `${context.label ?? ""}`.trim().toLowerCase()
// Render dice tooltip HTML if a roll was provided
const diceTooltip = context._roll ? await context._roll.render() : null
return foundry.applications.handlebars.renderTemplate(`systems/${SYSTEM_ID}/templates/chat-message.hbs`, {
...context,
modeClass: context.mode ?? "generic",
eyebrow: "",
outcomeClass,
diceTooltip,
})
}
@@ -156,6 +160,7 @@ export default class MGNERoll {
damageItemId: showDamageButton ? item.id : null,
damageFormula: showDamageButton ? (item.system.damage || "1") : null,
damageCritical: showDamageButton && critical,
_roll: roll,
})
await ChatMessage.create({
@@ -181,6 +186,7 @@ export default class MGNERoll {
total: roll.total,
outcome: broken ? t("MGNE.Roll.OutcomeBroken") : t("MGNE.Roll.OutcomeSteady"),
specialText: broken ? t("MGNE.Roll.MoraleBrokenText") : "",
_roll: roll,
})
await ChatMessage.create({
@@ -236,6 +242,7 @@ export default class MGNERoll {
showApplyButton: true,
damageTotal: roll.total,
damageCritical: isCritical,
_roll: roll,
})
await ChatMessage.create({
@@ -267,6 +274,7 @@ export default class MGNERoll {
showApplyButton: true,
damageTotal: roll.total,
damageCritical: isCritical,
_roll: roll,
})
await ChatMessage.create({
@@ -278,6 +286,39 @@ export default class MGNERoll {
return { roll }
}
static async rollArmorSave(actor) {
const formula = actor.getArmorRollFormula()
const items = actor.getEquippedArmorItems()
if (formula === "0") {
ui.notifications.warn(t("MGNE.Notification.NoArmorEquipped"))
return null
}
const roll = await (new Roll(formula)).evaluate()
const armorNames = items.map(i => i.name).join(" + ")
const contentHtml = await renderCard({
mode: "armor",
actorName: actor.name,
actorImg: actor.img,
label: t("MGNE.Roll.ArmorSave"),
subtitle: armorNames,
formula: roll.formula,
total: roll.total,
outcome: f("MGNE.Roll.ArmorAbsorbed", { amount: roll.total }),
_roll: roll,
})
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
rolls: [roll],
content: contentHtml,
})
return roll
}
static async rollUsage(item) {
const currentDie = item.system.usageDie
if (!currentDie || currentDie === "depleted") {
@@ -303,6 +344,7 @@ export default class MGNERoll {
total: roll.total,
outcome: depleted ? f("MGNE.Roll.DowngradedTo", { die: nextDie.toUpperCase() }) : t("MGNE.Roll.NoChange"),
specialText: depleted && nextDie === "depleted" ? t("MGNE.Roll.ItemNowDepleted") : "",
_roll: roll,
})
await ChatMessage.create({
@@ -314,6 +356,48 @@ export default class MGNERoll {
return { roll, depleted, nextDie }
}
static async rollDurability(item) {
const currentDie = item.system.durabilityDie
if (!currentDie || currentDie === "depleted") {
ui.notifications.warn(f("MGNE.Notification.ItemDurabilityDepleted", { item: item.name }))
return null
}
const roll = await (new Roll(`1${currentDie}`)).evaluate()
const degraded = roll.total <= 2
const nextDie = degraded ? stepDownDie(currentDie) : currentDie
const nowBroken = degraded && nextDie === "depleted"
const updates = { "system.durabilityDie": nextDie }
if (nowBroken) {
updates["system.broken"] = true
if ("equipped" in (item.system ?? {})) updates["system.equipped"] = false
}
await item.update(updates)
const contentHtml = await renderCard({
mode: "durability",
actorName: item.parent?.name ?? item.name,
actorImg: item.img,
label: f("MGNE.Roll.DurabilityLabel", { item: item.name }),
subtitle: f("MGNE.Roll.CurrentDie", { die: currentDie.toUpperCase() }),
formula: roll.formula,
total: roll.total,
outcome: degraded
? f("MGNE.Roll.DowngradedTo", { die: nextDie.toUpperCase() })
: t("MGNE.Roll.NoChange"),
specialText: nowBroken ? t("MGNE.Roll.ItemNowBroken") : "",
_roll: roll,
})
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: item.parent ?? null }),
rolls: [roll],
content: contentHtml,
})
return { roll, degraded, nextDie, nowBroken }
}
static async applyDamageCard({ actor, sourceActor = null, sourceItem = null, amount, armorRoll = null, appliedDamage, newHp, breakText = "", defenseFumbleText = "", criticalArmorText = "" }) {
const contentHtml = await renderCard({
mode: "apply-damage",
@@ -335,6 +419,7 @@ export default class MGNERoll {
criticalArmorText,
breakText ? f("MGNE.Roll.BreakText", { text: breakText }) : "",
]),
_roll: armorRoll ?? null,
})
await ChatMessage.create({
@@ -359,6 +444,7 @@ export default class MGNERoll {
total: roll.total,
outcome,
specialText,
_roll: roll,
})
await ChatMessage.create({
+2
View File
@@ -1,6 +1,7 @@
export { default as MGNECharacter } from "./character.mjs"
export { default as MGNECreature } from "./creature.mjs"
export { default as MGNECompanion } from "./companion.mjs"
export { default as MGNEParty } from "./party.mjs"
export { default as MGNEWeapon } from "./weapon.mjs"
export { default as MGNEArmor } from "./armor.mjs"
export { default as MGNEShield } from "./shield.mjs"
@@ -8,3 +9,4 @@ export { default as MGNEEquipment } from "./equipment.mjs"
export { default as MGNEResonanceCore } from "./resonance-core.mjs"
export { default as MGNEArtifact } from "./artifact.mjs"
export { default as MGNEFeature } from "./feature.mjs"
export { default as MGNECreatureTrait } from "./creature-trait.mjs"
+8
View File
@@ -12,8 +12,16 @@ export default class MGNEArmor extends foundry.abstract.TypeDataModel {
choices: SYSTEM.armorDieChoices,
}),
penalty: numberField(0, 0, 6),
weight: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "heavy",
choices: SYSTEM.weightCategories,
}),
equipped: booleanField(false),
broken: booleanField(false),
durabilityDie: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "d6",
choices: SYSTEM.usageDieChoices,
}),
}
}
+8 -6
View File
@@ -5,12 +5,6 @@ export default class MGNEArtifact extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
description: htmlField(""),
artifactId: new foundry.data.fields.StringField({
required: true,
nullable: false,
initial: "shiver-lens",
choices: SYSTEM.artifactChoices,
}),
synchronized: booleanField(false),
synchronizedTo: stringField(""),
usageDie: new foundry.data.fields.StringField({
@@ -20,6 +14,14 @@ export default class MGNEArtifact extends foundry.abstract.TypeDataModel {
choices: SYSTEM.usageDieChoices,
}),
broken: booleanField(false),
durabilityDie: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "d6",
choices: SYSTEM.usageDieChoices,
}),
weight: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "normal",
choices: SYSTEM.weightCategories,
}),
}
}
+28
View File
@@ -45,6 +45,34 @@ export default class MGNECharacter extends foundry.abstract.TypeDataModel {
this.syncLimit = Math.max(0, this.abilities.toughness?.value ?? 0)
this.syncRemaining = Math.max(0, this.syncLimit - (this.artifactSync.used ?? 0))
this.armorFormula = this.parent?.getArmorRollFormula?.() ?? "0"
// Compute current load per RAW:
// trivial = 0, light = 10 per slot, normal = 1, heavy = fills remaining capacity (max 1)
let normalLoad = 0
let lightCount = 0
let heavyCount = 0
for (const item of (this.parent?.items ?? [])) {
if (item.system?.carried === false) continue // not being carried
const w = item.system?.weight ?? "normal"
if (w === "trivial") continue
else if (w === "light") lightCount++
else if (w === "normal") normalLoad++
else if (w === "heavy") heavyCount++
}
normalLoad += Math.floor(lightCount / 10)
this.lightItemCount = lightCount
this.heavyItemCount = heavyCount
if (heavyCount >= 2) {
// Can't carry two heavy items — automatically overloaded
this.currentLoad = this.carryCapacity + (heavyCount - 1)
} else if (heavyCount === 1) {
// Heavy fills remaining capacity; other items fit alongside it
this.currentLoad = Math.max(normalLoad, this.carryCapacity)
} else {
this.currentLoad = normalLoad
}
this.overloaded = this.currentLoad > this.carryCapacity
}
/** @override */
+1 -2
View File
@@ -1,12 +1,11 @@
import { SYSTEM } from "../config/system.mjs"
import { abilitySchema, htmlField, numberField, stringField, trackSchema } from "./shared.mjs"
import { htmlField, numberField, stringField, trackSchema } from "./shared.mjs"
export default class MGNECompanion extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
return {
abilities: abilitySchema(),
hp: trackSchema(1, 1),
morale: numberField(7, 2, 12),
armor: new fields.SchemaField({
+13
View File
@@ -0,0 +1,13 @@
import { htmlField, stringField } from "./shared.mjs"
export default class MGNECreatureTrait extends foundry.abstract.TypeDataModel {
static defineSchema() {
return {
description: htmlField(""),
trigger: stringField(""),
}
}
/** @override */
static LOCALIZATION_PREFIXES = ["MGNE.CreatureTrait"]
}
+20 -8
View File
@@ -1,27 +1,39 @@
import { SYSTEM } from "../config/system.mjs"
import { abilitySchema, htmlField, numberField, stringField, trackSchema } from "./shared.mjs"
import { htmlField, numberField, stringField, trackSchema } from "./shared.mjs"
const CREATURE_TYPES = ["human", "construct", "animal"]
export default class MGNECreature extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
return {
abilities: abilitySchema(),
hp: trackSchema(1, 1),
morale: numberField(7, 2, 12),
armor: new fields.SchemaField({
die: new fields.StringField({ required: true, nullable: false, initial: "0", choices: SYSTEM.armorDieChoices }),
}),
attack: new fields.SchemaField({
label: stringField("Attack"),
damage: stringField("1d4"),
}),
creatureType: new fields.SetField(
new fields.StringField({ required: true, choices: CREATURE_TYPES }),
{ required: true, nullable: false, initial: [] }
),
number: stringField("1"),
actionTableUuid: stringField(""),
description: htmlField(""),
special: htmlField(""),
notes: htmlField(""),
}
}
/** @override */
static LOCALIZATION_PREFIXES = ["MGNE.Creature"]
/** @override */
static migrateData(source) {
// Remove old attack field if present (no longer part of the schema)
if ("attack" in source) delete source.attack
// Form submissions send null for unchecked checkboxes in array fields — filter them out
if (Array.isArray(source.creatureType)) {
source.creatureType = source.creatureType.filter(v => v != null && v !== "")
}
return super.migrateData(source)
}
}
+9
View File
@@ -21,6 +21,15 @@ export default class MGNEEquipment extends foundry.abstract.TypeDataModel {
choices: SYSTEM.usageDieChoices,
}),
consumable: booleanField(false),
broken: booleanField(false),
durabilityDie: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "d6",
choices: SYSTEM.usageDieChoices,
}),
weight: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "normal",
choices: SYSTEM.weightCategories,
}),
}
}
+20
View File
@@ -0,0 +1,20 @@
import { htmlField } from "./shared.mjs"
export default class MGNEParty extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
return {
memberRefs: new fields.ArrayField(
new fields.SchemaField({
id: new fields.StringField({ required: true, nullable: false, blank: false }),
})
),
credits: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
notes: htmlField(""),
}
}
/** @override */
static LOCALIZATION_PREFIXES = ["MGNE.Party"]
}
+9
View File
@@ -18,6 +18,15 @@ export default class MGNEResonanceCore extends foundry.abstract.TypeDataModel {
choices: SYSTEM.usageDieChoices,
}),
burnedOut: booleanField(false),
broken: booleanField(false),
durabilityDie: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "d6",
choices: SYSTEM.usageDieChoices,
}),
weight: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "trivial",
choices: SYSTEM.weightCategories,
}),
}
}
+8
View File
@@ -12,8 +12,16 @@ export default class MGNEShield extends foundry.abstract.TypeDataModel {
choices: SYSTEM.armorDieChoices,
}),
penalty: numberField(0, 0, 4),
weight: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "normal",
choices: SYSTEM.weightCategories,
}),
equipped: booleanField(false),
broken: booleanField(false),
durabilityDie: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "d6",
choices: SYSTEM.usageDieChoices,
}),
}
}
+28 -1
View File
@@ -13,7 +13,14 @@ export default class MGNEWeapon extends foundry.abstract.TypeDataModel {
}),
damage: stringField("1d4"),
range: stringField("Touch"),
properties: stringField(""),
properties: new foundry.data.fields.SetField(
new foundry.data.fields.StringField({ required: true, nullable: false, blank: false }),
{ required: true, nullable: false, initial: [] }
),
weight: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "normal",
choices: SYSTEM.weightCategories,
}),
usageDie: new foundry.data.fields.StringField({
required: true,
nullable: false,
@@ -23,9 +30,29 @@ export default class MGNEWeapon extends foundry.abstract.TypeDataModel {
quantity: numberField(1, 0),
equipped: booleanField(false),
broken: booleanField(false),
durabilityDie: new foundry.data.fields.StringField({
required: true, nullable: false, initial: "d6",
choices: SYSTEM.usageDieChoices,
}),
}
}
/** @override */
static LOCALIZATION_PREFIXES = ["MGNE.Weapon"]
/**
* Migrate old string-based properties field to the new SetField format.
* @override
*/
static migrateData(source) {
// Old data stored properties as a plain string; convert to empty array
if (typeof source.properties === "string") {
source.properties = []
}
// Remove any null/undefined/blank entries that may have crept in
if (Array.isArray(source.properties)) {
source.properties = source.properties.filter(p => p != null && p !== "")
}
return super.migrateData(source)
}
}