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(`
  • ${b.name} — ${b.system.taxRevenue} = ${r.total} gp
  • `) + } + + const content = ` +
    +
    + 🏛 ${actor.name} + ${game.i18n.localize("OATHHAMMER.Settlement.CollectTaxes")} +
    +
    + +
    +
    + ${total} gp + ${game.i18n.localize("OATHHAMMER.Settlement.TotalRevenue")} +
    +
    ` + + const msgData = { + speaker: ChatMessage.getSpeaker({ actor }), + content, + rolls, + sound: CONFIG.sounds.dice, + } + ChatMessage.applyRollMode(msgData, game.settings.get("core", "rollMode")) + await ChatMessage.create(msgData) + } } diff --git a/module/applications/sheets/skillnpc-sheet.mjs b/module/applications/sheets/skillnpc-sheet.mjs new file mode 100644 index 0000000..620ee71 --- /dev/null +++ b/module/applications/sheets/skillnpc-sheet.mjs @@ -0,0 +1,37 @@ +import OathHammerItemSheet from "./base-item-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" + +export default class OathHammerSkillNPCSheet extends OathHammerItemSheet { + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["skillnpc"], + position: { width: 460 }, + window: { contentClasses: ["skillnpc-content"] }, + } + + /** @override */ + static PARTS = { + main: { template: "systems/fvtt-oath-hammer/templates/item/skillnpc-sheet.hbs" }, + } + + /** @override */ + async _prepareContext() { + const context = await super._prepareContext() + // Build dicePool selector (0–20) + context.dicePoolChoices = Object.fromEntries( + Array.from({ length: 21 }, (_, i) => [i, String(i)]) + ) + // Color choices (localized labels) + context.colorChoices = Object.fromEntries( + Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) + ) + // Skill reference choices (optional) + context.skillRefChoices = { + "": `— ${game.i18n.localize("OATHHAMMER.Label.None")} —`, + ...Object.fromEntries( + Object.entries(SYSTEM.SKILLS).map(([k, v]) => [k, game.i18n.localize(v.label)]) + ) + } + return context + } +} diff --git a/module/applications/weapon-dialog.mjs b/module/applications/weapon-dialog.mjs index e153555..c9c1fe2 100644 --- a/module/applications/weapon-dialog.mjs +++ b/module/applications/weapon-dialog.mjs @@ -20,7 +20,7 @@ export default class OathHammerWeaponDialog { const actorSys = actor.system const isRanged = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing" - const skillKey = isRanged ? "shooting" : "fighting" + const skillKey = (sys.skillOverride && SYSTEM.SKILLS[sys.skillOverride]) ? sys.skillOverride : (isRanged ? "shooting" : "fighting") const skillDef = SYSTEM.SKILLS[skillKey] const defaultAttr = skillDef.attribute const attrRank = actorSys.attributes[defaultAttr].rank @@ -53,7 +53,8 @@ export default class OathHammerWeaponDialog { 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 damageAttrRank = actorSys.attributes[skillDef.attribute].rank + const baseDamageDice = sys.usesMight ? Math.max(damageAttrRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1) const traitLabels = [...sys.traits].map(t => { const key = SYSTEM.WEAPON_TRAITS[t] @@ -329,8 +330,11 @@ export default class OathHammerWeaponDialog { 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 isRanged = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing" + const damageSkillKey = (sys.skillOverride && SYSTEM.SKILLS[sys.skillOverride]) ? sys.skillOverride : (isRanged ? "shooting" : "fighting") + const damageAttrKey = SYSTEM.SKILLS[damageSkillKey].attribute + const damageAttrRank = actorSys.attributes[damageAttrKey].rank + const baseDamageDice = sys.usesMight ? Math.max(damageAttrRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1) // Auto-bonuses from special properties let autoDamageBonus = 0 diff --git a/module/config/system.mjs b/module/config/system.mjs index c6bca10..a59e404 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -177,6 +177,19 @@ export const MAGIC_QUALITY_CHOICES = { legendary: "OATHHAMMER.MagicQuality.Legendary" } +export const CLASS_RESTRICTION_CHOICES = { + berserker: "OATHHAMMER.Class.Berserker", + champion: "OATHHAMMER.Class.Champion", + delver: "OATHHAMMER.Class.Delver", + knight: "OATHHAMMER.Class.Knight", + mage: "OATHHAMMER.Class.Mage", + priest: "OATHHAMMER.Class.Priest", + scout: "OATHHAMMER.Class.Scout", + soldier: "OATHHAMMER.Class.Soldier", + spellblade: "OATHHAMMER.Class.Spellblade", + troubadour: "OATHHAMMER.Class.Troubadour", +} + export const RARITY_CHOICES = { always: "OATHHAMMER.Rarity.Always", common: "OATHHAMMER.Rarity.Common", @@ -198,9 +211,22 @@ export const RARITY_DV = { // Two types of trait per the rulebook terminology export const TRAIT_TYPE_CHOICES = { - "special-trait": "OATHHAMMER.TraitType.SpecialTrait", - "class-trait": "OATHHAMMER.TraitType.ClassTrait", - "lineage-trait": "OATHHAMMER.TraitType.LineageTrait" + "special-trait": "OATHHAMMER.TraitType.SpecialTrait", + "class-trait": "OATHHAMMER.TraitType.ClassTrait", + "lineage-trait": "OATHHAMMER.TraitType.LineageTrait", + "npc-trait": "OATHHAMMER.TraitType.NpcTrait", + "creature-trait": "OATHHAMMER.TraitType.CreatureTrait" +} + +export const NPC_SUBTYPES = { + "creature": "OATHHAMMER.NpcSubtype.Creature", + "npc": "OATHHAMMER.NpcSubtype.Npc" +} + +export const DICE_COLOR_TYPES = { + "white": "OATHHAMMER.DiceColor.White", + "red": "OATHHAMMER.DiceColor.Red", + "black": "OATHHAMMER.DiceColor.Black" } // When a trait's uses reset (none = passive/always on) @@ -395,10 +421,13 @@ export const SYSTEM = { EQUIPMENT_TYPE_CHOICES, MAGIC_ITEM_TYPE_CHOICES, MAGIC_QUALITY_CHOICES, + CLASS_RESTRICTION_CHOICES, RARITY_CHOICES, RARITY_DV, TRAIT_TYPE_CHOICES, TRAIT_USAGE_PERIOD, + NPC_SUBTYPES, + DICE_COLOR_TYPES, BUILDING_SKILL_CHOICES, SETTLEMENT_ARCHETYPES, STATUS_EFFECTS, diff --git a/module/models/_module.mjs b/module/models/_module.mjs index 829da3e..734ac17 100644 --- a/module/models/_module.mjs +++ b/module/models/_module.mjs @@ -12,3 +12,6 @@ export { default as OathHammerOath } from "./oath.mjs" export { default as OathHammerClass } from "./class.mjs" export { default as OathHammerBuilding } from "./building.mjs" export { default as OathHammerSettlement } from "./settlement.mjs" +export { default as OathHammerSkillNPC } from "./skillnpc.mjs" +export { default as OathHammerNpcAttack } from "./npcattack.mjs" +export { default as OathHammerRegiment } from "./regiment.mjs" diff --git a/module/models/character.mjs b/module/models/character.mjs index be6b456..57841b3 100644 --- a/module/models/character.mjs +++ b/module/models/character.mjs @@ -31,7 +31,7 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel // Total dice = attr rank + skill rank. Modifier = bonus (+) or penalty (-) dice. // Color dice: type (white 4+, red 3+, black 2+) + count of colored dice in the pool. const skillField = () => new fields.SchemaField({ - rank: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 4 }), + rank: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 6 }), modifier: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), colorDiceType: new fields.StringField({ required: true, nullable: false, initial: "white", choices: { white: "OATHHAMMER.ColorDice.White", red: "OATHHAMMER.ColorDice.Red", black: "OATHHAMMER.ColorDice.Black" } }), diff --git a/module/models/npc.mjs b/module/models/npc.mjs index 4730e1b..6e34241 100644 --- a/module/models/npc.mjs +++ b/module/models/npc.mjs @@ -9,16 +9,9 @@ export default class OathHammerNPC extends foundry.abstract.TypeDataModel { schema.description = new fields.HTMLField({ required: true, textSearch: true }) schema.notes = new fields.HTMLField({ required: true, textSearch: true }) - const attributeField = () => new fields.SchemaField({ - rank: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1, max: 4 }) - }) - schema.attributes = new fields.SchemaField({ - might: attributeField(), - toughness: attributeField(), - agility: attributeField(), - willpower: attributeField(), - intelligence: attributeField(), - fate: attributeField() + // NPC (humanoid, needs light) vs Creature (monster, darkvision) + schema.subtype = new fields.StringField({ + required: true, initial: "creature", choices: SYSTEM.NPC_SUBTYPES }) schema.grit = new fields.SchemaField({ @@ -26,8 +19,15 @@ export default class OathHammerNPC extends foundry.abstract.TypeDataModel { max: new fields.NumberField({ ...requiredInteger, initial: 2, min: 0 }) }) + // Armor dice pool (value + color) + schema.armorDice = new fields.SchemaField({ + value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 20 }), + colorDiceType: new fields.StringField({ required: true, initial: "white", choices: SYSTEM.DICE_COLOR_TYPES }) + }) + schema.defense = new fields.SchemaField({ - value: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }) + value: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }), + colorDiceType: new fields.StringField({ required: true, initial: "white", choices: SYSTEM.DICE_COLOR_TYPES }) }) schema.movement = new fields.SchemaField({ @@ -37,7 +37,7 @@ export default class OathHammerNPC extends foundry.abstract.TypeDataModel { schema.attackBonus = new fields.NumberField({ ...requiredInteger, initial: 0 }) schema.damageBonus = new fields.NumberField({ ...requiredInteger, initial: 0 }) schema.initiativeBonus = new fields.NumberField({ ...requiredInteger, initial: 0 }) - schema.challengeRating = new fields.StringField({ required: true, nullable: false, initial: "1" }) + schema.challengeRating = new fields.StringField({ required: true, nullable: false, initial: "1" }) return schema } @@ -46,6 +46,5 @@ export default class OathHammerNPC extends foundry.abstract.TypeDataModel { prepareDerivedData() { super.prepareDerivedData() - this.grit.max = this.attributes.might.rank + this.attributes.toughness.rank } } diff --git a/module/models/npcattack.mjs b/module/models/npcattack.mjs new file mode 100644 index 0000000..71ac93d --- /dev/null +++ b/module/models/npcattack.mjs @@ -0,0 +1,42 @@ +import { SYSTEM } from "../config/system.mjs" + +export default class OathHammerNpcAttack extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const requiredInteger = { required: true, nullable: false, integer: true } + const schema = {} + + schema.description = new fields.HTMLField({ required: true, textSearch: true }) + + // Flat damage dice pool (no Might) + schema.damageDice = new fields.NumberField({ + ...requiredInteger, initial: 1, min: 0, max: 20 + }) + + // Dice color: white (4+), red (3+), black (2+) + schema.colorDiceType = new fields.StringField({ + required: true, initial: "white", choices: SYSTEM.DICE_COLOR_TYPES + }) + + // AP (Armor Penetration): penalty imposed on armor rolls + schema.ap = new fields.NumberField({ + ...requiredInteger, initial: 0, min: 0, max: 16 + }) + + return schema + } + + static LOCALIZATION_PREFIXES = ["OATHHAMMER.NpcAttack"] + + get threshold() { + return this.colorDiceType === "black" ? 2 : this.colorDiceType === "red" ? 3 : 4 + } + + get colorEmoji() { + return this.colorDiceType === "black" ? "⬛" : this.colorDiceType === "red" ? "🔴" : "⬜" + } + + get damageLabel() { + return `${this.colorEmoji} ${this.damageDice}d (${this.threshold}+)` + } +} diff --git a/module/models/regiment.mjs b/module/models/regiment.mjs new file mode 100644 index 0000000..0ea998e --- /dev/null +++ b/module/models/regiment.mjs @@ -0,0 +1,57 @@ +import { SYSTEM } from "../config/system.mjs" + +export default class OathHammerRegiment extends foundry.abstract.TypeDataModel { + static defineSchema() { + const { fields } = foundry.data + const requiredInteger = { required: true, nullable: false, integer: true } + const schema = {} + + schema.description = new fields.HTMLField({ required: false, nullable: true, initial: "" }) + schema.notes = new fields.StringField({ required: false, nullable: true, initial: "" }) + + schema.grit = new fields.SchemaField({ + max: new fields.NumberField({ ...requiredInteger, initial: 20, min: 0, max: 200 }), + }) + + schema.armorDice = new fields.SchemaField({ + value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0, max: 20 }), + colorDiceType: new fields.StringField({ required: true, nullable: false, initial: "white", choices: SYSTEM.DICE_COLOR_TYPES }), + }) + + schema.movement = new fields.NumberField({ ...requiredInteger, initial: 60, min: 0, max: 500 }) + + // Embedded skill rows: [{name, value, colorDiceType}] + schema.skills = new fields.ArrayField(new fields.SchemaField({ + name: new fields.StringField({ required: true, nullable: false, initial: "" }), + value: new fields.NumberField({ ...requiredInteger, initial: 2, min: 0, max: 6 }), + colorDiceType: new fields.StringField({ required: true, nullable: false, initial: "white", choices: SYSTEM.DICE_COLOR_TYPES }), + })) + + // Embedded attack rows: [{name, damageDice, colorDiceType, ap, special}] + schema.attacks = new fields.ArrayField(new fields.SchemaField({ + name: new fields.StringField({ required: true, nullable: false, initial: "" }), + damageDice: new fields.NumberField({ ...requiredInteger, initial: 6, min: 0, max: 20 }), + colorDiceType: new fields.StringField({ required: true, nullable: false, initial: "white", choices: SYSTEM.DICE_COLOR_TYPES }), + ap: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 6 }), + special: new fields.StringField({ required: false, nullable: true, initial: "" }), + })) + + // Embedded trait rows: [{name, description}] + schema.traits = new fields.ArrayField(new fields.SchemaField({ + name: new fields.StringField({ required: true, nullable: false, initial: "" }), + description: new fields.StringField({ required: false, nullable: true, initial: "" }), + })) + + return schema + } + + static LOCALIZATION_PREFIXES = ["OATHHAMMER.Regiment"] + + get colorEmoji() { + return { white: "⬜", red: "🔴", black: "⬛" }[this.armorDice.colorDiceType] ?? "⬜" + } + + get armorLabel() { + return `${this.colorEmoji} ${this.armorDice.value}d` + } +} diff --git a/module/models/skillnpc.mjs b/module/models/skillnpc.mjs new file mode 100644 index 0000000..8770028 --- /dev/null +++ b/module/models/skillnpc.mjs @@ -0,0 +1,36 @@ +import { SYSTEM } from "../config/system.mjs" + +export default class OathHammerSkillNPC extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const schema = {} + + schema.description = new fields.HTMLField({ required: true, textSearch: true }) + + // Total dice pool for this skill (attribute + skill ranks combined) + schema.dicePool = new fields.NumberField({ + required: true, nullable: false, integer: true, initial: 1, min: 0, max: 20 + }) + + // Dice color: white (4+), red (3+), black (2+) + schema.colorDiceType = new fields.StringField({ + required: true, initial: "white", choices: SYSTEM.DICE_COLOR_TYPES + }) + + // Optional reference to a system skill key (e.g. "fighting", "perception") + // Used for display/tooltip only — does not restrict the roll. + schema.skillRef = new fields.StringField({ required: false, nullable: true, initial: null }) + + return schema + } + + static LOCALIZATION_PREFIXES = ["OATHHAMMER.SkillNPC"] + + get threshold() { + return this.colorDiceType === "black" ? 2 : this.colorDiceType === "red" ? 3 : 4 + } + + get colorEmoji() { + return this.colorDiceType === "black" ? "⬛" : this.colorDiceType === "red" ? "🔴" : "⬜" + } +} diff --git a/module/models/weapon.mjs b/module/models/weapon.mjs index 33d7dbd..37338b6 100644 --- a/module/models/weapon.mjs +++ b/module/models/weapon.mjs @@ -57,7 +57,12 @@ export default class OathHammerWeapon extends foundry.abstract.TypeDataModel { // Enchantment description (displayed when isMagic is true) schema.magicEffect = new fields.HTMLField({ required: false, textSearch: true }) // Class/lineage restriction, e.g. "Dwarves only" (empty = no restriction) - schema.classRestriction = new fields.StringField({ required: true, nullable: false, initial: "" }) + schema.classRestriction = new fields.StringField({ required: false, nullable: true, initial: null, choices: SYSTEM.CLASS_RESTRICTION_CHOICES }) + + // Override which skill (and its linked attribute) is used for attack rolls. + // Null / "" = auto-detect (fighting for melee, shooting for ranged). + // Use this for abilities like Magic Bolt that roll Magic+Willpower instead. + schema.skillOverride = new fields.StringField({ required: false, nullable: true, initial: null }) return schema } diff --git a/module/rolls.mjs b/module/rolls.mjs index 7a460c5..dfe8ead 100644 --- a/module/rolls.mjs +++ b/module/rolls.mjs @@ -242,7 +242,7 @@ export async function rollWeaponAttack(actor, weapon, options = {}) { const actorSys = actor.system const isRanged = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing" - const skillKey = isRanged ? "shooting" : "fighting" + const skillKey = (sys.skillOverride && SYSTEM.SKILLS[sys.skillOverride]) ? sys.skillOverride : (isRanged ? "shooting" : "fighting") const skillDef = SYSTEM.SKILLS[skillKey] const defaultAttr = skillDef.attribute @@ -339,8 +339,11 @@ export async function rollWeaponDamage(actor, weapon, options = {}) { 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 isRangedDmg = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing" + const dmgSkillKey = (sys.skillOverride && SYSTEM.SKILLS[sys.skillOverride]) ? sys.skillOverride : (isRangedDmg ? "shooting" : "fighting") + const dmgAttrKey = SYSTEM.SKILLS[dmgSkillKey].attribute + const dmgAttrRank = actorSys.attributes[dmgAttrKey].rank + const baseDamageDice = sys.usesMight ? Math.max(dmgAttrRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1) const totalDice = Math.max(baseDamageDice + sv + damageBonus + autoDamageBonus, 1) const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold) @@ -951,3 +954,330 @@ export async function rollInitiativeCheck(actor, options = {}) { return { successes, dv: 0, isSuccess: null } } + +// ============================================================ +// NPC SKILL ROLL +// ============================================================ + +/** + * Roll an NPC skill check (skillnpc item) and post to chat. + * + * @param {Actor} actor The NPC/creature actor + * @param {Item} skillItem The skillnpc item + * @param {object} options + */ +export async function rollNPCSkill(actor, skillItem, options = {}) { + const { bonus = 0, colorOverride, visibility } = options + const sys = skillItem.system + + const colorType = colorOverride || sys.colorDiceType + const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 + const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" + + const totalDice = Math.max(sys.dicePool + bonus, 1) + + const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false) + const diceHtml = _diceHtml(diceResults, threshold) + + const modLine = bonus !== 0 + ? `
    ${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}
    ` + : "" + + const content = ` +
    +
    + ${skillItem.name} + ${skillItem.name} — ${actor.name} +
    +
    + ${colorEmoji} ${totalDice}d6 (${threshold}+) +
    + ${modLine} +
    ${diceHtml}
    +
    + ${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")} +
    +
    + ` + + const rollMode = visibility ?? game.settings.get("core", "rollMode") + const msgData = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice } + ChatMessage.applyRollMode(msgData, rollMode) + await ChatMessage.create(msgData) + + return { successes } +} + +/** + * NPC weapon attack roll — uses NPC's flat attackBonus as dice pool. + * Rolls white dice (4+) with optional bonus modifier. + */ +export async function rollNPCWeaponAttack(actor, weapon, options = {}) { + const { bonus = 0, visibility } = options + const sys = actor.system + const basePool = sys.attackBonus ?? 0 + const totalDice = Math.max(basePool + bonus, 1) + const threshold = 4 + const colorEmoji = "⬜" + + const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false) + const diceHtml = _diceHtml(diceResults, threshold) + + const modParts = [] + if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) + const modLine = modParts.length ? `
    ${modParts.join(" · ")}
    ` : "" + + const content = ` +
    +
    + ${weapon.name} + ${weapon.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Attack")} (${actor.name}) +
    +
    + ${colorEmoji} ${totalDice}d6 (${threshold}+) +
    + ${modLine} +
    ${diceHtml}
    +
    + ${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")} +
    +
    + ` + + const rollMode = visibility ?? game.settings.get("core", "rollMode") + const msgData = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice } + ChatMessage.applyRollMode(msgData, rollMode) + await ChatMessage.create(msgData) + return { successes } +} + +/** + * NPC weapon damage roll — uses NPC damageBonus + weapon damageMod as dice pool. + */ +export async function rollNPCWeaponDamage(actor, weapon, options = {}) { + const { bonus = 0, visibility } = options + const sys = actor.system + const basePool = (sys.damageBonus ?? 0) + (weapon.system.damageMod ?? 0) + const totalDice = Math.max(basePool + bonus, 1) + const threshold = 4 + const colorEmoji = "⬜" + + const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false) + const diceHtml = _diceHtml(diceResults, threshold) + + const modParts = [] + if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) + const modLine = modParts.length ? `
    ${modParts.join(" · ")}
    ` : "" + + const content = ` +
    +
    + ${weapon.name} + ${weapon.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Damage")} (${actor.name}) +
    +
    + ${colorEmoji} ${totalDice}d6 (${threshold}+) + ${game.i18n.localize("OATHHAMMER.Label.Damage")}: ${weapon.system.damageLabel} +
    + ${modLine} +
    ${diceHtml}
    +
    + ${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")} +
    +
    + ` + + const rollMode = visibility ?? game.settings.get("core", "rollMode") + const msgData = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice } + ChatMessage.applyRollMode(msgData, rollMode) + await ChatMessage.create(msgData) + return { successes } +} + +/** + * NPC armor dice roll — rolls actor's armorDice.value dice with armorDice.colorDiceType color. + */ +export async function rollNPCArmor(actor, options = {}) { + const { bonus = 0, colorOverride, visibility } = options + const sys = actor.system + + const basePool = sys.armorDice?.value ?? 0 + const colorType = colorOverride || sys.armorDice?.colorDiceType || "white" + const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 + const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" + const totalDice = Math.max(basePool + bonus, 1) + + const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false) + const diceHtml = _diceHtml(diceResults, threshold) + + const modLine = bonus !== 0 + ? `
    ${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}
    ` + : "" + + const label = game.i18n.localize("OATHHAMMER.Label.ArmorDice") + const content = ` +
    +
    + ${actor.name} + ${label} — ${actor.name} +
    +
    + ${colorEmoji} ${totalDice}d6 (${threshold}+) +
    + ${modLine} +
    ${diceHtml}
    +
    + ${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")} + −${successes} ${game.i18n.localize("OATHHAMMER.Roll.Damage")} +
    +
    + ` + + const rollMode = visibility ?? game.settings.get("core", "rollMode") + const msgData = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice } + ChatMessage.applyRollMode(msgData, rollMode) + await ChatMessage.create(msgData) + return { successes } +} + +/** + * NPC spell cast — flat dice pool, no arcane stress, posts DV success/failure to chat. + */ +export async function rollNPCSpell(actor, spell, options = {}) { + const { dicePool = 3, bonus = 0, colorOverride, visibility } = options + const dv = spell.system.difficultyValue ?? 1 + const colorType = colorOverride || "white" + const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 + const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" + const totalDice = Math.max(dicePool + bonus, 1) + + const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false) + const diceHtml = _diceHtml(diceResults, threshold) + const isSuccess = successes >= dv + + const modLine = bonus !== 0 + ? `
    ${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}
    ` + : "" + + const resultClass = isSuccess ? "roll-success" : "roll-failure" + const resultLabel = isSuccess + ? game.i18n.localize("OATHHAMMER.Roll.Success") + : game.i18n.localize("OATHHAMMER.Roll.Failure") + + const content = ` +
    +
    + ${spell.name} + ${spell.name} (DV ${dv}) — ${actor.name} +
    +
    + ${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, sound: CONFIG.sounds.dice } + ChatMessage.applyRollMode(msgData, rollMode) + await ChatMessage.create(msgData) + return { successes, dv, isSuccess } +} + +/** + * NPC miracle invocation — flat dice pool, no blocked tracking, posts DV success/failure to chat. + */ +export async function rollNPCMiracle(actor, miracle, options = {}) { + const { dicePool = 3, bonus = 0, visibility } = options + const dv = 1 + const threshold = 4 + const colorEmoji = "⬜" + const totalDice = Math.max(dicePool + bonus, 1) + + const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false) + const diceHtml = _diceHtml(diceResults, threshold) + const isSuccess = successes >= dv + + const modLine = bonus !== 0 + ? `
    ${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}
    ` + : "" + + const resultClass = isSuccess ? "roll-success" : "roll-failure" + const resultLabel = isSuccess + ? game.i18n.localize("OATHHAMMER.Roll.Success") + : game.i18n.localize("OATHHAMMER.Roll.Failure") + + const content = ` +
    +
    + ${miracle.name} + ${miracle.name} — ${actor.name} +
    +
    + ${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, sound: CONFIG.sounds.dice } + ChatMessage.applyRollMode(msgData, rollMode) + await ChatMessage.create(msgData) + return { successes, dv, isSuccess } +} + +/** + * NPC attack damage roll — flat dice pool from the npcattack item, no Might. + */ +export async function rollNPCAttackDamage(actor, attack, options = {}) { + const { bonus = 0, visibility } = options + const sys = attack.system + const colorType = sys.colorDiceType || "white" + const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 + const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" + const totalDice = Math.max((sys.damageDice ?? 1) + bonus, 1) + const ap = sys.ap ?? 0 + + const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false) + const diceHtml = _diceHtml(diceResults, threshold) + + const modParts = [] + if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) + if (ap > 0) modParts.push(`AP ${ap}`) + const modLine = modParts.length ? `
    ${modParts.join(" · ")}
    ` : "" + + const content = ` +
    +
    + ${attack.name} + ${attack.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Damage")} (${actor.name}) +
    +
    + ${colorEmoji} ${totalDice}d6 (${threshold}+) + ${ap > 0 ? `AP ${ap}` : ""} +
    + ${modLine} +
    ${diceHtml}
    +
    + ${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")} +
    +
    + ` + + const rollMode = visibility ?? game.settings.get("core", "rollMode") + const msgData = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls, sound: CONFIG.sounds.dice } + ChatMessage.applyRollMode(msgData, rollMode) + await ChatMessage.create(msgData) + return { successes } +} diff --git a/oath-hammer.mjs b/oath-hammer.mjs index 89b11be..56adf03 100644 --- a/oath-hammer.mjs +++ b/oath-hammer.mjs @@ -39,7 +39,10 @@ Hooks.once("init", function () { trait: models.OathHammerTrait, oath: models.OathHammerOath, "class": models.OathHammerClass, - building: models.OathHammerBuilding + building: models.OathHammerBuilding, + skillnpc: models.OathHammerSkillNPC, + npcattack: models.OathHammerNpcAttack, + regiment: models.OathHammerRegiment, } foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet) @@ -71,6 +74,9 @@ Hooks.once("init", function () { foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerOathSheet, { types: ["oath"], makeDefault: true, label: "OATHHAMMER.Sheet.Oath" }) foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerClassSheet, { types: ["class"], makeDefault: true, label: "OATHHAMMER.Sheet.Class" }) foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerBuildingSheet, { types: ["building"], makeDefault: true, label: "OATHHAMMER.Sheet.Building" }) + foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerSkillNPCSheet, { types: ["skillnpc"], makeDefault: true, label: "OATHHAMMER.Sheet.SkillNPC" }) + foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerNpcAttackSheet, { types: ["npcattack"], makeDefault: true, label: "OATHHAMMER.Sheet.NpcAttack" }) + foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerRegimentSheet, { types: ["regiment"], makeDefault: true, label: "OATHHAMMER.Sheet.Regiment" }) CONFIG.statusEffects = STATUS_EFFECTS diff --git a/system.json b/system.json index 0d68d90..aec6905 100644 --- a/system.json +++ b/system.json @@ -105,7 +105,10 @@ "description", "notes" ] - } + }, + "skillnpc": {}, + "npcattack": {}, + "regiment": {} } }, "grid": { diff --git a/templates/actor/character-identity.hbs b/templates/actor/character-identity.hbs index 0e45496..3a6f281 100644 --- a/templates/actor/character-identity.hbs +++ b/templates/actor/character-identity.hbs @@ -66,10 +66,4 @@ {{/if}} - {{!-- Background --}} -
    - {{localize "OATHHAMMER.Label.Background"}} - {{formInput systemFields.background enriched=enrichedBackground value=system.background name="system.background" toggled=true}} -
    - diff --git a/templates/actor/character-notes.hbs b/templates/actor/character-notes.hbs index ad18886..98f5d5e 100644 --- a/templates/actor/character-notes.hbs +++ b/templates/actor/character-notes.hbs @@ -1,4 +1,8 @@
    +
    + {{localize "OATHHAMMER.Label.Background"}} + {{formInput systemFields.background enriched=enrichedBackground value=system.background name="system.background" toggled=true}} +
    {{localize "OATHHAMMER.Label.Description"}} {{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} diff --git a/templates/actor/npc-combat.hbs b/templates/actor/npc-combat.hbs index d00a763..9430b01 100644 --- a/templates/actor/npc-combat.hbs +++ b/templates/actor/npc-combat.hbs @@ -1,29 +1,41 @@ -
    +
    {{localize "OATHHAMMER.Dialog.RollInitiative"}} {{#if combatantInitiative}}⚔ {{combatantInitiative}}{{/if}}
    +
    - {{localize "OATHHAMMER.Label.Weapons"}} - {{#if weapons.length}} -
      - {{#each weapons as |weapon|}} -
    • - - {{weapon.name}} - {{weapon.system.damageLabel}} - AP: {{weapon.system.ap}} - {{#unless ../isPlayMode}} - - - {{/unless}} + {{localize "OATHHAMMER.Label.Attacks"}} + {{#unless isPlayMode}}{{/unless}} + + {{#if npcAttacks.length}} +
        +
      • + + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.Damage"}} + AP + +
      • + {{#each npcAttacks as |attack|}} +
      • + + {{attack.name}} + {{attack.system.damageLabel}} + {{#if attack.system.ap}}{{attack.system.ap}}{{else}}—{{/if}} +
        + + + +
      • {{/each}}
      {{else}} -

      {{localize "OATHHAMMER.Label.NoWeapons"}}

      +

      {{localize "OATHHAMMER.Label.NoAttacks"}}

      {{/if}}
    +
    diff --git a/templates/actor/npc-equipment.hbs b/templates/actor/npc-equipment.hbs new file mode 100644 index 0000000..557cb21 --- /dev/null +++ b/templates/actor/npc-equipment.hbs @@ -0,0 +1,65 @@ +
    + {{#unless isPlayMode}} +

    {{localize "OATHHAMMER.Label.EquipmentNPCHint"}}

    + {{/unless}} + + {{! Armor }} +
    + {{localize "OATHHAMMER.Label.Armor"}} + {{#if armors.length}} +
      +
    • + + {{localize "OATHHAMMER.Label.Name"}} + AV + {{localize "OATHHAMMER.Label.Penalty"}} + +
    • + {{#each armors as |armor|}} +
    • + + {{armor.name}} + {{armor.system.armorValue}} + {{#if armor.system.penalty}}{{armor.system.penalty}}{{else}}—{{/if}} +
      + {{#unless ../isPlayMode}} + + + {{/unless}} +
      +
    • + {{/each}} +
    + {{else}} +

    {{localize "OATHHAMMER.Label.NoArmor"}}

    + {{/if}} +
    + + {{! Equipment }} +
    + {{localize "OATHHAMMER.Label.Equipment"}} + {{#if equipment.length}} +
      +
    • + + {{localize "OATHHAMMER.Label.Name"}} + +
    • + {{#each equipment as |item|}} +
    • + + {{item.name}} +
      + {{#unless ../isPlayMode}} + + + {{/unless}} +
      +
    • + {{/each}} +
    + {{else}} +

    {{localize "OATHHAMMER.Label.NoEquipment"}}

    + {{/if}} +
    +
    diff --git a/templates/actor/npc-magic.hbs b/templates/actor/npc-magic.hbs new file mode 100644 index 0000000..682bb14 --- /dev/null +++ b/templates/actor/npc-magic.hbs @@ -0,0 +1,69 @@ +
    + +
    + {{localize "OATHHAMMER.Label.Spells"}} + {{#unless isPlayMode}}{{/unless}} + + {{#if spells.length}} +
      +
    • + + {{localize "OATHHAMMER.Label.Name"}} + DV + {{localize "OATHHAMMER.Label.Tradition"}} + {{localize "OATHHAMMER.Label.Range"}} + {{localize "OATHHAMMER.Label.Duration"}} + +
    • + {{#each spells as |spell|}} +
    • + + {{spell.name}} + {{spell.system.difficultyValue}} + {{localize spell.system.tradition}} + {{#if spell.system.range}}{{spell.system.range}}{{else}}—{{/if}} + {{#if spell.system.duration}}{{spell.system.duration}}{{else}}—{{/if}} +
      + + + +
      +
    • + {{/each}} +
    + {{else}} +

    {{localize "OATHHAMMER.Label.NoSpells"}}

    + {{/if}} +
    + +
    + {{localize "OATHHAMMER.Label.Miracles"}} + {{#unless isPlayMode}}{{/unless}} + + {{#if miracles.length}} +
      +
    • + + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.DivineTradition"}} + +
    • + {{#each miracles as |miracle|}} +
    • + + {{miracle.name}} + {{miracle.system.divineTradition}} +
      + + + +
      +
    • + {{/each}} +
    + {{else}} +

    {{localize "OATHHAMMER.Label.NoMiracles"}}

    + {{/if}} +
    + +
    diff --git a/templates/actor/npc-notes.hbs b/templates/actor/npc-notes.hbs index 4852ca4..ad18886 100644 --- a/templates/actor/npc-notes.hbs +++ b/templates/actor/npc-notes.hbs @@ -1,4 +1,4 @@ -
    +
    {{localize "OATHHAMMER.Label.Description"}} {{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} diff --git a/templates/actor/npc-sheet.hbs b/templates/actor/npc-sheet.hbs index 51ac35f..2e10049 100644 --- a/templates/actor/npc-sheet.hbs +++ b/templates/actor/npc-sheet.hbs @@ -1,26 +1,24 @@
    - {{localize "OATHHAMMER.Label.NPC"}} + + {{#if isPlayMode}} + + {{localize (lookup subtypeLabels system.subtype)}} + + {{else}} + + {{/if}} +
    + +
    -
    -
    - {{localize "OATHHAMMER.Label.Grit"}} - {{formInput systemFields.grit.fields.value value=system.grit.value name="system.grit.value" disabled=isPlayMode}} - / - -
    -
    - {{localize "OATHHAMMER.Label.Defense"}} - {{formInput systemFields.defense.fields.value value=system.defense.value name="system.defense.value" disabled=isPlayMode}} -
    -
    - {{localize "OATHHAMMER.Label.Movement"}} - {{formInput systemFields.movement.fields.base value=system.movement.base name="system.movement.base" disabled=isPlayMode}} -
    -
    + +
    {{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}} @@ -28,39 +26,49 @@
    -
    - {{localize "OATHHAMMER.Label.Stats"}} -
    -
    - - {{formInput systemFields.challengeRating value=system.challengeRating name="system.challengeRating" disabled=isPlayMode}} -
    -
    - - {{formInput systemFields.attackBonus value=system.attackBonus name="system.attackBonus" disabled=isPlayMode}} -
    -
    - - {{formInput systemFields.damageBonus value=system.damageBonus name="system.damageBonus" disabled=isPlayMode}} -
    -
    - - {{formInput systemFields.initiativeBonus value=system.initiativeBonus name="system.initiativeBonus" disabled=isPlayMode}} -
    + + +
    + +
    + {{localize "OATHHAMMER.Label.Grit"}} + + + + / + {{formInput systemFields.grit.fields.max value=system.grit.max name="system.grit.max" disabled=isPlayMode}} + + +
    -
    -
    - {{localize "OATHHAMMER.Label.Attributes"}} -
    - {{#each system.attributes as |attr key|}} -
    - - {{formInput (lookup ../systemFields.attributes.fields key).fields.rank value=attr.rank name=(concat "system.attributes." key ".rank") disabled=../isPlayMode}} -
    - {{/each}} + +
    + + {{#if isPlayMode}}{{/if}} + {{localize "OATHHAMMER.Label.ArmorDice"}} + + + + {{#if isPlayMode}} + {{armorDiceEmoji}} + {{else}} + + {{/if}} +
    -
    -
    + +
    + {{localize "OATHHAMMER.Label.Movement"}} + + {{formInput systemFields.movement.fields.base value=system.movement.base name="system.movement.base" disabled=isPlayMode}} + +
    + +
    + +
    diff --git a/templates/actor/npc-skills.hbs b/templates/actor/npc-skills.hbs new file mode 100644 index 0000000..d082a83 --- /dev/null +++ b/templates/actor/npc-skills.hbs @@ -0,0 +1,39 @@ +
    + {{#unless isPlayMode}} +

    {{localize "OATHHAMMER.Label.SkillNPCHint"}}

    + {{/unless}} + {{#if skills.length}} +
      +
    • + + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.DicePool"}} + {{localize "OATHHAMMER.Label.Threshold"}} + + +
    • + {{#each skills as |skill|}} +
    • + + {{skill.system.colorEmoji}} + + {{skill.name}} + {{skill.system.dicePool}}d + {{skill.system.threshold}} + + + +
      + + +
      +
    • + {{/each}} +
    + {{else}} +

    {{localize "OATHHAMMER.Label.NoSkills"}}

    + {{/if}} +
    diff --git a/templates/actor/npc-traits.hbs b/templates/actor/npc-traits.hbs new file mode 100644 index 0000000..34823d2 --- /dev/null +++ b/templates/actor/npc-traits.hbs @@ -0,0 +1,36 @@ +
    + {{#unless isPlayMode}} +

    {{localize "OATHHAMMER.Label.TraitNPCHint"}}

    + {{/unless}} + {{#if traits.length}} +
      +
    • + + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.Type"}} + +
    • + {{#each traits as |trait|}} +
    • + + {{trait.name}} + {{#if trait.system.traitType}} + + {{localize (lookup ../traitTypeLabels trait.system.traitType)}} + + {{else}} + + {{/if}} +
      + {{#unless ../isPlayMode}} + + + {{/unless}} +
      +
    • + {{/each}} +
    + {{else}} +

    {{localize "OATHHAMMER.Label.NoTraits"}}

    + {{/if}} +
    diff --git a/templates/actor/settlement-buildings.hbs b/templates/actor/settlement-buildings.hbs index c2dd169..69d62d0 100644 --- a/templates/actor/settlement-buildings.hbs +++ b/templates/actor/settlement-buildings.hbs @@ -1,5 +1,12 @@
    -

    {{localize "OATHHAMMER.Settlement.BuildingHint"}}

    +
    +

    {{localize "OATHHAMMER.Settlement.BuildingHint"}}

    + {{#if hasTaxBuildings}} + + {{localize "OATHHAMMER.Settlement.CollectTaxes"}} + + {{/if}} +
    {{#if buildings.length}}
      @@ -14,7 +21,7 @@ {{#each buildings as |building|}}
    • - {{building.name}} + {{building.name}} {{#unless ../isPlayMode}} @@ -30,7 +37,7 @@ {{building.system.cost}} gp
      - {{#unless ../isPlayMode}}{{/unless}} +
    • {{/each}} diff --git a/templates/actor/settlement-garrison.hbs b/templates/actor/settlement-garrison.hbs new file mode 100644 index 0000000..99164cd --- /dev/null +++ b/templates/actor/settlement-garrison.hbs @@ -0,0 +1,36 @@ +
      + +
      + {{localize "OATHHAMMER.Label.Garrison"}} + {{#unless isPlayMode}}{{/unless}} + + {{#if regiments.length}} +
        +
      • + + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.GritMax"}} + {{localize "OATHHAMMER.Label.ArmorDice"}} + {{localize "OATHHAMMER.Label.Movement"}} + +
      • + {{#each regiments as |regiment|}} +
      • + + {{regiment.name}} + {{regiment.system.grit.max}} + {{regiment.system.armorLabel}} + {{regiment.system.movement}} ft +
        + + +
        +
      • + {{/each}} +
      + {{else}} +

      {{localize "OATHHAMMER.Label.NoRegiments"}}

      + {{/if}} +
      + +
      diff --git a/templates/actor/settlement-inventory.hbs b/templates/actor/settlement-inventory.hbs index 9fc3db7..4746a61 100644 --- a/templates/actor/settlement-inventory.hbs +++ b/templates/actor/settlement-inventory.hbs @@ -18,7 +18,7 @@ {{weapon.system.weaponType}}
      - {{#unless ../isPlayMode}}{{/unless}} +
      {{/each}} @@ -43,7 +43,7 @@ {{localize armor.system.armorType}}
      - {{#unless ../isPlayMode}}{{/unless}} +
      {{/each}} @@ -78,7 +78,7 @@
      - {{#unless ../isPlayMode}}{{/unless}} +
      {{/each}} diff --git a/templates/actor/settlement-overview.hbs b/templates/actor/settlement-overview.hbs index 7ea61d4..0e65860 100644 --- a/templates/actor/settlement-overview.hbs +++ b/templates/actor/settlement-overview.hbs @@ -11,22 +11,6 @@ + -
      - -
      - - {{formInput systemFields.currency.fields.silver value=system.currency.silver name="system.currency.silver"}} - + -
      -
      -
      - -
      - - {{formInput systemFields.currency.fields.copper value=system.currency.copper name="system.currency.copper"}} - + -
      -
    diff --git a/templates/actor/settlement-sheet.hbs b/templates/actor/settlement-sheet.hbs index c0c31b5..1a40e1a 100644 --- a/templates/actor/settlement-sheet.hbs +++ b/templates/actor/settlement-sheet.hbs @@ -14,10 +14,6 @@
    -
    - - {{formInput systemFields.renown value=system.renown name="system.renown" disabled=isPlayMode}} -
    {{formInput systemFields.territory value=system.territory name="system.territory" disabled=isPlayMode}} diff --git a/templates/item/npcattack-sheet.hbs b/templates/item/npcattack-sheet.hbs new file mode 100644 index 0000000..adcacf4 --- /dev/null +++ b/templates/item/npcattack-sheet.hbs @@ -0,0 +1,38 @@ +
    +
    + + {{formInput fields.name value=source.name}} +
    + +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + {{localize "OATHHAMMER.NpcAttack.FIELDS.description.label"}} + {{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} +
    +
    diff --git a/templates/item/regiment-sheet.hbs b/templates/item/regiment-sheet.hbs new file mode 100644 index 0000000..c513472 --- /dev/null +++ b/templates/item/regiment-sheet.hbs @@ -0,0 +1,122 @@ +
    + +
    + + {{formInput fields.name value=source.name}} +
    + +
    + {{localize "OATHHAMMER.Label.Stats"}} +
    +
    + +
    + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + ft +
    +
    +
    +
    + +
    + + {{localize "OATHHAMMER.Tab.Skills"}} + + + {{#if system.skills.length}} +
    + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.Rank"}} + {{localize "OATHHAMMER.Label.DiceColor"}} + +
    + {{#each system.skills as |skill idx|}} +
    + + + + +
    + {{/each}} + {{else}} +

    {{localize "OATHHAMMER.Label.NoSkills"}}

    + {{/if}} +
    + +
    + + {{localize "OATHHAMMER.Label.Attacks"}} + + + {{#if system.attacks.length}} +
    + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.Dice"}} + {{localize "OATHHAMMER.Label.DiceColor"}} + AP + {{localize "OATHHAMMER.Label.Special"}} + +
    + {{#each system.attacks as |attack idx|}} +
    + + + + + + +
    + {{/each}} + {{else}} +

    {{localize "OATHHAMMER.Label.NoAttacks"}}

    + {{/if}} +
    + +
    + + {{localize "OATHHAMMER.Tab.Traits"}} + + + {{#if system.traits.length}} + {{#each system.traits as |trait idx|}} +
    + + + +
    + {{/each}} + {{else}} +

    {{localize "OATHHAMMER.Label.NoTraits"}}

    + {{/if}} +
    + +
    + {{localize "OATHHAMMER.Label.Description"}} + + {{{system.description}}} + +
    + +
    diff --git a/templates/item/skillnpc-sheet.hbs b/templates/item/skillnpc-sheet.hbs new file mode 100644 index 0000000..847a2b3 --- /dev/null +++ b/templates/item/skillnpc-sheet.hbs @@ -0,0 +1,38 @@ +
    +
    + + {{formInput fields.name value=source.name}} +
    + +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + {{localize "OATHHAMMER.SkillNPC.FIELDS.description.label"}} + {{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} +
    +
    diff --git a/templates/item/weapon-sheet.hbs b/templates/item/weapon-sheet.hbs index 290bc60..02b226c 100644 --- a/templates/item/weapon-sheet.hbs +++ b/templates/item/weapon-sheet.hbs @@ -6,6 +6,14 @@
    {{formField systemFields.proficiencyGroup value=system.proficiencyGroup name="system.proficiencyGroup" localize=true}} +
    + +
    + +
    +
    @@ -43,12 +51,29 @@ {{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
    {{#if system.isMagic}} -
    +
    {{localize "OATHHAMMER.Label.Enchantment"}} -
    - {{formField systemFields.magicQuality value=system.magicQuality name="system.magicQuality" localize=true}} - {{formField systemFields.isCursed value=system.isCursed name="system.isCursed"}} - {{formField systemFields.classRestriction value=system.classRestriction name="system.classRestriction"}} +
    + +
    + +
    + +
    +
    + +
    + +
    {{formInput systemFields.magicEffect enriched=enrichedMagicEffect value=system.magicEffect name="system.magicEffect" toggled=true}}
    diff --git a/templates/npc-magic-dialog.hbs b/templates/npc-magic-dialog.hbs new file mode 100644 index 0000000..bba6e96 --- /dev/null +++ b/templates/npc-magic-dialog.hbs @@ -0,0 +1,49 @@ +
    + +
    + {{itemName}} +
    + {{itemName}} +
    + {{#if dv}}DV {{dv}}{{/if}} +
    +
    +
    + +
    + {{localize "OATHHAMMER.Dialog.Options"}} + +
    + + +
    + +
    + + +
    + + {{#if showColor}} +
    + + +
    + {{/if}} + +
    + +
    + {{localize "OATHHAMMER.Dialog.Visibility"}} + +
    + +
    diff --git a/templates/npc-skill-dialog.hbs b/templates/npc-skill-dialog.hbs new file mode 100644 index 0000000..2c74299 --- /dev/null +++ b/templates/npc-skill-dialog.hbs @@ -0,0 +1,40 @@ +
    + +
    + {{skillName}} +
    + {{skillName}} +
    + {{colorEmoji}} {{dicePool}}d6 ({{threshold}}+) +
    +
    +
    + +
    + {{localize "OATHHAMMER.Dialog.Options"}} + +
    + + +
    + +
    + + +
    + +
    + +
    + {{localize "OATHHAMMER.Dialog.Visibility"}} + +
    + +
    diff --git a/templates/npc-weapon-roll-dialog.hbs b/templates/npc-weapon-roll-dialog.hbs new file mode 100644 index 0000000..61aafdf --- /dev/null +++ b/templates/npc-weapon-roll-dialog.hbs @@ -0,0 +1,31 @@ +
    + +
    + {{weaponName}} +
    + {{weaponName}} +
    + ⬜ {{basePool}}d6 (4+) + {{rollType}} +
    +
    +
    + +
    + {{localize "OATHHAMMER.Dialog.Modifier"}} +
    + + +
    +
    + +
    + {{localize "OATHHAMMER.Dialog.Visibility"}} + +
    + +