diff --git a/css/fvtt-oath-hammer.css b/css/fvtt-oath-hammer.css index bf70c40..233ff74 100644 --- a/css/fvtt-oath-hammer.css +++ b/css/fvtt-oath-hammer.css @@ -646,19 +646,241 @@ text-align: center; font-weight: bold; } -.oathhammer .npc-main .npc-left { - min-width: 160px; - max-width: 160px; +.oathhammer .stress-controls { display: flex; - flex-direction: column; align-items: center; - gap: 4px; + gap: 6px; + flex-wrap: nowrap; + padding: 4px 0; +} +.oathhammer .stress-controls .stress-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border: 1px solid #535128; + border-radius: 3px; + background: rgba(42, 26, 10, 0.06); + color: #2a1a0a; + font-size: 1rem; + line-height: 1; + cursor: pointer; + flex-shrink: 0; + text-decoration: none; +} +.oathhammer .stress-controls .stress-btn:hover { + background: rgba(8, 74, 116, 0.15); + border-color: #084a74; +} +.oathhammer .stress-controls .arcane-stress-display { + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: calc(0.86rem * 1.1); + font-weight: bold; + color: #2a1a0a; + min-width: 3rem; + text-align: center; + flex-shrink: 0; +} +.oathhammer .stress-controls .arcane-stress-display.stress-at-limit { + color: #c0392b; +} +.oathhammer .stress-controls .stress-bonus-label { + margin-left: auto; + font-size: calc(0.86rem * 0.85); + color: #535128; + white-space: nowrap; + flex-shrink: 0; +} +.oathhammer .stress-controls .stress-bonus-input { + width: 3rem; + text-align: center; + flex-shrink: 0; +} +.oathhammer .npc-main .npc-left { + min-width: 120px; + max-width: 120px; + flex-shrink: 0; +} +.oathhammer .npc-main .npc-left .actor-img { + width: 100%; + height: 170px; + -o-object-fit: cover; + object-fit: cover; + -o-object-position: center top; + object-position: center top; } .oathhammer .npc-main .npc-right { flex: 1; display: flex; flex-direction: column; + gap: 6px; + min-width: 0; +} +.oathhammer .npc-main .character-name { + display: flex; + align-items: center; gap: 4px; + border-bottom: 1px solid #535128; + padding-bottom: 4px; +} +.oathhammer .npc-main .character-name input { + flex: 1; + font-family: "Sherwood", "Palatino Linotype", serif; + font-size: calc(0.86rem * 1.1); +} +.oathhammer .npc-main .npc-vitals-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 4px 10px; + padding: 4px 6px; + border: 1px solid #535128; + border-radius: 3px; + background: rgba(0, 0, 0, 0.08); +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital { + display: flex; + align-items: center; + gap: 4px; +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital .vital-label { + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: calc(0.86rem * 0.85); + font-weight: bold; + color: #2a1a0a; + white-space: nowrap; + min-width: 4.5rem; +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital .vital-roll-label { + cursor: pointer; + color: #084a74; + transition: color 0.15s; +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital .vital-roll-label i { + margin-right: 2px; + font-size: 0.75rem; +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital .vital-roll-label:hover { + color: #c8a84b; + text-decoration: underline; +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital .vital-value { + display: flex; + align-items: center; + gap: 3px; + flex: 1; +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital .vital-value div.form-group { + display: contents; +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital .vital-value input, +.oathhammer .npc-main .npc-vitals-grid .npc-vital .vital-value .npc-num-input { + width: 2.8rem; + text-align: center; + font-size: calc(0.86rem * 0.85); + padding: 1px 2px; +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital .vital-value .res-sep { + opacity: 0.6; + font-size: calc(0.86rem * 0.9); +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital-grit .vital-value .grit-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.2rem; + height: 1.2rem; + font-size: 0.85rem; + font-weight: bold; + line-height: 1; + border: 1px solid #535128; + border-radius: 3px; + background: rgba(83, 81, 40, 0.2); + color: #2a1a0a; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-shrink: 0; +} +.oathhammer .npc-main .npc-vitals-grid .npc-vital-grit .vital-value .grit-btn:hover { + background: #c8a84b; + border-color: #c8a84b; +} +.oathhammer .npc-color-badge { + font-size: 0.85rem; + flex-shrink: 0; + line-height: 1; +} +.oathhammer .npc-color-select { + width: 88px; + max-width: 88px; + flex-shrink: 0; + font-size: calc(0.86rem * 0.9); + padding: 0 2px; + height: 20px; + min-width: 0; +} +.oathhammer .npc-subtype-badge { + font-size: 0.86rem; + font-weight: 600; + color: #2a1a0a; + background: rgba(200, 168, 75, 0.2); + border: 1px solid rgba(200, 168, 75, 0.5); + border-radius: 3px; + padding: 1px 8px; +} +.oathhammer .npc-subtype-select { + font-size: calc(0.86rem * 0.85); + padding: 0 2px; + height: 20px; + min-width: 80px; +} +.oathhammer .npc-skill-color { + font-size: 0.95rem; + text-align: center; + line-height: 1; + justify-self: center; +} +.oathhammer .npc-skill-color-white { + opacity: 0.75; +} +.oathhammer .npc-skill-color-red { + color: #cc4444; +} +.oathhammer .npc-skill-color-black { + color: #333; + font-weight: bold; +} +.oathhammer .npc-skill-roll-btn { + color: #c8a84b; + padding: 2px 4px; + border: 1px solid rgba(200, 168, 75, 0.4); + border-radius: 3px; + font-size: calc(0.86rem * 0.85); + text-align: center; + justify-self: center; +} +.oathhammer .npc-skill-roll-btn:hover { + background: rgba(200, 168, 75, 0.15); +} +.oathhammer .npc-trait-type-badge { + font-size: 0.72rem; + padding: 1px 5px; + border-radius: 3px; + white-space: nowrap; + background: rgba(200, 168, 75, 0.15); + border: 1px solid rgba(200, 168, 75, 0.35); + color: #2a1a0a; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; +} +.npc-skill-dialog { + display: flex; + flex-direction: column; + gap: 8px; + padding: 4px 0; } .oathhammer .item-list { list-style: none; @@ -772,10 +994,18 @@ .oathhammer .item-list--weapon .item-entry { grid-template-columns: 24px 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 9rem; } +.oathhammer .item-list--weapons .item-list-header, +.oathhammer .item-list--weapons .item-entry { + grid-template-columns: 24px 1fr 6rem 4rem; +} .oathhammer .item-list--armor .item-list-header, .oathhammer .item-list--armor .item-entry { grid-template-columns: 24px 1fr 3.5rem 2.5rem 3.5rem 1.5rem 1.8rem 5.5rem; } +.oathhammer .item-list--armors .item-list-header, +.oathhammer .item-list--armors .item-entry { + grid-template-columns: 24px 1fr 6rem 4rem; +} .oathhammer .item-list--ammo .item-list-header, .oathhammer .item-list--ammo .item-entry { grid-template-columns: 24px 1fr 4rem 3.5rem; @@ -812,6 +1042,30 @@ .oathhammer .item-list--oath .item-entry { grid-template-columns: 24px 1fr 7rem 3.5rem 3.5rem; } +.oathhammer .item-list--npc-skill .item-list-header, +.oathhammer .item-list--npc-skill .item-entry { + grid-template-columns: 1.8rem 1fr 3.5rem 3.5rem 2rem 4.5rem; +} +.oathhammer .item-list--npc-weapon .item-list-header, +.oathhammer .item-list--npc-weapon .item-entry { + grid-template-columns: 24px 1fr 3.5rem 2.5rem 7rem; +} +.oathhammer .item-list--npc-trait .item-list-header, +.oathhammer .item-list--npc-trait .item-entry { + grid-template-columns: 24px 1fr 6rem 3.5rem; +} +.oathhammer .item-list--npc-armor .item-list-header, +.oathhammer .item-list--npc-armor .item-entry { + grid-template-columns: 24px 1fr 3rem 3rem 3.5rem; +} +.oathhammer .item-list--npc-equip .item-list-header, +.oathhammer .item-list--npc-equip .item-entry { + grid-template-columns: 24px 1fr 3.5rem; +} +.oathhammer .item-list--npc-attack .item-list-header, +.oathhammer .item-list--npc-attack .item-entry { + grid-template-columns: 24px 1fr 7rem 3rem 6rem; +} .oathhammer .item-usage { font-size: calc(0.86rem * 0.9); color: #2a1a0a; @@ -909,6 +1163,10 @@ background: rgba(192, 57, 43, 0.1); border-color: rgba(192, 57, 43, 0.4); } +.oathhammer .item-list--regiment .item-list-header, +.oathhammer .item-list--regiment .item-entry { + grid-template-columns: 24px 1fr 4rem 5rem 4rem 4.5rem; +} .oathhammer .item-sheet-common { overflow: auto; padding: 10px 20px; @@ -1007,6 +1265,20 @@ font-weight: bold; color: #084a74; } +.oathhammer .item-sheet-common .enchantment-fieldset .enchant-cursed-label { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + margin-left: 8px; + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: 0.86rem; + color: #2a1a0a; + white-space: nowrap; +} +.oathhammer .item-sheet-common .enchantment-fieldset .enchant-cursed-label input[type="checkbox"] { + margin: 0; +} .oathhammer .item-sheet-common .proficiency-section { display: flex; gap: 8px; @@ -1036,6 +1308,91 @@ height: auto; accent-color: #084a74; } +.oathhammer .skillnpc-sheet .skillnpc-stats { + display: flex; + flex-direction: column; + gap: 2px; + margin-bottom: 6px; +} +.oathhammer .skillnpc-sheet .skillnpc-stats .form-group > label { + flex: 0 0 9rem; +} +.oathhammer .npcattack-sheet .npcattack-stats { + display: flex; + flex-direction: column; + gap: 2px; + margin-bottom: 6px; +} +.oathhammer .npcattack-sheet .npcattack-stats .form-group > label { + flex: 0 0 9rem; +} +.oathhammer .regiment-sheet { + display: flex; + flex-direction: column; + gap: 6px; +} +.oathhammer .regiment-sheet .regiment-stats-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 4px; +} +.oathhammer .regiment-sheet .regiment-stats-row .form-group > label { + flex: 0 0 6rem; +} +.oathhammer .regiment-sheet .regiment-armor-fields { + display: flex; + gap: 4px; +} +.oathhammer .regiment-sheet .regiment-armor-fields input[type="number"] { + width: 3rem; +} +.oathhammer .regiment-sheet .regiment-skill-row, +.oathhammer .regiment-sheet .regiment-attack-row, +.oathhammer .regiment-sheet .regiment-trait-row { + display: grid; + gap: 4px; + margin-bottom: 2px; + align-items: center; +} +.oathhammer .regiment-sheet .regiment-skill-row input, +.oathhammer .regiment-sheet .regiment-attack-row input, +.oathhammer .regiment-sheet .regiment-trait-row input, +.oathhammer .regiment-sheet .regiment-skill-row select, +.oathhammer .regiment-sheet .regiment-attack-row select, +.oathhammer .regiment-sheet .regiment-trait-row select { + font-size: calc(0.86rem * 0.85); + padding: 1px 3px; +} +.oathhammer .regiment-sheet .regiment-skill-row a.item-delete, +.oathhammer .regiment-sheet .regiment-attack-row a.item-delete, +.oathhammer .regiment-sheet .regiment-trait-row a.item-delete { + text-align: center; + color: #2a1a0a; + opacity: 0.4; +} +.oathhammer .regiment-sheet .regiment-skill-row a.item-delete:hover, +.oathhammer .regiment-sheet .regiment-attack-row a.item-delete:hover, +.oathhammer .regiment-sheet .regiment-trait-row a.item-delete:hover { + color: #c0392b; + opacity: 1; +} +.oathhammer .regiment-sheet .regiment-skill-header, +.oathhammer .regiment-sheet .regiment-attack-header { + font-weight: bold; + font-size: calc(0.86rem * 0.9); + color: #2a1a0a; + opacity: 0.6; + text-transform: uppercase; +} +.oathhammer .regiment-sheet .regiment-skill-row { + grid-template-columns: 1fr 3rem 6rem 1.5rem; +} +.oathhammer .regiment-sheet .regiment-attack-row { + grid-template-columns: 1fr 3.5rem 6rem 3rem 1fr 1.5rem; +} +.oathhammer .regiment-sheet .regiment-trait-row { + grid-template-columns: 1fr 2fr 1.5rem; +} .oh-roll-card { font-family: "Calibri", "Segoe UI", sans-serif; border: 1px solid #535128; @@ -1044,6 +1401,9 @@ background: rgba(245, 234, 208, 0.4); } .oh-roll-card .oh-roll-header { + display: flex; + align-items: center; + gap: 6px; font-family: "BlueDragon", "Palatino Linotype", serif; font-size: 0.86rem; font-weight: bold; @@ -1052,6 +1412,15 @@ border-bottom: 1px solid rgba(83, 81, 40, 0.2); padding-bottom: 3px; } +.oh-roll-card .oh-roll-header .oh-card-weapon-img { + width: 28px; + height: 28px; + -o-object-fit: contain; + object-fit: contain; + border: 1px solid rgba(83, 81, 40, 0.2); + border-radius: 3px; + flex-shrink: 0; +} .oh-roll-card .oh-roll-info { display: flex; justify-content: space-between; @@ -1368,6 +1737,31 @@ gap: 4px; align-items: center; } +.oh-weapon-dialog .weapon-header .weapon-badges .roll-color-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 7px; + border-radius: 4px; + font-size: calc(0.86rem * 0.9); + font-weight: bold; + border: 1px solid; +} +.oh-weapon-dialog .weapon-header .weapon-badges .color-badge-white { + background: #f0f0f0; + color: #555; + border-color: #ccc; +} +.oh-weapon-dialog .weapon-header .weapon-badges .color-badge-red { + background: rgba(231, 76, 60, 0.12); + color: #c0392b; + border-color: rgba(231, 76, 60, 0.35); +} +.oh-weapon-dialog .weapon-header .weapon-badges .color-badge-black { + background: rgba(44, 62, 80, 0.1); + color: #2c3e50; + border-color: rgba(44, 62, 80, 0.35); +} .oh-weapon-dialog .weapon-header .damage-formula-badge { font-family: "Calibri", "Segoe UI", sans-serif; font-size: calc(0.86rem * 0.9); @@ -1786,12 +2180,13 @@ gap: 8px; } .oathhammer .settlement-archetype-badge { - font-size: calc(0.86rem * 0.85); - color: #2a1a0a; - background: rgba(200, 168, 75, 0.15); - border: 1px solid rgba(200, 168, 75, 0.4); + font-size: 0.86rem; + font-weight: 600; + color: #150d05; + background: rgba(200, 168, 75, 0.2); + border: 1px solid rgba(200, 168, 75, 0.55); border-radius: 3px; - padding: 1px 6px; + padding: 1px 8px; white-space: nowrap; } .oathhammer .settlement-stats { @@ -1903,3 +2298,33 @@ .oathhammer .construct-toggle:hover { color: #214621; } +.oathhammer .settlement-buildings-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} +.oathhammer .settlement-buildings-header .settlement-hint { + margin: 0; +} +.oathhammer .settlement-buildings-header .collect-taxes-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 10px; + background: rgba(200, 168, 75, 0.2); + border: 1px solid rgba(200, 168, 75, 0.6); + border-radius: 4px; + font-size: calc(0.86rem * 0.85); + font-weight: bold; + color: #2a1a0a; + cursor: pointer; + white-space: nowrap; +} +.oathhammer .settlement-buildings-header .collect-taxes-btn:hover { + background: rgba(200, 168, 75, 0.4); + border-color: #c8a84b; +} +.oathhammer .settlement-buildings-header .collect-taxes-btn i { + color: #c8a84b; +} diff --git a/lang/en.json b/lang/en.json index daeb149..5f52ff7 100644 --- a/lang/en.json +++ b/lang/en.json @@ -15,10 +15,13 @@ "Condition": "Oath Hammer Condition Sheet", "Class": "Oath Hammer Class Sheet", "Building": "Oath Hammer Building Sheet", - "Settlement": "Oath Hammer Settlement Sheet" + "Settlement": "Oath Hammer Settlement Sheet", + "SkillNPC": "Oath Hammer NPC Skill Sheet", + "NpcAttack": "Oath Hammer NPC Attack Sheet", + "Regiment": "Oath Hammer Regiment Sheet" }, "Tab": { - "Identity": "Identity", + "Identity": "Oaths / Traits", "Skills": "Skills", "Combat": "Combat", "Magic": "Magic", @@ -26,7 +29,9 @@ "Notes": "Notes", "Overview": "Overview", "Buildings": "Buildings", - "Inventory": "Inventory" + "Inventory": "Inventory", + "Traits": "Traits", + "Garrison": "Garrison" }, "Attribute": { "Might": "Might", @@ -112,14 +117,27 @@ }, "Building": { "FIELDS": { - "skillCheck": { "label": "Skill Check" }, - "cost": { "label": "Cost (gp)" }, - "buildTime": { "label": "Build Time" }, - "taxRevenue": { "label": "Tax Revenue / month" }, - "constructed":{ "label": "Constructed" }, - - "description":{ "label": "Description" }, - "notes": { "label": "Notes" } + "skillCheck": { + "label": "Skill Check" + }, + "cost": { + "label": "Cost (gp)" + }, + "buildTime": { + "label": "Build Time" + }, + "taxRevenue": { + "label": "Tax Revenue / month" + }, + "constructed": { + "label": "Constructed" + }, + "description": { + "label": "Description" + }, + "notes": { + "label": "Notes" + } } }, "ArmorType": { @@ -166,7 +184,9 @@ "TraitType": { "SpecialTrait": "Special Trait", "ClassTrait": "Class Trait", - "LineageTrait": "Lineage Trait" + "LineageTrait": "Lineage Trait", + "NpcTrait": "NPC Trait", + "CreatureTrait": "Creature Trait" }, "Condition": { "Blinded": "Blinded", @@ -220,9 +240,10 @@ "Level": "Level", "XP": "Current XP", "TotalXP": "Total XP", - "Traits": "Special Traits", + "Traits": "Traits", "Oaths": "Oaths", "Weapons": "Weapons", + "Attacks": "Attacks", "Armor": "Armor & Shields", "Ammunition": "Ammunition", "ItemSlots": "Item Slots", @@ -243,14 +264,18 @@ "Components": "Components", "Charges": "Charges", "NoWeapons": "No weapons equipped.", + "NoAttacks": "No attacks defined.", "NoArmor": "No armor or shields.", "NoSpells": "No spells known.", "NoMiracles": "No miracles known.", "MiracleBlocked": "Divine favour lost — no more miracles today.", "NoEquipment": "No equipment.", - "NoTraits": "Drop traits here.", + "NoTraits": "No traits. Drag trait items here.", "NoOaths": "No oaths yet.", "Enchantment": "Enchantment", + "MagicQuality": "Quality", + "Cursed": "Cursed", + "ClassRestriction": "Restriction", "Tenet": "Tenet", "Boon": "Boon", "Bane": "Bane", @@ -262,7 +287,6 @@ "Lineage": "Lineage", "DropClass": "Drop Class Here", "OpenClass": "Click to open class details", - "Traits": "Traits", "Features": "Features", "Name": "Name", "Type": "Type", @@ -300,7 +324,28 @@ "TaxRevenue": "Tax Revenue", "Cost": "Cost", "Built": "Built", - "Armors": "Armors & Shields" + "Armors": "Armors & Shields", + "ArmorDice": "Armor Dice", + "Skills": "Skills", + "NoSkills": "No skills. Drag skill items here.", + "DicePool": "Dice Pool", + "SkillNPCHint": "Drag NPC skill items here to define skill pools.", + "TraitNPCHint": "Drag trait items here.", + "EquipmentNPCHint": "Drag armor and equipment items here.", + "Threshold": "Threshold", + "GritMax": "Grit (Max)", + "Dice": "Dice", + "DiceColor": "Color", + "Special": "Special", + "Movement": "Move", + "Stats": "Stats", + "NoRegiments": "No regiments. Add one with the + button.", + "SkillName": "Skill name", + "AttackName": "Attack name", + "TraitName": "Trait name", + "Edit": "Edit", + "Delete": "Delete", + "Rank": "Rank" }, "ColorDice": { "White": "White (4+)", @@ -314,9 +359,17 @@ "Equipment": "New Equipment", "Building": "New Building", "Trait": "New Trait", - "Oath": "New Oath" + "Oath": "New Oath", + "NpcAttack": "New NPC Attack", + "Regiment": "New Regiment", + "RegimentSkill": "Add Skill", + "RegimentAttack": "Add Attack", + "RegimentTrait": "Add Trait" }, "ToggleSheet": "Toggle Edit/Play Mode", + "Tooltip": { + "RollArmor": "Roll Armor Dice" + }, "Action": { "CastSpell": "Cast Spell", "InvokeMiracle": "Invoke Miracle", @@ -558,10 +611,26 @@ "label": "Attributes" }, "grit": { - "label": "Grit" + "label": "Grit", + "fields": { + "max": { + "label": "Max Grit" + }, + "value": { + "label": "Grit" + } + } }, "defense": { - "label": "Defense" + "label": "Defense", + "fields": { + "value": { + "label": "Defense" + }, + "colorDiceType": { + "label": "Defense Dice Color" + } + } }, "movement": { "label": "Movement" @@ -580,6 +649,12 @@ }, "notes": { "label": "Notes" + }, + "subtype": { + "label": "Subtype" + }, + "armorDice": { + "label": "Armor Dice" } } }, @@ -641,8 +716,13 @@ }, "classRestriction": { "label": "Restriction" + }, + "skillOverride": { + "label": "Attack Skill", + "hint": "Override the skill and attribute used for attack and damage rolls. Leave blank to auto-detect (Fighting for melee, Shooting for ranged)." } - } + }, + "SkillOverrideAuto": "Auto (Fighting / Shooting)" }, "Armor": { "FIELDS": { @@ -922,6 +1002,9 @@ "Greater": "Greater", "Legendary": "Legendary" }, + "ClassRestriction": { + "None": "No restriction" + }, "OathType": { "Compassion": "Oath of Compassion", "Courage": "Oath of Courage", @@ -976,20 +1059,71 @@ "MiracleBlocked": "Divine favour has been lost — you cannot invoke miracles until a new day." }, "SettlementArchetype": { - "CenterOfLearning": "Center of Learning", - "DwarvenBorough": "Dwarven Borough", - "FreeCity": "Free City", + "CenterOfLearning": "Center of Learning", + "DwarvenBorough": "Dwarven Borough", + "FreeCity": "Free City", "GuildMunicipality": "Guild Municipality", "NocklanderOutpost": "Nocklander Outpost", - "PilgrimMission": "Pilgrim Mission", - "PortTown": "Port Town", - "VelathiColony": "Velathi Colony" + "PilgrimMission": "Pilgrim Mission", + "PortTown": "Port Town", + "VelathiColony": "Velathi Colony" }, "Settlement": { "BuildingHint": "Drag & drop building items here. Toggle the checkbox to mark them as constructed.", "InventoryHint": "Drag & drop weapons, armor, or equipment here to store them in the settlement reserve.", "NoBuildings": "No buildings yet. Drag building items here.", - "NoInventory": "No stored inventory. Drag weapons, armor, or equipment here." + "NoInventory": "No stored inventory. Drag weapons, armor, or equipment here.", + "CollectTaxes": "Collect Taxes", + "CollectTaxesTooltip": "Roll tax revenue for all constructed buildings and total the result.", + "NoTaxRevenue": "No constructed buildings with tax revenue defined.", + "TotalRevenue": "Total Revenue" + }, + "SkillNPC": { + "FIELDS": { + "dicePool": { + "label": "Dice Pool", + "hint": "Total number of dice rolled for this skill." + }, + "colorDiceType": { + "label": "Dice Color", + "hint": "White = 4+, Red = 3+, Black = 2+" + }, + "skillRef": { + "label": "Skill Reference", + "hint": "Optional link to a system skill for display purposes." + }, + "description": { + "label": "Description" + } + } + }, + "NpcAttack": { + "FIELDS": { + "damageDice": { + "label": "Damage Dice", + "hint": "Number of damage dice rolled (no Might added)." + }, + "colorDiceType": { + "label": "Dice Color", + "hint": "White = 4+, Red = 3+, Black = 2+" + }, + "ap": { + "label": "AP", + "hint": "Armor Penetration: penalty applied to the target's armor roll." + }, + "description": { + "label": "Description" + } + } + }, + "DiceColor": { + "White": "White (4+)", + "Red": "Red (3+)", + "Black": "Black (2+)" + }, + "NpcSubtype": { + "Creature": "Creature", + "Npc": "NPC" } }, "TYPES": { @@ -1004,11 +1138,15 @@ "magic-item": "Magic Item", "trait": "Trait", "oath": "Oath", - "class": "Class" + "class": "Class", + "skillnpc": "NPC Skill", + "npcattack": "NPC Attack", + "regiment": "Regiment", + "building": "Building" }, "Actor": { "character": "Character", - "npc": "NPC", + "npc": "NPC / Creature", "settlement": "Settlement" } } diff --git a/less/actor-sheet.less b/less/actor-sheet.less index 26aaa6b..5b19130 100644 --- a/less/actor-sheet.less +++ b/less/actor-sheet.less @@ -325,4 +325,58 @@ text-align: center; font-weight: bold; } + + // ── Magic tab — Arcane Stress row ────────────────────────── + .stress-controls { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: nowrap; + padding: 4px 0; + + .stress-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border: 1px solid @color-olive; + border-radius: 3px; + background: fade(@color-dark, 6%); + color: @color-dark; + font-size: 1rem; + line-height: 1; + cursor: pointer; + flex-shrink: 0; + text-decoration: none; + + &:hover { background: fade(@color-blue, 15%); border-color: @color-blue; } + } + + .arcane-stress-display { + font-family: @font-secondary; + font-size: @font-size-lg; + font-weight: bold; + color: @color-dark; + min-width: 3rem; + text-align: center; + flex-shrink: 0; + + &.stress-at-limit { color: #c0392b; } + } + + .stress-bonus-label { + margin-left: auto; // push threshold bonus to the right + font-size: @font-size-sm; + color: @color-olive; + white-space: nowrap; + flex-shrink: 0; + } + + .stress-bonus-input { + width: 3rem; + text-align: center; + flex-shrink: 0; + } + } } diff --git a/less/item-list.less b/less/item-list.less index 0369e3b..f89a952 100644 --- a/less/item-list.less +++ b/less/item-list.less @@ -121,12 +121,25 @@ } } + // Settlement inventory: simpler lists (no equip slot, no qty) + .item-list--weapons { + .item-list-header, .item-entry { + grid-template-columns: @item-img-size 1fr 6rem 4rem; + } + } + .item-list--armor { .item-list-header, .item-entry { grid-template-columns: @item-img-size 1fr 3.5rem 2.5rem 3.5rem 1.5rem 1.8rem 5.5rem; } } + .item-list--armors { + .item-list-header, .item-entry { + grid-template-columns: @item-img-size 1fr 6rem 4rem; + } + } + .item-list--ammo { .item-list-header, .item-entry { grid-template-columns: @item-img-size 1fr 4rem 3.5rem; @@ -180,6 +193,49 @@ } } + // ── NPC-specific grid variants ─────────────────────────────── + // NPC Skill list: [color emoji] [name] [pool] [threshold] [roll btn] [actions] + .item-list--npc-skill { + .item-list-header, .item-entry { + grid-template-columns: 1.8rem 1fr 3.5rem 3.5rem 2rem 4.5rem; + } + } + + // NPC Weapon list: [img] [name] [damage] [AP] [actions incl attack+dmg+edit+del] + .item-list--npc-weapon { + .item-list-header, .item-entry { + grid-template-columns: @item-img-size 1fr 3.5rem 2.5rem 7rem; + } + } + + // NPC Trait list: [img] [name] [type badge] [actions] + .item-list--npc-trait { + .item-list-header, .item-entry { + grid-template-columns: @item-img-size 1fr 6rem 3.5rem; + } + } + + // NPC Armor list: [img] [name] [AV] [penalty] [actions] + .item-list--npc-armor { + .item-list-header, .item-entry { + grid-template-columns: @item-img-size 1fr 3rem 3rem 3.5rem; + } + } + + // NPC Equipment list: [img] [name] [actions] + .item-list--npc-equip { + .item-list-header, .item-entry { + grid-template-columns: @item-img-size 1fr 3.5rem; + } + } + + // NPC Attack list: [img] [name] [damage] [AP] [actions: roll+edit+del] + .item-list--npc-attack { + .item-list-header, .item-entry { + grid-template-columns: @item-img-size 1fr 7rem 3rem 6rem; + } + } + .item-usage { font-size: @font-size-xs; color: @color-dark; @@ -273,3 +329,12 @@ } } } + +// ── Regiment list (settlement garrison tab) ────────────────────────────────── +.oathhammer { + .item-list--regiment { + .item-list-header, .item-entry { + grid-template-columns: @item-img-size 1fr 4rem 5rem 4rem 4.5rem; + } + } +} diff --git a/less/item-sheets.less b/less/item-sheets.less index 595d8c2..2751ed1 100644 --- a/less/item-sheets.less +++ b/less/item-sheets.less @@ -115,6 +115,25 @@ color: @color-blue; } + // ── Enchantment fieldset ───────────────────────────────────── + .enchantment-fieldset { + .enchant-cursed-label { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + margin-left: 8px; + font-family: @font-secondary; + font-size: @font-size-base; + color: @color-dark; + white-space: nowrap; + + input[type="checkbox"] { + margin: 0; + } + } + } + // ── Class proficiency checkboxes ──────────────────────────── .proficiency-section { display: flex; @@ -150,3 +169,78 @@ } } } + + +// SkillNPC sheet — vertical stack layout +.oathhammer .skillnpc-sheet { + .skillnpc-stats { + display: flex; + flex-direction: column; + gap: 2px; + margin-bottom: 6px; + + .form-group > label { + flex: 0 0 9rem; + } + } +} + +// NpcAttack sheet — vertical stack layout (mirrors skillnpc) +.oathhammer .npcattack-sheet { + .npcattack-stats { + display: flex; + flex-direction: column; + gap: 2px; + margin-bottom: 6px; + + .form-group > label { + flex: 0 0 9rem; + } + } +} + +// Regiment sheet +.oathhammer .regiment-sheet { + display: flex; + flex-direction: column; + gap: 6px; + + .regiment-stats-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 4px; + + .form-group > label { flex: 0 0 6rem; } + } + + .regiment-armor-fields { + display: flex; + gap: 4px; + input[type="number"] { width: 3rem; } + } + + .regiment-skill-row, + .regiment-attack-row, + .regiment-trait-row { + display: grid; + gap: 4px; + margin-bottom: 2px; + align-items: center; + + input, select { font-size: @font-size-sm; padding: 1px 3px; } + a.item-delete { text-align: center; color: @color-dark; opacity: 0.4; &:hover { color: #c0392b; opacity: 1; } } + } + + .regiment-skill-header, + .regiment-attack-header { + font-weight: bold; + font-size: @font-size-xs; + color: @color-dark; + opacity: 0.6; + text-transform: uppercase; + } + + .regiment-skill-row { grid-template-columns: 1fr 3rem 6rem 1.5rem; } + .regiment-attack-row { grid-template-columns: 1fr 3.5rem 6rem 3rem 1fr 1.5rem; } + .regiment-trait-row { grid-template-columns: 1fr 2fr 1.5rem; } +} diff --git a/less/npc-sheet.less b/less/npc-sheet.less index 460e1e0..bcec37f 100644 --- a/less/npc-sheet.less +++ b/less/npc-sheet.less @@ -1,22 +1,209 @@ // ============================================================ -// NPC SHEET — NPC-specific layout +// NPC SHEET — layout, vitals, skills, traits // ============================================================ .oathhammer .npc-main { - .npc-left { min-width: @npc-left-width; max-width: @npc-left-width; - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; + flex-shrink: 0; + + .actor-img { + width: 100%; + height: 170px; + object-fit: cover; + object-position: center top; + } } .npc-right { flex: 1; display: flex; flex-direction: column; - gap: 4px; + gap: 6px; + min-width: 0; } + + // Name row + .character-name { + display: flex; + align-items: center; + gap: 4px; + border-bottom: 1px solid @color-olive; + padding-bottom: 4px; + + input { + flex: 1; + font-family: @font-primary; + font-size: @font-size-lg; + } + } + + // Vitals: 2×2 grid (Grit | Defense / Armor | Movement) + .npc-vitals-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 4px 10px; + padding: 4px 6px; + border: 1px solid @color-olive; + border-radius: 3px; + background: rgba(0, 0, 0, 0.08); + + .npc-vital { + display: flex; + align-items: center; + gap: 4px; + + .vital-label { + font-family: @font-secondary; + font-size: @font-size-sm; + font-weight: bold; + color: @color-dark; + white-space: nowrap; + min-width: 4.5rem; + } + + .vital-roll-label { + cursor: pointer; + color: @color-blue; + transition: color 0.15s; + i { margin-right: 2px; font-size: 0.75rem; } + &:hover { color: @color-gold; text-decoration: underline; } + } + + .vital-value { + display: flex; + align-items: center; + gap: 3px; + flex: 1; + + div.form-group { display: contents; } + + input, .npc-num-input { + width: 2.8rem; + text-align: center; + font-size: @font-size-sm; + padding: 1px 2px; + } + + .res-sep { opacity: 0.6; font-size: @font-size-xs; } + } + } + + .npc-vital-grit .vital-value { + .grit-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.2rem; + height: 1.2rem; + font-size: 0.85rem; + font-weight: bold; + line-height: 1; + border: 1px solid @color-olive; + border-radius: 3px; + background: @color-olive-faint; + color: @color-dark; + cursor: pointer; + user-select: none; + flex-shrink: 0; + &:hover { background: @color-gold; border-color: @color-gold; } + } + } + } + + // Statistics fieldset grid } + +// Inline color badge (emoji in play mode) +.oathhammer .npc-color-badge { + font-size: 0.85rem; + flex-shrink: 0; + line-height: 1; +} + +// Inline color dropdown (in edit mode) — fixed width so it doesn't stretch the grid +.oathhammer .npc-color-select { + width: 88px; + max-width: 88px; + flex-shrink: 0; + font-size: @font-size-xs; + padding: 0 2px; + height: 20px; + min-width: 0; +} + +// Subtype badge (play mode) / select (edit mode) +.oathhammer .npc-subtype-badge { + font-size: @font-size-base; + font-weight: 600; + color: @color-dark; + background: fade(@color-gold, 20%); + border: 1px solid fade(@color-gold, 50%); + border-radius: 3px; + padding: 1px 8px; +} + +.oathhammer .npc-subtype-select { + font-size: @font-size-sm; + padding: 0 2px; + height: 20px; + min-width: 80px; +} + +// ============================================================ +// NPC SKILLS TAB +// ============================================================ + +// Color emoji indicator per dice type +.oathhammer .npc-skill-color { + font-size: 0.95rem; + text-align: center; + line-height: 1; + justify-self: center; +} +.oathhammer .npc-skill-color-white { opacity: 0.75; } +.oathhammer .npc-skill-color-red { color: #cc4444; } +.oathhammer .npc-skill-color-black { color: #333; font-weight: bold; } + +// Roll button in skill list +.oathhammer .npc-skill-roll-btn { + color: @color-gold; + padding: 2px 4px; + border: 1px solid fade(@color-gold, 40%); + border-radius: 3px; + font-size: @font-size-sm; + text-align: center; + justify-self: center; + &:hover { background: fade(@color-gold, 15%); } +} + +// ============================================================ +// NPC TRAITS TAB +// ============================================================ + +// Type badge used in traits list +.oathhammer .npc-trait-type-badge { + font-size: 0.72rem; + padding: 1px 5px; + border-radius: 3px; + white-space: nowrap; + background: fade(@color-gold, 15%); + border: 1px solid fade(@color-gold, 35%); + color: @color-dark; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; +} + +// ============================================================ +// NPC SKILL DIALOG +// ============================================================ + +.npc-skill-dialog { + display: flex; + flex-direction: column; + gap: 8px; + padding: 4px 0; +} + diff --git a/less/roll-dialog.less b/less/roll-dialog.less index 7851fbf..6e5529f 100644 --- a/less/roll-dialog.less +++ b/less/roll-dialog.less @@ -284,6 +284,21 @@ flex-wrap: wrap; gap: 4px; align-items: center; + + // Allow color badges here too (shared with .roll-info-block) + .roll-color-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 7px; + 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%); } } .damage-formula-badge { diff --git a/less/rolls.less b/less/rolls.less index 95386a5..104c90b 100644 --- a/less/rolls.less +++ b/less/rolls.less @@ -10,6 +10,9 @@ background: fade(#f5ead0, 40%); .oh-roll-header { + display: flex; + align-items: center; + gap: 6px; font-family: @font-secondary; font-size: @font-size-base; font-weight: bold; @@ -17,6 +20,15 @@ margin-bottom: 4px; border-bottom: 1px solid @color-olive-faint; padding-bottom: 3px; + + .oh-card-weapon-img { + width: 28px; + height: 28px; + object-fit: contain; + border: 1px solid @color-olive-faint; + border-radius: 3px; + flex-shrink: 0; + } } .oh-roll-info { diff --git a/less/settlement-sheet.less b/less/settlement-sheet.less index 12e59c1..7db8df1 100644 --- a/less/settlement-sheet.less +++ b/less/settlement-sheet.less @@ -42,12 +42,13 @@ } .oathhammer .settlement-archetype-badge { - font-size: @font-size-sm; - color: @color-dark; - background: fade(@color-gold, 15%); - border: 1px solid fade(@color-gold, 40%); + font-size: @font-size-base; + font-weight: 600; + color: darken(@color-dark, 5%); + background: fade(@color-gold, 20%); + border: 1px solid fade(@color-gold, 55%); border-radius: 3px; - padding: 1px 6px; + padding: 1px 8px; white-space: nowrap; } @@ -176,3 +177,34 @@ &:hover { color: darken(#3a7a3a, 15%); } } + +.oathhammer .settlement-buildings-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; + + .settlement-hint { margin: 0; } + + .collect-taxes-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 10px; + background: fade(@color-gold, 20%); + border: 1px solid fade(@color-gold, 60%); + border-radius: 4px; + font-size: @font-size-sm; + font-weight: bold; + color: @color-dark; + cursor: pointer; + white-space: nowrap; + + &:hover { + background: fade(@color-gold, 40%); + border-color: @color-gold; + } + + i { color: @color-gold; } + } +} diff --git a/less/variables.less b/less/variables.less index 635fa45..4dddbb9 100644 --- a/less/variables.less +++ b/less/variables.less @@ -26,7 +26,7 @@ // Layout @portrait-height: 150px; @left-panel-width: 180px; -@npc-left-width: 160px; +@npc-left-width: 120px; @item-img-size: 24px; @item-sheet-img: 52px; @label-min-width: 9rem; diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index cd574e0..9c15b57 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -12,6 +12,9 @@ export { default as OathHammerOathSheet } from "./sheets/oath-sheet.mjs" export { default as OathHammerClassSheet } from "./sheets/class-sheet.mjs" export { default as OathHammerBuildingSheet } from "./sheets/building-sheet.mjs" export { default as OathHammerSettlementSheet } from "./sheets/settlement-sheet.mjs" +export { default as OathHammerSkillNPCSheet } from "./sheets/skillnpc-sheet.mjs" +export { default as OathHammerNpcAttackSheet } from "./sheets/npcattack-sheet.mjs" +export { default as OathHammerRegimentSheet } from "./sheets/regiment-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" diff --git a/module/applications/sheets/base-item-sheet.mjs b/module/applications/sheets/base-item-sheet.mjs index b785dc0..438e3f8 100644 --- a/module/applications/sheets/base-item-sheet.mjs +++ b/module/applications/sheets/base-item-sheet.mjs @@ -1,5 +1,5 @@ const { HandlebarsApplicationMixin } = foundry.applications.api -import { ARMOR_TYPE_CHOICES, WEAPON_PROFICIENCY_GROUPS } from "../../config/system.mjs" +import { ARMOR_TYPE_CHOICES, CLASS_RESTRICTION_CHOICES, SYSTEM, WEAPON_PROFICIENCY_GROUPS } from "../../config/system.mjs" import { rollRarityCheck } from "../../rolls.mjs" export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) { @@ -57,6 +57,10 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun if (this.document.system.description !== undefined) { context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description ?? "", { async: true }) } + if (this.document.system.magicEffect !== undefined) { + context.enrichedMagicEffect = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.magicEffect ?? "", { async: true }) + } + context.classRestrictionChoices = CLASS_RESTRICTION_CHOICES // Armor-specific numeric selects context.armorValueChoices = Object.fromEntries( Array.from({ length: 13 }, (_, i) => [i, String(i)]) @@ -71,6 +75,13 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun context.apChoices = Object.fromEntries( Array.from({ length: 7 }, (_, i) => [i, String(i)]) ) + // Skill choices for weapon skill override (empty = auto-detect) + context.skillChoices = { + "": `— ${game.i18n.localize("OATHHAMMER.Weapon.SkillOverrideAuto")} —`, + ...Object.fromEntries( + Object.entries(SYSTEM.SKILLS).map(([k, v]) => [k, game.i18n.localize(v.label)]) + ) + } // Class proficiency choices (for class-sheet checkboxes) context.armorTypeChoices = ARMOR_TYPE_CHOICES context.weaponGroupChoices = WEAPON_PROFICIENCY_GROUPS diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index b136b51..9a9762b 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -118,7 +118,6 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { _descTooltip: _stripHtml(parts.join(" ")) } }) - context.enrichedBackground = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.background ?? "", { async: true }) break case "skills": { context.tab = context.tabs.skills @@ -149,7 +148,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { modifierName: `system.skills.${skillKey}.modifier`, colorDiceName: `system.skills.${skillKey}.colorDice`, colorDiceTypeName: `system.skills.${skillKey}.colorDiceType`, - rankOptions: [0,1,2,3,4].map(v => ({ value: v, label: String(v), selected: v === sk.rank })), + rankOptions: [0,1,2,3,4,5,6].map(v => ({ value: v, label: String(v), selected: v === sk.rank })), total: attrRanks[attr] + sk.rank, // legacy - kept for formInput compatibility name: `system.skills.${skillKey}.rank`, @@ -232,6 +231,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { break case "notes": context.tab = context.tabs.notes + context.enrichedBackground = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.background ?? "", { async: true }) context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.description ?? "", { async: true }) context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes ?? "", { async: true }) break diff --git a/module/applications/sheets/npc-sheet.mjs b/module/applications/sheets/npc-sheet.mjs index b64bf98..634b697 100644 --- a/module/applications/sheets/npc-sheet.mjs +++ b/module/applications/sheets/npc-sheet.mjs @@ -1,50 +1,56 @@ import OathHammerActorSheet from "./base-actor-sheet.mjs" -import { rollInitiativeCheck } from "../../rolls.mjs" +import { SYSTEM } from "../../config/system.mjs" +import { rollInitiativeCheck, rollNPCSkill, rollNPCArmor, rollNPCSpell, rollNPCMiracle, rollNPCAttackDamage } from "../../rolls.mjs" export default class OathHammerNPCSheet extends OathHammerActorSheet { - /** @override */ static DEFAULT_OPTIONS = { classes: ["npc"], - position: { - width: 720, - height: "auto", - }, - window: { - contentClasses: ["npc-content"], - }, + position: { width: 720, height: "auto" }, + window: { contentClasses: ["npc-content"] }, actions: { - rollInitiative: OathHammerNPCSheet.#onRollInitiative, + rollInitiative: OathHammerNPCSheet.#onRollInitiative, + adjustGrit: OathHammerNPCSheet.#onAdjustGrit, + rollSkillNPC: OathHammerNPCSheet.#onRollSkillNPC, + rollArmor: OathHammerNPCSheet.#onRollArmor, + createSpell: OathHammerNPCSheet.#onCreateSpell, + createMiracle: OathHammerNPCSheet.#onCreateMiracle, + castNPCSpell: OathHammerNPCSheet.#onCastNPCSpell, + castNPCMiracle: OathHammerNPCSheet.#onCastNPCMiracle, + createNpcAttack: OathHammerNPCSheet.#onCreateNpcAttack, + rollNpcAttack: OathHammerNPCSheet.#onRollNpcAttack, }, } - /** @override */ static PARTS = { - main: { - template: "systems/fvtt-oath-hammer/templates/actor/npc-sheet.hbs", - }, - tabs: { - template: "templates/generic/tab-navigation.hbs", - }, - combat: { - template: "systems/fvtt-oath-hammer/templates/actor/npc-combat.hbs", - }, - notes: { - template: "systems/fvtt-oath-hammer/templates/actor/npc-notes.hbs", - }, + main: { template: "systems/fvtt-oath-hammer/templates/actor/npc-sheet.hbs" }, + tabs: { template: "templates/generic/tab-navigation.hbs" }, + skills: { template: "systems/fvtt-oath-hammer/templates/actor/npc-skills.hbs" }, + combat: { template: "systems/fvtt-oath-hammer/templates/actor/npc-combat.hbs" }, + traits: { template: "systems/fvtt-oath-hammer/templates/actor/npc-traits.hbs" }, + magic: { template: "systems/fvtt-oath-hammer/templates/actor/npc-magic.hbs" }, + equipment: { template: "systems/fvtt-oath-hammer/templates/actor/npc-equipment.hbs" }, + notes: { template: "systems/fvtt-oath-hammer/templates/actor/npc-notes.hbs" }, } - /** @override */ - tabGroups = { - sheet: "combat", - } + tabGroups = { sheet: "skills" } #getTabs() { + const isNPC = this.document.system.subtype === "npc" + const hasMagic = this.document.items.some(i => i.type === "spell" || i.type === "miracle") const tabs = { - combat: { id: "combat", group: "sheet", icon: "fa-solid fa-swords", label: "OATHHAMMER.Tab.Combat" }, - notes: { id: "notes", group: "sheet", icon: "fa-solid fa-book", label: "OATHHAMMER.Tab.Notes" }, + skills: { id: "skills", group: "sheet", icon: "fa-solid fa-dice-d6", label: "OATHHAMMER.Tab.Skills" }, + combat: { id: "combat", group: "sheet", icon: "fa-solid fa-swords", label: "OATHHAMMER.Tab.Combat" }, + traits: { id: "traits", group: "sheet", icon: "fa-solid fa-star", label: "OATHHAMMER.Tab.Traits" }, + notes: { id: "notes", group: "sheet", icon: "fa-solid fa-book", label: "OATHHAMMER.Tab.Notes" }, + } + if (isNPC) { + tabs.equipment = { id: "equipment", group: "sheet", icon: "fa-solid fa-backpack", label: "OATHHAMMER.Tab.Equipment" } + } + if (hasMagic || !this.isPlayMode) { + tabs.magic = { id: "magic", group: "sheet", icon: "fa-solid fa-wand-sparkles", label: "OATHHAMMER.Tab.Magic" } } for (const v of Object.values(tabs)) { - v.active = this.tabGroups[v.group] === v.id + v.active = this.tabGroups[v.group] === v.id v.cssClass = v.active ? "active" : "" } return tabs @@ -54,6 +60,23 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { async _prepareContext() { const context = await super._prepareContext() context.tabs = this.#getTabs() + + context.subtypeChoices = Object.fromEntries( + Object.entries(SYSTEM.NPC_SUBTYPES).map(([k, v]) => [k, game.i18n.localize(v)]) + ) + context.subtypeLabels = context.subtypeChoices + + const armorColor = this.document.system.armorDice?.colorDiceType ?? "white" + context.armorDiceEmoji = armorColor === "black" ? "⬛" : armorColor === "red" ? "🔴" : "⬜" + + context.colorChoices = Object.fromEntries( + Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) + ) + + context.traitTypeLabels = Object.fromEntries( + Object.entries(SYSTEM.TRAIT_TYPE_CHOICES).map(([k, v]) => [k, v]) + ) + return context } @@ -61,29 +84,265 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { async _preparePartContext(partId, context) { const doc = this.document switch (partId) { - case "main": + case "skills": + context.tab = context.tabs.skills + context.skills = (doc.itemTypes.skillnpc ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)) break case "combat": context.tab = context.tabs.combat - context.weapons = doc.itemTypes.weapon + context.npcAttacks = (doc.itemTypes.npcattack ?? []).map(a => ({ + id: a.id, uuid: a.uuid, img: a.img, name: a.name, system: a.system, + _descTooltip: a.system.description?.replace(/<[^>]+>/g, "").slice(0, 300) ?? "" + })) context.combatantInitiative = game.combat?.combatants.find(c => c.actor?.id === doc.id)?.initiative ?? null break + case "traits": + context.tab = context.tabs.traits + context.traits = (doc.itemTypes.trait ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)).map(t => ({ + id: t.id, uuid: t.uuid, img: t.img, name: t.name, system: t.system, + _descTooltip: t.system.description?.replace(/<[^>]+>/g, "").slice(0, 300) ?? "" + })) + break + case "magic": + context.tab = context.tabs.magic + context.spells = (doc.itemTypes.spell ?? []).map(s => ({ + id: s.id, uuid: s.uuid, img: s.img, name: s.name, system: s.system, + _descTooltip: s.system.effect?.replace(/<[^>]+>/g, "").slice(0, 300) ?? "" + })) + context.miracles = (doc.itemTypes.miracle ?? []).map(m => ({ + id: m.id, uuid: m.uuid, img: m.img, name: m.name, system: m.system, + _descTooltip: m.system.effect?.replace(/<[^>]+>/g, "").slice(0, 300) ?? "" + })) + break + case "equipment": + context.tab = context.tabs.equipment + context.armors = doc.itemTypes.armor ?? [] + context.equipment = doc.itemTypes.equipment ?? [] + break case "notes": context.tab = context.tabs.notes context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.description, { async: true }) - context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes, { async: true }) + context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes, { async: true }) break } return context } + /** @override */ async _onDrop(event) { if (!this.isEditable || !this.isEditMode) return const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event) - if (data.type === "Item") { - const item = await fromUuid(data.uuid) - return this._onDropItem(item) - } + if (data.type !== "Item") return + const item = await fromUuid(data.uuid) + if (!item) return + const ALLOWED = new Set(["skillnpc", "npcattack", "trait", "armor", "equipment", "spell", "miracle"]) + if (!ALLOWED.has(item.type)) return + return this._onDropItem(item) + } + + static async #onAdjustGrit(event, target) { + const delta = parseInt(target.dataset.delta, 10) + const current = this.document.system.grit?.value ?? 0 + const max = this.document.system.grit?.max ?? current + await this.document.update({ "system.grit.value": Math.max(0, Math.min(max, current + delta)) }) + } + + static #onCreateNpcAttack() { + this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.NpcAttack"), type: "npcattack" }]) + } + + static async #onRollNpcAttack(event, target) { + const attack = this.document.items.get(target.dataset.itemId) + if (!attack) return + + const bonusOptions = Array.from({ length: 13 }, (_, i) => { + const v = i - 6 + return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } + }) + + const content = await foundry.applications.handlebars.renderTemplate( + "systems/fvtt-oath-hammer/templates/npc-skill-dialog.hbs", + { + skillName: attack.name, + skillImg: attack.img, + dicePool: attack.system.damageDice, + colorEmoji: attack.system.colorEmoji, + colorType: attack.system.colorDiceType, + threshold: attack.system.threshold, + bonusOptions, + colorChoices: Object.fromEntries( + Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) + ), + rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), + visibility: game.settings.get("core", "rollMode"), + } + ) + + const result = await foundry.applications.api.DialogV2.prompt({ + window: { title: attack.name, resizable: true }, + classes: ["fvtt-oath-hammer"], + position: { width: 420 }, + content, + ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-burst" }, + }) + if (!result) return + + const form = new DOMParser().parseFromString(result, "text/html") + const getValue = name => form.querySelector(`[name="${name}"]`)?.value + + await rollNPCAttackDamage(this.document, attack, { + bonus: parseInt(getValue("bonus")) || 0, + visibility: getValue("visibility"), + }) + } + + static #onCreateSpell() { + this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Spell"), type: "spell" }]) + } + + static #onCreateMiracle() { + this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Miracle"), type: "miracle" }]) + } + + static async #onCastNPCSpell(event, target) { + const spell = this.document.items.get(target.dataset.itemId) + if (!spell) return + + const colorChoices = Object.fromEntries( + Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) + ) + const poolOptions = Array.from({ length: 10 }, (_, i) => { + const v = i + 1 + return { value: v, label: String(v), selected: v === 3 } + }) + const bonusOptions = Array.from({ length: 13 }, (_, i) => { + const v = i - 6 + return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } + }) + + const content = await foundry.applications.handlebars.renderTemplate( + "systems/fvtt-oath-hammer/templates/npc-magic-dialog.hbs", + { + itemName: spell.name, itemImg: spell.img, + dv: spell.system.difficultyValue, + poolOptions, bonusOptions, colorChoices, showColor: true, + rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), + visibility: game.settings.get("core", "rollMode"), + } + ) + + const result = await foundry.applications.api.DialogV2.prompt({ + window: { title: spell.name, resizable: true }, + classes: ["fvtt-oath-hammer"], + position: { width: 420 }, + content, + ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-wand-sparkles" }, + }) + if (!result) return + + const form = new DOMParser().parseFromString(result, "text/html") + const getValue = name => form.querySelector(`[name="${name}"]`)?.value + + await rollNPCSpell(this.document, spell, { + dicePool: parseInt(getValue("dicePool")) || 3, + bonus: parseInt(getValue("bonus")) || 0, + colorOverride: getValue("colorOverride") || null, + visibility: getValue("visibility"), + }) + } + + static async #onCastNPCMiracle(event, target) { + const miracle = this.document.items.get(target.dataset.itemId) + if (!miracle) return + + const poolOptions = Array.from({ length: 10 }, (_, i) => { + const v = i + 1 + return { value: v, label: String(v), selected: v === 3 } + }) + const bonusOptions = Array.from({ length: 13 }, (_, i) => { + const v = i - 6 + return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } + }) + + const content = await foundry.applications.handlebars.renderTemplate( + "systems/fvtt-oath-hammer/templates/npc-magic-dialog.hbs", + { + itemName: miracle.name, itemImg: miracle.img, + dv: null, showColor: false, + poolOptions, bonusOptions, + rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), + visibility: game.settings.get("core", "rollMode"), + } + ) + + const result = await foundry.applications.api.DialogV2.prompt({ + window: { title: miracle.name, resizable: true }, + classes: ["fvtt-oath-hammer"], + position: { width: 420 }, + content, + ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-hands-praying" }, + }) + if (!result) return + + const form = new DOMParser().parseFromString(result, "text/html") + const getValue = name => form.querySelector(`[name="${name}"]`)?.value + + await rollNPCMiracle(this.document, miracle, { + dicePool: parseInt(getValue("dicePool")) || 3, + bonus: parseInt(getValue("bonus")) || 0, + visibility: getValue("visibility"), + }) + } + + static async #onRollArmor() { + const actor = this.document + const sys = actor.system + const colorType = sys.armorDice?.colorDiceType || "white" + const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 + const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" + const dicePool = sys.armorDice?.value ?? 0 + + const colorChoices = Object.fromEntries( + Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) + ) + const bonusOptions = Array.from({ length: 13 }, (_, i) => { + const v = i - 6 + return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } + }) + + const content = await foundry.applications.handlebars.renderTemplate( + "systems/fvtt-oath-hammer/templates/npc-skill-dialog.hbs", + { + skillName: game.i18n.localize("OATHHAMMER.Label.ArmorDice"), + skillImg: actor.img, + dicePool, + colorEmoji, + colorType, + threshold, + bonusOptions, + colorChoices, + rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), + visibility: game.settings.get("core", "rollMode"), + } + ) + + const result = await foundry.applications.api.DialogV2.prompt({ + window: { title: game.i18n.localize("OATHHAMMER.Label.ArmorDice"), resizable: true }, + classes: ["fvtt-oath-hammer"], + position: { width: 420 }, + content, + ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6" }, + }) + if (!result) return + + const form = new DOMParser().parseFromString(result, "text/html") + const getValue = name => form.querySelector(`[name="${name}"]`)?.value + + await rollNPCArmor(actor, { + bonus: parseInt(getValue("bonus")) || 0, + colorOverride: getValue("colorOverride") || null, + visibility: getValue("visibility"), + }) } static async #onRollInitiative() { @@ -95,4 +354,53 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { await rollInitiativeCheck(actor) } } + + static async #onRollSkillNPC(event, target) { + const itemId = target.dataset.itemId + const item = this.document.items.get(itemId) + if (!item) return + + const colorChoices = Object.fromEntries( + Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) + ) + const bonusOptions = Array.from({ length: 13 }, (_, i) => { + const v = i - 6 + return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } + }) + + const content = await foundry.applications.handlebars.renderTemplate( + "systems/fvtt-oath-hammer/templates/npc-skill-dialog.hbs", + { + skillName: item.name, + skillImg: item.img, + dicePool: item.system.dicePool, + colorEmoji: item.system.colorEmoji, + colorType: item.system.colorDiceType, + threshold: item.system.threshold, + bonusOptions, + colorChoices, + rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), + visibility: game.settings.get("core", "rollMode"), + } + ) + + const result = await foundry.applications.api.DialogV2.prompt({ + window: { title: item.name, resizable: true }, + classes: ["fvtt-oath-hammer"], + position: { width: 420 }, + content, + ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6" }, + }) + if (!result) return + + const form = new DOMParser().parseFromString(result, "text/html") + const getValue = name => form.querySelector(`[name="${name}"]`)?.value + + await rollNPCSkill(this.document, item, { + bonus: parseInt(getValue("bonus")) || 0, + colorOverride: getValue("colorOverride") || null, + visibility: getValue("visibility"), + }) + } + } diff --git a/module/applications/sheets/npcattack-sheet.mjs b/module/applications/sheets/npcattack-sheet.mjs new file mode 100644 index 0000000..c165e9b --- /dev/null +++ b/module/applications/sheets/npcattack-sheet.mjs @@ -0,0 +1,28 @@ +import OathHammerItemSheet from "./base-item-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" + +export default class OathHammerNpcAttackSheet extends OathHammerItemSheet { + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["npcattack"], + position: { width: 460 }, + window: { contentClasses: ["npcattack-content"] }, + } + + /** @override */ + static PARTS = { + main: { template: "systems/fvtt-oath-hammer/templates/item/npcattack-sheet.hbs" }, + } + + /** @override */ + async _prepareContext() { + const context = await super._prepareContext() + context.dicePoolChoices = Object.fromEntries( + Array.from({ length: 21 }, (_, i) => [i, String(i)]) + ) + context.colorChoices = Object.fromEntries( + Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) + ) + return context + } +} diff --git a/module/applications/sheets/regiment-sheet.mjs b/module/applications/sheets/regiment-sheet.mjs new file mode 100644 index 0000000..3dbb63d --- /dev/null +++ b/module/applications/sheets/regiment-sheet.mjs @@ -0,0 +1,80 @@ +import OathHammerItemSheet from "./base-item-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" + +export default class OathHammerRegimentSheet extends OathHammerItemSheet { + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["regiment"], + position: { width: 560, height: "auto" }, + window: { contentClasses: ["regiment-content"] }, + actions: { + addSkill: OathHammerRegimentSheet.#onAddSkill, + removeSkill: OathHammerRegimentSheet.#onRemoveSkill, + addAttack: OathHammerRegimentSheet.#onAddAttack, + removeAttack:OathHammerRegimentSheet.#onRemoveAttack, + addTrait: OathHammerRegimentSheet.#onAddTrait, + removeTrait: OathHammerRegimentSheet.#onRemoveTrait, + }, + } + + /** @override */ + static PARTS = { + main: { template: "systems/fvtt-oath-hammer/templates/item/regiment-sheet.hbs" }, + } + + /** @override */ + async _prepareContext() { + const context = await super._prepareContext() + context.colorChoices = Object.fromEntries( + Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) + ) + context.dicePoolChoices = Object.fromEntries( + Array.from({ length: 21 }, (_, i) => [i, String(i)]) + ) + context.apChoices = Object.fromEntries( + Array.from({ length: 7 }, (_, i) => [i, String(i)]) + ) + return context + } + + // ── Array helpers ──────────────────────────────────────────────────────────── + + static async #onAddSkill() { + const skills = foundry.utils.deepClone(this.document.system.skills ?? []) + skills.push({ name: "", value: 2, colorDiceType: "white" }) + await this.document.update({ "system.skills": skills }) + } + + static async #onRemoveSkill(event, target) { + const idx = parseInt(target.dataset.idx, 10) + const skills = foundry.utils.deepClone(this.document.system.skills ?? []) + skills.splice(idx, 1) + await this.document.update({ "system.skills": skills }) + } + + static async #onAddAttack() { + const attacks = foundry.utils.deepClone(this.document.system.attacks ?? []) + attacks.push({ name: "", damageDice: 6, colorDiceType: "white", ap: 0, special: "" }) + await this.document.update({ "system.attacks": attacks }) + } + + static async #onRemoveAttack(event, target) { + const idx = parseInt(target.dataset.idx, 10) + const attacks = foundry.utils.deepClone(this.document.system.attacks ?? []) + attacks.splice(idx, 1) + await this.document.update({ "system.attacks": attacks }) + } + + static async #onAddTrait() { + const traits = foundry.utils.deepClone(this.document.system.traits ?? []) + traits.push({ name: "", description: "" }) + await this.document.update({ "system.traits": traits }) + } + + static async #onRemoveTrait(event, target) { + const idx = parseInt(target.dataset.idx, 10) + const traits = foundry.utils.deepClone(this.document.system.traits ?? []) + traits.splice(idx, 1) + await this.document.update({ "system.traits": traits }) + } +} diff --git a/module/applications/sheets/settlement-sheet.mjs b/module/applications/sheets/settlement-sheet.mjs index ead6f46..9763a35 100644 --- a/module/applications/sheets/settlement-sheet.mjs +++ b/module/applications/sheets/settlement-sheet.mjs @@ -1,6 +1,6 @@ import OathHammerActorSheet from "./base-actor-sheet.mjs" -const ALLOWED_ITEM_TYPES = new Set(["building", "equipment", "weapon", "armor"]) +const ALLOWED_ITEM_TYPES = new Set(["building", "equipment", "weapon", "armor", "regiment"]) export default class OathHammerSettlementSheet extends OathHammerActorSheet { /** @override */ @@ -14,9 +14,11 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet { contentClasses: ["settlement-content"], }, actions: { - adjustCurrency: OathHammerSettlementSheet.#onAdjustCurrency, - adjustQty: OathHammerSettlementSheet.#onAdjustQty, + adjustCurrency: OathHammerSettlementSheet.#onAdjustCurrency, + adjustQty: OathHammerSettlementSheet.#onAdjustQty, toggleConstructed: OathHammerSettlementSheet.#onToggleConstructed, + createRegiment: OathHammerSettlementSheet.#onCreateRegiment, + collectTaxes: OathHammerSettlementSheet.#onCollectTaxes, }, } @@ -37,6 +39,9 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet { inventory: { template: "systems/fvtt-oath-hammer/templates/actor/settlement-inventory.hbs", }, + garrison: { + template: "systems/fvtt-oath-hammer/templates/actor/settlement-garrison.hbs", + }, } /** @override */ @@ -46,9 +51,10 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet { #getTabs() { const tabs = { - overview: { id: "overview", group: "sheet", icon: "fa-solid fa-city", label: "OATHHAMMER.Tab.Overview" }, - buildings: { id: "buildings", group: "sheet", icon: "fa-solid fa-building", label: "OATHHAMMER.Tab.Buildings" }, - inventory: { id: "inventory", group: "sheet", icon: "fa-solid fa-boxes-stacked", label: "OATHHAMMER.Tab.Inventory" }, + overview: { id: "overview", group: "sheet", icon: "fa-solid fa-city", label: "OATHHAMMER.Tab.Overview" }, + buildings: { id: "buildings", group: "sheet", icon: "fa-solid fa-building", label: "OATHHAMMER.Tab.Buildings" }, + inventory: { id: "inventory", group: "sheet", icon: "fa-solid fa-boxes-stacked", label: "OATHHAMMER.Tab.Inventory" }, + garrison: { id: "garrison", group: "sheet", icon: "fa-solid fa-shield-halved", label: "OATHHAMMER.Tab.Garrison" }, } for (const v of Object.values(tabs)) { v.active = this.tabGroups[v.group] === v.id @@ -81,7 +87,11 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet { break case "buildings": context.tab = context.tabs.buildings - context.buildings = doc.itemTypes.building + context.buildings = doc.itemTypes.building.map(b => ({ + id: b.id, uuid: b.uuid, img: b.img, name: b.name, system: b.system, + _descTooltip: b.system.description?.replace(/<[^>]+>/g, "").trim().slice(0, 400) ?? "" + })) + context.hasTaxBuildings = doc.itemTypes.building.some(b => b.system.constructed && b.system.taxRevenue?.trim()) break case "inventory": { context.tab = context.tabs.inventory @@ -90,6 +100,10 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet { context.equipments = doc.itemTypes.equipment break } + case "garrison": + context.tab = context.tabs.garrison + context.regiments = doc.itemTypes.regiment ?? [] + break } return context } @@ -129,4 +143,59 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet { if (!item) return await item.update({ "system.constructed": !item.system.constructed }) } + + static async #onCreateRegiment() { + await this.document.createEmbeddedDocuments("Item", [{ + name: game.i18n.localize("OATHHAMMER.NewItem.Regiment"), + type: "regiment", + }]) + } + + static async #onCollectTaxes() { + const actor = this.document + // Only constructed buildings with a non-empty taxRevenue formula + const taxBuildings = actor.itemTypes.building.filter( + b => b.system.constructed && b.system.taxRevenue?.trim() + ) + if (!taxBuildings.length) { + ui.notifications.warn(game.i18n.localize("OATHHAMMER.Settlement.NoTaxRevenue")) + return + } + + // Roll each building's formula individually, sum totals + const rolls = [] + const lines = [] + let total = 0 + for (const b of taxBuildings) { + const r = new Roll(b.system.taxRevenue.trim()) + await r.evaluate() + rolls.push(r) + total += r.total + lines.push(`