Files
fvtt-oath-hammer/oath-hammer.mjs
LeRatierBretonnier b4211c121d
All checks were successful
Release Creation / build (release) Successful in 1m36s
Add luck option after roll, attributes above 6, fix miracle icon and grit bonus
2026-04-20 08:23:33 +02:00

250 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { SYSTEM, STATUS_EFFECTS } from "./module/config/system.mjs"
globalThis.SYSTEM = SYSTEM
import * as models from "./module/models/_module.mjs"
import * as documents from "./module/documents/_module.mjs"
import * as applications from "./module/applications/_module.mjs"
import OathHammerUtils from "./module/utils.mjs"
import OathHammerWeaponDialog from "./module/applications/weapon-dialog.mjs"
import OathHammerCombat from "./module/combat.mjs"
import { rollWeaponDamage } from "./module/rolls.mjs"
import { rollPostRollLuck } from "./module/rolls.mjs"
import { injectFreeRollBar } from "./module/applications/free-roll.mjs"
import OathHammerLuckRollDialog from "./module/applications/luck-roll-dialog.mjs"
Hooks.once("init", function () {
console.info(SYSTEM.ASCII)
console.info("Oath Hammer | Initializing System")
globalThis.oathHammer = game.system
game.system.CONST = SYSTEM
game.system.api = { applications, models, documents }
CONFIG.Actor.documentClass = documents.OathHammerActor
CONFIG.Combat.documentClass = OathHammerCombat
CONFIG.Actor.dataModels = {
character: models.OathHammerCharacter,
npc: models.OathHammerNPC,
settlement: models.OathHammerSettlement,
regiment: models.OathHammerRegiment,
party: models.OathHammerParty,
army: models.OathHammerArmy,
}
CONFIG.Item.documentClass = documents.OathHammerItem
CONFIG.Item.dataModels = {
weapon: models.OathHammerWeapon,
armor: models.OathHammerArmor,
ammunition: models.OathHammerAmmunition,
equipment: models.OathHammerEquipment,
spell: models.OathHammerSpell,
miracle: models.OathHammerMiracle,
"magic-item": models.OathHammerMagicItem,
trait: models.OathHammerTrait,
oath: models.OathHammerOath,
"class": models.OathHammerClass,
building: models.OathHammerBuilding,
skillnpc: models.OathHammerSkillNPC,
npcattack: models.OathHammerNpcAttack,
}
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerCharacterSheet, {
types: ["character"],
makeDefault: true,
label: "OATHHAMMER.Sheet.Character"
})
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerNPCSheet, {
types: ["npc"],
makeDefault: true,
label: "OATHHAMMER.Sheet.NPC"
})
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerSettlementSheet, {
types: ["settlement"],
makeDefault: true,
label: "OATHHAMMER.Sheet.Settlement"
})
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerRegimentSheet, {
types: ["regiment"],
makeDefault: true,
label: "OATHHAMMER.Sheet.Regiment"
})
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerPartySheet, {
types: ["party"],
makeDefault: true,
label: "OATHHAMMER.Sheet.Party"
})
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerArmySheet, {
types: ["army"],
makeDefault: true,
label: "OATHHAMMER.Sheet.Army"
})
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerWeaponSheet, { types: ["weapon"], makeDefault: true, label: "OATHHAMMER.Sheet.Weapon" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerArmorSheet, { types: ["armor"], makeDefault: true, label: "OATHHAMMER.Sheet.Armor" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerAmmunitionSheet, { types: ["ammunition"], makeDefault: true, label: "OATHHAMMER.Sheet.Ammunition" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerEquipmentSheet, { types: ["equipment"], makeDefault: true, label: "OATHHAMMER.Sheet.Equipment" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerSpellSheet, { types: ["spell"], makeDefault: true, label: "OATHHAMMER.Sheet.Spell" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerMiracleSheet, { types: ["miracle"], makeDefault: true, label: "OATHHAMMER.Sheet.Miracle" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerMagicItemSheet, { types: ["magic-item"], makeDefault: true, label: "OATHHAMMER.Sheet.MagicItem" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerTraitSheet, { types: ["trait"], makeDefault: true, label: "OATHHAMMER.Sheet.Trait" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerOathSheet, { types: ["oath"], makeDefault: true, label: "OATHHAMMER.Sheet.Oath" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerClassSheet, { types: ["class"], makeDefault: true, label: "OATHHAMMER.Sheet.Class" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerBuildingSheet, { types: ["building"], makeDefault: true, label: "OATHHAMMER.Sheet.Building" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerSkillNPCSheet, { types: ["skillnpc"], makeDefault: true, label: "OATHHAMMER.Sheet.SkillNPC" })
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerNpcAttackSheet, { types: ["npcattack"], makeDefault: true, label: "OATHHAMMER.Sheet.NpcAttack" })
CONFIG.statusEffects = STATUS_EFFECTS
OathHammerUtils.registerHandlebarsHelpers()
// Pre-register Handlebars partials so {{> "path"}} works in item templates
foundry.applications.handlebars.loadTemplates([
"systems/fvtt-oath-hammer/templates/item/rune-zone.hbs",
])
console.info("Oath Hammer | System Initialized")
})
Hooks.once("ready", async function () {
console.info("Oath Hammer | System Ready")
// Migration: remove orphaned items with removed types (lineage → actor field, ability → trait, regiment → actor type)
const removedTypes = new Set(["lineage", "ability", "regiment"])
for (const actor of game.actors) {
const toDelete = []
// Catch items that failed validation and landed in invalidDocumentIds
for (const id of (actor.items.invalidDocumentIds ?? [])) {
const raw = actor.items.getInvalid(id)
if (raw && removedTypes.has(raw._source?.type)) toDelete.push(id)
}
// Also catch any that slipped through in _source (belt-and-suspenders)
for (const src of (actor._source.items ?? [])) {
if (removedTypes.has(src.type) && !toDelete.includes(src._id)) toDelete.push(src._id)
}
if (toDelete.length) {
console.info(`Oath Hammer | Migrating ${actor.name}: removing ${toDelete.length} obsolete item(s)`)
await actor.deleteEmbeddedDocuments("Item", toDelete)
}
}
// Purge invalid world items of removed types
for (const id of game.items.invalidDocumentIds) {
const item = game.items.getInvalid(id)
if (item && removedTypes.has(item._source?.type)) {
console.info(`Oath Hammer | Deleting world item: ${item._source.name} (${item._source.type})`)
await item.delete()
}
}
// Auto-import the welcome scene if GM and not already present
if (game.user.isGM) await _importWelcomeScene()
})
// Auto-link regiment (and army) actor tokens so they can be added to garrisons/armies
Hooks.on("preCreateActor", (actor, _data, _options, _userId) => {
if (actor.type === "regiment" || actor.type === "army") {
actor.updateSource({ "prototypeToken.actorLink": true })
}
})
// Handle "Roll Damage" button in weapon attack chat cards
Hooks.on("renderChatMessageHTML", async (message, html) => {
// Weapon damage button
const btn = html.querySelector("[data-action=\"rollWeaponDamage\"]")
if (btn) {
btn.addEventListener("click", async () => {
const flagData = message.getFlag("fvtt-oath-hammer", "weaponAttack")
if (!flagData) return
const { actorUuid, weaponUuid, attackSuccesses } = flagData
const actor = await fromUuid(actorUuid)
const weapon = await fromUuid(weaponUuid)
if (!actor || !weapon) return ui.notifications.warn(game.i18n.localize("OATHHAMMER.Roll.NoActor"))
const opts = await OathHammerWeaponDialog.promptDamage(actor, weapon, attackSuccesses ?? 0)
if (opts) await rollWeaponDamage(actor, weapon, opts)
})
}
// Luck post-roll button
const luckFlag = message.getFlag("fvtt-oath-hammer", "luckRoll")
if (!luckFlag) return
const resultDiv = html.querySelector(".oh-roll-result")
if (!resultDiv) return
if (luckFlag.used) {
// Show luck result section
const bonusLabel = luckFlag.bonusSuccesses > 0
? `+${luckFlag.bonusSuccesses} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}`
: game.i18n.localize("OATHHAMMER.Roll.NoBonus")
const resultHtml = `
<div class="oh-luck-result">
<span class="oh-luck-result-icon">🍀</span>
<span>${game.i18n.localize("OATHHAMMER.Roll.LuckResult")} ${bonusLabel}</span>
<span class="oh-luck-dice">${luckFlag.luckDiceHtml ?? ""}</span>
</div>`
resultDiv.insertAdjacentHTML("afterend", resultHtml)
} else {
// Show "Spend Luck" button if actor owns the message and has luck left
const actor = await fromUuid(luckFlag.actorUuid).catch(() => null)
if (!actor?.isOwner) return
const availableLuck = actor.system.luck?.value ?? 0
if (availableLuck <= 0) return
const btnHtml = `
<div class="oh-luck-btn-row">
<button type="button" class="oh-post-luck-btn" data-action="postRollLuck">
🍀 ${game.i18n.localize("OATHHAMMER.Roll.LuckRollPost")}
</button>
</div>`
resultDiv.insertAdjacentHTML("afterend", btnHtml)
html.querySelector("[data-action=\"postRollLuck\"]")?.addEventListener("click", async () => {
const actor = await fromUuid(luckFlag.actorUuid).catch(() => null)
if (!actor) return
const opts = await OathHammerLuckRollDialog.prompt(actor)
if (!opts) return
await rollPostRollLuck(message, opts.luckSpend, opts.luckIsHuman)
})
}
})
// Inject Free Roll bar into the chat sidebar
Hooks.on("renderChatLog", (_chatLog, html) => injectFreeRollBar(_chatLog, html))
// ============================================================
// WELCOME SCENE — auto-create on first world load (GM only)
// ============================================================
const WELCOME_SCENE_NAME = "Oath Hammer"
const WELCOME_SCENE_MAP = "systems/fvtt-oath-hammer/assets/images/oathhammer_map.webp"
/** Scene data for the welcome map (3600×5400 px, no grid — world map). */
function _welcomeSceneData() {
return {
name: WELCOME_SCENE_NAME,
background: { src: WELCOME_SCENE_MAP },
width: 3600,
height: 5400,
grid: { type: 0, size: 100 }, // gridless
padding: 0,
initial: { x: 1800, y: 2700, scale: 0.25 },
tokenVision: false,
flags: { "fvtt-oath-hammer": { welcomeScene: true } },
}
}
async function _importWelcomeScene() {
// Skip if the scene already exists in the world
if (game.scenes.find(s => s.name === WELCOME_SCENE_NAME)) return
console.info("Oath Hammer | Creating welcome scene…")
const scene = await Scene.create(_welcomeSceneData())
await scene.activate()
console.info("Oath Hammer | Welcome scene created and activated.")
}