Nombreuses corrections sur les fiches settlement/NPC

This commit is contained in:
2026-03-22 21:35:47 +01:00
parent ec291e9c60
commit b46c6d804c
51 changed files with 2892 additions and 227 deletions

View File

@@ -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;
}

View File

@@ -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,11 +611,27 @@
"label": "Attributes"
},
"grit": {
"label": "Grit",
"fields": {
"max": {
"label": "Max Grit"
},
"value": {
"label": "Grit"
}
}
},
"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",
@@ -989,7 +1072,58 @@
"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"
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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; }
}

View File

@@ -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: 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;
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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; }
}
}

View File

@@ -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;

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -1,48 +1,54 @@
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,
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 = {
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.cssClass = v.active ? "active" : ""
@@ -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,13 +84,41 @@ 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 })
@@ -77,13 +128,221 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
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") {
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"),
})
}
}

View File

@@ -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
}
}

View File

@@ -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 })
}
}

View File

@@ -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 */
@@ -17,6 +17,8 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
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 */
@@ -49,6 +54,7 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
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(`<li><strong>${b.name}</strong> — ${b.system.taxRevenue} = <em>${r.total} gp</em></li>`)
}
const content = `
<div class="oh-roll-card oh-weapon-card">
<div class="oh-roll-header">
<span class="oh-roll-title">🏛 ${actor.name}</span>
<span class="oh-roll-subtitle">${game.i18n.localize("OATHHAMMER.Settlement.CollectTaxes")}</span>
</div>
<div class="oh-roll-info">
<ul style="margin:4px 0;padding-left:1.2em;">${lines.join("")}</ul>
</div>
<div class="oh-roll-result">
<span class="oh-roll-successes">${total} gp</span>
<span class="oh-roll-verdict">${game.i18n.localize("OATHHAMMER.Settlement.TotalRevenue")}</span>
</div>
</div>`
const msgData = {
speaker: ChatMessage.getSpeaker({ actor }),
content,
rolls,
sound: CONFIG.sounds.dice,
}
ChatMessage.applyRollMode(msgData, game.settings.get("core", "rollMode"))
await ChatMessage.create(msgData)
}
}

View File

@@ -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 (020)
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
}
}

View File

@@ -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

View File

@@ -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",
@@ -200,7 +213,20 @@ export const RARITY_DV = {
export const TRAIT_TYPE_CHOICES = {
"special-trait": "OATHHAMMER.TraitType.SpecialTrait",
"class-trait": "OATHHAMMER.TraitType.ClassTrait",
"lineage-trait": "OATHHAMMER.TraitType.LineageTrait"
"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,

View File

@@ -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"

View File

@@ -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" } }),

View File

@@ -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({
@@ -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
}
}

View File

@@ -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}+)`
}
}

View File

@@ -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`
}
}

View File

@@ -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" ? "🔴" : "⬜"
}
}

View File

@@ -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
}

View File

