diff --git a/.gitignore b/.gitignore
index f0eb598..33bf50f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
_docs_private/
+node_modules/
+package-lock.json
diff --git a/.history/lang/en_20260308133951.json b/.history/lang/en_20260308133951.json
new file mode 100644
index 0000000..1711313
--- /dev/null
+++ b/.history/lang/en_20260308133951.json
@@ -0,0 +1,788 @@
+{
+ "OATHHAMMER": {
+ "Sheet": {
+ "Character": "Oath Hammer Character Sheet",
+ "NPC": "Oath Hammer NPC Sheet",
+ "Weapon": "Oath Hammer Weapon Sheet",
+ "Armor": "Oath Hammer Armor Sheet",
+ "Ammunition": "Oath Hammer Ammunition Sheet",
+ "Equipment": "Oath Hammer Equipment Sheet",
+ "Spell": "Oath Hammer Spell Sheet",
+ "Miracle": "Oath Hammer Miracle Sheet",
+ "MagicItem": "Oath Hammer Magic Item Sheet",
+ "Ability": "Oath Hammer Ability Sheet",
+ "Oath": "Oath Hammer Oath Sheet",
+ "Condition": "Oath Hammer Condition Sheet",
+ "Lineage": "Oath Hammer Lineage Sheet",
+ "Class": "Oath Hammer Class Sheet"
+ },
+ "Tab": {
+ "Identity": "Identity",
+ "Skills": "Skills",
+ "Combat": "Combat",
+ "Magic": "Magic",
+ "Equipment": "Equipment",
+ "Notes": "Notes"
+ },
+ "Attribute": {
+ "Might": "Might",
+ "Toughness": "Toughness",
+ "Agility": "Agility",
+ "Willpower": "Willpower",
+ "Intelligence": "Intelligence",
+ "Fate": "Fate"
+ },
+ "Skill": {
+ "Academics": "Academics",
+ "Acrobatics": "Acrobatics",
+ "AnimalHandling": "Animal Handling",
+ "Athletics": "Athletics",
+ "Brewing": "Brewing",
+ "Carpentry": "Carpentry",
+ "Defense": "Defense",
+ "Dexterity": "Dexterity",
+ "Diplomacy": "Diplomacy",
+ "Discipline": "Discipline",
+ "Fighting": "Fighting",
+ "Folklore": "Folklore",
+ "Fortune": "Fortune",
+ "Heal": "Heal",
+ "Leadership": "Leadership",
+ "Magic": "Magic",
+ "Masonry": "Masonry",
+ "Orientation": "Orientation",
+ "Perception": "Perception",
+ "Resilience": "Resilience",
+ "Ride": "Ride",
+ "Shooting": "Shooting",
+ "Smithing": "Smithing",
+ "Stealth": "Stealth",
+ "Survival": "Survival",
+ "Tracking": "Tracking"
+ },
+ "Lineage": {
+ "Dwarf": "Dwarf",
+ "Firbolg": "Firbolg",
+ "Halfling": "Halfling",
+ "HighElf": "High Elf",
+ "Human": "Human",
+ "WoodElf": "Wood Elf",
+ "FIELDS": {
+ "description": {
+ "label": "Description"
+ },
+ "traits": {
+ "label": "Traits"
+ },
+ "movement": {
+ "label": "Movement (ft)"
+ },
+ "gritModifier": {
+ "label": "Grit Modifier"
+ }
+ }
+ },
+ "Class": {
+ "Berserker": "Berserker",
+ "Champion": "Champion",
+ "Delver": "Delver",
+ "Knight": "Knight",
+ "Mage": "Mage",
+ "Priest": "Priest",
+ "Scout": "Scout",
+ "Soldier": "Soldier",
+ "Spellblade": "Spellblade",
+ "Troubadour": "Troubadour",
+ "FIELDS": {
+ "description": {
+ "label": "Description"
+ },
+ "features": {
+ "label": "Features"
+ },
+ "armorProficiency": {
+ "label": "Armor Proficiency"
+ },
+ "weaponProficiency": {
+ "label": "Weapon Proficiency"
+ }
+ }
+ },
+ "Tradition": {
+ "Elemental": "Elemental",
+ "Illusionist": "Illusionist",
+ "Imperial": "Imperial",
+ "Infernal": "Infernal",
+ "Runic": "Runic",
+ "Stygian": "Stygian"
+ },
+ "ArmorType": {
+ "Light": "Light",
+ "Medium": "Medium",
+ "Heavy": "Heavy"
+ },
+ "Currency": {
+ "GP": "Gold Pieces",
+ "SP": "Silver Pieces",
+ "CP": "Copper Pieces"
+ },
+ "AmmoType": {
+ "Standard": "Arrow / Bolt",
+ "Bodkin": "Bodkin (−1 armor)",
+ "Envenomed": "Envenomed (poison)",
+ "Incendiary": "Incendiary (flaming)"
+ },
+ "EquipmentType": {
+ "Potion": "Potion",
+ "Container": "Container",
+ "Mount": "Mount",
+ "HealingSupply": "Healing Supply",
+ "Food": "Food & Drink",
+ "LightSource": "Light Source",
+ "Misc": "Miscellaneous",
+ "Vehicle": "Vehicle",
+ "Animal": "Animal",
+ "WarMachine": "War Machine"
+ },
+ "MagicItemType": {
+ "Focus": "Focus",
+ "Talisman": "Talisman",
+ "Trinket": "Trinket"
+ },
+ "Rarity": {
+ "Always": "Always",
+ "Common": "1 – Common",
+ "Uncommon": "2 – Uncommon",
+ "Rare": "3 – Rare",
+ "VeryRare": "4 – Very Rare",
+ "Legendary": "5 – Legendary"
+ },
+ "AbilityType": {
+ "ClassAbility": "Class Ability",
+ "LineageTrait": "Lineage Trait"
+ },
+ "Condition": {
+ "Blinded": "Blinded",
+ "BlindedDesc": "Cannot see. -3 penalty to defense and melee attack rolls. Cannot perform ranged attacks or cast spells.",
+ "Confused": "Confused",
+ "ConfusedDesc": "Must make a DV2 Discipline check at the start of each turn or may not move or perform actions.",
+ "Dazed": "Dazed",
+ "DazedDesc": "Cannot perform Magic checks or move more than 10 ft. -1 penalty to attack and defense rolls.",
+ "Deafened": "Deafened",
+ "DeafenedDesc": "Cannot hear. -3 penalty to Magic checks due to verbal components required for miracles and spells.",
+ "Demoralized": "Demoralized",
+ "DemoralizedDesc": "−1 penalty to Discipline checks. Cannot perform Leadership checks.",
+ "Diseased": "Diseased",
+ "DiseasedDesc": "Lose 1 rank in all attributes. Can be treated with a DV5 Heal check once per day.",
+ "Enfeebled": "Enfeebled",
+ "EnfeebledDesc": "-2 penalty to attack and defense rolls. Cannot move more than 10 ft or perform the Run action.",
+ "Fatigued": "Fatigued",
+ "FatiguedDesc": "-1 penalty to attack and defense rolls, and to Resilience checks. Removed by a full night's rest.",
+ "Frightened": "Frightened",
+ "FrightenedDesc": "Cannot approach enemies. May attempt a DV2 Discipline check as an action to end this condition. Cannot perform Leadership checks.",
+ "Ignited": "Ignited",
+ "IgnitedDesc": "Suffers 1DD flaming damage at the end of each round, ignoring armor. Can remove condition as an action (ends turn prone).",
+ "Inspired": "Inspired",
+ "InspiredDesc": "+1 bonus to Discipline and Leadership checks.",
+ "Invisible": "Invisible",
+ "InvisibleDesc": "Cannot be seen by ordinary means. Cannot be targeted by magic or ranged attacks. -3 penalty to melee attacks against invisible targets.",
+ "Poisoned": "Poisoned",
+ "PoisonedDesc": "Suffers 1DD poison damage (black die) at the end of each round, ignoring armor.",
+ "Restrained": "Restrained",
+ "RestrainedDesc": "Cannot move.",
+ "Stunned": "Stunned",
+ "StunnedDesc": "Cannot speak, move, or perform actions. -3 penalty to defense rolls."
+ },
+ "Label": {
+ "Character": "Character",
+ "NPC": "NPC",
+ "Grit": "Grit",
+ "Luck": "Luck",
+ "Defense": "Defense",
+ "DefenseValue": "Defense Value",
+ "ArmorRating": "Armor Rating",
+ "DefenseBonus": "Defense Bonus",
+ "Movement": "Movement",
+ "ArcaneStress": "Arcane Stress",
+ "StressValue": "Stress",
+ "Attributes": "Attributes",
+ "Biodata": "Background",
+ "Background": "Background",
+ "Experience": "Experience",
+ "Level": "Level",
+ "XP": "Current XP",
+ "TotalXP": "Total XP",
+ "Abilities": "Abilities & Traits",
+ "Oaths": "Oaths",
+ "Weapons": "Weapons",
+ "Armor": "Armor & Shields",
+ "Ammunition": "Ammunition",
+ "Spells": "Spells",
+ "Miracles": "Miracles",
+ "Equipment": "Equipment",
+ "MagicItems": "Magic Items",
+ "Conditions": "Conditions",
+ "Description": "Description",
+ "Notes": "Notes",
+ "Stats": "Statistics",
+ "CR": "Challenge Rating",
+ "AttackBonus": "Attack Bonus",
+ "DamageBonus": "Damage Bonus",
+ "Currency": "Currency",
+ "None": "None",
+ "Effect": "Effect",
+ "Components": "Components",
+ "Charges": "Charges",
+ "NoWeapons": "No weapons equipped.",
+ "NoArmor": "No armor or shields.",
+ "NoSpells": "No spells known.",
+ "NoMiracles": "No miracles known.",
+ "NoEquipment": "No equipment.",
+ "Enchantment": "Enchantment",
+ "Tenet": "Tenet",
+ "Boon": "Boon",
+ "Bane": "Bane",
+ "Skill": "Skill",
+ "SkillRank": "Rank",
+ "SkillModifier": "Mod",
+ "TotalDice": "Total",
+ "ColorDice": "Color",
+ "DropLineage": "Drop Lineage Here",
+ "DropClass": "Drop Class Here",
+ "Traits": "Traits",
+ "Features": "Features",
+ "Name": "Name",
+ "Type": "Type",
+ "Damage": "Damage",
+ "Tradition": "Tradition",
+ "Piety": "Piety",
+ "Quantity": "Qty",
+ "Rarity": "Rarity",
+ "Penalty": "Penalty",
+ "Equipped": "Eq.",
+ "XPCurrent": "Current XP"
+ },
+ "ColorDice": {
+ "White": "White (4+)",
+ "Red": "Red (3+)",
+ "Black": "Black (2+)"
+ },
+ "NewItem": {
+ "Weapon": "New Weapon",
+ "Spell": "New Spell",
+ "Miracle": "New Miracle",
+ "Equipment": "New Equipment"
+ },
+ "ToggleSheet": "Toggle Edit/Play Mode",
+ "Character": {
+ "FIELDS": {
+ "attributes": {
+ "label": "Attributes"
+ },
+ "grit": {
+ "label": "Grit"
+ },
+ "luck": {
+ "label": "Luck",
+ "fields": {
+ "value": {
+ "label": "Luck"
+ },
+ "max": {
+ "label": "Luck Max"
+ }
+ }
+ },
+ "arcaneStress": {
+ "label": "Arcane Stress"
+ },
+ "movement": {
+ "label": "Movement"
+ },
+ "defense": {
+ "label": "Defense"
+ },
+ "experience": {
+ "label": "Experience"
+ },
+ "biodata": {
+ "label": "Background",
+ "fields": {
+ "lineage": {
+ "label": "Lineage"
+ },
+ "class": {
+ "label": "Class"
+ },
+ "age": {
+ "label": "Age"
+ },
+ "gender": {
+ "label": "Gender"
+ },
+ "height": {
+ "label": "Height"
+ },
+ "weight": {
+ "label": "Weight"
+ },
+ "eyes": {
+ "label": "Eye Color"
+ },
+ "hair": {
+ "label": "Hair Color"
+ },
+ "alignment": {
+ "label": "Alignment"
+ }
+ }
+ },
+ "currency": {
+ "label": "Currency",
+ "gold": {
+ "label": "Gold"
+ },
+ "silver": {
+ "label": "Silver"
+ },
+ "copper": {
+ "label": "Copper"
+ }
+ },
+ "description": {
+ "label": "Description"
+ },
+ "notes": {
+ "label": "Notes"
+ },
+ "skills": {
+ "label": "Skills"
+ }
+ }
+ },
+ "NPC": {
+ "FIELDS": {
+ "attributes": {
+ "label": "Attributes"
+ },
+ "grit": {
+ "label": "Grit"
+ },
+ "defense": {
+ "label": "Defense"
+ },
+ "movement": {
+ "label": "Movement"
+ },
+ "attackBonus": {
+ "label": "Attack Bonus"
+ },
+ "damageBonus": {
+ "label": "Damage Bonus"
+ },
+ "challengeRating": {
+ "label": "Challenge Rating"
+ },
+ "description": {
+ "label": "Description"
+ },
+ "notes": {
+ "label": "Notes"
+ }
+ }
+ },
+ "Weapon": {
+ "FIELDS": {
+ "proficiencyGroup": {
+ "label": "Proficiency Group"
+ },
+ "usesMight": {
+ "label": "Uses Might"
+ },
+ "damageMod": {
+ "label": "Damage Modifier"
+ },
+ "ap": {
+ "label": "Armor Penetration (AP)"
+ },
+ "reach": {
+ "label": "Reach (ft)"
+ },
+ "shortRange": {
+ "label": "Short Range (ft)"
+ },
+ "longRange": {
+ "label": "Long Range (ft)"
+ },
+ "traits": {
+ "label": "Traits"
+ },
+ "slots": {
+ "label": "Item Slots"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "equipped": {
+ "label": "Equipped"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ },
+ "description": {
+ "label": "Description"
+ },
+ "isMagic": {
+ "label": "Magic Item"
+ },
+ "magicQuality": {
+ "label": "Quality"
+ },
+ "isCursed": {
+ "label": "Cursed"
+ },
+ "magicEffect": {
+ "label": "Enchantment"
+ },
+ "classRestriction": {
+ "label": "Restriction"
+ }
+ }
+ },
+ "Armor": {
+ "FIELDS": {
+ "armorType": {
+ "label": "Armor Type"
+ },
+ "armorValue": {
+ "label": "Armor Value (AV)"
+ },
+ "penalty": {
+ "label": "Penalty"
+ },
+ "slots": {
+ "label": "Slots"
+ },
+ "traits": {
+ "label": "Traits"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "equipped": {
+ "label": "Equipped"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ },
+ "isMagic": {
+ "label": "Magic Item"
+ },
+ "magicQuality": {
+ "label": "Quality"
+ },
+ "isCursed": {
+ "label": "Cursed"
+ },
+ "magicEffect": {
+ "label": "Enchantment"
+ },
+ "classRestriction": {
+ "label": "Restriction"
+ }
+ }
+ },
+ "Ammunition": {
+ "FIELDS": {
+ "ammoType": {
+ "label": "Ammunition Type"
+ },
+ "quantity": {
+ "label": "Quantity"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ }
+ }
+ },
+ "Equipment": {
+ "FIELDS": {
+ "itemType": {
+ "label": "Category"
+ },
+ "quantity": {
+ "label": "Quantity"
+ },
+ "slots": {
+ "label": "Slots"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "lightRadius": {
+ "label": "Light Radius (ft)"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ }
+ }
+ },
+ "Spell": {
+ "FIELDS": {
+ "tradition": {
+ "label": "Tradition"
+ },
+ "difficultyValue": {
+ "label": "Difficulty Value (DV)"
+ },
+ "isRitual": {
+ "label": "Ritual"
+ },
+ "isMagicMissile": {
+ "label": "Magic Missile"
+ },
+ "range": {
+ "label": "Range"
+ },
+ "duration": {
+ "label": "Duration"
+ },
+ "spellSave": {
+ "label": "Spell Save"
+ },
+ "element": {
+ "label": "Element"
+ },
+ "runeType": {
+ "label": "Rune Type"
+ },
+ "isExalted": {
+ "label": "Exalted"
+ },
+ "effect": {
+ "label": "Effect"
+ }
+ }
+ },
+ "Miracle": {
+ "FIELDS": {
+ "divineTradition": {
+ "label": "Divine Tradition"
+ },
+ "difficultyValue": {
+ "label": "Difficulty Value (DV)"
+ },
+ "isRitual": {
+ "label": "Ritual"
+ },
+ "range": {
+ "label": "Range"
+ },
+ "duration": {
+ "label": "Duration"
+ },
+ "spellSave": {
+ "label": "Spell Save"
+ },
+ "effect": {
+ "label": "Effect"
+ }
+ }
+ },
+ "MagicItem": {
+ "FIELDS": {
+ "itemType": {
+ "label": "Type"
+ },
+ "quality": {
+ "label": "Quality"
+ },
+ "isCursed": {
+ "label": "Cursed"
+ },
+ "isBonded": {
+ "label": "Bonded"
+ },
+ "classRestriction": {
+ "label": "Restriction"
+ },
+ "usagePeriod": {
+ "label": "Usage Period"
+ },
+ "maxUses": {
+ "label": "Max Uses"
+ },
+ "slots": {
+ "label": "Slots"
+ },
+ "equipped": {
+ "label": "Equipped"
+ },
+ "effect": {
+ "label": "Effect"
+ }
+ }
+ },
+ "Ability": {
+ "FIELDS": {
+ "abilityType": {
+ "label": "Type"
+ },
+ "source": {
+ "label": "Source (Class / Lineage)"
+ },
+ "usagePeriod": {
+ "label": "Usage Period"
+ },
+ "maxUses": {
+ "label": "Max Uses"
+ },
+ "description": {
+ "label": "Description"
+ }
+ }
+ },
+ "WeaponGroup": {
+ "Common": "Common",
+ "Dueling": "Dueling",
+ "Heavy": "Heavy",
+ "Polearms": "Polearms",
+ "Bows": "Bows",
+ "Throwing": "Throwing"
+ },
+ "WeaponTrait": {
+ "Block": "Block",
+ "Brutal": "Brutal",
+ "Clumsy": "Clumsy",
+ "Couched": "Couched",
+ "Deadly": "Deadly",
+ "Fast": "Fast",
+ "Flaming": "Flaming",
+ "Nimble": "Nimble",
+ "Parry": "Parry",
+ "Reload": "Reload",
+ "Repel": "Repel",
+ "Stunning": "Stunning",
+ "Sweep": "Sweep",
+ "TwoHanded": "Two-handed",
+ "Versatile": "Versatile"
+ },
+ "DivineTradition": {
+ "Druidic": "Druidic",
+ "Profane": "Profane",
+ "Sanctified": "Sanctified"
+ },
+ "Element": {
+ "Air": "Air",
+ "Earth": "Earth",
+ "Fire": "Fire",
+ "Water": "Water",
+ "Varies": "Varies"
+ },
+ "RuneType": {
+ "Armor": "Armor",
+ "Talisman": "Talisman",
+ "Warding": "Warding",
+ "Weapon": "Weapon"
+ },
+ "ArmorTrait": {
+ "Clanging": "Clanging",
+ "Reinforced": "Reinforced"
+ },
+ "UsagePeriod": {
+ "None": "Passive (always on)",
+ "Encounter": "Per Encounter",
+ "Day": "Per Day"
+ },
+ "MagicQuality": {
+ "Lesser": "Lesser",
+ "Greater": "Greater",
+ "Legendary": "Legendary"
+ },
+ "OathType": {
+ "Compassion": "Oath of Compassion",
+ "Courage": "Oath of Courage",
+ "Diligence": "Oath of Diligence",
+ "Faith": "Oath of Faith",
+ "Humility": "Oath of Humility",
+ "Justice": "Oath of Justice",
+ "Loyalty": "Oath of Loyalty",
+ "Peace": "Oath of Peace",
+ "Perseverance": "Oath of Perseverance",
+ "Purity": "Oath of Purity",
+ "Truth": "Oath of Truth",
+ "Wisdom": "Oath of Wisdom"
+ },
+ "OathFields": {
+ "oathType": {
+ "label": "Oath"
+ },
+ "tenet": {
+ "label": "Tenet"
+ },
+ "boon": {
+ "label": "Boon"
+ },
+ "bane": {
+ "label": "Bane"
+ },
+ "violated": {
+ "label": "Violated"
+ }
+ },
+ "Oath": {
+ "FIELDS": {
+ "oathType": {
+ "label": "Oath"
+ },
+ "tenet": {
+ "label": "Tenet"
+ },
+ "boon": {
+ "label": "Boon"
+ },
+ "bane": {
+ "label": "Bane"
+ },
+ "violated": {
+ "label": "Violated"
+ }
+ }
+ }
+ },
+ "TYPES": {
+ "Item": {
+ "weapon": "Weapon",
+ "armor": "Armor",
+ "ammunition": "Ammunition",
+ "equipment": "Equipment",
+ "spell": "Spell",
+ "miracle": "Miracle",
+ "magic_item": "Magic Item",
+ "magic-item": "Magic Item",
+ "ability": "Ability",
+ "oath": "Oath",
+ "lineage": "Lineage",
+ "class": "Class"
+ },
+ "Actor": {
+ "character": "Character",
+ "npc": "NPC"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.history/lang/en_20260308154927.json b/.history/lang/en_20260308154927.json
new file mode 100644
index 0000000..1711313
--- /dev/null
+++ b/.history/lang/en_20260308154927.json
@@ -0,0 +1,788 @@
+{
+ "OATHHAMMER": {
+ "Sheet": {
+ "Character": "Oath Hammer Character Sheet",
+ "NPC": "Oath Hammer NPC Sheet",
+ "Weapon": "Oath Hammer Weapon Sheet",
+ "Armor": "Oath Hammer Armor Sheet",
+ "Ammunition": "Oath Hammer Ammunition Sheet",
+ "Equipment": "Oath Hammer Equipment Sheet",
+ "Spell": "Oath Hammer Spell Sheet",
+ "Miracle": "Oath Hammer Miracle Sheet",
+ "MagicItem": "Oath Hammer Magic Item Sheet",
+ "Ability": "Oath Hammer Ability Sheet",
+ "Oath": "Oath Hammer Oath Sheet",
+ "Condition": "Oath Hammer Condition Sheet",
+ "Lineage": "Oath Hammer Lineage Sheet",
+ "Class": "Oath Hammer Class Sheet"
+ },
+ "Tab": {
+ "Identity": "Identity",
+ "Skills": "Skills",
+ "Combat": "Combat",
+ "Magic": "Magic",
+ "Equipment": "Equipment",
+ "Notes": "Notes"
+ },
+ "Attribute": {
+ "Might": "Might",
+ "Toughness": "Toughness",
+ "Agility": "Agility",
+ "Willpower": "Willpower",
+ "Intelligence": "Intelligence",
+ "Fate": "Fate"
+ },
+ "Skill": {
+ "Academics": "Academics",
+ "Acrobatics": "Acrobatics",
+ "AnimalHandling": "Animal Handling",
+ "Athletics": "Athletics",
+ "Brewing": "Brewing",
+ "Carpentry": "Carpentry",
+ "Defense": "Defense",
+ "Dexterity": "Dexterity",
+ "Diplomacy": "Diplomacy",
+ "Discipline": "Discipline",
+ "Fighting": "Fighting",
+ "Folklore": "Folklore",
+ "Fortune": "Fortune",
+ "Heal": "Heal",
+ "Leadership": "Leadership",
+ "Magic": "Magic",
+ "Masonry": "Masonry",
+ "Orientation": "Orientation",
+ "Perception": "Perception",
+ "Resilience": "Resilience",
+ "Ride": "Ride",
+ "Shooting": "Shooting",
+ "Smithing": "Smithing",
+ "Stealth": "Stealth",
+ "Survival": "Survival",
+ "Tracking": "Tracking"
+ },
+ "Lineage": {
+ "Dwarf": "Dwarf",
+ "Firbolg": "Firbolg",
+ "Halfling": "Halfling",
+ "HighElf": "High Elf",
+ "Human": "Human",
+ "WoodElf": "Wood Elf",
+ "FIELDS": {
+ "description": {
+ "label": "Description"
+ },
+ "traits": {
+ "label": "Traits"
+ },
+ "movement": {
+ "label": "Movement (ft)"
+ },
+ "gritModifier": {
+ "label": "Grit Modifier"
+ }
+ }
+ },
+ "Class": {
+ "Berserker": "Berserker",
+ "Champion": "Champion",
+ "Delver": "Delver",
+ "Knight": "Knight",
+ "Mage": "Mage",
+ "Priest": "Priest",
+ "Scout": "Scout",
+ "Soldier": "Soldier",
+ "Spellblade": "Spellblade",
+ "Troubadour": "Troubadour",
+ "FIELDS": {
+ "description": {
+ "label": "Description"
+ },
+ "features": {
+ "label": "Features"
+ },
+ "armorProficiency": {
+ "label": "Armor Proficiency"
+ },
+ "weaponProficiency": {
+ "label": "Weapon Proficiency"
+ }
+ }
+ },
+ "Tradition": {
+ "Elemental": "Elemental",
+ "Illusionist": "Illusionist",
+ "Imperial": "Imperial",
+ "Infernal": "Infernal",
+ "Runic": "Runic",
+ "Stygian": "Stygian"
+ },
+ "ArmorType": {
+ "Light": "Light",
+ "Medium": "Medium",
+ "Heavy": "Heavy"
+ },
+ "Currency": {
+ "GP": "Gold Pieces",
+ "SP": "Silver Pieces",
+ "CP": "Copper Pieces"
+ },
+ "AmmoType": {
+ "Standard": "Arrow / Bolt",
+ "Bodkin": "Bodkin (−1 armor)",
+ "Envenomed": "Envenomed (poison)",
+ "Incendiary": "Incendiary (flaming)"
+ },
+ "EquipmentType": {
+ "Potion": "Potion",
+ "Container": "Container",
+ "Mount": "Mount",
+ "HealingSupply": "Healing Supply",
+ "Food": "Food & Drink",
+ "LightSource": "Light Source",
+ "Misc": "Miscellaneous",
+ "Vehicle": "Vehicle",
+ "Animal": "Animal",
+ "WarMachine": "War Machine"
+ },
+ "MagicItemType": {
+ "Focus": "Focus",
+ "Talisman": "Talisman",
+ "Trinket": "Trinket"
+ },
+ "Rarity": {
+ "Always": "Always",
+ "Common": "1 – Common",
+ "Uncommon": "2 – Uncommon",
+ "Rare": "3 – Rare",
+ "VeryRare": "4 – Very Rare",
+ "Legendary": "5 – Legendary"
+ },
+ "AbilityType": {
+ "ClassAbility": "Class Ability",
+ "LineageTrait": "Lineage Trait"
+ },
+ "Condition": {
+ "Blinded": "Blinded",
+ "BlindedDesc": "Cannot see. -3 penalty to defense and melee attack rolls. Cannot perform ranged attacks or cast spells.",
+ "Confused": "Confused",
+ "ConfusedDesc": "Must make a DV2 Discipline check at the start of each turn or may not move or perform actions.",
+ "Dazed": "Dazed",
+ "DazedDesc": "Cannot perform Magic checks or move more than 10 ft. -1 penalty to attack and defense rolls.",
+ "Deafened": "Deafened",
+ "DeafenedDesc": "Cannot hear. -3 penalty to Magic checks due to verbal components required for miracles and spells.",
+ "Demoralized": "Demoralized",
+ "DemoralizedDesc": "−1 penalty to Discipline checks. Cannot perform Leadership checks.",
+ "Diseased": "Diseased",
+ "DiseasedDesc": "Lose 1 rank in all attributes. Can be treated with a DV5 Heal check once per day.",
+ "Enfeebled": "Enfeebled",
+ "EnfeebledDesc": "-2 penalty to attack and defense rolls. Cannot move more than 10 ft or perform the Run action.",
+ "Fatigued": "Fatigued",
+ "FatiguedDesc": "-1 penalty to attack and defense rolls, and to Resilience checks. Removed by a full night's rest.",
+ "Frightened": "Frightened",
+ "FrightenedDesc": "Cannot approach enemies. May attempt a DV2 Discipline check as an action to end this condition. Cannot perform Leadership checks.",
+ "Ignited": "Ignited",
+ "IgnitedDesc": "Suffers 1DD flaming damage at the end of each round, ignoring armor. Can remove condition as an action (ends turn prone).",
+ "Inspired": "Inspired",
+ "InspiredDesc": "+1 bonus to Discipline and Leadership checks.",
+ "Invisible": "Invisible",
+ "InvisibleDesc": "Cannot be seen by ordinary means. Cannot be targeted by magic or ranged attacks. -3 penalty to melee attacks against invisible targets.",
+ "Poisoned": "Poisoned",
+ "PoisonedDesc": "Suffers 1DD poison damage (black die) at the end of each round, ignoring armor.",
+ "Restrained": "Restrained",
+ "RestrainedDesc": "Cannot move.",
+ "Stunned": "Stunned",
+ "StunnedDesc": "Cannot speak, move, or perform actions. -3 penalty to defense rolls."
+ },
+ "Label": {
+ "Character": "Character",
+ "NPC": "NPC",
+ "Grit": "Grit",
+ "Luck": "Luck",
+ "Defense": "Defense",
+ "DefenseValue": "Defense Value",
+ "ArmorRating": "Armor Rating",
+ "DefenseBonus": "Defense Bonus",
+ "Movement": "Movement",
+ "ArcaneStress": "Arcane Stress",
+ "StressValue": "Stress",
+ "Attributes": "Attributes",
+ "Biodata": "Background",
+ "Background": "Background",
+ "Experience": "Experience",
+ "Level": "Level",
+ "XP": "Current XP",
+ "TotalXP": "Total XP",
+ "Abilities": "Abilities & Traits",
+ "Oaths": "Oaths",
+ "Weapons": "Weapons",
+ "Armor": "Armor & Shields",
+ "Ammunition": "Ammunition",
+ "Spells": "Spells",
+ "Miracles": "Miracles",
+ "Equipment": "Equipment",
+ "MagicItems": "Magic Items",
+ "Conditions": "Conditions",
+ "Description": "Description",
+ "Notes": "Notes",
+ "Stats": "Statistics",
+ "CR": "Challenge Rating",
+ "AttackBonus": "Attack Bonus",
+ "DamageBonus": "Damage Bonus",
+ "Currency": "Currency",
+ "None": "None",
+ "Effect": "Effect",
+ "Components": "Components",
+ "Charges": "Charges",
+ "NoWeapons": "No weapons equipped.",
+ "NoArmor": "No armor or shields.",
+ "NoSpells": "No spells known.",
+ "NoMiracles": "No miracles known.",
+ "NoEquipment": "No equipment.",
+ "Enchantment": "Enchantment",
+ "Tenet": "Tenet",
+ "Boon": "Boon",
+ "Bane": "Bane",
+ "Skill": "Skill",
+ "SkillRank": "Rank",
+ "SkillModifier": "Mod",
+ "TotalDice": "Total",
+ "ColorDice": "Color",
+ "DropLineage": "Drop Lineage Here",
+ "DropClass": "Drop Class Here",
+ "Traits": "Traits",
+ "Features": "Features",
+ "Name": "Name",
+ "Type": "Type",
+ "Damage": "Damage",
+ "Tradition": "Tradition",
+ "Piety": "Piety",
+ "Quantity": "Qty",
+ "Rarity": "Rarity",
+ "Penalty": "Penalty",
+ "Equipped": "Eq.",
+ "XPCurrent": "Current XP"
+ },
+ "ColorDice": {
+ "White": "White (4+)",
+ "Red": "Red (3+)",
+ "Black": "Black (2+)"
+ },
+ "NewItem": {
+ "Weapon": "New Weapon",
+ "Spell": "New Spell",
+ "Miracle": "New Miracle",
+ "Equipment": "New Equipment"
+ },
+ "ToggleSheet": "Toggle Edit/Play Mode",
+ "Character": {
+ "FIELDS": {
+ "attributes": {
+ "label": "Attributes"
+ },
+ "grit": {
+ "label": "Grit"
+ },
+ "luck": {
+ "label": "Luck",
+ "fields": {
+ "value": {
+ "label": "Luck"
+ },
+ "max": {
+ "label": "Luck Max"
+ }
+ }
+ },
+ "arcaneStress": {
+ "label": "Arcane Stress"
+ },
+ "movement": {
+ "label": "Movement"
+ },
+ "defense": {
+ "label": "Defense"
+ },
+ "experience": {
+ "label": "Experience"
+ },
+ "biodata": {
+ "label": "Background",
+ "fields": {
+ "lineage": {
+ "label": "Lineage"
+ },
+ "class": {
+ "label": "Class"
+ },
+ "age": {
+ "label": "Age"
+ },
+ "gender": {
+ "label": "Gender"
+ },
+ "height": {
+ "label": "Height"
+ },
+ "weight": {
+ "label": "Weight"
+ },
+ "eyes": {
+ "label": "Eye Color"
+ },
+ "hair": {
+ "label": "Hair Color"
+ },
+ "alignment": {
+ "label": "Alignment"
+ }
+ }
+ },
+ "currency": {
+ "label": "Currency",
+ "gold": {
+ "label": "Gold"
+ },
+ "silver": {
+ "label": "Silver"
+ },
+ "copper": {
+ "label": "Copper"
+ }
+ },
+ "description": {
+ "label": "Description"
+ },
+ "notes": {
+ "label": "Notes"
+ },
+ "skills": {
+ "label": "Skills"
+ }
+ }
+ },
+ "NPC": {
+ "FIELDS": {
+ "attributes": {
+ "label": "Attributes"
+ },
+ "grit": {
+ "label": "Grit"
+ },
+ "defense": {
+ "label": "Defense"
+ },
+ "movement": {
+ "label": "Movement"
+ },
+ "attackBonus": {
+ "label": "Attack Bonus"
+ },
+ "damageBonus": {
+ "label": "Damage Bonus"
+ },
+ "challengeRating": {
+ "label": "Challenge Rating"
+ },
+ "description": {
+ "label": "Description"
+ },
+ "notes": {
+ "label": "Notes"
+ }
+ }
+ },
+ "Weapon": {
+ "FIELDS": {
+ "proficiencyGroup": {
+ "label": "Proficiency Group"
+ },
+ "usesMight": {
+ "label": "Uses Might"
+ },
+ "damageMod": {
+ "label": "Damage Modifier"
+ },
+ "ap": {
+ "label": "Armor Penetration (AP)"
+ },
+ "reach": {
+ "label": "Reach (ft)"
+ },
+ "shortRange": {
+ "label": "Short Range (ft)"
+ },
+ "longRange": {
+ "label": "Long Range (ft)"
+ },
+ "traits": {
+ "label": "Traits"
+ },
+ "slots": {
+ "label": "Item Slots"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "equipped": {
+ "label": "Equipped"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ },
+ "description": {
+ "label": "Description"
+ },
+ "isMagic": {
+ "label": "Magic Item"
+ },
+ "magicQuality": {
+ "label": "Quality"
+ },
+ "isCursed": {
+ "label": "Cursed"
+ },
+ "magicEffect": {
+ "label": "Enchantment"
+ },
+ "classRestriction": {
+ "label": "Restriction"
+ }
+ }
+ },
+ "Armor": {
+ "FIELDS": {
+ "armorType": {
+ "label": "Armor Type"
+ },
+ "armorValue": {
+ "label": "Armor Value (AV)"
+ },
+ "penalty": {
+ "label": "Penalty"
+ },
+ "slots": {
+ "label": "Slots"
+ },
+ "traits": {
+ "label": "Traits"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "equipped": {
+ "label": "Equipped"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ },
+ "isMagic": {
+ "label": "Magic Item"
+ },
+ "magicQuality": {
+ "label": "Quality"
+ },
+ "isCursed": {
+ "label": "Cursed"
+ },
+ "magicEffect": {
+ "label": "Enchantment"
+ },
+ "classRestriction": {
+ "label": "Restriction"
+ }
+ }
+ },
+ "Ammunition": {
+ "FIELDS": {
+ "ammoType": {
+ "label": "Ammunition Type"
+ },
+ "quantity": {
+ "label": "Quantity"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ }
+ }
+ },
+ "Equipment": {
+ "FIELDS": {
+ "itemType": {
+ "label": "Category"
+ },
+ "quantity": {
+ "label": "Quantity"
+ },
+ "slots": {
+ "label": "Slots"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "lightRadius": {
+ "label": "Light Radius (ft)"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ }
+ }
+ },
+ "Spell": {
+ "FIELDS": {
+ "tradition": {
+ "label": "Tradition"
+ },
+ "difficultyValue": {
+ "label": "Difficulty Value (DV)"
+ },
+ "isRitual": {
+ "label": "Ritual"
+ },
+ "isMagicMissile": {
+ "label": "Magic Missile"
+ },
+ "range": {
+ "label": "Range"
+ },
+ "duration": {
+ "label": "Duration"
+ },
+ "spellSave": {
+ "label": "Spell Save"
+ },
+ "element": {
+ "label": "Element"
+ },
+ "runeType": {
+ "label": "Rune Type"
+ },
+ "isExalted": {
+ "label": "Exalted"
+ },
+ "effect": {
+ "label": "Effect"
+ }
+ }
+ },
+ "Miracle": {
+ "FIELDS": {
+ "divineTradition": {
+ "label": "Divine Tradition"
+ },
+ "difficultyValue": {
+ "label": "Difficulty Value (DV)"
+ },
+ "isRitual": {
+ "label": "Ritual"
+ },
+ "range": {
+ "label": "Range"
+ },
+ "duration": {
+ "label": "Duration"
+ },
+ "spellSave": {
+ "label": "Spell Save"
+ },
+ "effect": {
+ "label": "Effect"
+ }
+ }
+ },
+ "MagicItem": {
+ "FIELDS": {
+ "itemType": {
+ "label": "Type"
+ },
+ "quality": {
+ "label": "Quality"
+ },
+ "isCursed": {
+ "label": "Cursed"
+ },
+ "isBonded": {
+ "label": "Bonded"
+ },
+ "classRestriction": {
+ "label": "Restriction"
+ },
+ "usagePeriod": {
+ "label": "Usage Period"
+ },
+ "maxUses": {
+ "label": "Max Uses"
+ },
+ "slots": {
+ "label": "Slots"
+ },
+ "equipped": {
+ "label": "Equipped"
+ },
+ "effect": {
+ "label": "Effect"
+ }
+ }
+ },
+ "Ability": {
+ "FIELDS": {
+ "abilityType": {
+ "label": "Type"
+ },
+ "source": {
+ "label": "Source (Class / Lineage)"
+ },
+ "usagePeriod": {
+ "label": "Usage Period"
+ },
+ "maxUses": {
+ "label": "Max Uses"
+ },
+ "description": {
+ "label": "Description"
+ }
+ }
+ },
+ "WeaponGroup": {
+ "Common": "Common",
+ "Dueling": "Dueling",
+ "Heavy": "Heavy",
+ "Polearms": "Polearms",
+ "Bows": "Bows",
+ "Throwing": "Throwing"
+ },
+ "WeaponTrait": {
+ "Block": "Block",
+ "Brutal": "Brutal",
+ "Clumsy": "Clumsy",
+ "Couched": "Couched",
+ "Deadly": "Deadly",
+ "Fast": "Fast",
+ "Flaming": "Flaming",
+ "Nimble": "Nimble",
+ "Parry": "Parry",
+ "Reload": "Reload",
+ "Repel": "Repel",
+ "Stunning": "Stunning",
+ "Sweep": "Sweep",
+ "TwoHanded": "Two-handed",
+ "Versatile": "Versatile"
+ },
+ "DivineTradition": {
+ "Druidic": "Druidic",
+ "Profane": "Profane",
+ "Sanctified": "Sanctified"
+ },
+ "Element": {
+ "Air": "Air",
+ "Earth": "Earth",
+ "Fire": "Fire",
+ "Water": "Water",
+ "Varies": "Varies"
+ },
+ "RuneType": {
+ "Armor": "Armor",
+ "Talisman": "Talisman",
+ "Warding": "Warding",
+ "Weapon": "Weapon"
+ },
+ "ArmorTrait": {
+ "Clanging": "Clanging",
+ "Reinforced": "Reinforced"
+ },
+ "UsagePeriod": {
+ "None": "Passive (always on)",
+ "Encounter": "Per Encounter",
+ "Day": "Per Day"
+ },
+ "MagicQuality": {
+ "Lesser": "Lesser",
+ "Greater": "Greater",
+ "Legendary": "Legendary"
+ },
+ "OathType": {
+ "Compassion": "Oath of Compassion",
+ "Courage": "Oath of Courage",
+ "Diligence": "Oath of Diligence",
+ "Faith": "Oath of Faith",
+ "Humility": "Oath of Humility",
+ "Justice": "Oath of Justice",
+ "Loyalty": "Oath of Loyalty",
+ "Peace": "Oath of Peace",
+ "Perseverance": "Oath of Perseverance",
+ "Purity": "Oath of Purity",
+ "Truth": "Oath of Truth",
+ "Wisdom": "Oath of Wisdom"
+ },
+ "OathFields": {
+ "oathType": {
+ "label": "Oath"
+ },
+ "tenet": {
+ "label": "Tenet"
+ },
+ "boon": {
+ "label": "Boon"
+ },
+ "bane": {
+ "label": "Bane"
+ },
+ "violated": {
+ "label": "Violated"
+ }
+ },
+ "Oath": {
+ "FIELDS": {
+ "oathType": {
+ "label": "Oath"
+ },
+ "tenet": {
+ "label": "Tenet"
+ },
+ "boon": {
+ "label": "Boon"
+ },
+ "bane": {
+ "label": "Bane"
+ },
+ "violated": {
+ "label": "Violated"
+ }
+ }
+ }
+ },
+ "TYPES": {
+ "Item": {
+ "weapon": "Weapon",
+ "armor": "Armor",
+ "ammunition": "Ammunition",
+ "equipment": "Equipment",
+ "spell": "Spell",
+ "miracle": "Miracle",
+ "magic_item": "Magic Item",
+ "magic-item": "Magic Item",
+ "ability": "Ability",
+ "oath": "Oath",
+ "lineage": "Lineage",
+ "class": "Class"
+ },
+ "Actor": {
+ "character": "Character",
+ "npc": "NPC"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.history/lang/en_20260308154930.json b/.history/lang/en_20260308154930.json
new file mode 100644
index 0000000..1711313
--- /dev/null
+++ b/.history/lang/en_20260308154930.json
@@ -0,0 +1,788 @@
+{
+ "OATHHAMMER": {
+ "Sheet": {
+ "Character": "Oath Hammer Character Sheet",
+ "NPC": "Oath Hammer NPC Sheet",
+ "Weapon": "Oath Hammer Weapon Sheet",
+ "Armor": "Oath Hammer Armor Sheet",
+ "Ammunition": "Oath Hammer Ammunition Sheet",
+ "Equipment": "Oath Hammer Equipment Sheet",
+ "Spell": "Oath Hammer Spell Sheet",
+ "Miracle": "Oath Hammer Miracle Sheet",
+ "MagicItem": "Oath Hammer Magic Item Sheet",
+ "Ability": "Oath Hammer Ability Sheet",
+ "Oath": "Oath Hammer Oath Sheet",
+ "Condition": "Oath Hammer Condition Sheet",
+ "Lineage": "Oath Hammer Lineage Sheet",
+ "Class": "Oath Hammer Class Sheet"
+ },
+ "Tab": {
+ "Identity": "Identity",
+ "Skills": "Skills",
+ "Combat": "Combat",
+ "Magic": "Magic",
+ "Equipment": "Equipment",
+ "Notes": "Notes"
+ },
+ "Attribute": {
+ "Might": "Might",
+ "Toughness": "Toughness",
+ "Agility": "Agility",
+ "Willpower": "Willpower",
+ "Intelligence": "Intelligence",
+ "Fate": "Fate"
+ },
+ "Skill": {
+ "Academics": "Academics",
+ "Acrobatics": "Acrobatics",
+ "AnimalHandling": "Animal Handling",
+ "Athletics": "Athletics",
+ "Brewing": "Brewing",
+ "Carpentry": "Carpentry",
+ "Defense": "Defense",
+ "Dexterity": "Dexterity",
+ "Diplomacy": "Diplomacy",
+ "Discipline": "Discipline",
+ "Fighting": "Fighting",
+ "Folklore": "Folklore",
+ "Fortune": "Fortune",
+ "Heal": "Heal",
+ "Leadership": "Leadership",
+ "Magic": "Magic",
+ "Masonry": "Masonry",
+ "Orientation": "Orientation",
+ "Perception": "Perception",
+ "Resilience": "Resilience",
+ "Ride": "Ride",
+ "Shooting": "Shooting",
+ "Smithing": "Smithing",
+ "Stealth": "Stealth",
+ "Survival": "Survival",
+ "Tracking": "Tracking"
+ },
+ "Lineage": {
+ "Dwarf": "Dwarf",
+ "Firbolg": "Firbolg",
+ "Halfling": "Halfling",
+ "HighElf": "High Elf",
+ "Human": "Human",
+ "WoodElf": "Wood Elf",
+ "FIELDS": {
+ "description": {
+ "label": "Description"
+ },
+ "traits": {
+ "label": "Traits"
+ },
+ "movement": {
+ "label": "Movement (ft)"
+ },
+ "gritModifier": {
+ "label": "Grit Modifier"
+ }
+ }
+ },
+ "Class": {
+ "Berserker": "Berserker",
+ "Champion": "Champion",
+ "Delver": "Delver",
+ "Knight": "Knight",
+ "Mage": "Mage",
+ "Priest": "Priest",
+ "Scout": "Scout",
+ "Soldier": "Soldier",
+ "Spellblade": "Spellblade",
+ "Troubadour": "Troubadour",
+ "FIELDS": {
+ "description": {
+ "label": "Description"
+ },
+ "features": {
+ "label": "Features"
+ },
+ "armorProficiency": {
+ "label": "Armor Proficiency"
+ },
+ "weaponProficiency": {
+ "label": "Weapon Proficiency"
+ }
+ }
+ },
+ "Tradition": {
+ "Elemental": "Elemental",
+ "Illusionist": "Illusionist",
+ "Imperial": "Imperial",
+ "Infernal": "Infernal",
+ "Runic": "Runic",
+ "Stygian": "Stygian"
+ },
+ "ArmorType": {
+ "Light": "Light",
+ "Medium": "Medium",
+ "Heavy": "Heavy"
+ },
+ "Currency": {
+ "GP": "Gold Pieces",
+ "SP": "Silver Pieces",
+ "CP": "Copper Pieces"
+ },
+ "AmmoType": {
+ "Standard": "Arrow / Bolt",
+ "Bodkin": "Bodkin (−1 armor)",
+ "Envenomed": "Envenomed (poison)",
+ "Incendiary": "Incendiary (flaming)"
+ },
+ "EquipmentType": {
+ "Potion": "Potion",
+ "Container": "Container",
+ "Mount": "Mount",
+ "HealingSupply": "Healing Supply",
+ "Food": "Food & Drink",
+ "LightSource": "Light Source",
+ "Misc": "Miscellaneous",
+ "Vehicle": "Vehicle",
+ "Animal": "Animal",
+ "WarMachine": "War Machine"
+ },
+ "MagicItemType": {
+ "Focus": "Focus",
+ "Talisman": "Talisman",
+ "Trinket": "Trinket"
+ },
+ "Rarity": {
+ "Always": "Always",
+ "Common": "1 – Common",
+ "Uncommon": "2 – Uncommon",
+ "Rare": "3 – Rare",
+ "VeryRare": "4 – Very Rare",
+ "Legendary": "5 – Legendary"
+ },
+ "AbilityType": {
+ "ClassAbility": "Class Ability",
+ "LineageTrait": "Lineage Trait"
+ },
+ "Condition": {
+ "Blinded": "Blinded",
+ "BlindedDesc": "Cannot see. -3 penalty to defense and melee attack rolls. Cannot perform ranged attacks or cast spells.",
+ "Confused": "Confused",
+ "ConfusedDesc": "Must make a DV2 Discipline check at the start of each turn or may not move or perform actions.",
+ "Dazed": "Dazed",
+ "DazedDesc": "Cannot perform Magic checks or move more than 10 ft. -1 penalty to attack and defense rolls.",
+ "Deafened": "Deafened",
+ "DeafenedDesc": "Cannot hear. -3 penalty to Magic checks due to verbal components required for miracles and spells.",
+ "Demoralized": "Demoralized",
+ "DemoralizedDesc": "−1 penalty to Discipline checks. Cannot perform Leadership checks.",
+ "Diseased": "Diseased",
+ "DiseasedDesc": "Lose 1 rank in all attributes. Can be treated with a DV5 Heal check once per day.",
+ "Enfeebled": "Enfeebled",
+ "EnfeebledDesc": "-2 penalty to attack and defense rolls. Cannot move more than 10 ft or perform the Run action.",
+ "Fatigued": "Fatigued",
+ "FatiguedDesc": "-1 penalty to attack and defense rolls, and to Resilience checks. Removed by a full night's rest.",
+ "Frightened": "Frightened",
+ "FrightenedDesc": "Cannot approach enemies. May attempt a DV2 Discipline check as an action to end this condition. Cannot perform Leadership checks.",
+ "Ignited": "Ignited",
+ "IgnitedDesc": "Suffers 1DD flaming damage at the end of each round, ignoring armor. Can remove condition as an action (ends turn prone).",
+ "Inspired": "Inspired",
+ "InspiredDesc": "+1 bonus to Discipline and Leadership checks.",
+ "Invisible": "Invisible",
+ "InvisibleDesc": "Cannot be seen by ordinary means. Cannot be targeted by magic or ranged attacks. -3 penalty to melee attacks against invisible targets.",
+ "Poisoned": "Poisoned",
+ "PoisonedDesc": "Suffers 1DD poison damage (black die) at the end of each round, ignoring armor.",
+ "Restrained": "Restrained",
+ "RestrainedDesc": "Cannot move.",
+ "Stunned": "Stunned",
+ "StunnedDesc": "Cannot speak, move, or perform actions. -3 penalty to defense rolls."
+ },
+ "Label": {
+ "Character": "Character",
+ "NPC": "NPC",
+ "Grit": "Grit",
+ "Luck": "Luck",
+ "Defense": "Defense",
+ "DefenseValue": "Defense Value",
+ "ArmorRating": "Armor Rating",
+ "DefenseBonus": "Defense Bonus",
+ "Movement": "Movement",
+ "ArcaneStress": "Arcane Stress",
+ "StressValue": "Stress",
+ "Attributes": "Attributes",
+ "Biodata": "Background",
+ "Background": "Background",
+ "Experience": "Experience",
+ "Level": "Level",
+ "XP": "Current XP",
+ "TotalXP": "Total XP",
+ "Abilities": "Abilities & Traits",
+ "Oaths": "Oaths",
+ "Weapons": "Weapons",
+ "Armor": "Armor & Shields",
+ "Ammunition": "Ammunition",
+ "Spells": "Spells",
+ "Miracles": "Miracles",
+ "Equipment": "Equipment",
+ "MagicItems": "Magic Items",
+ "Conditions": "Conditions",
+ "Description": "Description",
+ "Notes": "Notes",
+ "Stats": "Statistics",
+ "CR": "Challenge Rating",
+ "AttackBonus": "Attack Bonus",
+ "DamageBonus": "Damage Bonus",
+ "Currency": "Currency",
+ "None": "None",
+ "Effect": "Effect",
+ "Components": "Components",
+ "Charges": "Charges",
+ "NoWeapons": "No weapons equipped.",
+ "NoArmor": "No armor or shields.",
+ "NoSpells": "No spells known.",
+ "NoMiracles": "No miracles known.",
+ "NoEquipment": "No equipment.",
+ "Enchantment": "Enchantment",
+ "Tenet": "Tenet",
+ "Boon": "Boon",
+ "Bane": "Bane",
+ "Skill": "Skill",
+ "SkillRank": "Rank",
+ "SkillModifier": "Mod",
+ "TotalDice": "Total",
+ "ColorDice": "Color",
+ "DropLineage": "Drop Lineage Here",
+ "DropClass": "Drop Class Here",
+ "Traits": "Traits",
+ "Features": "Features",
+ "Name": "Name",
+ "Type": "Type",
+ "Damage": "Damage",
+ "Tradition": "Tradition",
+ "Piety": "Piety",
+ "Quantity": "Qty",
+ "Rarity": "Rarity",
+ "Penalty": "Penalty",
+ "Equipped": "Eq.",
+ "XPCurrent": "Current XP"
+ },
+ "ColorDice": {
+ "White": "White (4+)",
+ "Red": "Red (3+)",
+ "Black": "Black (2+)"
+ },
+ "NewItem": {
+ "Weapon": "New Weapon",
+ "Spell": "New Spell",
+ "Miracle": "New Miracle",
+ "Equipment": "New Equipment"
+ },
+ "ToggleSheet": "Toggle Edit/Play Mode",
+ "Character": {
+ "FIELDS": {
+ "attributes": {
+ "label": "Attributes"
+ },
+ "grit": {
+ "label": "Grit"
+ },
+ "luck": {
+ "label": "Luck",
+ "fields": {
+ "value": {
+ "label": "Luck"
+ },
+ "max": {
+ "label": "Luck Max"
+ }
+ }
+ },
+ "arcaneStress": {
+ "label": "Arcane Stress"
+ },
+ "movement": {
+ "label": "Movement"
+ },
+ "defense": {
+ "label": "Defense"
+ },
+ "experience": {
+ "label": "Experience"
+ },
+ "biodata": {
+ "label": "Background",
+ "fields": {
+ "lineage": {
+ "label": "Lineage"
+ },
+ "class": {
+ "label": "Class"
+ },
+ "age": {
+ "label": "Age"
+ },
+ "gender": {
+ "label": "Gender"
+ },
+ "height": {
+ "label": "Height"
+ },
+ "weight": {
+ "label": "Weight"
+ },
+ "eyes": {
+ "label": "Eye Color"
+ },
+ "hair": {
+ "label": "Hair Color"
+ },
+ "alignment": {
+ "label": "Alignment"
+ }
+ }
+ },
+ "currency": {
+ "label": "Currency",
+ "gold": {
+ "label": "Gold"
+ },
+ "silver": {
+ "label": "Silver"
+ },
+ "copper": {
+ "label": "Copper"
+ }
+ },
+ "description": {
+ "label": "Description"
+ },
+ "notes": {
+ "label": "Notes"
+ },
+ "skills": {
+ "label": "Skills"
+ }
+ }
+ },
+ "NPC": {
+ "FIELDS": {
+ "attributes": {
+ "label": "Attributes"
+ },
+ "grit": {
+ "label": "Grit"
+ },
+ "defense": {
+ "label": "Defense"
+ },
+ "movement": {
+ "label": "Movement"
+ },
+ "attackBonus": {
+ "label": "Attack Bonus"
+ },
+ "damageBonus": {
+ "label": "Damage Bonus"
+ },
+ "challengeRating": {
+ "label": "Challenge Rating"
+ },
+ "description": {
+ "label": "Description"
+ },
+ "notes": {
+ "label": "Notes"
+ }
+ }
+ },
+ "Weapon": {
+ "FIELDS": {
+ "proficiencyGroup": {
+ "label": "Proficiency Group"
+ },
+ "usesMight": {
+ "label": "Uses Might"
+ },
+ "damageMod": {
+ "label": "Damage Modifier"
+ },
+ "ap": {
+ "label": "Armor Penetration (AP)"
+ },
+ "reach": {
+ "label": "Reach (ft)"
+ },
+ "shortRange": {
+ "label": "Short Range (ft)"
+ },
+ "longRange": {
+ "label": "Long Range (ft)"
+ },
+ "traits": {
+ "label": "Traits"
+ },
+ "slots": {
+ "label": "Item Slots"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "equipped": {
+ "label": "Equipped"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ },
+ "description": {
+ "label": "Description"
+ },
+ "isMagic": {
+ "label": "Magic Item"
+ },
+ "magicQuality": {
+ "label": "Quality"
+ },
+ "isCursed": {
+ "label": "Cursed"
+ },
+ "magicEffect": {
+ "label": "Enchantment"
+ },
+ "classRestriction": {
+ "label": "Restriction"
+ }
+ }
+ },
+ "Armor": {
+ "FIELDS": {
+ "armorType": {
+ "label": "Armor Type"
+ },
+ "armorValue": {
+ "label": "Armor Value (AV)"
+ },
+ "penalty": {
+ "label": "Penalty"
+ },
+ "slots": {
+ "label": "Slots"
+ },
+ "traits": {
+ "label": "Traits"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "equipped": {
+ "label": "Equipped"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ },
+ "isMagic": {
+ "label": "Magic Item"
+ },
+ "magicQuality": {
+ "label": "Quality"
+ },
+ "isCursed": {
+ "label": "Cursed"
+ },
+ "magicEffect": {
+ "label": "Enchantment"
+ },
+ "classRestriction": {
+ "label": "Restriction"
+ }
+ }
+ },
+ "Ammunition": {
+ "FIELDS": {
+ "ammoType": {
+ "label": "Ammunition Type"
+ },
+ "quantity": {
+ "label": "Quantity"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ }
+ }
+ },
+ "Equipment": {
+ "FIELDS": {
+ "itemType": {
+ "label": "Category"
+ },
+ "quantity": {
+ "label": "Quantity"
+ },
+ "slots": {
+ "label": "Slots"
+ },
+ "rarity": {
+ "label": "Rarity"
+ },
+ "lightRadius": {
+ "label": "Light Radius (ft)"
+ },
+ "cost": {
+ "label": "Cost"
+ },
+ "currency": {
+ "label": "Currency"
+ }
+ }
+ },
+ "Spell": {
+ "FIELDS": {
+ "tradition": {
+ "label": "Tradition"
+ },
+ "difficultyValue": {
+ "label": "Difficulty Value (DV)"
+ },
+ "isRitual": {
+ "label": "Ritual"
+ },
+ "isMagicMissile": {
+ "label": "Magic Missile"
+ },
+ "range": {
+ "label": "Range"
+ },
+ "duration": {
+ "label": "Duration"
+ },
+ "spellSave": {
+ "label": "Spell Save"
+ },
+ "element": {
+ "label": "Element"
+ },
+ "runeType": {
+ "label": "Rune Type"
+ },
+ "isExalted": {
+ "label": "Exalted"
+ },
+ "effect": {
+ "label": "Effect"
+ }
+ }
+ },
+ "Miracle": {
+ "FIELDS": {
+ "divineTradition": {
+ "label": "Divine Tradition"
+ },
+ "difficultyValue": {
+ "label": "Difficulty Value (DV)"
+ },
+ "isRitual": {
+ "label": "Ritual"
+ },
+ "range": {
+ "label": "Range"
+ },
+ "duration": {
+ "label": "Duration"
+ },
+ "spellSave": {
+ "label": "Spell Save"
+ },
+ "effect": {
+ "label": "Effect"
+ }
+ }
+ },
+ "MagicItem": {
+ "FIELDS": {
+ "itemType": {
+ "label": "Type"
+ },
+ "quality": {
+ "label": "Quality"
+ },
+ "isCursed": {
+ "label": "Cursed"
+ },
+ "isBonded": {
+ "label": "Bonded"
+ },
+ "classRestriction": {
+ "label": "Restriction"
+ },
+ "usagePeriod": {
+ "label": "Usage Period"
+ },
+ "maxUses": {
+ "label": "Max Uses"
+ },
+ "slots": {
+ "label": "Slots"
+ },
+ "equipped": {
+ "label": "Equipped"
+ },
+ "effect": {
+ "label": "Effect"
+ }
+ }
+ },
+ "Ability": {
+ "FIELDS": {
+ "abilityType": {
+ "label": "Type"
+ },
+ "source": {
+ "label": "Source (Class / Lineage)"
+ },
+ "usagePeriod": {
+ "label": "Usage Period"
+ },
+ "maxUses": {
+ "label": "Max Uses"
+ },
+ "description": {
+ "label": "Description"
+ }
+ }
+ },
+ "WeaponGroup": {
+ "Common": "Common",
+ "Dueling": "Dueling",
+ "Heavy": "Heavy",
+ "Polearms": "Polearms",
+ "Bows": "Bows",
+ "Throwing": "Throwing"
+ },
+ "WeaponTrait": {
+ "Block": "Block",
+ "Brutal": "Brutal",
+ "Clumsy": "Clumsy",
+ "Couched": "Couched",
+ "Deadly": "Deadly",
+ "Fast": "Fast",
+ "Flaming": "Flaming",
+ "Nimble": "Nimble",
+ "Parry": "Parry",
+ "Reload": "Reload",
+ "Repel": "Repel",
+ "Stunning": "Stunning",
+ "Sweep": "Sweep",
+ "TwoHanded": "Two-handed",
+ "Versatile": "Versatile"
+ },
+ "DivineTradition": {
+ "Druidic": "Druidic",
+ "Profane": "Profane",
+ "Sanctified": "Sanctified"
+ },
+ "Element": {
+ "Air": "Air",
+ "Earth": "Earth",
+ "Fire": "Fire",
+ "Water": "Water",
+ "Varies": "Varies"
+ },
+ "RuneType": {
+ "Armor": "Armor",
+ "Talisman": "Talisman",
+ "Warding": "Warding",
+ "Weapon": "Weapon"
+ },
+ "ArmorTrait": {
+ "Clanging": "Clanging",
+ "Reinforced": "Reinforced"
+ },
+ "UsagePeriod": {
+ "None": "Passive (always on)",
+ "Encounter": "Per Encounter",
+ "Day": "Per Day"
+ },
+ "MagicQuality": {
+ "Lesser": "Lesser",
+ "Greater": "Greater",
+ "Legendary": "Legendary"
+ },
+ "OathType": {
+ "Compassion": "Oath of Compassion",
+ "Courage": "Oath of Courage",
+ "Diligence": "Oath of Diligence",
+ "Faith": "Oath of Faith",
+ "Humility": "Oath of Humility",
+ "Justice": "Oath of Justice",
+ "Loyalty": "Oath of Loyalty",
+ "Peace": "Oath of Peace",
+ "Perseverance": "Oath of Perseverance",
+ "Purity": "Oath of Purity",
+ "Truth": "Oath of Truth",
+ "Wisdom": "Oath of Wisdom"
+ },
+ "OathFields": {
+ "oathType": {
+ "label": "Oath"
+ },
+ "tenet": {
+ "label": "Tenet"
+ },
+ "boon": {
+ "label": "Boon"
+ },
+ "bane": {
+ "label": "Bane"
+ },
+ "violated": {
+ "label": "Violated"
+ }
+ },
+ "Oath": {
+ "FIELDS": {
+ "oathType": {
+ "label": "Oath"
+ },
+ "tenet": {
+ "label": "Tenet"
+ },
+ "boon": {
+ "label": "Boon"
+ },
+ "bane": {
+ "label": "Bane"
+ },
+ "violated": {
+ "label": "Violated"
+ }
+ }
+ }
+ },
+ "TYPES": {
+ "Item": {
+ "weapon": "Weapon",
+ "armor": "Armor",
+ "ammunition": "Ammunition",
+ "equipment": "Equipment",
+ "spell": "Spell",
+ "miracle": "Miracle",
+ "magic_item": "Magic Item",
+ "magic-item": "Magic Item",
+ "ability": "Ability",
+ "oath": "Oath",
+ "lineage": "Lineage",
+ "class": "Class"
+ },
+ "Actor": {
+ "character": "Character",
+ "npc": "NPC"
+ }
+ }
+}
\ No newline at end of file
diff --git a/.history/system_20260308195653.json b/.history/system_20260308195653.json
new file mode 100644
index 0000000..f56deb1
--- /dev/null
+++ b/.history/system_20260308195653.json
@@ -0,0 +1,130 @@
+{
+ "id": "fvtt-oath-hammer",
+ "title": "Oath Hammer RPG",
+ "description": "Oath Hammer RPG System for FoundryVTT",
+ "version": "13.0.0",
+ "compatibility": {
+ "minimum": "13",
+ "verified": "13"
+ },
+ "esmodules": [
+ "oath-hammer.mjs"
+ ],
+ "styles": [
+ "css/fvtt-oath-hammer.css"
+ ],
+ "languages": [
+ {
+ "lang": "en",
+ "name": "English",
+ "path": "lang/en.json"
+ }
+ ],
+ "documentTypes": {
+ "Actor": {
+ "character": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ },
+ "npc": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ }
+ },
+ "Item": {
+ "weapon": {
+ "htmlFields": [
+ "description",
+ "magicEffect"
+ ]
+ },
+ "armor": {
+ "htmlFields": [
+ "description",
+ "magicEffect"
+ ]
+ },
+ "ammunition": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "equipment": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "spell": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "miracle": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "magic-item": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "trait": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "oath": {
+ "htmlFields": [
+ "tenet",
+ "boon",
+ "bane"
+ ]
+ },
+ "lineage": {
+ "htmlFields": [
+ "description",
+ "traits"
+ ]
+ },
+ "class": {
+ "htmlFields": [
+ "description",
+ "features"
+ ]
+ },
+ "building": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ }
+ }
+ },
+ "grid": {
+ "distance": 5,
+ "units": "ft"
+ },
+ "primaryTokenAttribute": "grit",
+ "socket": true,
+ "background": "systems/fvtt-oath-hammer/assets/ui/oath_hammer_paper.webp",
+ "flags": {
+ "hotReload": {
+ "extensions": [
+ "css",
+ "hbs",
+ "json"
+ ],
+ "paths": [
+ "css/",
+ "lang/",
+ "assets/",
+ "templates/"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.history/system_20260308204838.json b/.history/system_20260308204838.json
new file mode 100644
index 0000000..7ba25bb
--- /dev/null
+++ b/.history/system_20260308204838.json
@@ -0,0 +1,130 @@
+{
+ "id": "fvtt-oath-hammer",
+ "title": "Oath Hammer RPG",
+ "description": "Oath Hammer RPG System for FoundryVTT",
+ "version": "13.0.0",
+ "compatibility": {
+ "minimum": "13",
+ "verified": "13"
+ },
+ "esmodules": [
+ "oath-hammer.mjs"
+ ],
+ "styles": [
+ "css/fvtt-oath-hammer.css"
+ ],
+ "languages": [
+ {
+ "lang": "en",
+ "name": "English",
+ "path": "lang/en.json"
+ }
+ ],
+ "documentTypes": {
+ "Actor": {
+ "character": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ },
+ "npc": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ }
+ },
+ "Item": {
+ "weapon": {
+ "htmlFields": [
+ "description",
+ "magicEffect"
+ ]
+ },
+ "armor": {
+ "htmlFields": [
+ "description",
+ "magicEffect"
+ ]
+ },
+ "ammunition": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "equipment": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "spell": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "miracle": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "magic-item": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "trait": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "oath": {
+ "htmlFields": [
+ "tenet",
+ "boon",
+ "bane"
+ ]
+ },
+ "lineage": {
+ "htmlFields": [
+ "description",
+ "traits"
+ ]
+ },
+ "class": {
+ "htmlFields": [
+ "description",
+ "features"
+ ]
+ },
+ "building": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ }
+ }
+ },
+ "grid": {
+ "distance": 5,
+ "units": "ft"
+ },
+ "primaryTokenAttribute": "grit",
+ "socket": true,
+ "background": "assets/images/cover_art.webp",
+ "flags": {
+ "hotReload": {
+ "extensions": [
+ "css",
+ "hbs",
+ "json"
+ ],
+ "paths": [
+ "css/",
+ "lang/",
+ "assets/",
+ "templates/"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.history/system_20260308204842.json b/.history/system_20260308204842.json
new file mode 100644
index 0000000..560fff4
--- /dev/null
+++ b/.history/system_20260308204842.json
@@ -0,0 +1,130 @@
+{
+ "id": "fvtt-oath-hammer",
+ "title": "Oath Hammer RPG",
+ "description": "Oath Hammer RPG System for FoundryVTT",
+ "version": "13.0.0",
+ "compatibility": {
+ "minimum": "13",
+ "verified": "13"
+ },
+ "esmodules": [
+ "oath-hammer.mjs"
+ ],
+ "styles": [
+ "css/fvtt-oath-hammer.css"
+ ],
+ "languages": [
+ {
+ "lang": "en",
+ "name": "English",
+ "path": "lang/en.json"
+ }
+ ],
+ "documentTypes": {
+ "Actor": {
+ "character": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ },
+ "npc": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ }
+ },
+ "Item": {
+ "weapon": {
+ "htmlFields": [
+ "description",
+ "magicEffect"
+ ]
+ },
+ "armor": {
+ "htmlFields": [
+ "description",
+ "magicEffect"
+ ]
+ },
+ "ammunition": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "equipment": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "spell": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "miracle": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "magic-item": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "trait": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "oath": {
+ "htmlFields": [
+ "tenet",
+ "boon",
+ "bane"
+ ]
+ },
+ "lineage": {
+ "htmlFields": [
+ "description",
+ "traits"
+ ]
+ },
+ "class": {
+ "htmlFields": [
+ "description",
+ "features"
+ ]
+ },
+ "building": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ }
+ }
+ },
+ "grid": {
+ "distance": 5,
+ "units": "ft"
+ },
+ "primaryTokenAttribute": "grit",
+ "socket": true,
+ "background": "systems/fvtt-oath-hammer/assets/images/cover_art.webp",
+ "flags": {
+ "hotReload": {
+ "extensions": [
+ "css",
+ "hbs",
+ "json"
+ ],
+ "paths": [
+ "css/",
+ "lang/",
+ "assets/",
+ "templates/"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.history/system_20260308204843.json b/.history/system_20260308204843.json
new file mode 100644
index 0000000..560fff4
--- /dev/null
+++ b/.history/system_20260308204843.json
@@ -0,0 +1,130 @@
+{
+ "id": "fvtt-oath-hammer",
+ "title": "Oath Hammer RPG",
+ "description": "Oath Hammer RPG System for FoundryVTT",
+ "version": "13.0.0",
+ "compatibility": {
+ "minimum": "13",
+ "verified": "13"
+ },
+ "esmodules": [
+ "oath-hammer.mjs"
+ ],
+ "styles": [
+ "css/fvtt-oath-hammer.css"
+ ],
+ "languages": [
+ {
+ "lang": "en",
+ "name": "English",
+ "path": "lang/en.json"
+ }
+ ],
+ "documentTypes": {
+ "Actor": {
+ "character": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ },
+ "npc": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ }
+ },
+ "Item": {
+ "weapon": {
+ "htmlFields": [
+ "description",
+ "magicEffect"
+ ]
+ },
+ "armor": {
+ "htmlFields": [
+ "description",
+ "magicEffect"
+ ]
+ },
+ "ammunition": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "equipment": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "spell": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "miracle": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "magic-item": {
+ "htmlFields": [
+ "effect"
+ ]
+ },
+ "trait": {
+ "htmlFields": [
+ "description"
+ ]
+ },
+ "oath": {
+ "htmlFields": [
+ "tenet",
+ "boon",
+ "bane"
+ ]
+ },
+ "lineage": {
+ "htmlFields": [
+ "description",
+ "traits"
+ ]
+ },
+ "class": {
+ "htmlFields": [
+ "description",
+ "features"
+ ]
+ },
+ "building": {
+ "htmlFields": [
+ "description",
+ "notes"
+ ]
+ }
+ }
+ },
+ "grid": {
+ "distance": 5,
+ "units": "ft"
+ },
+ "primaryTokenAttribute": "grit",
+ "socket": true,
+ "background": "systems/fvtt-oath-hammer/assets/images/cover_art.webp",
+ "flags": {
+ "hotReload": {
+ "extensions": [
+ "css",
+ "hbs",
+ "json"
+ ],
+ "paths": [
+ "css/",
+ "lang/",
+ "assets/",
+ "templates/"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.history/templates/item/armor-sheet_20260308134234.hbs b/.history/templates/item/armor-sheet_20260308134234.hbs
new file mode 100644
index 0000000..3dc5c61
--- /dev/null
+++ b/.history/templates/item/armor-sheet_20260308134234.hbs
@@ -0,0 +1,51 @@
+
+
+
+
+ {{formField systemFields.armorType value=system.armorType name="system.armorType" localize=true}}
+
+
+ {{formField systemFields.slots value=system.slots name="system.slots"}}
+ {{formField systemFields.traits value=system.traits name="system.traits" localize=true}}
+
+
+ {{formField systemFields.rarity value=system.rarity name="system.rarity" localize=true}}
+ {{formField systemFields.isMagic value=system.isMagic name="system.isMagic"}}
+ {{formField systemFields.equipped value=system.equipped name="system.equipped"}}
+ {{formField systemFields.cost value=system.cost name="system.cost"}}
+ {{formField systemFields.currency value=system.currency name="system.currency" localize=true}}
+
+
+
+ {{#if system.isMagic}}
+
+ {{/if}}
+
diff --git a/.history/templates/item/armor-sheet_20260308155000.hbs b/.history/templates/item/armor-sheet_20260308155000.hbs
new file mode 100644
index 0000000..ec53173
--- /dev/null
+++ b/.history/templates/item/armor-sheet_20260308155000.hbs
@@ -0,0 +1,109 @@
+
+
+
+
+ {{formField
+ systemFields.armorType
+ value=system.armorType
+ name="system.armorType"
+ }}
+
+
+ {{formField systemFields.slots value=system.slots name="system.slots"}}
+ {{formField
+ systemFields.traits
+ value=system.traits
+ name="system.traits"
+ localize=true
+ }}
+
+
+ {{formField
+ systemFields.rarity
+ value=system.rarity
+ name="system.rarity"
+ localize=true
+ }}
+ {{formField
+ systemFields.isMagic
+ value=system.isMagic
+ name="system.isMagic"
+ }}
+ {{formField
+ systemFields.equipped
+ value=system.equipped
+ name="system.equipped"
+ }}
+ {{formField systemFields.cost value=system.cost name="system.cost"}}
+ {{formField
+ systemFields.currency
+ value=system.currency
+ name="system.currency"
+ localize=true
+ }}
+
+
+
+ {{#if system.isMagic}}
+
+ {{/if}}
+
\ No newline at end of file
diff --git a/.history/templates/item/armor-sheet_20260308155017.hbs b/.history/templates/item/armor-sheet_20260308155017.hbs
new file mode 100644
index 0000000..2bdd08e
--- /dev/null
+++ b/.history/templates/item/armor-sheet_20260308155017.hbs
@@ -0,0 +1,51 @@
+
+
+
+
+ {{formField systemFields.armorType value=system.armorType name="system.armorType"}}
+
+
+ {{formField systemFields.slots value=system.slots name="system.slots"}}
+ {{formField systemFields.traits value=system.traits name="system.traits" localize=true}}
+
+
+ {{formField systemFields.rarity value=system.rarity name="system.rarity" localize=true}}
+ {{formField systemFields.isMagic value=system.isMagic name="system.isMagic"}}
+ {{formField systemFields.equipped value=system.equipped name="system.equipped"}}
+ {{formField systemFields.cost value=system.cost name="system.cost"}}
+ {{formField systemFields.currency value=system.currency name="system.currency" localize=true}}
+
+
+
+ {{#if system.isMagic}}
+
+ {{/if}}
+
diff --git a/.history/templates/item/armor-sheet_20260308155019.hbs b/.history/templates/item/armor-sheet_20260308155019.hbs
new file mode 100644
index 0000000..ec53173
--- /dev/null
+++ b/.history/templates/item/armor-sheet_20260308155019.hbs
@@ -0,0 +1,109 @@
+
+
+
+
+ {{formField
+ systemFields.armorType
+ value=system.armorType
+ name="system.armorType"
+ }}
+
+
+ {{formField systemFields.slots value=system.slots name="system.slots"}}
+ {{formField
+ systemFields.traits
+ value=system.traits
+ name="system.traits"
+ localize=true
+ }}
+
+
+ {{formField
+ systemFields.rarity
+ value=system.rarity
+ name="system.rarity"
+ localize=true
+ }}
+ {{formField
+ systemFields.isMagic
+ value=system.isMagic
+ name="system.isMagic"
+ }}
+ {{formField
+ systemFields.equipped
+ value=system.equipped
+ name="system.equipped"
+ }}
+ {{formField systemFields.cost value=system.cost name="system.cost"}}
+ {{formField
+ systemFields.currency
+ value=system.currency
+ name="system.currency"
+ localize=true
+ }}
+
+
+
+ {{#if system.isMagic}}
+
+ {{/if}}
+
\ No newline at end of file
diff --git a/.history/templates/item/armor-sheet_20260308155021.hbs b/.history/templates/item/armor-sheet_20260308155021.hbs
new file mode 100644
index 0000000..d8d62c4
--- /dev/null
+++ b/.history/templates/item/armor-sheet_20260308155021.hbs
@@ -0,0 +1,110 @@
+
+
+
+
+ {{formField
+ systemFields.armorType
+ value=system.armorType
+ name="system.armorType"
+ localize=true
+ }}
+
+
+ {{formField systemFields.slots value=system.slots name="system.slots"}}
+ {{formField
+ systemFields.traits
+ value=system.traits
+ name="system.traits"
+ localize=true
+ }}
+
+
+ {{formField
+ systemFields.rarity
+ value=system.rarity
+ name="system.rarity"
+ localize=true
+ }}
+ {{formField
+ systemFields.isMagic
+ value=system.isMagic
+ name="system.isMagic"
+ }}
+ {{formField
+ systemFields.equipped
+ value=system.equipped
+ name="system.equipped"
+ }}
+ {{formField systemFields.cost value=system.cost name="system.cost"}}
+ {{formField
+ systemFields.currency
+ value=system.currency
+ name="system.currency"
+ localize=true
+ }}
+
+
+
+ {{#if system.isMagic}}
+
+ {{/if}}
+
\ No newline at end of file
diff --git a/css/fvtt-oath-hammer.css b/css/fvtt-oath-hammer.css
index 45a20fd..aadef32 100644
--- a/css/fvtt-oath-hammer.css
+++ b/css/fvtt-oath-hammer.css
@@ -754,7 +754,7 @@
}
.oathhammer .item-list--weapon .item-list-header,
.oathhammer .item-list--weapon .item-entry {
- grid-template-columns: 24px 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 3.5rem;
+ grid-template-columns: 24px 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 5.5rem;
}
.oathhammer .item-list--armor .item-list-header,
.oathhammer .item-list--armor .item-entry {
@@ -766,11 +766,11 @@
}
.oathhammer .item-list--spell .item-list-header,
.oathhammer .item-list--spell .item-entry {
- grid-template-columns: 24px 1fr 3rem 6rem 3rem 3.5rem;
+ grid-template-columns: 24px 1fr 3rem 6rem 3rem 5.5rem;
}
.oathhammer .item-list--miracle .item-list-header,
.oathhammer .item-list--miracle .item-entry {
- grid-template-columns: 24px 1fr 4.5rem 3.5rem;
+ grid-template-columns: 24px 1fr 4.5rem 5.5rem;
}
.oathhammer .item-list--equipment .item-list-header,
.oathhammer .item-list--equipment .item-entry {
@@ -958,3 +958,610 @@
height: auto;
accent-color: #084a74;
}
+.oh-roll-card {
+ font-family: "Calibri", "Segoe UI", sans-serif;
+ border: 1px solid #535128;
+ border-radius: 4px;
+ padding: 6px 8px;
+ background: rgba(245, 234, 208, 0.4);
+}
+.oh-roll-card .oh-roll-header {
+ font-family: "BlueDragon", "Palatino Linotype", serif;
+ font-size: 0.86rem;
+ font-weight: bold;
+ color: #2a1a0a;
+ margin-bottom: 4px;
+ border-bottom: 1px solid rgba(83, 81, 40, 0.2);
+ padding-bottom: 3px;
+}
+.oh-roll-card .oh-roll-info {
+ display: flex;
+ justify-content: space-between;
+ font-size: calc(0.86rem * 0.9);
+ color: #535128;
+ margin-bottom: 6px;
+}
+.oh-roll-card .oh-roll-dice {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ margin-bottom: 6px;
+}
+.oh-roll-card .oh-roll-dice .oh-die {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border-radius: 3px;
+ font-weight: bold;
+ font-size: 13px;
+ border: 1px solid #535128;
+}
+.oh-roll-card .oh-roll-dice .die-success {
+ background: #2ecc71;
+ color: #fff;
+ border-color: #27ae60;
+}
+.oh-roll-card .oh-roll-dice .die-fail {
+ background: #ecf0f1;
+ color: #7f8c8d;
+ border-color: #bdc3c7;
+}
+.oh-roll-card .oh-roll-result {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 6px;
+ border-radius: 3px;
+ font-weight: bold;
+ font-size: calc(0.86rem * 0.85);
+}
+.oh-roll-card .roll-success {
+ background: rgba(46, 204, 113, 0.2);
+ color: #1e8449;
+}
+.oh-roll-card .roll-failure {
+ background: rgba(231, 76, 60, 0.15);
+ color: #c0392b;
+}
+.oathhammer .rarity-roll-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ cursor: pointer;
+ font-size: calc(0.86rem * 0.9);
+ color: #084a74;
+ opacity: 0.6;
+ transition: opacity 0.2s;
+}
+.oathhammer .rarity-roll-btn:hover {
+ opacity: 1;
+}
+.oathhammer .rarity-roll-btn:hover {
+ color: #2a1a0a;
+ text-decoration: underline;
+}
+.oathhammer .rarity-roll-btn i {
+ font-size: 0.85em;
+}
+.fvtt-oath-hammer .window-content {
+ background: #f5ead0;
+ padding: 6px 8px;
+}
+.fvtt-oath-hammer .oh-roll-dialog {
+ font-family: "Calibri", "Segoe UI", sans-serif;
+ min-width: 320px;
+ padding: 4px 2px;
+ background: #f5ead0;
+ color: #2a1a0a;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-actor-name {
+ text-align: center;
+ font-family: "BlueDragon", "Palatino Linotype", serif;
+ font-size: calc(0.86rem * 1.2);
+ color: #084a74;
+ margin-bottom: 8px;
+ padding-bottom: 4px;
+ border-bottom: 1px solid rgba(83, 81, 40, 0.2);
+}
+.fvtt-oath-hammer .oh-roll-dialog fieldset {
+ border: 1px solid rgba(83, 81, 40, 0.2);
+ border-radius: 4px;
+ padding: 8px 10px;
+ margin-bottom: 8px;
+}
+.fvtt-oath-hammer .oh-roll-dialog fieldset legend {
+ font-family: "BlueDragon", "Palatino Linotype", serif;
+ color: #535128;
+ font-size: calc(0.86rem * 0.9);
+ padding: 0 6px;
+ letter-spacing: 0.04em;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block {
+ background: rgba(83, 81, 40, 0.06);
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .roll-skill-line {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ margin-bottom: 8px;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .roll-skill-name {
+ font-weight: bold;
+ font-size: calc(0.86rem * 1.1);
+ color: #2a1a0a;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .roll-attr-info {
+ font-size: calc(0.86rem * 0.9);
+ color: #535128;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .roll-dice-preview {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .roll-pool {
+ font-size: 1.6rem;
+ font-weight: bold;
+ color: #084a74;
+ line-height: 1;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .roll-color-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 3px 8px;
+ border-radius: 4px;
+ font-size: calc(0.86rem * 0.9);
+ font-weight: bold;
+ border: 1px solid;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .color-badge-white {
+ background: #f0f0f0;
+ color: #555;
+ border-color: #ccc;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .color-badge-red {
+ background: rgba(231, 76, 60, 0.12);
+ color: #c0392b;
+ border-color: rgba(231, 76, 60, 0.35);
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .color-badge-black {
+ background: rgba(44, 62, 80, 0.1);
+ color: #2c3e50;
+ border-color: rgba(44, 62, 80, 0.35);
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .roll-dual-attr {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 8px;
+ padding-top: 6px;
+ border-top: 1px dashed rgba(83, 81, 40, 0.2);
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .roll-dual-attr label {
+ font-size: calc(0.86rem * 0.9);
+ color: #535128;
+ white-space: nowrap;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-info-block .roll-dual-attr select {
+ flex: 1;
+ font-size: calc(0.86rem * 0.9);
+ font-family: "Calibri", "Segoe UI", sans-serif;
+ padding: 2px 4px;
+ border: 1px solid rgba(83, 81, 40, 0.2);
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.85);
+ color: #2a1a0a;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px 8px;
+ padding: 5px 0;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row:not(:last-child) {
+ border-bottom: 1px solid rgba(83, 81, 40, 0.1);
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row label {
+ flex: 1 1 120px;
+ font-size: calc(0.86rem * 0.9);
+ color: #2a1a0a;
+ white-space: nowrap;
+ font-family: "Calibri", "Segoe UI", sans-serif;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row select {
+ flex: 0 0 90px;
+ padding: 3px 6px;
+ border: 1px solid rgba(49, 47, 23, 0.2);
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.85);
+ color: #2a1a0a;
+ font-family: "Calibri", "Segoe UI", sans-serif;
+ font-size: calc(0.86rem * 0.9);
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row .roll-option-hint {
+ flex: 1 1 100%;
+ font-size: calc(calc(0.86rem * 0.9) * 0.85);
+ color: #535128;
+ font-style: italic;
+ padding-left: 4px;
+ white-space: normal;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-luck label {
+ color: #c8a84b;
+ font-weight: bold;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-luck select {
+ border-color: #c8a84b;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-luck .luck-icon {
+ color: #c8a84b;
+ font-size: 0.8em;
+}
+.fvtt-oath-hammer .oh-roll-dialog .roll-visibility-block select {
+ width: 100%;
+ padding: 4px 6px;
+ border: 1px solid rgba(83, 81, 40, 0.2);
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.85);
+ color: #2a1a0a;
+ font-family: "Calibri", "Segoe UI", sans-serif;
+ font-size: calc(0.86rem * 0.9);
+}
+.fvtt-oath-hammer .skills-list a.skill-name-col {
+ cursor: pointer;
+ color: #2a1a0a;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ opacity: 0.6;
+ transition: opacity 0.2s;
+}
+.fvtt-oath-hammer .skills-list a.skill-name-col:hover {
+ opacity: 1;
+}
+.fvtt-oath-hammer .skills-list a.skill-name-col .skill-roll-icon {
+ font-size: 0.75em;
+ color: #535128;
+ flex-shrink: 0;
+}
+.fvtt-oath-hammer .skills-list a.skill-name-col:hover {
+ color: #084a74;
+ text-decoration: underline;
+}
+.fvtt-oath-hammer .skills-list a.skill-name-col:hover .skill-roll-icon {
+ color: #084a74;
+}
+.oh-weapon-dialog .weapon-header {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 6px 0 8px;
+ border-bottom: 1px solid rgba(83, 81, 40, 0.2);
+ margin-bottom: 8px;
+}
+.oh-weapon-dialog .weapon-header .weapon-img-sm {
+ width: 40px;
+ height: 40px;
+ -o-object-fit: contain;
+ object-fit: contain;
+ border: 1px solid rgba(83, 81, 40, 0.2);
+ border-radius: 4px;
+ flex-shrink: 0;
+}
+.oh-weapon-dialog .weapon-header .weapon-header-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.oh-weapon-dialog .weapon-header .weapon-name-lg {
+ font-family: "BlueDragon", "Palatino Linotype", serif;
+ font-size: calc(0.86rem * 1.1);
+ color: #084a74;
+ font-weight: bold;
+}
+.oh-weapon-dialog .weapon-header .weapon-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ align-items: center;
+}
+.oh-weapon-dialog .weapon-header .damage-formula-badge {
+ font-family: "Calibri", "Segoe UI", sans-serif;
+ font-size: calc(0.86rem * 0.9);
+ font-weight: bold;
+ color: #2a1a0a;
+ background: rgba(83, 81, 40, 0.1);
+ border: 1px solid rgba(83, 81, 40, 0.2);
+ border-radius: 3px;
+ padding: 2px 6px;
+}
+.oh-weapon-dialog .weapon-header .ap-badge {
+ font-size: calc(0.86rem * 0.9);
+ font-weight: bold;
+ color: #8b0000;
+ background: rgba(139, 0, 0, 0.08);
+ border: 1px solid rgba(139, 0, 0, 0.25);
+ border-radius: 3px;
+ padding: 2px 6px;
+}
+.oh-weapon-dialog .weapon-header .range-badge {
+ font-size: calc(0.86rem * 0.9);
+ color: #535128;
+ background: rgba(83, 81, 40, 0.08);
+ border: 1px solid rgba(83, 81, 40, 0.2);
+ border-radius: 3px;
+ padding: 2px 6px;
+}
+.oh-weapon-dialog .weapon-header .auto-bonus-badge {
+ font-size: calc(0.86rem * 0.9);
+ font-weight: bold;
+ color: #987d2e;
+ background: rgba(200, 168, 75, 0.12);
+ border: 1px solid rgba(200, 168, 75, 0.4);
+ border-radius: 3px;
+ padding: 2px 6px;
+}
+.oh-weapon-dialog .weapon-header .weapon-traits-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ margin-top: 2px;
+}
+.oh-weapon-dialog .weapon-header .trait-tag-sm {
+ font-size: calc(calc(0.86rem * 0.9) * 0.85);
+ color: #535128;
+ background: rgba(83, 81, 40, 0.08);
+ border: 1px solid rgba(83, 81, 40, 0.2);
+ border-radius: 3px;
+ padding: 1px 5px;
+}
+.oh-weapon-dialog .pool-info-line {
+ font-size: calc(0.86rem * 0.9);
+ color: #2a1a0a;
+ padding: 4px 0 6px;
+ font-family: "Calibri", "Segoe UI", sans-serif;
+}
+.oh-weapon-dialog .pool-info-line strong {
+ color: #084a74;
+ font-size: calc(0.86rem * 1.1);
+}
+.oh-weapon-dialog .pool-info-line .auto-bonus {
+ color: #ac8d34;
+ font-weight: bold;
+ font-size: calc(0.86rem * 0.9);
+}
+.oh-weapon-card .oh-roll-header,
+.oh-spell-card .oh-roll-header,
+.oh-miracle-card .oh-roll-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.oh-weapon-card .oh-card-weapon-img,
+.oh-spell-card .oh-card-weapon-img,
+.oh-miracle-card .oh-card-weapon-img {
+ width: 28px;
+ height: 28px;
+ -o-object-fit: contain;
+ object-fit: contain;
+ border-radius: 3px;
+ border: 1px solid rgba(83, 81, 40, 0.2);
+ flex-shrink: 0;
+}
+.oh-weapon-card .oh-weapon-damage-btn-row,
+.oh-spell-card .oh-weapon-damage-btn-row,
+.oh-miracle-card .oh-weapon-damage-btn-row {
+ margin-top: 8px;
+ text-align: center;
+}
+.oh-weapon-card .oh-roll-damage-btn,
+.oh-spell-card .oh-roll-damage-btn,
+.oh-miracle-card .oh-roll-damage-btn {
+ background: #084a74;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 14px;
+ font-family: "Calibri", "Segoe UI", sans-serif;
+ font-size: calc(0.86rem * 0.9);
+ cursor: pointer;
+ opacity: 0.6;
+ transition: opacity 0.2s;
+}
+.oh-weapon-card .oh-roll-damage-btn:hover,
+.oh-spell-card .oh-roll-damage-btn:hover,
+.oh-miracle-card .oh-roll-damage-btn:hover {
+ opacity: 1;
+}
+.oh-weapon-card .oh-roll-damage-btn:hover,
+.oh-spell-card .oh-roll-damage-btn:hover,
+.oh-miracle-card .oh-roll-damage-btn:hover {
+ background: #0b68a4;
+ opacity: 1;
+}
+.oh-weapon-card .oh-ap-note,
+.oh-spell-card .oh-ap-note,
+.oh-miracle-card .oh-ap-note {
+ font-size: calc(0.86rem * 0.9);
+ color: #8b0000;
+ font-weight: bold;
+ margin-left: 8px;
+}
+.item-list--weapon .item-actions {
+ gap: 8px;
+}
+.item-list--weapon .item-actions a[data-action="attackWeapon"] {
+ color: #084a74;
+ font-size: 1.1em;
+ opacity: 0.6;
+ transition: opacity 0.2s;
+}
+.item-list--weapon .item-actions a[data-action="attackWeapon"]:hover {
+ opacity: 1;
+}
+.item-list--weapon .item-actions a[data-action="damageWeapon"] {
+ color: #8b0000;
+ font-size: 1.1em;
+ opacity: 0.6;
+ transition: opacity 0.2s;
+}
+.item-list--weapon .item-actions a[data-action="damageWeapon"]:hover {
+ opacity: 1;
+}
+.item-list--weapon .item-actions a[data-action="edit"] {
+ margin-left: 6px;
+}
+.oh-spell-dialog .dv-badge,
+.oh-miracle-dialog .dv-badge {
+ background: #3a0e6b;
+ color: #e8d9ff;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.78em;
+ font-weight: bold;
+}
+.oh-spell-dialog .tradition-badge,
+.oh-miracle-dialog .tradition-badge {
+ background: #0e3a6b;
+ color: #d9eeff;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.78em;
+}
+.oh-spell-dialog .ritual-badge,
+.oh-miracle-dialog .ritual-badge {
+ background: #4a3000;
+ color: #ffe8a0;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.78em;
+}
+.oh-spell-dialog .missile-badge,
+.oh-miracle-dialog .missile-badge {
+ background: #1a4a1a;
+ color: #b8ffb8;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.78em;
+}
+.oh-spell-dialog .range-badge,
+.oh-miracle-dialog .range-badge,
+.oh-spell-dialog .duration-badge,
+.oh-miracle-dialog .duration-badge {
+ background: rgba(42, 26, 10, 0.12);
+ color: #2a1a0a;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.75em;
+}
+.oh-spell-dialog .save-info,
+.oh-miracle-dialog .save-info {
+ font-size: 0.8em;
+ color: #2a1a0a;
+ font-style: italic;
+ margin-top: 3px;
+}
+.oh-spell-dialog .stress-tracker,
+.oh-miracle-dialog .stress-tracker {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ margin: 8px 0 4px;
+ background: rgba(83, 81, 40, 0.1);
+ border: 1px solid rgba(83, 81, 40, 0.3);
+ border-radius: 4px;
+ font-size: 0.88em;
+}
+.oh-spell-dialog .stress-tracker i,
+.oh-miracle-dialog .stress-tracker i {
+ color: #2a1a0a;
+}
+.oh-spell-dialog .stress-tracker .stress-warning,
+.oh-miracle-dialog .stress-tracker .stress-warning {
+ margin-left: auto;
+ color: #c00;
+ font-weight: bold;
+ font-size: 0.9em;
+}
+.oh-spell-dialog .stress-tracker.stress-danger,
+.oh-miracle-dialog .stress-tracker.stress-danger {
+ background: rgba(204, 0, 0, 0.08);
+ border-color: rgba(204, 0, 0, 0.4);
+}
+.oh-spell-dialog .miracle-warning,
+.oh-miracle-dialog .miracle-warning {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ margin: 8px 0 4px;
+ background: rgba(139, 0, 0, 0.08);
+ border: 1px solid rgba(139, 0, 0, 0.3);
+ border-radius: 4px;
+ font-size: 0.83em;
+ color: #8b0000;
+ font-style: italic;
+}
+.oh-spell-dialog .miracle-warning i,
+.oh-miracle-dialog .miracle-warning i {
+ flex-shrink: 0;
+}
+.oh-spell-dialog select.enhancement-select,
+.oh-miracle-dialog select.enhancement-select {
+ min-width: 220px;
+}
+.oh-spell-card .oh-stress-line,
+.oh-miracle-card .oh-stress-line {
+ margin-top: 6px;
+ padding: 4px 8px;
+ background: rgba(83, 81, 40, 0.1);
+ border: 1px solid rgba(83, 81, 40, 0.3);
+ border-radius: 3px;
+ font-size: 0.82em;
+ color: #2a1a0a;
+}
+.oh-spell-card .oh-stress-line.stress-blocked,
+.oh-miracle-card .oh-stress-line.stress-blocked {
+ background: rgba(204, 0, 0, 0.08);
+ border-color: rgba(204, 0, 0, 0.4);
+ color: #c00;
+}
+.oh-spell-card .oh-miracle-blocked,
+.oh-miracle-card .oh-miracle-blocked {
+ margin-top: 6px;
+ padding: 5px 8px;
+ background: rgba(139, 0, 0, 0.08);
+ border: 1px solid rgba(139, 0, 0, 0.35);
+ border-radius: 3px;
+ font-size: 0.85em;
+ color: #8b0000;
+ font-weight: bold;
+ text-align: center;
+}
+.item-list--spell .item-actions,
+.item-list--miracle .item-actions {
+ gap: 8px;
+}
+.item-list--spell .item-actions a[data-action="castSpell"] .spell-cast-icon,
+.item-list--miracle .item-actions a[data-action="castSpell"] .spell-cast-icon {
+ color: #3a0e6b;
+ font-size: 1.05em;
+}
+.item-list--spell .item-actions a[data-action="castMiracle"] .miracle-cast-icon,
+.item-list--miracle .item-actions a[data-action="castMiracle"] .miracle-cast-icon {
+ color: #5a3000;
+ font-size: 1.05em;
+}
+.item-list--spell .item-actions a[data-action="edit"],
+.item-list--miracle .item-actions a[data-action="edit"] {
+ margin-left: 6px;
+}
diff --git a/lang/en.json b/lang/en.json
index f18ed6f..8476064 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -13,7 +13,6 @@
"Trait": "Oath Hammer Trait Sheet",
"Oath": "Oath Hammer Oath Sheet",
"Condition": "Oath Hammer Condition Sheet",
- "Lineage": "Oath Hammer Lineage Sheet",
"Class": "Oath Hammer Class Sheet",
"Building": "Oath Hammer Building Sheet"
},
@@ -61,28 +60,6 @@
"Survival": "Survival",
"Tracking": "Tracking"
},
- "Lineage": {
- "Dwarf": "Dwarf",
- "Firbolg": "Firbolg",
- "Halfling": "Halfling",
- "HighElf": "High Elf",
- "Human": "Human",
- "WoodElf": "Wood Elf",
- "FIELDS": {
- "description": {
- "label": "Description"
- },
- "traits": {
- "label": "Traits"
- },
- "movement": {
- "label": "Movement (ft)"
- },
- "gritModifier": {
- "label": "Grit Modifier"
- }
- }
- },
"Class": {
"Berserker": "Berserker",
"Champion": "Champion",
@@ -128,7 +105,7 @@
"buildTime": { "label": "Build Time" },
"taxRevenue": { "label": "Tax Revenue / month" },
"constructed":{ "label": "Constructed" },
- "settlement": { "label": "Settlement" },
+
"description":{ "label": "Description" },
"notes": { "label": "Notes" }
}
@@ -264,7 +241,7 @@
"SkillModifier": "Mod",
"TotalDice": "Total",
"ColorDice": "Color",
- "DropLineage": "Drop Lineage Here",
+ "Lineage": "Lineage",
"DropClass": "Drop Class Here",
"Traits": "Traits",
"Features": "Features",
@@ -276,12 +253,18 @@
"Magic": "Magic",
"Damage": "Damage",
"Tradition": "Tradition",
+ "DivineTradition": "Divine Tradition",
"Piety": "Piety",
"Quantity": "Qty",
"Rarity": "Rarity",
"Penalty": "Penalty",
"Equipped": "Eq.",
- "XPCurrent": "Current XP"
+ "XPCurrent": "Current XP",
+ "Ritual": "Ritual",
+ "MagicMissile": "Magic Missile",
+ "SpellSave": "Save",
+ "StressBlocked": "BLOCKED — over stress threshold!",
+ "ArcaneStressShort": "AS"
},
"ColorDice": {
"White": "White (4+)",
@@ -296,8 +279,108 @@
"Building": "New Building"
},
"ToggleSheet": "Toggle Edit/Play Mode",
+ "Action": {
+ "CastSpell": "Cast Spell",
+ "InvokeMiracle": "Invoke Miracle"
+ },
+ "Dialog": {
+ "SkillCheckTitle": "Skill Check: {skill}",
+ "SkillCheck": "Skill Check",
+ "Options": "Options",
+ "Roll": "Roll",
+ "DV": "Difficulty (DV)",
+ "DVHint": "successes needed",
+ "Modifier": "Bonus / Penalty",
+ "ModifierHint": "extra or fewer dice",
+ "Supporters": "Supporters",
+ "SupportersHint": "+1 die each",
+ "LuckSpend": "Luck Points",
+ "LuckHint": "+2 dice each",
+ "Available": "available",
+ "Visibility": "Visibility",
+ "Attribute": "Attribute",
+ "RollSkill": "Click to roll skill check",
+ "AttackTitle": "Attack: {weapon}",
+ "DamageTitle": "Damage: {weapon}",
+ "Attack": "Attack",
+ "Damage": "Damage",
+ "AttackModifier": "Modifier",
+ "AttackModifierHint": "situational bonus or penalty",
+ "DamageModifier": "Damage Modifier",
+ "DamageModifierHint": "extra or fewer damage dice",
+ "RangeCondition": "Range Condition",
+ "RangeNormal": "Normal",
+ "RangeLong": "Long Range",
+ "RangeMoving": "Moving Before Shot",
+ "RangeConcealment": "Concealment",
+ "RangeCover": "Cover",
+ "RollAttack": "Roll Attack",
+ "RollDamage": "Roll Damage",
+ "SV": "Net Successes (SV)",
+ "SVHint": "attack successes − defense successes",
+ "NimbleHint": "nimble — use Agility",
+ "SpellCastTitle": "Cast Spell: {spell}",
+ "CastSpell": "Cast Spell",
+ "CastOptions": "Cast Options",
+ "Enhancement": "Enhancement",
+ "ElementCondition": "Elemental Condition",
+ "ElementNone": "Not met",
+ "ElementMet": "Element met",
+ "Grimoire": "Grimoire",
+ "GrimoireHas": "Has Grimoire",
+ "GrimoireNo": "No Grimoire (−2)",
+ "MiracleCastTitle": "Invoke Miracle: {miracle}",
+ "InvokeMiracle": "Invoke Miracle",
+ "InvokeOptions": "Invoke Options",
+ "MiracleDVNote": "miracle # today",
+ "MiracleCount": "Miracle # Today",
+ "MiracleCountHint": "1st = DV 1, 2nd = DV 2...",
+ "MiracleFailWarning": "Failure blocks ALL miracles for the rest of the day."
+ },
+ "Enhancement": {
+ "None": "None",
+ "Focused": "Focused (red dice, +1 stress)",
+ "Controlled": "Controlled (+1 stress)",
+ "Empowered": "Empowered (+2 stress)",
+ "Extended": "Extended (−1 die, +1 stress)",
+ "Penetrating": "Penetrating (−1 die, +1 stress)",
+ "Lethal": "Lethal (−2 dice, +2 stress)",
+ "Hastened": "Hastened (−3 dice, +3 stress)",
+ "Safe": "Safe Spell (−3 dice, no stress from 1s)"
+ },
+ "Roll": {
+ "Check": "Check",
+ "Success": "Success!",
+ "Failure": "Failure",
+ "AutoSuccess": "Automatically Available",
+ "RarityCheck": "Rarity Check",
+ "NoActor": "No character selected — assign a character to your user first.",
+ "Successes": "successes",
+ "Damage": "damage",
+ "RollDamage": "Roll Damage",
+ "SpellCast": "Spell Cast",
+ "MiracleCast": "Miracle Invocation",
+ "StressGained": "Arcane Stress Gained",
+ "MiracleBlocked": "You are now blocked from casting miracles for the rest of the day!",
+ "DualAttr": {
+ "DefenseMelee": "melee defense",
+ "FightingNimble": "nimble weapon",
+ "MagicSpells": "spells"
+ }
+ },
"Character": {
"FIELDS": {
+ "lineage": {
+ "label": "Lineage",
+ "fields": {
+ "name": {
+ "label": "Lineage"
+ },
+ "traits": {
+ "label": "Lineage Traits"
+ }
+ }
+ },
"attributes": {
"label": "Attributes"
},
@@ -330,12 +413,6 @@
"biodata": {
"label": "Background",
"fields": {
- "lineage": {
- "label": "Lineage"
- },
- "class": {
- "label": "Class"
- },
"age": {
"label": "Age"
},
@@ -439,9 +516,6 @@
"traits": {
"label": "Traits"
},
- "slots": {
- "label": "Item Slots"
- },
"rarity": {
"label": "Rarity"
},
@@ -491,6 +565,9 @@
"traits": {
"label": "Traits"
},
+ "specialProperties": {
+ "label": "Special Properties"
+ },
"rarity": {
"label": "Rarity"
},
@@ -704,6 +781,18 @@
"TwoHanded": "Two-handed",
"Versatile": "Versatile"
},
+ "WeaponProperty": {
+ "Accurate": "Accurate",
+ "AdvMechanism": "Adv. Mechanism",
+ "ArmorBane": "Armor Bane",
+ "Balanced": "Balanced",
+ "HeavyDraw": "Heavy Draw",
+ "MasterCrafted": "Master-Crafted",
+ "Ornate": "Ornate",
+ "Refined": "Refined",
+ "RuneEtched": "Rune-Etched",
+ "Tempered": "Tempered"
+ },
"DivineTradition": {
"Druidic": "Druidic",
"Profane": "Profane",
@@ -799,7 +888,6 @@
"magic-item": "Magic Item",
"trait": "Trait",
"oath": "Oath",
- "lineage": "Lineage",
"class": "Class"
},
"Actor": {
diff --git a/less/fvtt-oath-hammer.less b/less/fvtt-oath-hammer.less
index 1417909..f5285b2 100644
--- a/less/fvtt-oath-hammer.less
+++ b/less/fvtt-oath-hammer.less
@@ -9,3 +9,5 @@
@import "npc-sheet";
@import "item-list";
@import "item-sheets";
+@import "rolls";
+@import "roll-dialog";
diff --git a/less/item-list.less b/less/item-list.less
index 762c706..6ddf986 100644
--- a/less/item-list.less
+++ b/less/item-list.less
@@ -117,7 +117,7 @@
.item-list--weapon {
.item-list-header, .item-entry {
- grid-template-columns: @item-img-size 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 3.5rem;
+ grid-template-columns: @item-img-size 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 5.5rem;
}
}
@@ -135,13 +135,13 @@
.item-list--spell {
.item-list-header, .item-entry {
- grid-template-columns: @item-img-size 1fr 3rem 6rem 3rem 3.5rem;
+ grid-template-columns: @item-img-size 1fr 3rem 6rem 3rem 5.5rem;
}
}
.item-list--miracle {
.item-list-header, .item-entry {
- grid-template-columns: @item-img-size 1fr 4.5rem 3.5rem;
+ grid-template-columns: @item-img-size 1fr 4.5rem 5.5rem;
}
}
diff --git a/less/roll-dialog.less b/less/roll-dialog.less
new file mode 100644
index 0000000..2560635
--- /dev/null
+++ b/less/roll-dialog.less
@@ -0,0 +1,552 @@
+// ============================================================
+// ROLL DIALOG — Oath Hammer pre-roll configuration dialog
+// ============================================================
+
+// Target both the window chrome and the inner content to ensure background coverage
+.fvtt-oath-hammer {
+ .window-content {
+ background: @color-paper;
+ padding: 6px 8px;
+ }
+}
+
+.fvtt-oath-hammer .oh-roll-dialog {
+ font-family: @font-body;
+ min-width: 320px;
+ padding: 4px 2px;
+ background: @color-paper;
+ color: @color-dark;
+
+ .roll-actor-name {
+ text-align: center;
+ font-family: @font-secondary;
+ font-size: @font-size-xl;
+ color: @color-blue;
+ margin-bottom: 8px;
+ padding-bottom: 4px;
+ border-bottom: 1px solid @color-olive-faint;
+ }
+
+ fieldset {
+ border: 1px solid @color-olive-faint;
+ border-radius: 4px;
+ padding: 8px 10px;
+ margin-bottom: 8px;
+
+ legend {
+ font-family: @font-secondary;
+ color: @color-olive;
+ font-size: @font-size-xs;
+ padding: 0 6px;
+ letter-spacing: 0.04em;
+ }
+ }
+
+ // ——— Skill info block ———
+ .roll-info-block {
+ background: fade(@color-olive, 6%);
+
+ .roll-skill-line {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ margin-bottom: 8px;
+ }
+
+ .roll-skill-name {
+ font-weight: bold;
+ font-size: @font-size-lg;
+ color: @color-dark;
+ }
+
+ .roll-attr-info {
+ font-size: @font-size-xs;
+ color: @color-olive;
+ }
+
+ .roll-dice-preview {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .roll-pool {
+ font-size: 1.6rem;
+ font-weight: bold;
+ color: @color-blue;
+ line-height: 1;
+ }
+
+ .roll-color-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 3px 8px;
+ border-radius: 4px;
+ font-size: @font-size-xs;
+ font-weight: bold;
+ border: 1px solid;
+ }
+
+ .color-badge-white {
+ background: #f0f0f0;
+ color: #555;
+ border-color: #ccc;
+ }
+
+ .color-badge-red {
+ background: fade(#e74c3c, 12%);
+ color: #c0392b;
+ border-color: fade(#e74c3c, 35%);
+ }
+
+ .color-badge-black {
+ background: fade(#2c3e50, 10%);
+ color: #2c3e50;
+ border-color: fade(#2c3e50, 35%);
+ }
+
+ .roll-dual-attr {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 8px;
+ padding-top: 6px;
+ border-top: 1px dashed @color-olive-faint;
+
+ label {
+ font-size: @font-size-xs;
+ color: @color-olive;
+ white-space: nowrap;
+ }
+
+ select {
+ flex: 1;
+ font-size: @font-size-xs;
+ font-family: @font-body;
+ padding: 2px 4px;
+ border: 1px solid @color-olive-faint;
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.85);
+ color: @color-dark;
+ }
+ }
+ }
+
+ // ——— Options block ———
+ .roll-options-block {
+ .roll-option-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px 8px;
+ padding: 5px 0;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid fade(@color-olive, 10%);
+ }
+
+ label {
+ flex: 1 1 120px;
+ font-size: @font-size-xs;
+ color: @color-dark;
+ white-space: nowrap;
+ font-family: @font-body;
+ }
+
+ select {
+ flex: 0 0 90px;
+ padding: 3px 6px;
+ border: 1px solid darken(@color-olive-faint, 10%);
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.85);
+ color: @color-dark;
+ font-family: @font-body;
+ font-size: @font-size-xs;
+ }
+
+ .roll-option-hint {
+ flex: 1 1 100%;
+ font-size: calc(@font-size-xs * 0.85);
+ color: @color-olive;
+ font-style: italic;
+ padding-left: 4px;
+ white-space: normal;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .roll-option-luck {
+ label { color: @color-gold; font-weight: bold; }
+ select { border-color: @color-gold; }
+ .luck-icon { color: @color-gold; font-size: 0.8em; }
+ }
+ }
+
+ // ——— Visibility block ———
+ .roll-visibility-block {
+ select {
+ width: 100%;
+ padding: 4px 6px;
+ border: 1px solid @color-olive-faint;
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.85);
+ color: @color-dark;
+ font-family: @font-body;
+ font-size: @font-size-xs;
+ }
+ }
+}
+
+// Rollable skill name in the skills tab
+.fvtt-oath-hammer .skills-list {
+ a.skill-name-col {
+ cursor: pointer;
+ color: @color-dark;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ .transition-opacity();
+
+ .skill-roll-icon {
+ font-size: 0.75em;
+ color: @color-olive;
+ flex-shrink: 0;
+ }
+
+ &:hover {
+ color: @color-blue;
+ text-decoration: underline;
+ .skill-roll-icon { color: @color-blue; }
+ }
+ }
+}
+
+// ============================================================
+// WEAPON DIALOG
+// ============================================================
+
+.oh-weapon-dialog {
+
+ // ——— Weapon header bar ———
+ .weapon-header {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 6px 0 8px;
+ border-bottom: 1px solid @color-olive-faint;
+ margin-bottom: 8px;
+
+ .weapon-img-sm {
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
+ border: 1px solid @color-olive-faint;
+ border-radius: 4px;
+ flex-shrink: 0;
+ }
+
+ .weapon-header-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .weapon-name-lg {
+ font-family: @font-secondary;
+ font-size: @font-size-lg;
+ color: @color-blue;
+ font-weight: bold;
+ }
+
+ .weapon-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ align-items: center;
+ }
+
+ .damage-formula-badge {
+ font-family: @font-body;
+ font-size: @font-size-xs;
+ font-weight: bold;
+ color: @color-dark;
+ background: fade(@color-olive, 10%);
+ border: 1px solid @color-olive-faint;
+ border-radius: 3px;
+ padding: 2px 6px;
+ }
+
+ .ap-badge {
+ font-size: @font-size-xs;
+ font-weight: bold;
+ color: #8b0000;
+ background: fade(#8b0000, 8%);
+ border: 1px solid fade(#8b0000, 25%);
+ border-radius: 3px;
+ padding: 2px 6px;
+ }
+
+ .range-badge {
+ font-size: @font-size-xs;
+ color: @color-olive;
+ background: fade(@color-olive, 8%);
+ border: 1px solid @color-olive-faint;
+ border-radius: 3px;
+ padding: 2px 6px;
+ }
+
+ .auto-bonus-badge {
+ font-size: @font-size-xs;
+ font-weight: bold;
+ color: darken(@color-gold, 15%);
+ background: fade(@color-gold, 12%);
+ border: 1px solid fade(@color-gold, 40%);
+ border-radius: 3px;
+ padding: 2px 6px;
+ }
+
+ .weapon-traits-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ margin-top: 2px;
+ }
+
+ .trait-tag-sm {
+ font-size: calc(@font-size-xs * 0.85);
+ color: @color-olive;
+ background: fade(@color-olive, 8%);
+ border: 1px solid @color-olive-faint;
+ border-radius: 3px;
+ padding: 1px 5px;
+ }
+ }
+
+ // ——— Pool info line ———
+ .pool-info-line {
+ font-size: @font-size-xs;
+ color: @color-dark;
+ padding: 4px 0 6px;
+ font-family: @font-body;
+
+ strong { color: @color-blue; font-size: @font-size-lg; }
+
+ .auto-bonus {
+ color: darken(@color-gold, 10%);
+ font-weight: bold;
+ font-size: @font-size-xs;
+ }
+ }
+}
+
+// ——— Weapon / Spell / Miracle cards in chat ———
+.oh-weapon-card,
+.oh-spell-card,
+.oh-miracle-card {
+ .oh-roll-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .oh-card-weapon-img {
+ width: 28px;
+ height: 28px;
+ object-fit: contain;
+ border-radius: 3px;
+ border: 1px solid @color-olive-faint;
+ flex-shrink: 0;
+ }
+
+ .oh-weapon-damage-btn-row {
+ margin-top: 8px;
+ text-align: center;
+ }
+
+ .oh-roll-damage-btn {
+ background: @color-blue;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 14px;
+ font-family: @font-body;
+ font-size: @font-size-xs;
+ cursor: pointer;
+ .transition-opacity();
+
+ &:hover { background: lighten(@color-blue, 10%); opacity: 1; }
+ }
+
+ .oh-ap-note {
+ font-size: @font-size-xs;
+ color: #8b0000;
+ font-weight: bold;
+ margin-left: 8px;
+ }
+}
+
+// Attack/damage buttons in weapon list
+.item-list--weapon .item-actions {
+ gap: 8px;
+ a[data-action="attackWeapon"] {
+ color: @color-blue;
+ font-size: 1.1em;
+ .transition-opacity();
+ }
+ a[data-action="damageWeapon"] {
+ color: #8b0000;
+ font-size: 1.1em;
+ .transition-opacity();
+ }
+ a[data-action="edit"] { margin-left: 6px; }
+}
+
+// ============================================================
+// SPELL / MIRACLE DIALOG STYLES
+// ============================================================
+
+.oh-spell-dialog,
+.oh-miracle-dialog {
+
+ // Spell/miracle header (reuses .spell-header, .weapon-img-sm, .weapon-name-lg, .weapon-badges)
+ .dv-badge {
+ background: #3a0e6b;
+ color: #e8d9ff;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.78em;
+ font-weight: bold;
+ }
+
+ .tradition-badge {
+ background: #0e3a6b;
+ color: #d9eeff;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.78em;
+ }
+
+ .ritual-badge {
+ background: #4a3000;
+ color: #ffe8a0;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.78em;
+ }
+
+ .missile-badge {
+ background: #1a4a1a;
+ color: #b8ffb8;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.78em;
+ }
+
+ .range-badge, .duration-badge {
+ background: fade(@color-dark, 12%);
+ color: @color-dark;
+ border-radius: 3px;
+ padding: 1px 6px;
+ font-size: 0.75em;
+ }
+
+ .save-info {
+ font-size: 0.8em;
+ color: @color-dark;
+ font-style: italic;
+ margin-top: 3px;
+ }
+
+ // Arcane stress tracker
+ .stress-tracker {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ margin: 8px 0 4px;
+ background: fade(@color-olive, 10%);
+ border: 1px solid fade(@color-olive, 30%);
+ border-radius: 4px;
+ font-size: 0.88em;
+
+ i { color: @color-dark; }
+
+ .stress-warning {
+ margin-left: auto;
+ color: #c00;
+ font-weight: bold;
+ font-size: 0.9em;
+ }
+
+ &.stress-danger {
+ background: fade(#c00, 8%);
+ border-color: fade(#c00, 40%);
+ }
+ }
+
+ // Miracle failure warning banner
+ .miracle-warning {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ margin: 8px 0 4px;
+ background: fade(#8b0000, 8%);
+ border: 1px solid fade(#8b0000, 30%);
+ border-radius: 4px;
+ font-size: 0.83em;
+ color: #8b0000;
+ font-style: italic;
+
+ i { flex-shrink: 0; }
+ }
+
+ // Wide select for enhancement list
+ select.enhancement-select {
+ min-width: 220px;
+ }
+}
+
+// Chat card additions for spell/miracle
+.oh-spell-card, .oh-miracle-card {
+ .oh-stress-line {
+ margin-top: 6px;
+ padding: 4px 8px;
+ background: fade(@color-olive, 10%);
+ border: 1px solid fade(@color-olive, 30%);
+ border-radius: 3px;
+ font-size: 0.82em;
+ color: @color-dark;
+
+ &.stress-blocked {
+ background: fade(#c00, 8%);
+ border-color: fade(#c00, 40%);
+ color: #c00;
+ }
+ }
+
+ .oh-miracle-blocked {
+ margin-top: 6px;
+ padding: 5px 8px;
+ background: fade(#8b0000, 8%);
+ border: 1px solid fade(#8b0000, 35%);
+ border-radius: 3px;
+ font-size: 0.85em;
+ color: #8b0000;
+ font-weight: bold;
+ text-align: center;
+ }
+}
+
+// Cast icons in magic item lists
+.item-list--spell .item-actions,
+.item-list--miracle .item-actions {
+ gap: 8px;
+ a[data-action="castSpell"] .spell-cast-icon { color: #3a0e6b; font-size: 1.05em; }
+ a[data-action="castMiracle"] .miracle-cast-icon { color: #5a3000; font-size: 1.05em; }
+ a[data-action="edit"] { margin-left: 6px; }
+}
diff --git a/less/rolls.less b/less/rolls.less
new file mode 100644
index 0000000..8e37d29
--- /dev/null
+++ b/less/rolls.less
@@ -0,0 +1,100 @@
+// ============================================================
+// ROLL CARDS — Chat message styling for dice rolls
+// ============================================================
+
+.oh-roll-card {
+ font-family: @font-body;
+ border: 1px solid @color-olive;
+ border-radius: 4px;
+ padding: 6px 8px;
+ background: fade(#f5ead0, 40%);
+
+ .oh-roll-header {
+ font-family: @font-secondary;
+ font-size: @font-size-base;
+ font-weight: bold;
+ color: @color-dark;
+ margin-bottom: 4px;
+ border-bottom: 1px solid @color-olive-faint;
+ padding-bottom: 3px;
+ }
+
+ .oh-roll-info {
+ display: flex;
+ justify-content: space-between;
+ font-size: @font-size-xs;
+ color: @color-olive;
+ margin-bottom: 6px;
+ }
+
+ .oh-roll-dice {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ margin-bottom: 6px;
+
+ .oh-die {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border-radius: 3px;
+ font-weight: bold;
+ font-size: 13px;
+ border: 1px solid @color-olive;
+ }
+
+ .die-success {
+ background: #2ecc71;
+ color: #fff;
+ border-color: #27ae60;
+ }
+
+ .die-fail {
+ background: #ecf0f1;
+ color: #7f8c8d;
+ border-color: #bdc3c7;
+ }
+ }
+
+ .oh-roll-result {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 6px;
+ border-radius: 3px;
+ font-weight: bold;
+ font-size: @font-size-sm;
+ }
+
+ .roll-success {
+ background: fade(#2ecc71, 20%);
+ color: #1e8449;
+ }
+
+ .roll-failure {
+ background: fade(#e74c3c, 15%);
+ color: #c0392b;
+ }
+}
+
+// Rollable rarity button on item sheets
+.oathhammer {
+ .rarity-roll-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ cursor: pointer;
+ font-size: @font-size-xs;
+ color: @color-blue;
+ .transition-opacity();
+
+ &:hover {
+ color: @color-dark;
+ text-decoration: underline;
+ }
+
+ i { font-size: 0.85em; }
+ }
+}
diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs
index 7a9c397..c644d01 100644
--- a/module/applications/_module.mjs
+++ b/module/applications/_module.mjs
@@ -9,6 +9,9 @@ export { default as OathHammerMiracleSheet } from "./sheets/miracle-sheet.mjs"
export { default as OathHammerMagicItemSheet } from "./sheets/magic-item-sheet.mjs"
export { default as OathHammerTraitSheet } from "./sheets/trait-sheet.mjs"
export { default as OathHammerOathSheet } from "./sheets/oath-sheet.mjs"
-export { default as OathHammerLineageSheet } from "./sheets/lineage-sheet.mjs"
export { default as OathHammerClassSheet } from "./sheets/class-sheet.mjs"
export { default as OathHammerBuildingSheet } from "./sheets/building-sheet.mjs"
+export { default as OathHammerRollDialog } from "./roll-dialog.mjs"
+export { default as OathHammerWeaponDialog } from "./weapon-dialog.mjs"
+export { default as OathHammerSpellDialog } from "./spell-dialog.mjs"
+export { default as OathHammerMiracleDialog } from "./miracle-dialog.mjs"
diff --git a/module/applications/miracle-dialog.mjs b/module/applications/miracle-dialog.mjs
new file mode 100644
index 0000000..a7232b2
--- /dev/null
+++ b/module/applications/miracle-dialog.mjs
@@ -0,0 +1,89 @@
+import { SYSTEM } from "../config/system.mjs"
+
+export default class OathHammerMiracleDialog {
+
+ static async prompt(actor, miracle) {
+ const sys = miracle.system
+ const actorSys = actor.system
+
+ const wpRank = actorSys.attributes.willpower.rank
+ const magicRank = actorSys.skills.magic.rank
+ const basePool = wpRank + magicRank
+
+ const isRitual = sys.isRitual
+ const dv = isRitual ? (sys.difficultyValue || 1) : null
+
+ const traditionLabel = (() => {
+ const key = SYSTEM.DIVINE_TRADITIONS?.[sys.divineTradition]
+ return key ? game.i18n.localize(key) : sys.divineTradition
+ })()
+
+ // Miracle count options — DV = miracle number today
+ const miracleCountOptions = Array.from({ length: 10 }, (_, i) => ({
+ value: i + 1,
+ label: `${i + 1}${_ordinal(i + 1)} — DV${i + 1}`,
+ selected: i === 0,
+ }))
+
+ const bonusOptions = Array.from({ length: 13 }, (_, i) => {
+ const v = i - 6
+ return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
+ })
+
+ const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
+
+ const context = {
+ actorName: actor.name,
+ miracleName: miracle.name,
+ miracleImg: miracle.img,
+ dv,
+ traditionLabel,
+ isRitual,
+ range: sys.range,
+ duration: sys.duration,
+ spellSave: sys.spellSave,
+ wpRank,
+ magicRank,
+ basePool,
+ miracleCountOptions,
+ bonusOptions,
+ rollModes,
+ visibility: game.settings.get("core", "rollMode"),
+ }
+
+ const content = await foundry.applications.handlebars.renderTemplate(
+ "systems/fvtt-oath-hammer/templates/miracle-cast-dialog.hbs",
+ context
+ )
+
+ const result = await foundry.applications.api.DialogV2.wait({
+ window: { title: game.i18n.format("OATHHAMMER.Dialog.MiracleCastTitle", { miracle: miracle.name }) },
+ classes: ["fvtt-oath-hammer"],
+ content,
+ rejectClose: false,
+ buttons: [{
+ label: game.i18n.localize("OATHHAMMER.Dialog.InvokeMiracle"),
+ callback: (_ev, btn) => Object.fromEntries(
+ [...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
+ ),
+ }],
+ })
+
+ if (!result) return null
+
+ const computedDV = isRitual ? dv : (parseInt(result.miracleCount) || 1)
+
+ return {
+ dv: computedDV,
+ isRitual,
+ bonus: parseInt(result.bonus) || 0,
+ visibility: result.visibility ?? game.settings.get("core", "rollMode"),
+ }
+ }
+}
+
+function _ordinal(n) {
+ const s = ["th", "st", "nd", "rd"]
+ const v = n % 100
+ return s[(v - 20) % 10] ?? s[v] ?? s[0]
+}
diff --git a/module/applications/roll-dialog.mjs b/module/applications/roll-dialog.mjs
new file mode 100644
index 0000000..0d86640
--- /dev/null
+++ b/module/applications/roll-dialog.mjs
@@ -0,0 +1,154 @@
+import { SYSTEM } from "../config/system.mjs"
+
+/**
+ * Roll configuration dialog for Oath Hammer skill checks.
+ * Uses DialogV2.wait() — pattern from fvtt-hellborn / fvtt-lethal-fantasy.
+ *
+ * Dice rules (from rulebook):
+ * Pool = Attribute rank + Skill rank + per-skill modifier + bonus + (luckSpend × 2) + supporters
+ * White dice: succeed on 4+ | Red: 3+ | Black: 2+
+ * All dice explode on 6 (roll extra die).
+ * Luck Points: spending 1 LP adds +2 dice; LP restored each session.
+ * Supporters: each ally with ranks in the skill adds +1 die.
+ */
+export default class OathHammerRollDialog {
+ /**
+ * Dual-attribute skills: show an alternate attribute option in the dialog.
+ * key → { primary, alt, altLabel i18n key }
+ */
+ static DUAL_ATTRIBUTE_SKILLS = {
+ defense: { alt: "might", altLabelKey: "OATHHAMMER.Roll.DualAttr.DefenseMelee" },
+ fighting: { alt: "agility", altLabelKey: "OATHHAMMER.Roll.DualAttr.FightingNimble" },
+ magic: { alt: "intelligence", altLabelKey: "OATHHAMMER.Roll.DualAttr.MagicSpells" },
+ }
+
+ /**
+ * Show a skill check dialog and return the user's choices.
+ *
+ * @param {Actor} actor Actor performing the check
+ * @param {string} skillKey SYSTEM.SKILLS key (e.g. "fortune", "fighting")
+ * @returns {Promise<{dv, bonus, luckSpend, supporters, attrOverride, visibility}|null>}
+ * Resolved options, or null if the dialog was cancelled.
+ */
+ static async prompt(actor, skillKey) {
+ const sys = actor.system
+ const skillDef = SYSTEM.SKILLS[skillKey]
+ if (!skillDef) throw new Error(`Unknown skill: ${skillKey}`)
+
+ const defaultAttrKey = skillDef.attribute
+ const attrRank = sys.attributes[defaultAttrKey].rank
+ const skill = sys.skills[skillKey]
+ const skillRank = skill.rank
+ const skillMod = skill.modifier ?? 0
+ const baseTotal = attrRank + skillRank + skillMod
+ const colorType = skill.colorDiceType ?? "white"
+ const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
+ const colorLabel = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
+
+ const dualDef = this.DUAL_ATTRIBUTE_SKILLS[skillKey]
+ // Build attribute options for dual-attribute skills
+ let attrOptions = null
+ if (dualDef) {
+ const altRank = sys.attributes[dualDef.alt].rank
+ attrOptions = [
+ {
+ value: defaultAttrKey,
+ label: `${game.i18n.localize(`OATHHAMMER.Attribute.${defaultAttrKey.charAt(0).toUpperCase()}${defaultAttrKey.slice(1)}`)} (${attrRank}) — default`,
+ selected: true,
+ },
+ {
+ value: dualDef.alt,
+ label: `${game.i18n.localize(`OATHHAMMER.Attribute.${dualDef.alt.charAt(0).toUpperCase()}${dualDef.alt.slice(1)}`)} (${altRank}) — ${game.i18n.localize(dualDef.altLabelKey)}`,
+ selected: false,
+ },
+ ]
+ }
+
+ const availableLuck = sys.luck?.value ?? 0
+ const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
+
+ // Build select option arrays
+ const dvOptions = Array.from({ length: 10 }, (_, i) => {
+ const v = i + 1
+ return { value: v, label: String(v), selected: v === 2 }
+ })
+
+ const bonusOptions = Array.from({ length: 13 }, (_, i) => {
+ const v = i - 6
+ return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
+ })
+
+ const supportersOptions = Array.from({ length: 7 }, (_, i) => ({
+ value: i, label: String(i), selected: i === 0,
+ }))
+
+ const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({
+ value: i,
+ label: i === 0 ? `0` : `${i} (+${i * 2}d)`,
+ selected: i === 0,
+ }))
+
+ const context = {
+ actorName: actor.name,
+ skillKey,
+ skillLabel: game.i18n.localize(skillDef.label),
+ attrKey: defaultAttrKey,
+ attrLabel: game.i18n.localize(`OATHHAMMER.Attribute.${defaultAttrKey.charAt(0).toUpperCase()}${defaultAttrKey.slice(1)}`),
+ attrRank,
+ skillRank,
+ skillMod,
+ baseTotal,
+ colorType,
+ colorLabel,
+ threshold,
+ availableLuck,
+ attrOptions,
+ isDualAttr: !!dualDef,
+ rollModes,
+ visibility: game.settings.get("core", "rollMode"),
+ dvOptions,
+ bonusOptions,
+ supportersOptions,
+ luckOptions,
+ }
+
+ const content = await foundry.applications.handlebars.renderTemplate(
+ "systems/fvtt-oath-hammer/templates/roll-dialog.hbs",
+ context
+ )
+
+ const title = game.i18n.format("OATHHAMMER.Dialog.SkillCheckTitle", { skill: context.skillLabel })
+
+ const result = await foundry.applications.api.DialogV2.wait({
+ window: { title },
+ classes: ["fvtt-oath-hammer"],
+ content,
+ rejectClose: false,
+ buttons: [
+ {
+ label: game.i18n.localize("OATHHAMMER.Dialog.Roll"),
+ callback: (_event, button) => {
+ const out = {}
+ for (const el of button.form.elements) {
+ if (el.name) out[el.name] = el.value
+ }
+ return out
+ },
+ },
+ ],
+ })
+
+ if (!result) return null
+
+ const attrOverride = result.attrOverride || defaultAttrKey
+
+ return {
+ dv: Math.max(1, parseInt(result.dv) || 2),
+ bonus: parseInt(result.bonus) || 0,
+ luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck),
+ supporters: Math.max(0, parseInt(result.supporters) || 0),
+ attrOverride,
+ visibility: result.visibility ?? game.settings.get("core", "rollMode"),
+ }
+ }
+}
diff --git a/module/applications/sheets/base-actor-sheet.mjs b/module/applications/sheets/base-actor-sheet.mjs
index d5187e6..2ffe989 100644
--- a/module/applications/sheets/base-actor-sheet.mjs
+++ b/module/applications/sheets/base-actor-sheet.mjs
@@ -95,8 +95,8 @@ export default class OathHammerActorSheet extends HandlebarsApplicationMixin(fou
async _onDropItem(item) {
const itemData = item.toObject()
- // Lineage and class are unique: replace any existing item of the same type
- if (item.type === "lineage" || item.type === "class") {
+ // Class is unique: replace any existing item of the same type
+ if (item.type === "class") {
const existing = this.document.itemTypes[item.type]
if (existing.length > 0) {
await this.document.deleteEmbeddedDocuments("Item", existing.map(i => i.id))
diff --git a/module/applications/sheets/base-item-sheet.mjs b/module/applications/sheets/base-item-sheet.mjs
index 603c4e0..0418567 100644
--- a/module/applications/sheets/base-item-sheet.mjs
+++ b/module/applications/sheets/base-item-sheet.mjs
@@ -1,5 +1,6 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
import { ARMOR_TYPE_CHOICES, WEAPON_PROFICIENCY_GROUPS } from "../../config/system.mjs"
+import { rollRarityCheck } from "../../rolls.mjs"
export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
@@ -28,6 +29,7 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
actions: {
toggleSheet: OathHammerItemSheet.#onToggleSheet,
editImage: OathHammerItemSheet.#onEditImage,
+ rollRarity: OathHammerItemSheet.#onRollRarity,
},
}
@@ -134,4 +136,20 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
})
return fp.browse()
}
+
+ static async #onRollRarity(event, target) {
+ const rarity = this.document.system.rarity
+ if (!rarity) return
+ // Find the owning actor (embedded item) or prompt user to select a character
+ let actor = this.document.parent
+ if (!actor || actor.documentName !== "Actor") {
+ // Item not embedded — use the user's selected character
+ actor = game.user.character
+ if (!actor) {
+ ui.notifications.warn(game.i18n.localize("OATHHAMMER.Roll.NoActor"))
+ return
+ }
+ }
+ await rollRarityCheck(actor, rarity, this.document.name)
+ }
}
diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs
index ca258ec..38c07f5 100644
--- a/module/applications/sheets/character-sheet.mjs
+++ b/module/applications/sheets/character-sheet.mjs
@@ -1,5 +1,10 @@
import OathHammerActorSheet from "./base-actor-sheet.mjs"
import { SYSTEM } from "../../config/system.mjs"
+import OathHammerRollDialog from "../roll-dialog.mjs"
+import OathHammerWeaponDialog from "../weapon-dialog.mjs"
+import OathHammerSpellDialog from "../spell-dialog.mjs"
+import OathHammerMiracleDialog from "../miracle-dialog.mjs"
+import { rollSkillCheck, rollWeaponAttack, rollWeaponDamage, rollSpellCast, rollMiracleCast } from "../../rolls.mjs"
export default class OathHammerCharacterSheet extends OathHammerActorSheet {
/** @override */
@@ -13,10 +18,15 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
contentClasses: ["character-content"],
},
actions: {
- createWeapon: OathHammerCharacterSheet.#onCreateWeapon,
- createSpell: OathHammerCharacterSheet.#onCreateSpell,
- createMiracle: OathHammerCharacterSheet.#onCreateMiracle,
+ createWeapon: OathHammerCharacterSheet.#onCreateWeapon,
+ createSpell: OathHammerCharacterSheet.#onCreateSpell,
+ createMiracle: OathHammerCharacterSheet.#onCreateMiracle,
createEquipment: OathHammerCharacterSheet.#onCreateEquipment,
+ rollSkill: OathHammerCharacterSheet.#onRollSkill,
+ attackWeapon: OathHammerCharacterSheet.#onAttackWeapon,
+ damageWeapon: OathHammerCharacterSheet.#onDamageWeapon,
+ castSpell: OathHammerCharacterSheet.#onCastSpell,
+ castMiracle: OathHammerCharacterSheet.#onCastMiracle,
},
}
@@ -57,9 +67,8 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
- // lineage/class/experience available to all parts (header + identity tab)
+ // class/experience available to all parts (header + identity tab)
const doc = this.document
- context.lineage = doc.itemTypes.lineage?.[0] ?? null
context.characterClass = doc.itemTypes["class"]?.[0] ?? null
return context
}
@@ -236,4 +245,52 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
static #onCreateEquipment(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Equipment"), type: "equipment" }])
}
+
+ static async #onRollSkill(event, target) {
+ const skillKey = target.dataset.skill
+ if (!skillKey) return
+ const result = await OathHammerRollDialog.prompt(this.document, skillKey)
+ if (!result) return
+ await rollSkillCheck(this.document, skillKey, result.dv, result)
+ }
+
+ static async #onAttackWeapon(event, target) {
+ const weaponId = target.dataset.itemId
+ if (!weaponId) return
+ const weapon = this.document.items.get(weaponId)
+ if (!weapon) return
+ const opts = await OathHammerWeaponDialog.promptAttack(this.document, weapon)
+ if (!opts) return
+ await rollWeaponAttack(this.document, weapon, opts)
+ }
+
+ static async #onDamageWeapon(event, target) {
+ const weaponId = target.dataset.itemId
+ if (!weaponId) return
+ const weapon = this.document.items.get(weaponId)
+ if (!weapon) return
+ const opts = await OathHammerWeaponDialog.promptDamage(this.document, weapon, 0)
+ if (!opts) return
+ await rollWeaponDamage(this.document, weapon, opts)
+ }
+
+ static async #onCastSpell(event, target) {
+ const spellId = target.dataset.itemId
+ if (!spellId) return
+ const spell = this.document.items.get(spellId)
+ if (!spell) return
+ const opts = await OathHammerSpellDialog.prompt(this.document, spell)
+ if (!opts) return
+ await rollSpellCast(this.document, spell, opts)
+ }
+
+ static async #onCastMiracle(event, target) {
+ const miracleId = target.dataset.itemId
+ if (!miracleId) return
+ const miracle = this.document.items.get(miracleId)
+ if (!miracle) return
+ const opts = await OathHammerMiracleDialog.prompt(this.document, miracle)
+ if (!opts) return
+ await rollMiracleCast(this.document, miracle, opts)
+ }
}
diff --git a/module/applications/sheets/lineage-sheet.mjs b/module/applications/sheets/lineage-sheet.mjs
deleted file mode 100644
index a17043c..0000000
--- a/module/applications/sheets/lineage-sheet.mjs
+++ /dev/null
@@ -1,30 +0,0 @@
-import OathHammerItemSheet from "./base-item-sheet.mjs"
-
-export default class OathHammerLineageSheet extends OathHammerItemSheet {
- /** @override */
- static DEFAULT_OPTIONS = {
- classes: ["lineage"],
- position: {
- width: 640,
- },
- window: {
- contentClasses: ["lineage-content"],
- },
- }
-
- /** @override */
- static PARTS = {
- main: {
- template: "systems/fvtt-oath-hammer/templates/item/lineage-sheet.hbs",
- },
- }
-
- /** @override */
- async _prepareContext() {
- const context = await super._prepareContext()
- context.enrichedTraits = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
- this.document.system.traits ?? "", { async: true }
- )
- return context
- }
-}
diff --git a/module/applications/spell-dialog.mjs b/module/applications/spell-dialog.mjs
new file mode 100644
index 0000000..de6ca3f
--- /dev/null
+++ b/module/applications/spell-dialog.mjs
@@ -0,0 +1,118 @@
+import { SYSTEM } from "../config/system.mjs"
+
+/**
+ * Spell enhancements — applied before casting (p.96-97).
+ * stress: Arcane Stress cost regardless of roll result
+ * penalty: dice pool penalty
+ * redDice: true = roll red dice (3+ threshold) on the check
+ * noStress: true = 1s rolled do NOT add Arcane Stress (Safe Spell)
+ */
+export const SPELL_ENHANCEMENTS = {
+ none: { label: "OATHHAMMER.Enhancement.None", stress: 0, penalty: 0, redDice: false, noStress: false },
+ focused: { label: "OATHHAMMER.Enhancement.Focused", stress: 1, penalty: 0, redDice: true, noStress: false },
+ controlled: { label: "OATHHAMMER.Enhancement.Controlled", stress: 1, penalty: 0, redDice: false, noStress: false },
+ empowered: { label: "OATHHAMMER.Enhancement.Empowered", stress: 2, penalty: 0, redDice: false, noStress: false },
+ extended: { label: "OATHHAMMER.Enhancement.Extended", stress: 1, penalty: -1, redDice: false, noStress: false },
+ penetrating: { label: "OATHHAMMER.Enhancement.Penetrating", stress: 1, penalty: -1, redDice: false, noStress: false },
+ lethal: { label: "OATHHAMMER.Enhancement.Lethal", stress: 2, penalty: -2, redDice: false, noStress: false },
+ hastened: { label: "OATHHAMMER.Enhancement.Hastened", stress: 3, penalty: -3, redDice: false, noStress: false },
+ safe: { label: "OATHHAMMER.Enhancement.Safe", stress: 0, penalty: -3, redDice: false, noStress: true },
+}
+
+export default class OathHammerSpellDialog {
+
+ static async prompt(actor, spell) {
+ const sys = spell.system
+ const actorSys = actor.system
+
+ const intRank = actorSys.attributes.intelligence.rank
+ const magicRank = actorSys.skills.magic.rank
+ const basePool = intRank + magicRank
+
+ const currentStress = actorSys.arcaneStress.value
+ const stressThreshold = actorSys.arcaneStress.threshold
+ const isOverThreshold = currentStress >= stressThreshold
+
+ const isElemental = sys.tradition === "elemental"
+ const dv = sys.difficultyValue
+
+ const traditionLabel = (() => {
+ const entry = SYSTEM.SORCEROUS_TRADITIONS?.[sys.tradition]
+ return entry ? game.i18n.localize(entry.label) : (sys.tradition ?? "")
+ })()
+
+ const enhancementOptions = Object.entries(SPELL_ENHANCEMENTS).map(([key, def]) => ({
+ value: key,
+ label: game.i18n.localize(def.label),
+ selected: key === "none",
+ }))
+
+ const bonusOptions = Array.from({ length: 13 }, (_, i) => {
+ const v = i - 6
+ return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
+ })
+
+ const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
+
+ const context = {
+ actorName: actor.name,
+ spellName: spell.name,
+ spellImg: spell.img,
+ dv,
+ traditionLabel,
+ isRitual: sys.isRitual,
+ isMagicMissile: sys.isMagicMissile,
+ range: sys.range,
+ duration: sys.duration,
+ spellSave: sys.spellSave,
+ isElemental,
+ element: sys.element,
+ intRank,
+ magicRank,
+ basePool,
+ currentStress,
+ stressThreshold,
+ isOverThreshold,
+ enhancementOptions,
+ bonusOptions,
+ rollModes,
+ visibility: game.settings.get("core", "rollMode"),
+ }
+
+ const content = await foundry.applications.handlebars.renderTemplate(
+ "systems/fvtt-oath-hammer/templates/spell-cast-dialog.hbs",
+ context
+ )
+
+ const result = await foundry.applications.api.DialogV2.wait({
+ window: { title: game.i18n.format("OATHHAMMER.Dialog.SpellCastTitle", { spell: spell.name }) },
+ classes: ["fvtt-oath-hammer"],
+ content,
+ rejectClose: false,
+ buttons: [{
+ label: game.i18n.localize("OATHHAMMER.Dialog.CastSpell"),
+ callback: (_ev, btn) => Object.fromEntries(
+ [...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
+ ),
+ }],
+ })
+
+ if (!result) return null
+
+ const enhKey = result.enhancement ?? "none"
+ const enh = SPELL_ENHANCEMENTS[enhKey] ?? SPELL_ENHANCEMENTS.none
+
+ return {
+ dv,
+ enhancement: enhKey,
+ stressCost: enh.stress,
+ poolPenalty: enh.penalty,
+ redDice: enh.redDice,
+ noStress: enh.noStress,
+ elementalBonus: parseInt(result.elementalBonus) || 0,
+ bonus: parseInt(result.bonus) || 0,
+ grimPenalty: parseInt(result.noGrimoire) || 0,
+ visibility: result.visibility ?? game.settings.get("core", "rollMode"),
+ }
+ }
+}
diff --git a/module/applications/weapon-dialog.mjs b/module/applications/weapon-dialog.mjs
new file mode 100644
index 0000000..cbac4e3
--- /dev/null
+++ b/module/applications/weapon-dialog.mjs
@@ -0,0 +1,215 @@
+import { SYSTEM } from "../config/system.mjs"
+
+/**
+ * Roll dialogs for weapon attacks and damage.
+ *
+ * Attack flow:
+ * 1. promptAttack(actor, weapon) → options
+ * 2. rollWeaponAttack posts a chat card with a "Roll Damage" button
+ * 3. Clicking the button calls promptDamage with attackSuccesses pre-filled
+ * 4. rollWeaponDamage posts the damage chat card
+ */
+export default class OathHammerWeaponDialog {
+
+ // ------------------------------------------------------------------ //
+ // ATTACK DIALOG
+ // ------------------------------------------------------------------ //
+
+ static async promptAttack(actor, weapon) {
+ const sys = weapon.system
+ const actorSys = actor.system
+
+ const isRanged = !sys.usesMight && (sys.shortRange > 0 || sys.longRange > 0)
+ const skillKey = isRanged ? "shooting" : "fighting"
+ const skillDef = SYSTEM.SKILLS[skillKey]
+ const defaultAttr = skillDef.attribute
+ const attrRank = actorSys.attributes[defaultAttr].rank
+ const skillRank = actorSys.skills[skillKey].rank
+ const skillColor = actorSys.skills[skillKey].colorDiceType ?? "white"
+ const threshold = skillColor === "black" ? 2 : skillColor === "red" ? 3 : 4
+
+ const hasNimble = sys.traits.has("nimble")
+
+ // Auto-bonuses from special properties
+ let autoAttackBonus = 0
+ if (sys.specialProperties.has("master-crafted")) autoAttackBonus += 1
+ if (sys.specialProperties.has("accurate")) autoAttackBonus += 1 // bows
+ if (sys.specialProperties.has("balanced")) autoAttackBonus += 1 // grants Fast
+
+ // Damage info for reference
+ const hasBrutal = sys.traits.has("brutal")
+ const hasDeadly = sys.traits.has("deadly")
+ const damageColorType = hasDeadly ? "black" : hasBrutal ? "red" : "white"
+ const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4
+ const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
+ const mightRank = actorSys.attributes.might.rank
+ const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1)
+
+ const traitLabels = [...sys.traits].map(t => {
+ const key = SYSTEM.WEAPON_TRAITS[t]
+ return key ? game.i18n.localize(key) : t
+ })
+
+ // Option arrays
+ const attackBonusOptions = Array.from({ length: 13 }, (_, i) => {
+ const v = i - 6
+ return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
+ })
+
+ const rangeOptions = [
+ { value: 0, label: game.i18n.localize("OATHHAMMER.Dialog.RangeNormal") },
+ { value: -1, label: game.i18n.localize("OATHHAMMER.Dialog.RangeLong") + " (−1)" },
+ { value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeMoving") + " (−2)" },
+ { value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeConcealment") + " (−2)" },
+ { value: -3, label: game.i18n.localize("OATHHAMMER.Dialog.RangeCover") + " (−3)" },
+ ]
+
+ const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
+
+ const context = {
+ actorName: actor.name,
+ weaponName: weapon.name,
+ weaponImg: weapon.img,
+ skillKey,
+ skillLabel: game.i18n.localize(skillDef.label),
+ attrKey: defaultAttr,
+ attrLabel: game.i18n.localize(`OATHHAMMER.Attribute.${_cap(defaultAttr)}`),
+ attrRank,
+ skillRank,
+ colorType: skillColor,
+ threshold,
+ baseAttackPool: attrRank + skillRank,
+ autoAttackBonus,
+ hasNimble,
+ mightLabel: game.i18n.localize("OATHHAMMER.Attribute.Might"),
+ mightRank,
+ agilityLabel: game.i18n.localize("OATHHAMMER.Attribute.Agility"),
+ agilityRank: actorSys.attributes.agility.rank,
+ isRanged,
+ shortRange: sys.shortRange,
+ longRange: sys.longRange,
+ damageLabel: sys.damageLabel,
+ damageColorType,
+ damageThreshold,
+ damageColorLabel,
+ baseDamageDice,
+ apValue: sys.ap,
+ traits: traitLabels,
+ attackBonusOptions,
+ rangeOptions,
+ rollModes,
+ visibility: game.settings.get("core", "rollMode"),
+ }
+
+ const content = await foundry.applications.handlebars.renderTemplate(
+ "systems/fvtt-oath-hammer/templates/weapon-attack-dialog.hbs",
+ context
+ )
+
+ const result = await foundry.applications.api.DialogV2.wait({
+ window: { title: game.i18n.format("OATHHAMMER.Dialog.AttackTitle", { weapon: weapon.name }) },
+ classes: ["fvtt-oath-hammer"],
+ content,
+ rejectClose: false,
+ buttons: [{
+ label: game.i18n.localize("OATHHAMMER.Dialog.RollAttack"),
+ callback: (_ev, btn) => Object.fromEntries(
+ [...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
+ ),
+ }],
+ })
+
+ if (!result) return null
+ return {
+ attackBonus: parseInt(result.attackBonus) || 0,
+ rangeCondition: parseInt(result.rangeCondition) || 0,
+ attrOverride: result.attrOverride || defaultAttr,
+ visibility: result.visibility ?? game.settings.get("core", "rollMode"),
+ autoAttackBonus,
+ }
+ }
+
+ // ------------------------------------------------------------------ //
+ // DAMAGE DIALOG
+ // ------------------------------------------------------------------ //
+
+ static async promptDamage(actor, weapon, defaultSV = 0) {
+ const sys = weapon.system
+ const actorSys = actor.system
+
+ const hasBrutal = sys.traits.has("brutal")
+ const hasDeadly = sys.traits.has("deadly")
+ const damageColorType = hasDeadly ? "black" : hasBrutal ? "red" : "white"
+ const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4
+ const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
+ const mightRank = actorSys.attributes.might.rank
+ const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1)
+
+ // Auto-bonuses from special properties
+ let autoDamageBonus = 0
+ if (sys.specialProperties.has("master-crafted")) autoDamageBonus += 1
+ if (sys.specialProperties.has("tempered")) autoDamageBonus += 1
+ if (sys.specialProperties.has("heavy-draw")) autoDamageBonus += 1
+
+ const svOptions = Array.from({ length: 11 }, (_, i) => ({
+ value: i,
+ label: i === 0 ? "0" : `+${i}d`,
+ selected: i === defaultSV,
+ }))
+
+ const damageBonusOptions = Array.from({ length: 9 }, (_, i) => {
+ const v = i - 4
+ return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
+ })
+
+ const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
+
+ const context = {
+ actorName: actor.name,
+ weaponName: weapon.name,
+ weaponImg: weapon.img,
+ damageLabel: sys.damageLabel,
+ damageColorType,
+ damageThreshold,
+ damageColorLabel,
+ baseDamageDice,
+ autoDamageBonus,
+ apValue: sys.ap,
+ defaultSV,
+ svOptions,
+ damageBonusOptions,
+ rollModes,
+ visibility: game.settings.get("core", "rollMode"),
+ }
+
+ const content = await foundry.applications.handlebars.renderTemplate(
+ "systems/fvtt-oath-hammer/templates/weapon-damage-dialog.hbs",
+ context
+ )
+
+ const result = await foundry.applications.api.DialogV2.wait({
+ window: { title: game.i18n.format("OATHHAMMER.Dialog.DamageTitle", { weapon: weapon.name }) },
+ classes: ["fvtt-oath-hammer"],
+ content,
+ rejectClose: false,
+ buttons: [{
+ label: game.i18n.localize("OATHHAMMER.Dialog.RollDamage"),
+ callback: (_ev, btn) => Object.fromEntries(
+ [...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
+ ),
+ }],
+ })
+
+ if (!result) return null
+ return {
+ sv: parseInt(result.sv) || 0,
+ damageBonus: parseInt(result.damageBonus) || 0,
+ visibility: result.visibility ?? game.settings.get("core", "rollMode"),
+ autoDamageBonus,
+ }
+ }
+}
+
+function _cap(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+}
diff --git a/module/config/system.mjs b/module/config/system.mjs
index ea6c625..18f95f8 100644
--- a/module/config/system.mjs
+++ b/module/config/system.mjs
@@ -108,6 +108,20 @@ export const WEAPON_TRAITS = {
versatile: "OATHHAMMER.WeaponTrait.Versatile"
}
+// Special Properties that can be added to weapons via crafting (p.98)
+export const WEAPON_SPECIAL_PROPERTIES = {
+ accurate: "OATHHAMMER.WeaponProperty.Accurate",
+ "adv-mechanism":"OATHHAMMER.WeaponProperty.AdvMechanism",
+ "armor-bane": "OATHHAMMER.WeaponProperty.ArmorBane",
+ balanced: "OATHHAMMER.WeaponProperty.Balanced",
+ "heavy-draw": "OATHHAMMER.WeaponProperty.HeavyDraw",
+ "master-crafted":"OATHHAMMER.WeaponProperty.MasterCrafted",
+ ornate: "OATHHAMMER.WeaponProperty.Ornate",
+ refined: "OATHHAMMER.WeaponProperty.Refined",
+ "rune-etched": "OATHHAMMER.WeaponProperty.RuneEtched",
+ tempered: "OATHHAMMER.WeaponProperty.Tempered",
+}
+
export const CURRENCY_CHOICES = {
gp: "OATHHAMMER.Currency.GP",
sp: "OATHHAMMER.Currency.SP",
@@ -172,6 +186,16 @@ export const RARITY_CHOICES = {
legendary: "OATHHAMMER.Rarity.Legendary"
}
+// Rarity key → Difficulty Value for Fortune rolls
+export const RARITY_DV = {
+ always: 0,
+ common: 1,
+ uncommon: 2,
+ rare: 3,
+ "very-rare": 4,
+ legendary: 5
+}
+
// Two types of trait per the rulebook terminology
export const TRAIT_TYPE_CHOICES = {
"special-trait": "OATHHAMMER.TraitType.SpecialTrait",
@@ -350,6 +374,7 @@ export const SYSTEM = {
RUNE_TYPE_CHOICES,
WEAPON_PROFICIENCY_GROUPS,
WEAPON_TRAITS,
+ WEAPON_SPECIAL_PROPERTIES,
ARMOR_TYPE_CHOICES,
ARMOR_TRAITS,
CURRENCY_CHOICES,
@@ -358,6 +383,7 @@ export const SYSTEM = {
MAGIC_ITEM_TYPE_CHOICES,
MAGIC_QUALITY_CHOICES,
RARITY_CHOICES,
+ RARITY_DV,
TRAIT_TYPE_CHOICES,
TRAIT_USAGE_PERIOD,
BUILDING_SKILL_CHOICES,
diff --git a/module/models/_module.mjs b/module/models/_module.mjs
index 8f8f91e..e5f1e1a 100644
--- a/module/models/_module.mjs
+++ b/module/models/_module.mjs
@@ -9,6 +9,5 @@ export { default as OathHammerMiracle } from "./miracle.mjs"
export { default as OathHammerMagicItem } from "./magic-item.mjs"
export { default as OathHammerTrait } from "./trait.mjs"
export { default as OathHammerOath } from "./oath.mjs"
-export { default as OathHammerLineage } from "./lineage.mjs"
export { default as OathHammerClass } from "./class.mjs"
export { default as OathHammerBuilding } from "./building.mjs"
diff --git a/module/models/armor.mjs b/module/models/armor.mjs
index b89c3bc..4799343 100644
--- a/module/models/armor.mjs
+++ b/module/models/armor.mjs
@@ -12,7 +12,7 @@ export default class OathHammerArmor extends foundry.abstract.TypeDataModel {
schema.armorType = new fields.StringField({ required: true, initial: "light", choices: SYSTEM.ARMOR_TYPE_CHOICES })
// Armor Value (AV): number of armor dice rolled when receiving damage
- schema.armorValue = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 12 })
+ schema.armorValue = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 16 })
// Penalty: modifier to Acrobatics checks AND defense rolls (0, -1, -2, -3…)
schema.penalty = new fields.NumberField({ ...requiredInteger, initial: 0, min: -5, max: 0 })
diff --git a/module/models/building.mjs b/module/models/building.mjs
index 7de4104..84e6142 100644
--- a/module/models/building.mjs
+++ b/module/models/building.mjs
@@ -23,11 +23,10 @@ export default class OathHammerBuilding extends foundry.abstract.TypeDataModel {
// Monthly tax revenue formula ("3d6", "2d6", "1d3", "" = none)
schema.taxRevenue = new fields.StringField({ required: true, nullable: false, initial: "" })
- // Is this building currently constructed in the settlement?
+ // Is this building currently constructed?
schema.constructed = new fields.BooleanField({ required: true, initial: false })
- // Which settlement this building belongs to (free text or settlement name)
- schema.settlement = new fields.StringField({ required: true, nullable: false, initial: "" })
+
// Additional GM notes (special conditions, upgrades, damage, etc.)
schema.notes = new fields.HTMLField({ required: false, textSearch: true })
diff --git a/module/models/character.mjs b/module/models/character.mjs
index 3ce6368..323c08f 100644
--- a/module/models/character.mjs
+++ b/module/models/character.mjs
@@ -9,6 +9,12 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
+ // Lineage (simple fields on the actor — not an Item)
+ schema.lineage = new fields.SchemaField({
+ name: new fields.StringField({ required: true, nullable: false, initial: "" }),
+ traits: new fields.HTMLField({ required: true, textSearch: true }),
+ })
+
const attributeField = () => new fields.SchemaField({
rank: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1, max: 4 })
})
diff --git a/module/models/lineage.mjs b/module/models/lineage.mjs
deleted file mode 100644
index ecc5002..0000000
--- a/module/models/lineage.mjs
+++ /dev/null
@@ -1,21 +0,0 @@
-export default class OathHammerLineage extends foundry.abstract.TypeDataModel {
- static defineSchema() {
- const fields = foundry.data.fields
- const schema = {}
-
- schema.description = new fields.HTMLField({ required: true, textSearch: true })
-
- // Racial traits and special abilities (rich text)
- schema.traits = new fields.HTMLField({ required: true, textSearch: true })
-
- // Base movement speed in feet
- schema.movement = new fields.NumberField({ required: true, nullable: false, integer: true, initial: 30, min: 0 })
-
- // Modifier to max Grit Points (e.g. -1 for High Elf, Wood Elf)
- schema.gritModifier = new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 })
-
- return schema
- }
-
- static LOCALIZATION_PREFIXES = ["OATHHAMMER.Lineage"]
-}
diff --git a/module/models/magic-item.mjs b/module/models/magic-item.mjs
index 2b82590..1696fdd 100644
--- a/module/models/magic-item.mjs
+++ b/module/models/magic-item.mjs
@@ -35,8 +35,7 @@ export default class OathHammerMagicItem extends foundry.abstract.TypeDataModel
})
schema.maxUses = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
- // Item slots occupied when carried; 0 = small item (no slots)
- schema.slots = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
+
schema.equipped = new fields.BooleanField({ initial: false })
diff --git a/module/models/weapon.mjs b/module/models/weapon.mjs
index 053dd58..ffed064 100644
--- a/module/models/weapon.mjs
+++ b/module/models/weapon.mjs
@@ -17,10 +17,10 @@ export default class OathHammerWeapon extends foundry.abstract.TypeDataModel {
// usesMight=true → formula displayed as "M+2", "M-1", etc.
// usesMight=false → formula displayed as e.g. "6" (fixed dice for bows)
schema.usesMight = new fields.BooleanField({ required: true, initial: true })
- schema.damageMod = new fields.NumberField({ ...requiredInteger, initial: 0, min: -4, max: 5 })
+ schema.damageMod = new fields.NumberField({ ...requiredInteger, initial: 0, min: -4, max: 16 })
// AP (Armor Penetration): penalty imposed on armor/defense rolls
- schema.ap = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 6 })
+ schema.ap = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 16 })
// Reach (melee, in ft: 5 / 10 / 15) — ignored for ranged/throwing
schema.reach = new fields.NumberField({ ...requiredInteger, initial: 5, min: 5 })
@@ -35,6 +35,12 @@ export default class OathHammerWeapon extends foundry.abstract.TypeDataModel {
{ required: true, initial: [] }
)
+ // Special Properties — crafting enhancements (Accurate, Master-Crafted, etc. p.98)
+ schema.specialProperties = new fields.SetField(
+ new fields.StringField({ choices: SYSTEM.WEAPON_SPECIAL_PROPERTIES }),
+ { required: true, initial: [] }
+ )
+
// Item slots (when stowed; 0 = does not occupy slots)
schema.slots = new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 })
diff --git a/module/rolls.mjs b/module/rolls.mjs
new file mode 100644
index 0000000..37a9916
--- /dev/null
+++ b/module/rolls.mjs
@@ -0,0 +1,555 @@
+import { SYSTEM } from "./config/system.mjs"
+
+/**
+ * Perform an Oath Hammer skill check and post results to chat.
+ *
+ * Dice rules (p.38-40):
+ * - Pool = Attribute rank + Skill rank + per-skill modifier + bonus + (luckSpend × 2) + supporters
+ * - White dice succeed on 4+ | Red: 3+ | Black: 2+
+ * - All dice explode on 6 (roll extra die, continues while rolling 6s)
+ * - Pool can never drop below 1 die (penalties rule)
+ * - Luck Points: 1 LP spent = +2 dice; LP are deducted from actor.system.luck.value
+ * - Supporters: each ally with ranks in the skill adds +1 die
+ *
+ * @param {Actor} actor The actor performing the check
+ * @param {string} skillKey Skill key (e.g. "fortune")
+ * @param {number} dv Difficulty Value (successes required)
+ * @param {object} [options]
+ * @param {number} [options.bonus] Extra dice from dialog modifier (can be negative)
+ * @param {number} [options.luckSpend] Luck Points to spend (each adds +2 dice)
+ * @param {number} [options.supporters] Allies supporting the check (each adds +1 die)
+ * @param {string} [options.attrOverride] Override governing attribute (for dual-attr skills)
+ * @param {string} [options.visibility] Roll mode (public/gmroll/blindroll/selfroll)
+ * @param {string} [options.flavor] Optional flavor text for the chat card
+ * @returns {Promise<{successes: number, dv: number, isSuccess: boolean}>}
+ */
+export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
+ const { bonus = 0, luckSpend = 0, supporters = 0, attrOverride, visibility, flavor } = options
+
+ const sys = actor.system
+ const skillDef = SYSTEM.SKILLS[skillKey]
+ if (!skillDef) throw new Error(`Unknown skill: ${skillKey}`)
+
+ // Attribute — use override if provided (dual-attribute skills: Defense, Fighting, Magic)
+ const attrKey = attrOverride && sys.attributes[attrOverride] ? attrOverride : skillDef.attribute
+ const attrRank = sys.attributes[attrKey].rank
+
+ const skill = sys.skills[skillKey]
+ const skillRank = skill.rank
+ const skillMod = skill.modifier ?? 0
+ const colorType = skill.colorDiceType ?? "white"
+ const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
+
+ // Total dice pool (never below 1)
+ const totalDice = Math.max(attrRank + skillRank + skillMod + bonus + (luckSpend * 2) + supporters, 1)
+
+ // Deduct spent Luck Points from actor
+ if (luckSpend > 0) {
+ const currentLuck = sys.luck?.value ?? 0
+ await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) })
+ }
+
+ // Roll the dice pool
+ const roll = await new Roll(`${totalDice}d6`).evaluate()
+
+ // Count successes — exploding 6s produce additional dice
+ let successes = 0
+ const diceResults = []
+ let extraDice = 0
+
+ for (const r of roll.dice[0].results) {
+ const val = r.result
+ if (val >= threshold) successes++
+ if (val === 6) extraDice++
+ diceResults.push({ val, exploded: false })
+ }
+
+ while (extraDice > 0) {
+ const xRoll = await new Roll(`${extraDice}d6`).evaluate()
+ extraDice = 0
+ for (const r of xRoll.dice[0].results) {
+ const val = r.result
+ if (val >= threshold) successes++
+ if (val === 6) extraDice++
+ diceResults.push({ val, exploded: true })
+ }
+ }
+
+ const isSuccess = successes >= dv
+ const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
+ const skillLabel = game.i18n.localize(skillDef.label)
+ const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrKey.charAt(0).toUpperCase()}${attrKey.slice(1)}`)
+
+ // Build dice display HTML
+ const diceHtml = diceResults.map(({ val, exploded }) => {
+ const success = val >= threshold
+ const cssClass = success ? "die-success" : "die-fail"
+ const explodedClass = exploded ? " die-exploded" : ""
+ return `${val}`
+ }).join(" ")
+
+ // Build modifier summary
+ const modParts = []
+ if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
+ if (luckSpend > 0) modParts.push(`+${luckSpend * 2} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP)`)
+ if (supporters > 0) modParts.push(`+${supporters} ${game.i18n.localize("OATHHAMMER.Dialog.Supporters")}`)
+ const modLine = modParts.length ? `
${modParts.join(" · ")}
` : ""
+
+ const resultClass = isSuccess ? "roll-success" : "roll-failure"
+ const resultLabel = isSuccess
+ ? game.i18n.localize("OATHHAMMER.Roll.Success")
+ : game.i18n.localize("OATHHAMMER.Roll.Failure")
+
+ const cardFlavor = flavor ?? `${skillLabel} ${game.i18n.localize("OATHHAMMER.Roll.Check")} (DV ${dv})`
+
+ const content = `
+
+
+
+ ${attrLabel} ${attrRank} + ${skillLabel} ${skillRank}
+ ${colorEmoji} ${totalDice}d6 (${threshold}+)
+
+ ${modLine}
+
${diceHtml}
+
+ ${successes} / ${dv}
+ ${resultLabel}
+
+
+ `
+
+ const rollMode = visibility ?? game.settings.get("core", "rollMode")
+ const msgData = {
+ speaker: ChatMessage.getSpeaker({ actor }),
+ content,
+ rolls: [roll],
+ sound: CONFIG.sounds.dice,
+ }
+ ChatMessage.applyRollMode(msgData, rollMode)
+ await ChatMessage.create(msgData)
+
+ return { successes, dv, isSuccess }
+}
+
+/**
+ * Roll a Fortune check to find an item of a given rarity.
+ * Used by the rollable rarity button on item sheets.
+ * @param {Actor} actor The actor making the check
+ * @param {string} rarityKey Rarity key (e.g. "rare", "very-rare")
+ * @param {string} [itemName] Optional item name for flavor text
+ */
+export async function rollRarityCheck(actor, rarityKey, itemName) {
+ const dv = SYSTEM.RARITY_DV[rarityKey] ?? 1
+ if (rarityKey === "always") {
+ const rarityLabel = game.i18n.localize(SYSTEM.RARITY_CHOICES[rarityKey])
+ const content = `
+
+
+
+ ${rarityLabel} — ${game.i18n.localize("OATHHAMMER.Roll.AutoSuccess")}
+
+
+ `
+ await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor }), content })
+ return { successes: 0, dv: 0, isSuccess: true }
+ }
+
+ const rarityLabel = game.i18n.localize(SYSTEM.RARITY_CHOICES[rarityKey])
+ const flavor = `${game.i18n.localize("OATHHAMMER.Skill.Fortune")} — ${itemName ?? rarityLabel} (DV ${dv})`
+ return rollSkillCheck(actor, "fortune", dv, { flavor })
+}
+
+// ============================================================
+// SHARED DICE HELPER
+// ============================================================
+
+/**
+ * Roll a pool of dice, counting successes (including exploding 6s).
+ * @param {number} pool Number of dice to roll
+ * @param {number} threshold Minimum value to count as a success
+ * @returns {Promise<{roll: Roll, successes: number, diceResults: Array}>}
+ */
+async function _rollPool(pool, threshold) {
+ const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate()
+ let successes = 0
+ const diceResults = []
+ let extraDice = 0
+
+ for (const r of roll.dice[0].results) {
+ const val = r.result
+ if (val >= threshold) successes++
+ if (val === 6) extraDice++
+ diceResults.push({ val, exploded: false })
+ }
+
+ while (extraDice > 0) {
+ const xRoll = await new Roll(`${extraDice}d6`).evaluate()
+ extraDice = 0
+ for (const r of xRoll.dice[0].results) {
+ const val = r.result
+ if (val >= threshold) successes++
+ if (val === 6) extraDice++
+ diceResults.push({ val, exploded: true })
+ }
+ }
+
+ return { roll, successes, diceResults }
+}
+
+/**
+ * Render dice results as HTML spans.
+ */
+function _diceHtml(diceResults, threshold) {
+ return diceResults.map(({ val, exploded }) => {
+ const cssClass = val >= threshold ? "die-success" : "die-fail"
+ return `${val}`
+ }).join(" ")
+}
+
+// ============================================================
+// WEAPON ATTACK ROLL
+// ============================================================
+
+/**
+ * Roll a weapon attack and post the result to chat.
+ * The chat card includes a "Roll Damage" button that triggers rollWeaponDamage.
+ *
+ * @param {Actor} actor The attacking actor
+ * @param {Item} weapon The weapon item
+ * @param {object} options From OathHammerWeaponDialog.promptAttack()
+ */
+export async function rollWeaponAttack(actor, weapon, options = {}) {
+ const { attackBonus = 0, rangeCondition = 0, attrOverride, visibility, autoAttackBonus = 0 } = options
+
+ const sys = weapon.system
+ const actorSys = actor.system
+
+ const isRanged = !sys.usesMight && (sys.shortRange > 0 || sys.longRange > 0)
+ const skillKey = isRanged ? "shooting" : "fighting"
+ const skillDef = SYSTEM.SKILLS[skillKey]
+ const defaultAttr = skillDef.attribute
+
+ const attrKey = attrOverride && actorSys.attributes[attrOverride] ? attrOverride : defaultAttr
+ const attrRank = actorSys.attributes[attrKey].rank
+ const skillRank = actorSys.skills[skillKey].rank
+ const colorType = actorSys.skills[skillKey].colorDiceType ?? "white"
+ const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
+ const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
+
+ const totalDice = Math.max(attrRank + skillRank + attackBonus + rangeCondition + autoAttackBonus, 1)
+
+ const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
+
+ const skillLabel = game.i18n.localize(skillDef.label)
+ const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrKey.charAt(0).toUpperCase() + attrKey.slice(1)}`)
+ const diceHtml = _diceHtml(diceResults, threshold)
+
+ // Modifier summary
+ const modParts = []
+ if (attackBonus !== 0) modParts.push(`${attackBonus > 0 ? "+" : ""}${attackBonus} ${game.i18n.localize("OATHHAMMER.Dialog.AttackModifier")}`)
+ if (rangeCondition !== 0) modParts.push(`${rangeCondition} ${game.i18n.localize("OATHHAMMER.Dialog.RangeCondition")}`)
+ if (autoAttackBonus > 0) modParts.push(`+${autoAttackBonus} auto`)
+ const modLine = modParts.length ? `${modParts.join(" · ")}
` : ""
+
+ const content = `
+
+
+
+ ${skillLabel} (${attrLabel} ${attrRank}) + ${skillLabel} ${skillRank}
+ ${colorEmoji} ${totalDice}d6 (${threshold}+)
+
+ ${modLine}
+
${diceHtml}
+
+ ${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}
+
+
+
+
+
+ `
+
+ const rollMode = visibility ?? game.settings.get("core", "rollMode")
+ const flagData = { actorUuid: actor.uuid, weaponUuid: weapon.uuid, attackSuccesses: successes }
+ const msgData = {
+ speaker: ChatMessage.getSpeaker({ actor }),
+ content,
+ rolls: [roll],
+ sound: CONFIG.sounds.dice,
+ flags: { "fvtt-oath-hammer": { weaponAttack: flagData } },
+ }
+ ChatMessage.applyRollMode(msgData, rollMode)
+ await ChatMessage.create(msgData)
+
+ return { successes }
+}
+
+// ============================================================
+// WEAPON DAMAGE ROLL
+// ============================================================
+
+/**
+ * Roll weapon damage and post to chat.
+ *
+ * @param {Actor} actor The attacking actor
+ * @param {Item} weapon The weapon item
+ * @param {object} options From OathHammerWeaponDialog.promptDamage()
+ */
+export async function rollWeaponDamage(actor, weapon, options = {}) {
+ const { sv = 0, damageBonus = 0, visibility, autoDamageBonus = 0 } = options
+
+ const sys = weapon.system
+ const actorSys = actor.system
+
+ const hasBrutal = sys.traits.has("brutal")
+ const hasDeadly = sys.traits.has("deadly")
+ const colorType = hasDeadly ? "black" : hasBrutal ? "red" : "white"
+ const threshold = hasDeadly ? 2 : hasBrutal ? 3 : 4
+ const colorEmoji = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
+ const colorLabel = hasDeadly ? "Black" : hasBrutal ? "Red" : "White"
+
+ const mightRank = actorSys.attributes.might.rank
+ const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1)
+ const totalDice = Math.max(baseDamageDice + sv + damageBonus + autoDamageBonus, 1)
+
+ const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
+ const diceHtml = _diceHtml(diceResults, threshold)
+
+ const modParts = []
+ if (sv > 0) modParts.push(`+${sv} SV`)
+ if (damageBonus !== 0) modParts.push(`${damageBonus > 0 ? "+" : ""}${damageBonus} ${game.i18n.localize("OATHHAMMER.Dialog.DamageModifier")}`)
+ if (autoDamageBonus > 0) modParts.push(`+${autoDamageBonus} auto`)
+ const modLine = modParts.length ? `${modParts.join(" · ")}
` : ""
+
+ const apNote = sys.ap > 0 ? `AP ${sys.ap}` : ""
+
+ const content = `
+
+
+
+ ${sys.damageLabel} = ${baseDamageDice}d6 ${sv > 0 ? `+${sv} SV` : ""}
+ ${colorEmoji} ${totalDice}d6 (${threshold}+) ${colorLabel}
+
+ ${modLine}
+
${diceHtml}
+
+ ${successes} ${game.i18n.localize("OATHHAMMER.Roll.Damage")}
+ ${apNote}
+
+
+ `
+
+ const rollMode = visibility ?? game.settings.get("core", "rollMode")
+ const msgData = {
+ speaker: ChatMessage.getSpeaker({ actor }),
+ content,
+ rolls: [roll],
+ sound: CONFIG.sounds.dice,
+ }
+ ChatMessage.applyRollMode(msgData, rollMode)
+ await ChatMessage.create(msgData)
+
+ return { successes }
+}
+
+
+// ============================================================
+// SPELL CAST ROLL
+// ============================================================
+
+/**
+ * Roll a spell casting check (Magic / Intelligence) and post to chat.
+ * Counts dice showing 1 and adds Arcane Stress to the actor.
+ *
+ * @param {Actor} actor The caster
+ * @param {Item} spell The spell item
+ * @param {object} options From OathHammerSpellDialog.prompt()
+ */
+export async function rollSpellCast(actor, spell, options = {}) {
+ const {
+ dv = spell.system.difficultyValue,
+ enhancement = "none",
+ stressCost = 0,
+ poolPenalty = 0,
+ redDice = false,
+ noStress = false,
+ elementalBonus = 0,
+ bonus = 0,
+ grimPenalty = 0,
+ visibility,
+ } = options
+
+ const sys = spell.system
+ const actorSys = actor.system
+
+ const intRank = actorSys.attributes.intelligence.rank
+ const magicRank = actorSys.skills.magic.rank
+ const totalDice = Math.max(intRank + magicRank + bonus + poolPenalty + elementalBonus + grimPenalty, 1)
+ const threshold = redDice ? 3 : 4
+ const colorEmoji = redDice ? "🔴" : "⬜"
+
+ const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
+ const diceHtml = _diceHtml(diceResults, threshold)
+
+ // Count 1s for Arcane Stress (unless Safe Spell enhancement)
+ const onesCount = noStress ? 0 : diceResults.filter(d => d.val === 1 && !d.exploded).length
+ const totalStressGain = stressCost + onesCount
+ const isSuccess = successes >= dv
+
+ // Update arcane stress
+ if (totalStressGain > 0) {
+ const currentStress = actorSys.arcaneStress.value
+ await actor.update({ "system.arcaneStress.value": currentStress + totalStressGain })
+ }
+
+ const newStress = (actorSys.arcaneStress.value ?? 0) + totalStressGain
+ const stressMax = actorSys.arcaneStress.threshold
+ const isBlocked = newStress >= stressMax
+
+ const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Magic")
+ const attrLabel = game.i18n.localize("OATHHAMMER.Attribute.Intelligence")
+ const resultClass = isSuccess ? "roll-success" : "roll-failure"
+ const resultLabel = isSuccess
+ ? game.i18n.localize("OATHHAMMER.Roll.Success")
+ : game.i18n.localize("OATHHAMMER.Roll.Failure")
+
+ const modParts = []
+ if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
+ if (poolPenalty !== 0) modParts.push(`${poolPenalty} ${game.i18n.localize("OATHHAMMER.Enhancement." + _cap(enhancement))}`)
+ if (elementalBonus > 0) modParts.push(`+${elementalBonus} ${game.i18n.localize("OATHHAMMER.Dialog.ElementMet")}`)
+ if (grimPenalty < 0) modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`)
+ const modLine = modParts.length ? `${modParts.join(" · ")}
` : ""
+
+ const stressLine = `
+ 🧠 ${game.i18n.localize("OATHHAMMER.Label.ArcaneStress")}: +${totalStressGain}
+ (${onesCount} × 1s + ${stressCost} enh.) → ${newStress}/${stressMax}
+ ${isBlocked ? ` ⚠ ${game.i18n.localize("OATHHAMMER.Label.StressBlocked")}` : ""}
+
`
+
+ const content = `
+
+
+
+ ${attrLabel} ${intRank} + ${skillLabel} ${magicRank}
+ ${colorEmoji} ${totalDice}d6 (${threshold}+)
+
+ ${modLine}
+
${diceHtml}
+
+ ${successes} / ${dv}
+ ${resultLabel}
+
+ ${stressLine}
+
+ `
+
+ const rollMode = visibility ?? game.settings.get("core", "rollMode")
+ const msgData = {
+ speaker: ChatMessage.getSpeaker({ actor }),
+ content,
+ rolls: [roll],
+ sound: CONFIG.sounds.dice,
+ }
+ ChatMessage.applyRollMode(msgData, rollMode)
+ await ChatMessage.create(msgData)
+
+ return { successes, dv, isSuccess, totalStressGain, newStress }
+}
+
+// ============================================================
+// MIRACLE CAST ROLL
+// ============================================================
+
+/**
+ * Roll a miracle invocation check (Magic / Willpower) and post to chat.
+ * On failure, warns the player they are blocked from miracles for the day.
+ *
+ * @param {Actor} actor The caster
+ * @param {Item} miracle The miracle item
+ * @param {object} options From OathHammerMiracleDialog.prompt()
+ */
+export async function rollMiracleCast(actor, miracle, options = {}) {
+ const {
+ dv = 1,
+ isRitual = false,
+ bonus = 0,
+ visibility,
+ } = options
+
+ const sys = miracle.system
+ const actorSys = actor.system
+
+ const wpRank = actorSys.attributes.willpower.rank
+ const magicRank = actorSys.skills.magic.rank
+ const totalDice = Math.max(wpRank + magicRank + bonus, 1)
+ const threshold = 4
+ const colorEmoji = "⬜"
+
+ const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
+ const diceHtml = _diceHtml(diceResults, threshold)
+ const isSuccess = successes >= dv
+
+ const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Magic")
+ const attrLabel = game.i18n.localize("OATHHAMMER.Attribute.Willpower")
+ const resultClass = isSuccess ? "roll-success" : "roll-failure"
+ const resultLabel = isSuccess
+ ? game.i18n.localize("OATHHAMMER.Roll.Success")
+ : game.i18n.localize("OATHHAMMER.Roll.Failure")
+
+ const modLine = bonus !== 0
+ ? `${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}
`
+ : ""
+
+ const blockedLine = !isSuccess
+ ? `⚠ ${game.i18n.localize("OATHHAMMER.Roll.MiracleBlocked")}
`
+ : ""
+
+ const dvNote = isRitual
+ ? `DV ${dv} (${game.i18n.localize("OATHHAMMER.Label.Ritual")})`
+ : `DV ${dv}`
+
+ const content = `
+
+
+
+ ${attrLabel} ${wpRank} + ${skillLabel} ${magicRank}
+ ${colorEmoji} ${totalDice}d6 (${threshold}+)
+
+ ${modLine}
+
${diceHtml}
+
+ ${successes} / ${dv}
+ ${resultLabel}
+
+ ${blockedLine}
+
+ `
+
+ const rollMode = visibility ?? game.settings.get("core", "rollMode")
+ const msgData = {
+ speaker: ChatMessage.getSpeaker({ actor }),
+ content,
+ rolls: [roll],
+ sound: CONFIG.sounds.dice,
+ }
+ ChatMessage.applyRollMode(msgData, rollMode)
+ await ChatMessage.create(msgData)
+
+ return { successes, dv, isSuccess }
+}
+
+function _cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : "" }
diff --git a/oath-hammer.mjs b/oath-hammer.mjs
index 8e2150a..caf6ba4 100644
--- a/oath-hammer.mjs
+++ b/oath-hammer.mjs
@@ -5,6 +5,8 @@ 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 { rollWeaponDamage } from "./module/rolls.mjs"
Hooks.once("init", function () {
console.info(SYSTEM.ASCII)
@@ -32,7 +34,6 @@ Hooks.once("init", function () {
"magic-item": models.OathHammerMagicItem,
trait: models.OathHammerTrait,
oath: models.OathHammerOath,
- lineage: models.OathHammerLineage,
"class": models.OathHammerClass,
building: models.OathHammerBuilding
}
@@ -59,7 +60,6 @@ Hooks.once("init", function () {
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.OathHammerLineageSheet, { types: ["lineage"], makeDefault: true, label: "OATHHAMMER.Sheet.Lineage" })
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" })
@@ -70,6 +70,39 @@ Hooks.once("init", function () {
console.info("Oath Hammer | System Initialized")
})
-Hooks.once("ready", function () {
+Hooks.once("ready", async function () {
console.info("Oath Hammer | System Ready")
+
+ // Migration: remove orphaned items with removed types (lineage → actor field, ability → trait)
+ const removedTypes = new Set(["lineage", "ability"])
+ for (const actor of game.actors) {
+ const invalidItems = actor._source.items?.filter(i => removedTypes.has(i.type)) ?? []
+ if (invalidItems.length) {
+ console.info(`Oath Hammer | Migrating ${actor.name}: removing ${invalidItems.length} obsolete item(s)`)
+ await actor.deleteEmbeddedDocuments("Item", invalidItems.map(i => i._id))
+ }
+ }
+ 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()
+ }
+ }
+})
+
+// Handle "Roll Damage" button in weapon attack chat cards
+Hooks.on("renderChatMessageHTML", (message, html) => {
+ const btn = html.querySelector("[data-action=\"rollWeaponDamage\"]")
+ if (!btn) return
+ 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)
+ })
})
diff --git a/system.json b/system.json
index 560fff4..12c9dfd 100644
--- a/system.json
+++ b/system.json
@@ -25,7 +25,8 @@
"character": {
"htmlFields": [
"description",
- "notes"
+ "notes",
+ "lineage.traits"
]
},
"npc": {
@@ -85,12 +86,6 @@
"bane"
]
},
- "lineage": {
- "htmlFields": [
- "description",
- "traits"
- ]
- },
"class": {
"htmlFields": [
"description",
diff --git a/templates/actor/character-combat.hbs b/templates/actor/character-combat.hbs
index 41400c0..f146e6d 100644
--- a/templates/actor/character-combat.hbs
+++ b/templates/actor/character-combat.hbs
@@ -44,10 +44,10 @@
- {{#unless ../isPlayMode}}
+
+
- {{/unless}}
{{/each}}
@@ -83,10 +83,8 @@
- {{#unless ../isPlayMode}}
- {{/unless}}
{{/each}}
@@ -112,10 +110,8 @@
{{ammo.name}}
×{{ammo.system.quantity}}
- {{#unless ../isPlayMode}}
- {{/unless}}
{{/each}}
diff --git a/templates/actor/character-equipment.hbs b/templates/actor/character-equipment.hbs
index a8a78f8..145c54f 100644
--- a/templates/actor/character-equipment.hbs
+++ b/templates/actor/character-equipment.hbs
@@ -36,10 +36,8 @@
{{localize equip.system.itemType}}
{{equip.system.quantity}}
- {{#unless ../isPlayMode}}
- {{/unless}}
{{/each}}
@@ -65,10 +63,8 @@
{{mi.name}}
{{localize mi.system.rarity}}
- {{#unless ../isPlayMode}}
- {{/unless}}
{{/each}}
@@ -91,10 +87,8 @@
{{cond.name}}
{{localize cond.system.conditionType}}
- {{#unless ../isPlayMode}}
- {{/unless}}
{{/each}}
diff --git a/templates/actor/character-identity.hbs b/templates/actor/character-identity.hbs
index f4c4345..c17ba9b 100644
--- a/templates/actor/character-identity.hbs
+++ b/templates/actor/character-identity.hbs
@@ -21,10 +21,8 @@
{{trait._typeLabel}}
{{trait._usageLabel}}
- {{#unless ../isPlayMode}}
- {{/unless}}
{{/each}}
@@ -49,10 +47,8 @@
{{oath._typeLabel}}
{{#if oath._violated}}{{else}}{{/if}}
- {{#unless ../isPlayMode}}
- {{/unless}}
{{/each}}
diff --git a/templates/actor/character-magic.hbs b/templates/actor/character-magic.hbs
index 4699cf2..fcb2cb4 100644
--- a/templates/actor/character-magic.hbs
+++ b/templates/actor/character-magic.hbs
@@ -15,7 +15,7 @@
{{spell.name}}
- {{spell.system.level}}
+ {{spell.system.difficultyValue}}
{{localize spell.system.tradition}}
- {{spell.system.arcaneStress}}
+ —
- {{#unless ../isPlayMode}}
+
- {{/unless}}
{{/each}}
@@ -49,19 +48,18 @@
{{#each miracles as |miracle|}}
{{miracle.name}}
- {{miracle.system.piety}}
+ {{miracle.system.divineTradition}}
- {{#unless ../isPlayMode}}
+
- {{/unless}}
{{/each}}
diff --git a/templates/actor/character-sheet.hbs b/templates/actor/character-sheet.hbs
index 95fd07e..3e2258f 100644
--- a/templates/actor/character-sheet.hbs
+++ b/templates/actor/character-sheet.hbs
@@ -21,18 +21,9 @@
{{!-- Row 2: Identity bar (lineage + class + level/xp) --}}
-
- {{#if lineage}}
-

-
{{lineage.name}}
- {{#unless isPlayMode}}
-
-
- {{/unless}}
- {{else}}
+
- {{localize "OATHHAMMER.Label.DropLineage"}}
- {{/if}}
+ {{formInput systemFields.lineage.fields.name value=system.lineage.name name="system.lineage.name" placeholder=(localize "OATHHAMMER.Label.Lineage") disabled=isPlayMode}}
{{#if characterClass}}
diff --git a/templates/actor/character-skills.hbs b/templates/actor/character-skills.hbs
index 548f4fe..4a96904 100644
--- a/templates/actor/character-skills.hbs
+++ b/templates/actor/character-skills.hbs
@@ -16,7 +16,9 @@
{{#each group.skillData as |skill|}}
-
+
+ {{localize skill.label}}
+
-
- {{formField systemFields.cost value=system.cost name="system.cost"}}
+
{{localize "OATHHAMMER.Roll.RarityCheck"}}
{{formField systemFields.currency value=system.currency name="system.currency" localize=true}}
diff --git a/templates/item/armor-sheet.hbs b/templates/item/armor-sheet.hbs
index d8d62c4..536f4ef 100644
--- a/templates/item/armor-sheet.hbs
+++ b/templates/item/armor-sheet.hbs
@@ -48,6 +48,7 @@
name="system.rarity"
localize=true
}}
+
{{localize "OATHHAMMER.Roll.RarityCheck"}}
{{formField
systemFields.isMagic
value=system.isMagic
diff --git a/templates/item/building-sheet.hbs b/templates/item/building-sheet.hbs
index 6d41d49..08d755f 100644
--- a/templates/item/building-sheet.hbs
+++ b/templates/item/building-sheet.hbs
@@ -9,7 +9,7 @@
{{formField systemFields.skillCheck value=system.skillCheck name="system.skillCheck" localize=true}}
{{formField systemFields.cost value=system.cost name="system.cost"}}
{{formField systemFields.buildTime value=system.buildTime name="system.buildTime"}}
- {{formField systemFields.settlement value=system.settlement name="system.settlement"}}
+
{{formField systemFields.taxRevenue value=system.taxRevenue name="system.taxRevenue"}}
diff --git a/templates/item/equipment-sheet.hbs b/templates/item/equipment-sheet.hbs
index 1cef958..57ebc82 100644
--- a/templates/item/equipment-sheet.hbs
+++ b/templates/item/equipment-sheet.hbs
@@ -9,6 +9,7 @@
{{formField systemFields.quantity value=system.quantity name="system.quantity"}}
{{formField systemFields.slots value=system.slots name="system.slots"}}
{{formField systemFields.rarity value=system.rarity name="system.rarity" localize=true}}
+
{{localize "OATHHAMMER.Roll.RarityCheck"}}
{{#if system.lightRadius}}
{{formField systemFields.lightRadius value=system.lightRadius name="system.lightRadius"}}
{{/if}}
diff --git a/templates/item/lineage-sheet.hbs b/templates/item/lineage-sheet.hbs
deleted file mode 100644
index 6c4b451..0000000
--- a/templates/item/lineage-sheet.hbs
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
- {{formField systemFields.movement value=system.movement name="system.movement"}}
-
-
- {{formField systemFields.gritModifier value=system.gritModifier name="system.gritModifier"}}
-
-
-
-
-
diff --git a/templates/item/magic-item-sheet.hbs b/templates/item/magic-item-sheet.hbs
index 55afbbc..e0fec24 100644
--- a/templates/item/magic-item-sheet.hbs
+++ b/templates/item/magic-item-sheet.hbs
@@ -16,7 +16,7 @@
{{#unless (eq system.usagePeriod "none")}}
{{formField systemFields.maxUses value=system.maxUses name="system.maxUses"}}
{{/unless}}
- {{formField systemFields.slots value=system.slots name="system.slots"}}
+
{{formField systemFields.equipped value=system.equipped name="system.equipped"}}
diff --git a/templates/item/weapon-sheet.hbs b/templates/item/weapon-sheet.hbs
index 572090c..485210f 100644
--- a/templates/item/weapon-sheet.hbs
+++ b/templates/item/weapon-sheet.hbs
@@ -29,8 +29,10 @@
{{formField systemFields.traits value=system.traits name="system.traits" localize=true}}
+ {{formField systemFields.specialProperties value=system.specialProperties name="system.specialProperties" localize=true}}
{{formField systemFields.slots value=system.slots name="system.slots"}}
{{formField systemFields.rarity value=system.rarity name="system.rarity" localize=true}}
+
{{localize "OATHHAMMER.Roll.RarityCheck"}}
{{formField systemFields.isMagic value=system.isMagic name="system.isMagic"}}
{{formField systemFields.equipped value=system.equipped name="system.equipped"}}
{{formField systemFields.cost value=system.cost name="system.cost"}}
diff --git a/templates/miracle-cast-dialog.hbs b/templates/miracle-cast-dialog.hbs
new file mode 100644
index 0000000..55e0f7d
--- /dev/null
+++ b/templates/miracle-cast-dialog.hbs
@@ -0,0 +1,66 @@
+
+
+ {{!-- Miracle header --}}
+
+
+ {{!-- Failure warning --}}
+
+
+ {{localize "OATHHAMMER.Dialog.MiracleFailWarning"}}
+
+
+ {{!-- Cast options --}}
+
+
+ {{!-- Visibility --}}
+
+
+
diff --git a/templates/roll-dialog.hbs b/templates/roll-dialog.hbs
new file mode 100644
index 0000000..b626e7a
--- /dev/null
+++ b/templates/roll-dialog.hbs
@@ -0,0 +1,83 @@
+
+
+ {{!-- Actor name --}}
+
{{actorName}}
+
+ {{!-- Skill / pool info --}}
+
+
+ {{!-- Roll options --}}
+
+
+ {{!-- Visibility --}}
+
+
+
diff --git a/templates/spell-cast-dialog.hbs b/templates/spell-cast-dialog.hbs
new file mode 100644
index 0000000..a53b31f
--- /dev/null
+++ b/templates/spell-cast-dialog.hbs
@@ -0,0 +1,80 @@
+
+
+ {{!-- Spell header --}}
+
+
+ {{!-- Arcane stress tracker --}}
+
+
+ {{localize "OATHHAMMER.Label.ArcaneStress"}}: {{currentStress}} / {{stressThreshold}}
+ {{#if isOverThreshold}}⚠ {{localize "OATHHAMMER.Label.StressBlocked"}}{{/if}}
+
+
+ {{!-- Cast options --}}
+
+
+ {{!-- Visibility --}}
+
+
+
diff --git a/templates/weapon-attack-dialog.hbs b/templates/weapon-attack-dialog.hbs
new file mode 100644
index 0000000..37a8265
--- /dev/null
+++ b/templates/weapon-attack-dialog.hbs
@@ -0,0 +1,69 @@
+
+
+ {{!-- Weapon header --}}
+
+
+ {{!-- Attack roll config --}}
+
+
+ {{!-- Visibility --}}
+
+
+
diff --git a/templates/weapon-damage-dialog.hbs b/templates/weapon-damage-dialog.hbs
new file mode 100644
index 0000000..1ae91db
--- /dev/null
+++ b/templates/weapon-damage-dialog.hbs
@@ -0,0 +1,47 @@
+
+
+ {{!-- Weapon header --}}
+
+
+ {{!-- Damage options --}}
+
+
+ {{!-- Visibility --}}
+
+
+