@@ -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
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
: ""
const content = `
<div class="oh-roll-card">
<div class="oh-roll-header">
<img src="${skillItem.img}" class="oh-card-weapon-img" alt="${skillItem.name}" />
<span>${skillItem.name}${actor.name}</span>
</div>
<div class="oh-roll-info">
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result">
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
</div>
</div>
`
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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
<div class="oh-roll-card oh-weapon-card">
<div class="oh-roll-header">
<img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
<span>${weapon.name}${game.i18n.localize("OATHHAMMER.Dialog.Attack")} (${actor.name})</span>
</div>
<div class="oh-roll-info">
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result">
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
</div>
</div>
`
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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
<div class="oh-roll-card oh-weapon-card">
<div class="oh-roll-header">
<img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
<span>${weapon.name}${game.i18n.localize("OATHHAMMER.Dialog.Damage")} (${actor.name})</span>
</div>
<div class="oh-roll-info">
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
<span>${game.i18n.localize("OATHHAMMER.Label.Damage")}: ${weapon.system.damageLabel}</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result">
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
</div>
</div>
`
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
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
: ""
const label = game.i18n.localize("OATHHAMMER.Label.ArmorDice")
const content = `
<div class="oh-roll-card oh-armor-card">
<div class="oh-roll-header">
<img src="${actor.img}" class="oh-card-weapon-img" alt="${actor.name}" />
<span>${label}${actor.name}</span>
</div>
<div class="oh-roll-info">
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result ${successes > 0 ? "roll-success" : ""}">
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
<span class="oh-roll-verdict">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Damage")}</span>
</div>
</div>
`
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
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
: ""
const resultClass = isSuccess ? "roll-success" : "roll-failure"
const resultLabel = isSuccess
? game.i18n.localize("OATHHAMMER.Roll.Success")
: game.i18n.localize("OATHHAMMER.Roll.Failure")
const content = `
<div class="oh-roll-card oh-spell-card">
<div class="oh-roll-header">
<img src="${spell.img}" class="oh-card-weapon-img" alt="${spell.name}" />
<span>${spell.name} (DV ${dv}) — ${actor.name}</span>
</div>
<div class="oh-roll-info">
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result ${resultClass}">
<span class="oh-roll-successes">${successes} / ${dv}</span>
<span class="oh-roll-verdict">${resultLabel}</span>
</div>
</div>
`
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
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
: ""
const resultClass = isSuccess ? "roll-success" : "roll-failure"
const resultLabel = isSuccess
? game.i18n.localize("OATHHAMMER.Roll.Success")
: game.i18n.localize("OATHHAMMER.Roll.Failure")
const content = `
<div class="oh-roll-card oh-miracle-card">
<div class="oh-roll-header">
<img src="${miracle.img}" class="oh-card-weapon-img" alt="${miracle.name}" />
<span>${miracle.name}${actor.name}</span>
</div>
<div class="oh-roll-info">
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result ${resultClass}">
<span class="oh-roll-successes">${successes} / ${dv}</span>
<span class="oh-roll-verdict">${resultLabel}</span>
</div>
</div>
`
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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
<div class="oh-roll-card oh-weapon-card">
<div class="oh-roll-header">
<img src="${attack.img}" class="oh-card-weapon-img" alt="${attack.name}" />
<span>${attack.name}${game.i18n.localize("OATHHAMMER.Dialog.Damage")} (${actor.name})</span>
</div>
<div class="oh-roll-info">
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
${ap > 0 ? `<span>AP ${ap}</span>` : ""}
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result">
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
</div>
</div>
`
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 }
}

View File

@@ -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

View File

@@ -105,7 +105,10 @@
"description",
"notes"
]
}
},
"skillnpc": {},
"npcattack": {},
"regiment": {}
}
},
"grid": {

View File

@@ -66,10 +66,4 @@
{{/if}}
</fieldset>
{{!-- Background --}}
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Background"}}</legend>
{{formInput systemFields.background enriched=enrichedBackground value=system.background name="system.background" toggled=true}}
</fieldset>
</section>

View File

@@ -1,4 +1,8 @@
<section data-tab="notes" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Background"}}</legend>
{{formInput systemFields.background enriched=enrichedBackground value=system.background name="system.background" toggled=true}}
</fieldset>
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}

View File

@@ -1,29 +1,41 @@
<section data-tab="combat" class="tab">
<section data-tab="combat" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<div class="initiative-bar">
<a data-action="rollInitiative" class="initiative-roll-btn" data-tooltip="{{localize 'OATHHAMMER.Roll.InitiativeHint'}}">
<i class="fa-solid fa-swords"></i> {{localize "OATHHAMMER.Dialog.RollInitiative"}}
</a>
{{#if combatantInitiative}}<span class="initiative-score" data-tooltip="{{localize 'OATHHAMMER.Label.Initiative'}}">⚔ {{combatantInitiative}}</span>{{/if}}
</div>
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Weapons"}}</legend>
{{#if weapons.length}}
<ul class="item-list">
{{#each weapons as |weapon|}}
<li class="item-entry flexrow" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}">
<img src="{{weapon.img}}" class="item-img" />
<span class="item-name">{{weapon.name}}</span>
<span class="item-detail">{{weapon.system.damageLabel}}</span>
<span class="item-detail">AP: {{weapon.system.ap}}</span>
{{#unless ../isPlayMode}}
<a data-action="edit" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-trash"></i></a>
{{/unless}}
<legend>{{localize "OATHHAMMER.Label.Attacks"}}
{{#unless isPlayMode}}<a data-action="createNpcAttack" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
</legend>
{{#if npcAttacks.length}}
<ul class="item-list item-list--npc-attack">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Damage"}}</span>
<span title="Armor Penetration">AP</span>
<span></span>
</li>
{{#each npcAttacks as |attack|}}
<li class="item-entry" data-item-id="{{attack.id}}" data-item-uuid="{{attack.uuid}}">
<img src="{{attack.img}}" class="item-img" />
<span class="item-name" {{#if attack._descTooltip}}data-tooltip="{{attack._descTooltip}}"{{/if}}>{{attack.name}}</span>
<span class="item-detail">{{attack.system.damageLabel}}</span>
<span class="item-detail">{{#if attack.system.ap}}{{attack.system.ap}}{{else}}{{/if}}</span>
<div class="item-actions">
<a data-action="rollNpcAttack" data-item-id="{{attack.id}}" data-tooltip="{{localize 'OATHHAMMER.Dialog.Damage'}}"><i class="fa-solid fa-burst"></i></a>
<a data-action="edit" data-item-id="{{attack.id}}" data-item-uuid="{{attack.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{attack.id}}" data-item-uuid="{{attack.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoWeapons"}}</p>
<p class="no-items">{{localize "OATHHAMMER.Label.NoAttacks"}}</p>
{{/if}}
</fieldset>
</section>

View File

@@ -0,0 +1,65 @@
<section data-tab="equipment" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
{{#unless isPlayMode}}
<p class="settlement-hint">{{localize "OATHHAMMER.Label.EquipmentNPCHint"}}</p>
{{/unless}}
{{! Armor }}
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Armor"}}</legend>
{{#if armors.length}}
<ul class="item-list item-list--npc-armor">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>AV</span>
<span>{{localize "OATHHAMMER.Label.Penalty"}}</span>
<span></span>
</li>
{{#each armors as |armor|}}
<li class="item-entry" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}">
<img src="{{armor.img}}" class="item-img" />
<span class="item-name">{{armor.name}}</span>
<span class="item-detail">{{armor.system.armorValue}}</span>
<span class="item-detail">{{#if armor.system.penalty}}{{armor.system.penalty}}{{else}}{{/if}}</span>
<div class="item-actions">
{{#unless ../isPlayMode}}
<a data-action="edit" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-trash"></i></a>
{{/unless}}
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoArmor"}}</p>
{{/if}}
</fieldset>
{{! Equipment }}
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Equipment"}}</legend>
{{#if equipment.length}}
<ul class="item-list item-list--npc-equip">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span></span>
</li>
{{#each equipment as |item|}}
<li class="item-entry" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
<img src="{{item.img}}" class="item-img" />
<span class="item-name">{{item.name}}</span>
<div class="item-actions">
{{#unless ../isPlayMode}}
<a data-action="edit" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}"><i class="fa-solid fa-trash"></i></a>
{{/unless}}
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoEquipment"}}</p>
{{/if}}
</fieldset>
</section>

View File

@@ -0,0 +1,69 @@
<section data-tab="magic" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Spells"}}
{{#unless isPlayMode}}<a data-action="createSpell" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
</legend>
{{#if spells.length}}
<ul class="item-list item-list--spell">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>DV</span>
<span>{{localize "OATHHAMMER.Label.Tradition"}}</span>
<span>{{localize "OATHHAMMER.Label.Range"}}</span>
<span>{{localize "OATHHAMMER.Label.Duration"}}</span>
<span></span>
</li>
{{#each spells as |spell|}}
<li class="item-entry" data-item-id="{{spell.id}}" data-item-uuid="{{spell.uuid}}">
<img src="{{spell.img}}" class="item-img" />
<span class="item-name" {{#if spell._descTooltip}}data-tooltip="{{spell._descTooltip}}"{{/if}}>{{spell.name}}</span>
<span class="item-detail">{{spell.system.difficultyValue}}</span>
<span class="item-type">{{localize spell.system.tradition}}</span>
<span class="item-detail item-detail--small">{{#if spell.system.range}}{{spell.system.range}}{{else}}{{/if}}</span>
<span class="item-detail item-detail--small">{{#if spell.system.duration}}{{spell.system.duration}}{{else}}{{/if}}</span>
<div class="item-actions">
<a data-action="castNPCSpell" data-item-id="{{spell.id}}" title="{{localize 'OATHHAMMER.Action.CastSpell'}}"><i class="fa-solid fa-wand-sparkles spell-cast-icon"></i></a>
<a data-action="edit" data-item-id="{{spell.id}}" data-item-uuid="{{spell.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{spell.id}}" data-item-uuid="{{spell.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoSpells"}}</p>
{{/if}}
</fieldset>
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Miracles"}}
{{#unless isPlayMode}}<a data-action="createMiracle" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
</legend>
{{#if miracles.length}}
<ul class="item-list item-list--miracle">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.DivineTradition"}}</span>
<span></span>
</li>
{{#each miracles as |miracle|}}
<li class="item-entry" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}">
<img src="{{miracle.img}}" class="item-img" />
<span class="item-name" {{#if miracle._descTooltip}}data-tooltip="{{miracle._descTooltip}}"{{/if}}>{{miracle.name}}</span>
<span class="item-detail">{{miracle.system.divineTradition}}</span>
<div class="item-actions">
<a data-action="castNPCMiracle" data-item-id="{{miracle.id}}" title="{{localize 'OATHHAMMER.Action.InvokeMiracle'}}"><i class="fa-solid fa-hands-praying miracle-cast-icon"></i></a>
<a data-action="edit" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoMiracles"}}</p>
{{/if}}
</fieldset>
</section>

View File

@@ -1,4 +1,4 @@
<section data-tab="notes" class="tab">
<section data-tab="notes" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}

View File

@@ -1,26 +1,24 @@
<section class="npc-main npc-main-{{ifThen isPlayMode 'play' 'edit'}}">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.NPC"}}</legend>
<legend>
{{#if isPlayMode}}
<span class="npc-subtype-badge npc-subtype-{{system.subtype}}">
{{localize (lookup subtypeLabels system.subtype)}}
</span>
{{else}}
<select name="system.subtype" class="npc-subtype-select">
{{selectOptions subtypeChoices selected=system.subtype}}
</select>
{{/if}}
</legend>
<div class="npc-pc flexrow">
<!-- LEFT: portrait only -->
<div class="npc-left">
<img class="actor-img" src="{{actor.img}}" data-edit="img" data-action="editImage" data-tooltip="{{actor.name}}" />
<fieldset>
<div class="flexrow character-resource">
<span class="resource-label">{{localize "OATHHAMMER.Label.Grit"}}</span>
{{formInput systemFields.grit.fields.value value=system.grit.value name="system.grit.value" disabled=isPlayMode}}
<span>/</span>
<input type="text" value="{{system.grit.max}}" disabled />
</div>
<div class="flexrow character-resource">
<span class="resource-label">{{localize "OATHHAMMER.Label.Defense"}}</span>
{{formInput systemFields.defense.fields.value value=system.defense.value name="system.defense.value" disabled=isPlayMode}}
</div>
<div class="flexrow character-resource">
<span class="resource-label">{{localize "OATHHAMMER.Label.Movement"}}</span>
{{formInput systemFields.movement.fields.base value=system.movement.base name="system.movement.base" disabled=isPlayMode}}
</div>
</fieldset>
</div>
<!-- RIGHT: name + vitals grid + stats -->
<div class="npc-right">
<div class="character-name">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}}
@@ -28,39 +26,49 @@
<i class="fa-solid fa-user-{{ifThen isPlayMode 'lock' 'pen'}}"></i>
</a>
</div>
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Stats"}}</legend>
<div class="flexrow">
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.CR"}}</label>
{{formInput systemFields.challengeRating value=system.challengeRating name="system.challengeRating" disabled=isPlayMode}}
<!-- Vitals: 2×2 grid -->
<div class="npc-vitals-grid">
<div class="npc-vital npc-vital-grit">
<span class="vital-label">{{localize "OATHHAMMER.Label.Grit"}}</span>
<span class="vital-value">
<a class="grit-btn" data-action="adjustGrit" data-delta="-1" data-tooltip="1"></a>
<input type="number" class="npc-num-input" name="system.grit.value" value="{{system.grit.value}}" min="0" />
<span class="res-sep">/</span>
{{formInput systemFields.grit.fields.max value=system.grit.max name="system.grit.max" disabled=isPlayMode}}
<a class="grit-btn" data-action="adjustGrit" data-delta="1" data-tooltip="+1">+</a>
</span>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.AttackBonus"}}</label>
{{formInput systemFields.attackBonus value=system.attackBonus name="system.attackBonus" disabled=isPlayMode}}
<div class="npc-vital">
<span class="vital-label{{#if isPlayMode}} vital-roll-label{{/if}}"
{{#if isPlayMode}}data-action="rollArmor" data-tooltip="OATHHAMMER.Tooltip.RollArmor"{{/if}}>
{{#if isPlayMode}}<i class="fa-solid fa-dice-d6"></i>{{/if}}
{{localize "OATHHAMMER.Label.ArmorDice"}}
</span>
<span class="vital-value">
<input type="number" class="npc-num-input" name="system.armorDice.value" value="{{system.armorDice.value}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
{{#if isPlayMode}}
<span class="npc-color-badge">{{armorDiceEmoji}}</span>
{{else}}
<select name="system.armorDice.colorDiceType" class="npc-color-select">
{{selectOptions colorChoices selected=system.armorDice.colorDiceType}}
</select>
{{/if}}
</span>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.DamageBonus"}}</label>
{{formInput systemFields.damageBonus value=system.damageBonus name="system.damageBonus" disabled=isPlayMode}}
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.InitiativeBonus"}}</label>
{{formInput systemFields.initiativeBonus value=system.initiativeBonus name="system.initiativeBonus" disabled=isPlayMode}}
</div>
</div>
</fieldset>
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Attributes"}}</legend>
<div class="attributes-grid">
{{#each system.attributes as |attr key|}}
<div class="attribute-box">
<label>{{localize (concat "OATHHAMMER.Attribute." (capitalize key))}}</label>
{{formInput (lookup ../systemFields.attributes.fields key).fields.rank value=attr.rank name=(concat "system.attributes." key ".rank") disabled=../isPlayMode}}
</div>
{{/each}}
</div>
</fieldset>
<div class="npc-vital">
<span class="vital-label">{{localize "OATHHAMMER.Label.Movement"}}</span>
<span class="vital-value">
{{formInput systemFields.movement.fields.base value=system.movement.base name="system.movement.base" disabled=isPlayMode}}
</span>
</div>
</div><!-- /npc-vitals-grid -->
</div><!-- /npc-right -->
</div>
</fieldset>
</section>

View File

@@ -0,0 +1,39 @@
<section data-tab="skills" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
{{#unless isPlayMode}}
<p class="settlement-hint">{{localize "OATHHAMMER.Label.SkillNPCHint"}}</p>
{{/unless}}
{{#if skills.length}}
<ul class="item-list item-list--npc-skill">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span data-tooltip="{{localize 'OATHHAMMER.Label.DicePool'}}">{{localize "OATHHAMMER.Label.DicePool"}}</span>
<span>{{localize "OATHHAMMER.Label.Threshold"}}</span>
<span></span>
<span></span>
</li>
{{#each skills as |skill|}}
<li class="item-entry" data-item-id="{{skill.id}}" data-item-uuid="{{skill.uuid}}">
<span class="npc-skill-color npc-skill-color-{{skill.system.colorDiceType}}"
data-tooltip="{{skill.system.colorType}} ({{skill.system.threshold}})">
{{skill.system.colorEmoji}}
</span>
<span class="item-name">{{skill.name}}</span>
<span class="item-detail">{{skill.system.dicePool}}d</span>
<span class="item-detail">{{skill.system.threshold}}</span>
<a class="npc-skill-roll-btn" data-action="rollSkillNPC"
data-item-id="{{skill.id}}" data-item-uuid="{{skill.uuid}}"
data-tooltip="{{localize 'OATHHAMMER.Roll.RollSkill'}}">
<i class="fa-solid fa-dice-d6"></i>
</a>
<div class="item-actions">
<a data-action="edit" data-item-id="{{skill.id}}" data-item-uuid="{{skill.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{skill.id}}" data-item-uuid="{{skill.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoSkills"}}</p>
{{/if}}
</section>

View File

@@ -0,0 +1,36 @@
<section data-tab="traits" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
{{#unless isPlayMode}}
<p class="settlement-hint">{{localize "OATHHAMMER.Label.TraitNPCHint"}}</p>
{{/unless}}
{{#if traits.length}}
<ul class="item-list item-list--npc-trait">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Type"}}</span>
<span></span>
</li>
{{#each traits as |trait|}}
<li class="item-entry" data-item-id="{{trait.id}}" data-item-uuid="{{trait.uuid}}">
<img src="{{trait.img}}" class="item-img" />
<span class="item-name" {{#if trait._descTooltip}}data-tooltip="{{trait._descTooltip}}"{{/if}}>{{trait.name}}</span>
{{#if trait.system.traitType}}
<span class="npc-trait-type-badge npc-trait-type-{{trait.system.traitType}}">
{{localize (lookup ../traitTypeLabels trait.system.traitType)}}
</span>
{{else}}
<span></span>
{{/if}}
<div class="item-actions">
{{#unless ../isPlayMode}}
<a data-action="edit" data-item-id="{{trait.id}}" data-item-uuid="{{trait.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{trait.id}}" data-item-uuid="{{trait.uuid}}"><i class="fa-solid fa-trash"></i></a>
{{/unless}}
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoTraits"}}</p>
{{/if}}
</section>

View File

@@ -1,5 +1,12 @@
<section data-tab="buildings" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<div class="settlement-buildings-header">
<p class="settlement-hint">{{localize "OATHHAMMER.Settlement.BuildingHint"}}</p>
{{#if hasTaxBuildings}}
<a data-action="collectTaxes" class="collect-taxes-btn" data-tooltip="{{localize 'OATHHAMMER.Settlement.CollectTaxesTooltip'}}">
<i class="fa-solid fa-coins"></i> {{localize "OATHHAMMER.Settlement.CollectTaxes"}}
</a>
{{/if}}
</div>
{{#if buildings.length}}
<ul class="item-list item-list--buildings">
@@ -14,7 +21,7 @@
{{#each buildings as |building|}}
<li class="item-entry {{#if building.system.constructed}}building-constructed{{/if}}" data-item-id="{{building.id}}" data-item-uuid="{{building.uuid}}">
<img src="{{building.img}}" class="item-img" />
<span class="item-name" data-tooltip="{{building.name}}">{{building.name}}</span>
<span class="item-name" {{#if building._descTooltip}}data-tooltip="{{building._descTooltip}}"{{else}}data-tooltip="{{building.name}}"{{/if}}>{{building.name}}</span>
<span class="item-constructed">
{{#unless ../isPlayMode}}
<a data-action="toggleConstructed" data-item-id="{{building.id}}" class="construct-toggle">
@@ -30,7 +37,7 @@
<span class="item-cost">{{building.system.cost}} gp</span>
<div class="item-actions">
<a data-action="edit" data-item-id="{{building.id}}" data-item-uuid="{{building.uuid}}"><i class="fa-solid fa-edit"></i></a>
{{#unless ../isPlayMode}}<a data-action="delete" data-item-id="{{building.id}}" data-item-uuid="{{building.uuid}}"><i class="fa-solid fa-trash"></i></a>{{/unless}}
<a data-action="delete" data-item-id="{{building.id}}" data-item-uuid="{{building.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}

View File

@@ -0,0 +1,36 @@
<section data-tab="garrison" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Garrison"}}
{{#unless isPlayMode}}<a data-action="createRegiment" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
</legend>
{{#if regiments.length}}
<ul class="item-list item-list--regiment">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.GritMax"}}</span>
<span>{{localize "OATHHAMMER.Label.ArmorDice"}}</span>
<span>{{localize "OATHHAMMER.Label.Movement"}}</span>
<span></span>
</li>
{{#each regiments as |regiment|}}
<li class="item-entry" data-item-id="{{regiment.id}}" data-item-uuid="{{regiment.uuid}}">
<img src="{{regiment.img}}" class="item-img" />
<span class="item-name">{{regiment.name}}</span>
<span class="item-detail">{{regiment.system.grit.max}}</span>
<span class="item-detail">{{regiment.system.armorLabel}}</span>
<span class="item-detail">{{regiment.system.movement}} ft</span>
<div class="item-actions">
<a data-action="edit" data-item-id="{{regiment.id}}" data-item-uuid="{{regiment.uuid}}" data-tooltip="{{localize 'OATHHAMMER.Label.Edit'}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{regiment.id}}" data-item-uuid="{{regiment.uuid}}" data-tooltip="{{localize 'OATHHAMMER.Label.Delete'}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoRegiments"}}</p>
{{/if}}
</fieldset>
</section>

View File

@@ -18,7 +18,7 @@
<span class="item-type">{{weapon.system.weaponType}}</span>
<div class="item-actions">
<a data-action="edit" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-edit"></i></a>
{{#unless ../isPlayMode}}<a data-action="delete" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-trash"></i></a>{{/unless}}
<a data-action="delete" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}
@@ -43,7 +43,7 @@
<span class="item-type">{{localize armor.system.armorType}}</span>
<div class="item-actions">
<a data-action="edit" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-edit"></i></a>
{{#unless ../isPlayMode}}<a data-action="delete" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-trash"></i></a>{{/unless}}
<a data-action="delete" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}
@@ -78,7 +78,7 @@
</span>
<div class="item-actions">
<a data-action="edit" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}"><i class="fa-solid fa-edit"></i></a>
{{#unless ../isPlayMode}}<a data-action="delete" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}"><i class="fa-solid fa-trash"></i></a>{{/unless}}
<a data-action="delete" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}

View File

@@ -11,22 +11,6 @@
<a data-action="adjustCurrency" data-field="system.currency.gold" data-delta="1" class="qty-btn">+</a>
</div>
</div>
<div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.SP"}}</label>
<div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.currency.silver" data-delta="-1" class="qty-btn"></a>
{{formInput systemFields.currency.fields.silver value=system.currency.silver name="system.currency.silver"}}
<a data-action="adjustCurrency" data-field="system.currency.silver" data-delta="1" class="qty-btn">+</a>
</div>
</div>
<div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.CP"}}</label>
<div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.currency.copper" data-delta="-1" class="qty-btn"></a>
{{formInput systemFields.currency.fields.copper value=system.currency.copper name="system.currency.copper"}}
<a data-action="adjustCurrency" data-field="system.currency.copper" data-delta="1" class="qty-btn">+</a>
</div>
</div>
</div>
</fieldset>

View File

@@ -14,10 +14,6 @@
</a>
</div>
<div class="settlement-stats flexrow">
<div class="stat-item">
<label>{{localize "OATHHAMMER.Label.Renown"}}</label>
{{formInput systemFields.renown value=system.renown name="system.renown" disabled=isPlayMode}}
</div>
<div class="stat-item">
<label>{{localize "OATHHAMMER.Label.Territory"}}</label>
{{formInput systemFields.territory value=system.territory name="system.territory" disabled=isPlayMode}}

View File

@@ -0,0 +1,38 @@
<section class="item-sheet-common npcattack-sheet">
<div class="header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<div class="npcattack-stats">
<div class="form-group">
<label>{{localize "OATHHAMMER.NpcAttack.FIELDS.damageDice.label"}}</label>
<div class="form-fields">
<select name="system.damageDice">
{{selectOptions dicePoolChoices selected=system.damageDice}}
</select>
</div>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.NpcAttack.FIELDS.colorDiceType.label"}}</label>
<div class="form-fields">
<select name="system.colorDiceType">
{{selectOptions colorChoices selected=system.colorDiceType}}
</select>
</div>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.NpcAttack.FIELDS.ap.label"}}</label>
<div class="form-fields">
<select name="system.ap">
{{selectOptions apChoices selected=system.ap}}
</select>
</div>
</div>
</div>
<fieldset>
<legend>{{localize "OATHHAMMER.NpcAttack.FIELDS.description.label"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</fieldset>
</section>

View File

@@ -0,0 +1,122 @@
<section class="item-sheet-common regiment-sheet">
<div class="header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<fieldset class="regiment-stats">
<legend>{{localize "OATHHAMMER.Label.Stats"}}</legend>
<div class="regiment-stats-row">
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.GritMax"}}</label>
<div class="form-fields">
<input type="number" name="system.grit.max" value="{{system.grit.max}}" min="0" max="200" />
</div>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.ArmorDice"}}</label>
<div class="form-fields regiment-armor-fields">
<input type="number" name="system.armorDice.value" value="{{system.armorDice.value}}" min="0" max="20" />
<select name="system.armorDice.colorDiceType">
{{selectOptions colorChoices selected=system.armorDice.colorDiceType}}
</select>
</div>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.Movement"}}</label>
<div class="form-fields">
<input type="number" name="system.movement" value="{{system.movement}}" min="0" max="500" /> ft
</div>
</div>
</div>
</fieldset>
<fieldset class="regiment-skills">
<legend>
{{localize "OATHHAMMER.Tab.Skills"}}
<a data-action="addSkill" class="create-btn" data-tooltip="{{localize 'OATHHAMMER.NewItem.RegimentSkill'}}"><i class="fa-solid fa-plus"></i></a>
</legend>
{{#if system.skills.length}}
<div class="regiment-skill-header regiment-skill-row">
<span>{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Rank"}}</span>
<span>{{localize "OATHHAMMER.Label.DiceColor"}}</span>
<span></span>
</div>
{{#each system.skills as |skill idx|}}
<div class="regiment-skill-row" data-idx="{{idx}}">
<input type="text" name="system.skills.{{idx}}.name" value="{{skill.name}}" placeholder="{{localize 'OATHHAMMER.Label.SkillName'}}" />
<input type="number" name="system.skills.{{idx}}.value" value="{{skill.value}}" min="1" max="6" />
<select name="system.skills.{{idx}}.colorDiceType">
{{selectOptions ../colorChoices selected=skill.colorDiceType}}
</select>
<a data-action="removeSkill" data-idx="{{idx}}" class="item-delete"><i class="fa-solid fa-times"></i></a>
</div>
{{/each}}
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoSkills"}}</p>
{{/if}}
</fieldset>
<fieldset class="regiment-attacks">
<legend>
{{localize "OATHHAMMER.Label.Attacks"}}
<a data-action="addAttack" class="create-btn" data-tooltip="{{localize 'OATHHAMMER.NewItem.RegimentAttack'}}"><i class="fa-solid fa-plus"></i></a>
</legend>
{{#if system.attacks.length}}
<div class="regiment-attack-header regiment-attack-row">
<span>{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Dice"}}</span>
<span>{{localize "OATHHAMMER.Label.DiceColor"}}</span>
<span>AP</span>
<span>{{localize "OATHHAMMER.Label.Special"}}</span>
<span></span>
</div>
{{#each system.attacks as |attack idx|}}
<div class="regiment-attack-row" data-idx="{{idx}}">
<input type="text" name="system.attacks.{{idx}}.name" value="{{attack.name}}" placeholder="{{localize 'OATHHAMMER.Label.AttackName'}}" />
<select name="system.attacks.{{idx}}.damageDice">
{{selectOptions ../dicePoolChoices selected=attack.damageDice}}
</select>
<select name="system.attacks.{{idx}}.colorDiceType">
{{selectOptions ../colorChoices selected=attack.colorDiceType}}
</select>
<select name="system.attacks.{{idx}}.ap">
{{selectOptions ../apChoices selected=attack.ap}}
</select>
<input type="text" name="system.attacks.{{idx}}.special" value="{{attack.special}}" placeholder="—" />
<a data-action="removeAttack" data-idx="{{idx}}" class="item-delete"><i class="fa-solid fa-times"></i></a>
</div>
{{/each}}
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoAttacks"}}</p>
{{/if}}
</fieldset>
<fieldset class="regiment-traits">
<legend>
{{localize "OATHHAMMER.Tab.Traits"}}
<a data-action="addTrait" class="create-btn" data-tooltip="{{localize 'OATHHAMMER.NewItem.RegimentTrait'}}"><i class="fa-solid fa-plus"></i></a>
</legend>
{{#if system.traits.length}}
{{#each system.traits as |trait idx|}}
<div class="regiment-trait-row" data-idx="{{idx}}">
<input type="text" name="system.traits.{{idx}}.name" value="{{trait.name}}" placeholder="{{localize 'OATHHAMMER.Label.TraitName'}}" />
<input type="text" name="system.traits.{{idx}}.description" value="{{trait.description}}" placeholder="{{localize 'OATHHAMMER.Label.Description'}}" />
<a data-action="removeTrait" data-idx="{{idx}}" class="item-delete"><i class="fa-solid fa-times"></i></a>
</div>
{{/each}}
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoTraits"}}</p>
{{/if}}
</fieldset>
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Description"}}</legend>
<prose-mirror name="system.description" toggled="false" collaborate="false">
{{{system.description}}}
</prose-mirror>
</fieldset>
</section>

View File

@@ -0,0 +1,38 @@
<section class="item-sheet-common skillnpc-sheet">
<div class="header">
<img class="item-img" src="{{item.img}}" data-edit="img" data-action="editImage" data-tooltip="{{item.name}}" />
{{formInput fields.name value=source.name}}
</div>
<div class="skillnpc-stats">
<div class="form-group">
<label>{{localize "OATHHAMMER.SkillNPC.FIELDS.dicePool.label"}}</label>
<div class="form-fields">
<select name="system.dicePool">
{{selectOptions dicePoolChoices selected=system.dicePool}}
</select>
</div>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.SkillNPC.FIELDS.colorDiceType.label"}}</label>
<div class="form-fields">
<select name="system.colorDiceType">
{{selectOptions colorChoices selected=system.colorDiceType}}
</select>
</div>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.SkillNPC.FIELDS.skillRef.label"}}</label>
<div class="form-fields">
<select name="system.skillRef">
{{selectOptions skillRefChoices selected=system.skillRef}}
</select>
</div>
</div>
</div>
<fieldset>
<legend>{{localize "OATHHAMMER.SkillNPC.FIELDS.description.label"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</fieldset>
</section>

View File

@@ -6,6 +6,14 @@
<div class="flexrow">
<div class="align-top">
{{formField systemFields.proficiencyGroup value=system.proficiencyGroup name="system.proficiencyGroup" localize=true}}
<div class="form-group">
<label>{{localize "OATHHAMMER.Weapon.FIELDS.skillOverride.label"}}</label>
<div class="form-fields">
<select name="system.skillOverride">
{{selectOptions skillChoices selected=system.skillOverride}}
</select>
</div>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.Weapon.FIELDS.damageMod.label"}}</label>
<div class="form-fields">
@@ -43,12 +51,29 @@
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</fieldset>
{{#if system.isMagic}}
<fieldset>
<fieldset class="enchantment-fieldset">
<legend>{{localize "OATHHAMMER.Label.Enchantment"}}</legend>
<div class="flexrow">
{{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"}}
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.MagicQuality"}}</label>
<div class="form-fields">
<select name="system.magicQuality">
<option value="">—</option>
{{selectOptions systemFields.magicQuality.choices selected=system.magicQuality localize=true}}
</select>
</div>
<label class="enchant-cursed-label">
<input type="checkbox" name="system.isCursed" {{checked system.isCursed}}>
{{localize "OATHHAMMER.Label.Cursed"}}
</label>
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.ClassRestriction"}}</label>
<div class="form-fields">
<select name="system.classRestriction">
<option value="">— {{localize "OATHHAMMER.ClassRestriction.None"}} —</option>
{{selectOptions classRestrictionChoices selected=system.classRestriction localize=true}}
</select>
</div>
</div>
{{formInput systemFields.magicEffect enriched=enrichedMagicEffect value=system.magicEffect name="system.magicEffect" toggled=true}}
</fieldset>

View File

@@ -0,0 +1,49 @@
<div class="oh-roll-dialog oh-weapon-dialog">
<div class="weapon-header">
<img src="{{itemImg}}" class="weapon-img-sm" alt="{{itemName}}" />
<div class="weapon-header-info">
<span class="weapon-name-lg">{{itemName}}</span>
<div class="weapon-badges">
{{#if dv}}<span class="roll-color-badge color-badge-white">DV {{dv}}</span>{{/if}}
</div>
</div>
</div>
<fieldset class="attack-options-block">
<legend>{{localize "OATHHAMMER.Dialog.Options"}}</legend>
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.DicePool"}}</label>
<select name="dicePool">
{{#each poolOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
</div>
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.Modifier"}}</label>
<select name="bonus">
{{#each bonusOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
</div>
{{#if showColor}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.DiceColor"}}</label>
<select name="colorOverride">
<option value="">— {{localize "OATHHAMMER.Dialog.Default"}} (white) —</option>
{{selectOptions colorChoices selected=""}}
</select>
</div>
{{/if}}
</fieldset>
<fieldset class="roll-visibility-block">
<legend>{{localize "OATHHAMMER.Dialog.Visibility"}}</legend>
<select name="visibility">
{{selectOptions rollModes selected=visibility localize=true}}
</select>
</fieldset>
</div>

View File

@@ -0,0 +1,40 @@
<div class="oh-roll-dialog oh-weapon-dialog">
<div class="weapon-header">
<img src="{{skillImg}}" class="weapon-img-sm" alt="{{skillName}}" />
<div class="weapon-header-info">
<span class="weapon-name-lg">{{skillName}}</span>
<div class="weapon-badges">
<span class="roll-color-badge color-badge-{{colorType}}">{{colorEmoji}} {{dicePool}}d6 ({{threshold}}+)</span>
</div>
</div>
</div>
<fieldset class="attack-options-block">
<legend>{{localize "OATHHAMMER.Dialog.Options"}}</legend>
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.Modifier"}}</label>
<select name="bonus">
{{#each bonusOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
</div>
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.DiceColor"}}</label>
<select name="colorOverride">
<option value="">— {{localize "OATHHAMMER.Dialog.Default"}} ({{colorType}}) —</option>
{{selectOptions colorChoices selected=colorType}}
</select>
</div>
</fieldset>
<fieldset class="roll-visibility-block">
<legend>{{localize "OATHHAMMER.Dialog.Visibility"}}</legend>
<select name="visibility">
{{selectOptions rollModes selected=visibility localize=true}}
</select>
</fieldset>
</div>

View File

@@ -0,0 +1,31 @@
<div class="oh-roll-dialog oh-weapon-dialog">
<div class="weapon-header">
<img src="{{weaponImg}}" class="weapon-img-sm" alt="{{weaponName}}" />
<div class="weapon-header-info">
<span class="weapon-name-lg">{{weaponName}}</span>
<div class="weapon-badges">
<span class="roll-color-badge color-badge-white">⬜ {{basePool}}d6 (4+)</span>
<span class="damage-formula-badge">{{rollType}}</span>
</div>
</div>
</div>
<fieldset class="attack-options-block">
<legend>{{localize "OATHHAMMER.Dialog.Modifier"}}</legend>
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.AttackModifier"}}</label>
<select name="bonus">
{{#each bonusOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
</div>
</fieldset>
<fieldset class="roll-visibility-block">
<legend>{{localize "OATHHAMMER.Dialog.Visibility"}}</legend>
<select name="visibility">
{{selectOptions rollModes selected=visibility localize=true}}
</select>
</fieldset>
</div>