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; text-align: center;
font-weight: bold; font-weight: bold;
} }
.oathhammer .npc-main .npc-left { .oathhammer .stress-controls {
min-width: 160px;
max-width: 160px;
display: flex; display: flex;
flex-direction: column;
align-items: center; 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 { .oathhammer .npc-main .npc-right {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px;
min-width: 0;
}
.oathhammer .npc-main .character-name {
display: flex;
align-items: center;
gap: 4px; 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 { .oathhammer .item-list {
list-style: none; list-style: none;
@@ -772,10 +994,18 @@
.oathhammer .item-list--weapon .item-entry { .oathhammer .item-list--weapon .item-entry {
grid-template-columns: 24px 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 9rem; 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-list-header,
.oathhammer .item-list--armor .item-entry { .oathhammer .item-list--armor .item-entry {
grid-template-columns: 24px 1fr 3.5rem 2.5rem 3.5rem 1.5rem 1.8rem 5.5rem; 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-list-header,
.oathhammer .item-list--ammo .item-entry { .oathhammer .item-list--ammo .item-entry {
grid-template-columns: 24px 1fr 4rem 3.5rem; grid-template-columns: 24px 1fr 4rem 3.5rem;
@@ -812,6 +1042,30 @@
.oathhammer .item-list--oath .item-entry { .oathhammer .item-list--oath .item-entry {
grid-template-columns: 24px 1fr 7rem 3.5rem 3.5rem; 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 { .oathhammer .item-usage {
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
color: #2a1a0a; color: #2a1a0a;
@@ -909,6 +1163,10 @@
background: rgba(192, 57, 43, 0.1); background: rgba(192, 57, 43, 0.1);
border-color: rgba(192, 57, 43, 0.4); 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 { .oathhammer .item-sheet-common {
overflow: auto; overflow: auto;
padding: 10px 20px; padding: 10px 20px;
@@ -1007,6 +1265,20 @@
font-weight: bold; font-weight: bold;
color: #084a74; 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 { .oathhammer .item-sheet-common .proficiency-section {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -1036,6 +1308,91 @@
height: auto; height: auto;
accent-color: #084a74; 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 { .oh-roll-card {
font-family: "Calibri", "Segoe UI", sans-serif; font-family: "Calibri", "Segoe UI", sans-serif;
border: 1px solid #535128; border: 1px solid #535128;
@@ -1044,6 +1401,9 @@
background: rgba(245, 234, 208, 0.4); background: rgba(245, 234, 208, 0.4);
} }
.oh-roll-card .oh-roll-header { .oh-roll-card .oh-roll-header {
display: flex;
align-items: center;
gap: 6px;
font-family: "BlueDragon", "Palatino Linotype", serif; font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: 0.86rem; font-size: 0.86rem;
font-weight: bold; font-weight: bold;
@@ -1052,6 +1412,15 @@
border-bottom: 1px solid rgba(83, 81, 40, 0.2); border-bottom: 1px solid rgba(83, 81, 40, 0.2);
padding-bottom: 3px; 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 { .oh-roll-card .oh-roll-info {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -1368,6 +1737,31 @@
gap: 4px; gap: 4px;
align-items: center; 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 { .oh-weapon-dialog .weapon-header .damage-formula-badge {
font-family: "Calibri", "Segoe UI", sans-serif; font-family: "Calibri", "Segoe UI", sans-serif;
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
@@ -1786,12 +2180,13 @@
gap: 8px; gap: 8px;
} }
.oathhammer .settlement-archetype-badge { .oathhammer .settlement-archetype-badge {
font-size: calc(0.86rem * 0.85); font-size: 0.86rem;
color: #2a1a0a; font-weight: 600;
background: rgba(200, 168, 75, 0.15); color: #150d05;
border: 1px solid rgba(200, 168, 75, 0.4); background: rgba(200, 168, 75, 0.2);
border: 1px solid rgba(200, 168, 75, 0.55);
border-radius: 3px; border-radius: 3px;
padding: 1px 6px; padding: 1px 8px;
white-space: nowrap; white-space: nowrap;
} }
.oathhammer .settlement-stats { .oathhammer .settlement-stats {
@@ -1903,3 +2298,33 @@
.oathhammer .construct-toggle:hover { .oathhammer .construct-toggle:hover {
color: #214621; 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", "Condition": "Oath Hammer Condition Sheet",
"Class": "Oath Hammer Class Sheet", "Class": "Oath Hammer Class Sheet",
"Building": "Oath Hammer Building 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": { "Tab": {
"Identity": "Identity", "Identity": "Oaths / Traits",
"Skills": "Skills", "Skills": "Skills",
"Combat": "Combat", "Combat": "Combat",
"Magic": "Magic", "Magic": "Magic",
@@ -26,7 +29,9 @@
"Notes": "Notes", "Notes": "Notes",
"Overview": "Overview", "Overview": "Overview",
"Buildings": "Buildings", "Buildings": "Buildings",
"Inventory": "Inventory" "Inventory": "Inventory",
"Traits": "Traits",
"Garrison": "Garrison"
}, },
"Attribute": { "Attribute": {
"Might": "Might", "Might": "Might",
@@ -112,14 +117,27 @@
}, },
"Building": { "Building": {
"FIELDS": { "FIELDS": {
"skillCheck": { "label": "Skill Check" }, "skillCheck": {
"cost": { "label": "Cost (gp)" }, "label": "Skill Check"
"buildTime": { "label": "Build Time" }, },
"taxRevenue": { "label": "Tax Revenue / month" }, "cost": {
"constructed":{ "label": "Constructed" }, "label": "Cost (gp)"
},
"description":{ "label": "Description" }, "buildTime": {
"notes": { "label": "Notes" } "label": "Build Time"
},
"taxRevenue": {
"label": "Tax Revenue / month"
},
"constructed": {
"label": "Constructed"
},
"description": {
"label": "Description"
},
"notes": {
"label": "Notes"
}
} }
}, },
"ArmorType": { "ArmorType": {
@@ -166,7 +184,9 @@
"TraitType": { "TraitType": {
"SpecialTrait": "Special Trait", "SpecialTrait": "Special Trait",
"ClassTrait": "Class Trait", "ClassTrait": "Class Trait",
"LineageTrait": "Lineage Trait" "LineageTrait": "Lineage Trait",
"NpcTrait": "NPC Trait",
"CreatureTrait": "Creature Trait"
}, },
"Condition": { "Condition": {
"Blinded": "Blinded", "Blinded": "Blinded",
@@ -220,9 +240,10 @@
"Level": "Level", "Level": "Level",
"XP": "Current XP", "XP": "Current XP",
"TotalXP": "Total XP", "TotalXP": "Total XP",
"Traits": "Special Traits", "Traits": "Traits",
"Oaths": "Oaths", "Oaths": "Oaths",
"Weapons": "Weapons", "Weapons": "Weapons",
"Attacks": "Attacks",
"Armor": "Armor & Shields", "Armor": "Armor & Shields",
"Ammunition": "Ammunition", "Ammunition": "Ammunition",
"ItemSlots": "Item Slots", "ItemSlots": "Item Slots",
@@ -243,14 +264,18 @@
"Components": "Components", "Components": "Components",
"Charges": "Charges", "Charges": "Charges",
"NoWeapons": "No weapons equipped.", "NoWeapons": "No weapons equipped.",
"NoAttacks": "No attacks defined.",
"NoArmor": "No armor or shields.", "NoArmor": "No armor or shields.",
"NoSpells": "No spells known.", "NoSpells": "No spells known.",
"NoMiracles": "No miracles known.", "NoMiracles": "No miracles known.",
"MiracleBlocked": "Divine favour lost — no more miracles today.", "MiracleBlocked": "Divine favour lost — no more miracles today.",
"NoEquipment": "No equipment.", "NoEquipment": "No equipment.",
"NoTraits": "Drop traits here.", "NoTraits": "No traits. Drag trait items here.",
"NoOaths": "No oaths yet.", "NoOaths": "No oaths yet.",
"Enchantment": "Enchantment", "Enchantment": "Enchantment",
"MagicQuality": "Quality",
"Cursed": "Cursed",
"ClassRestriction": "Restriction",
"Tenet": "Tenet", "Tenet": "Tenet",
"Boon": "Boon", "Boon": "Boon",
"Bane": "Bane", "Bane": "Bane",
@@ -262,7 +287,6 @@
"Lineage": "Lineage", "Lineage": "Lineage",
"DropClass": "Drop Class Here", "DropClass": "Drop Class Here",
"OpenClass": "Click to open class details", "OpenClass": "Click to open class details",
"Traits": "Traits",
"Features": "Features", "Features": "Features",
"Name": "Name", "Name": "Name",
"Type": "Type", "Type": "Type",
@@ -300,7 +324,28 @@
"TaxRevenue": "Tax Revenue", "TaxRevenue": "Tax Revenue",
"Cost": "Cost", "Cost": "Cost",
"Built": "Built", "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": { "ColorDice": {
"White": "White (4+)", "White": "White (4+)",
@@ -314,9 +359,17 @@
"Equipment": "New Equipment", "Equipment": "New Equipment",
"Building": "New Building", "Building": "New Building",
"Trait": "New Trait", "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", "ToggleSheet": "Toggle Edit/Play Mode",
"Tooltip": {
"RollArmor": "Roll Armor Dice"
},
"Action": { "Action": {
"CastSpell": "Cast Spell", "CastSpell": "Cast Spell",
"InvokeMiracle": "Invoke Miracle", "InvokeMiracle": "Invoke Miracle",
@@ -558,10 +611,26 @@
"label": "Attributes" "label": "Attributes"
}, },
"grit": { "grit": {
"label": "Grit" "label": "Grit",
"fields": {
"max": {
"label": "Max Grit"
},
"value": {
"label": "Grit"
}
}
}, },
"defense": { "defense": {
"label": "Defense" "label": "Defense",
"fields": {
"value": {
"label": "Defense"
},
"colorDiceType": {
"label": "Defense Dice Color"
}
}
}, },
"movement": { "movement": {
"label": "Movement" "label": "Movement"
@@ -580,6 +649,12 @@
}, },
"notes": { "notes": {
"label": "Notes" "label": "Notes"
},
"subtype": {
"label": "Subtype"
},
"armorDice": {
"label": "Armor Dice"
} }
} }
}, },
@@ -641,8 +716,13 @@
}, },
"classRestriction": { "classRestriction": {
"label": "Restriction" "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": { "Armor": {
"FIELDS": { "FIELDS": {
@@ -922,6 +1002,9 @@
"Greater": "Greater", "Greater": "Greater",
"Legendary": "Legendary" "Legendary": "Legendary"
}, },
"ClassRestriction": {
"None": "No restriction"
},
"OathType": { "OathType": {
"Compassion": "Oath of Compassion", "Compassion": "Oath of Compassion",
"Courage": "Oath of Courage", "Courage": "Oath of Courage",
@@ -976,20 +1059,71 @@
"MiracleBlocked": "Divine favour has been lost — you cannot invoke miracles until a new day." "MiracleBlocked": "Divine favour has been lost — you cannot invoke miracles until a new day."
}, },
"SettlementArchetype": { "SettlementArchetype": {
"CenterOfLearning": "Center of Learning", "CenterOfLearning": "Center of Learning",
"DwarvenBorough": "Dwarven Borough", "DwarvenBorough": "Dwarven Borough",
"FreeCity": "Free City", "FreeCity": "Free City",
"GuildMunicipality": "Guild Municipality", "GuildMunicipality": "Guild Municipality",
"NocklanderOutpost": "Nocklander Outpost", "NocklanderOutpost": "Nocklander Outpost",
"PilgrimMission": "Pilgrim Mission", "PilgrimMission": "Pilgrim Mission",
"PortTown": "Port Town", "PortTown": "Port Town",
"VelathiColony": "Velathi Colony" "VelathiColony": "Velathi Colony"
}, },
"Settlement": { "Settlement": {
"BuildingHint": "Drag & drop building items here. Toggle the checkbox to mark them as constructed.", "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.", "InventoryHint": "Drag & drop weapons, armor, or equipment here to store them in the settlement reserve.",
"NoBuildings": "No buildings yet. Drag building items here.", "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": { "TYPES": {
@@ -1004,11 +1138,15 @@
"magic-item": "Magic Item", "magic-item": "Magic Item",
"trait": "Trait", "trait": "Trait",
"oath": "Oath", "oath": "Oath",
"class": "Class" "class": "Class",
"skillnpc": "NPC Skill",
"npcattack": "NPC Attack",
"regiment": "Regiment",
"building": "Building"
}, },
"Actor": { "Actor": {
"character": "Character", "character": "Character",
"npc": "NPC", "npc": "NPC / Creature",
"settlement": "Settlement" "settlement": "Settlement"
} }
} }

View File

@@ -325,4 +325,58 @@
text-align: center; text-align: center;
font-weight: bold; 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--armor {
.item-list-header, .item-entry { .item-list-header, .item-entry {
grid-template-columns: @item-img-size 1fr 3.5rem 2.5rem 3.5rem 1.5rem 1.8rem 5.5rem; 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--ammo {
.item-list-header, .item-entry { .item-list-header, .item-entry {
grid-template-columns: @item-img-size 1fr 4rem 3.5rem; 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 { .item-usage {
font-size: @font-size-xs; font-size: @font-size-xs;
color: @color-dark; 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; 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 ──────────────────────────── // ── Class proficiency checkboxes ────────────────────────────
.proficiency-section { .proficiency-section {
display: flex; 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 { .oathhammer .npc-main {
.npc-left { .npc-left {
min-width: @npc-left-width; min-width: @npc-left-width;
max-width: @npc-left-width; max-width: @npc-left-width;
display: flex; flex-shrink: 0;
flex-direction: column;
align-items: center; .actor-img {
gap: 4px; width: 100%;
height: 170px;
object-fit: cover;
object-position: center top;
}
} }
.npc-right { .npc-right {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 6px;
min-width: 0;
} }
// Name row
.character-name {
display: flex;
align-items: center;
gap: 4px;
border-bottom: 1px solid @color-olive;
padding-bottom: 4px;
input {
flex: 1;
font-family: @font-primary;
font-size: @font-size-lg;
}
}
// Vitals: 2×2 grid (Grit | Defense / Armor | Movement)
.npc-vitals-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 4px 10px;
padding: 4px 6px;
border: 1px solid @color-olive;
border-radius: 3px;
background: rgba(0, 0, 0, 0.08);
.npc-vital {
display: flex;
align-items: center;
gap: 4px;
.vital-label {
font-family: @font-secondary;
font-size: @font-size-sm;
font-weight: bold;
color: @color-dark;
white-space: nowrap;
min-width: 4.5rem;
}
.vital-roll-label {
cursor: pointer;
color: @color-blue;
transition: color 0.15s;
i { margin-right: 2px; font-size: 0.75rem; }
&:hover { color: @color-gold; text-decoration: underline; }
}
.vital-value {
display: flex;
align-items: center;
gap: 3px;
flex: 1;
div.form-group { display: contents; }
input, .npc-num-input {
width: 2.8rem;
text-align: center;
font-size: @font-size-sm;
padding: 1px 2px;
}
.res-sep { opacity: 0.6; font-size: @font-size-xs; }
}
}
.npc-vital-grit .vital-value {
.grit-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.2rem;
height: 1.2rem;
font-size: 0.85rem;
font-weight: bold;
line-height: 1;
border: 1px solid @color-olive;
border-radius: 3px;
background: @color-olive-faint;
color: @color-dark;
cursor: pointer;
user-select: none;
flex-shrink: 0;
&:hover { background: @color-gold; border-color: @color-gold; }
}
}
}
// Statistics fieldset grid
} }
// Inline color badge (emoji in play mode)
.oathhammer .npc-color-badge {
font-size: 0.85rem;
flex-shrink: 0;
line-height: 1;
}
// Inline color dropdown (in edit mode) — fixed width so it doesn't stretch the grid
.oathhammer .npc-color-select {
width: 88px;
max-width: 88px;
flex-shrink: 0;
font-size: @font-size-xs;
padding: 0 2px;
height: 20px;
min-width: 0;
}
// Subtype badge (play mode) / select (edit mode)
.oathhammer .npc-subtype-badge {
font-size: @font-size-base;
font-weight: 600;
color: @color-dark;
background: fade(@color-gold, 20%);
border: 1px solid fade(@color-gold, 50%);
border-radius: 3px;
padding: 1px 8px;
}
.oathhammer .npc-subtype-select {
font-size: @font-size-sm;
padding: 0 2px;
height: 20px;
min-width: 80px;
}
// ============================================================
// NPC SKILLS TAB
// ============================================================
// Color emoji indicator per dice type
.oathhammer .npc-skill-color {
font-size: 0.95rem;
text-align: center;
line-height: 1;
justify-self: center;
}
.oathhammer .npc-skill-color-white { opacity: 0.75; }
.oathhammer .npc-skill-color-red { color: #cc4444; }
.oathhammer .npc-skill-color-black { color: #333; font-weight: bold; }
// Roll button in skill list
.oathhammer .npc-skill-roll-btn {
color: @color-gold;
padding: 2px 4px;
border: 1px solid fade(@color-gold, 40%);
border-radius: 3px;
font-size: @font-size-sm;
text-align: center;
justify-self: center;
&:hover { background: fade(@color-gold, 15%); }
}
// ============================================================
// NPC TRAITS TAB
// ============================================================
// Type badge used in traits list
.oathhammer .npc-trait-type-badge {
font-size: 0.72rem;
padding: 1px 5px;
border-radius: 3px;
white-space: nowrap;
background: fade(@color-gold, 15%);
border: 1px solid fade(@color-gold, 35%);
color: @color-dark;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
}
// ============================================================
// NPC SKILL DIALOG
// ============================================================
.npc-skill-dialog {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px 0;
}

View File

@@ -284,6 +284,21 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
align-items: center; 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 { .damage-formula-badge {

View File

@@ -10,6 +10,9 @@
background: fade(#f5ead0, 40%); background: fade(#f5ead0, 40%);
.oh-roll-header { .oh-roll-header {
display: flex;
align-items: center;
gap: 6px;
font-family: @font-secondary; font-family: @font-secondary;
font-size: @font-size-base; font-size: @font-size-base;
font-weight: bold; font-weight: bold;
@@ -17,6 +20,15 @@
margin-bottom: 4px; margin-bottom: 4px;
border-bottom: 1px solid @color-olive-faint; border-bottom: 1px solid @color-olive-faint;
padding-bottom: 3px; 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 { .oh-roll-info {

View File

@@ -42,12 +42,13 @@
} }
.oathhammer .settlement-archetype-badge { .oathhammer .settlement-archetype-badge {
font-size: @font-size-sm; font-size: @font-size-base;
color: @color-dark; font-weight: 600;
background: fade(@color-gold, 15%); color: darken(@color-dark, 5%);
border: 1px solid fade(@color-gold, 40%); background: fade(@color-gold, 20%);
border: 1px solid fade(@color-gold, 55%);
border-radius: 3px; border-radius: 3px;
padding: 1px 6px; padding: 1px 8px;
white-space: nowrap; white-space: nowrap;
} }
@@ -176,3 +177,34 @@
&:hover { color: darken(#3a7a3a, 15%); } &: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 // Layout
@portrait-height: 150px; @portrait-height: 150px;
@left-panel-width: 180px; @left-panel-width: 180px;
@npc-left-width: 160px; @npc-left-width: 120px;
@item-img-size: 24px; @item-img-size: 24px;
@item-sheet-img: 52px; @item-sheet-img: 52px;
@label-min-width: 9rem; @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 OathHammerClassSheet } from "./sheets/class-sheet.mjs"
export { default as OathHammerBuildingSheet } from "./sheets/building-sheet.mjs" export { default as OathHammerBuildingSheet } from "./sheets/building-sheet.mjs"
export { default as OathHammerSettlementSheet } from "./sheets/settlement-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 OathHammerRollDialog } from "./roll-dialog.mjs"
export { default as OathHammerWeaponDialog } from "./weapon-dialog.mjs" export { default as OathHammerWeaponDialog } from "./weapon-dialog.mjs"
export { default as OathHammerSpellDialog } from "./spell-dialog.mjs" export { default as OathHammerSpellDialog } from "./spell-dialog.mjs"

View File

@@ -1,5 +1,5 @@
const { HandlebarsApplicationMixin } = foundry.applications.api 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" import { rollRarityCheck } from "../../rolls.mjs"
export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) { 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) { if (this.document.system.description !== undefined) {
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description ?? "", { async: true }) 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 // Armor-specific numeric selects
context.armorValueChoices = Object.fromEntries( context.armorValueChoices = Object.fromEntries(
Array.from({ length: 13 }, (_, i) => [i, String(i)]) Array.from({ length: 13 }, (_, i) => [i, String(i)])
@@ -71,6 +75,13 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
context.apChoices = Object.fromEntries( context.apChoices = Object.fromEntries(
Array.from({ length: 7 }, (_, i) => [i, String(i)]) 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) // Class proficiency choices (for class-sheet checkboxes)
context.armorTypeChoices = ARMOR_TYPE_CHOICES context.armorTypeChoices = ARMOR_TYPE_CHOICES
context.weaponGroupChoices = WEAPON_PROFICIENCY_GROUPS context.weaponGroupChoices = WEAPON_PROFICIENCY_GROUPS

View File

@@ -118,7 +118,6 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
_descTooltip: _stripHtml(parts.join(" ")) _descTooltip: _stripHtml(parts.join(" "))
} }
}) })
context.enrichedBackground = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.background ?? "", { async: true })
break break
case "skills": { case "skills": {
context.tab = context.tabs.skills context.tab = context.tabs.skills
@@ -149,7 +148,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
modifierName: `system.skills.${skillKey}.modifier`, modifierName: `system.skills.${skillKey}.modifier`,
colorDiceName: `system.skills.${skillKey}.colorDice`, colorDiceName: `system.skills.${skillKey}.colorDice`,
colorDiceTypeName: `system.skills.${skillKey}.colorDiceType`, 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, total: attrRanks[attr] + sk.rank,
// legacy - kept for formInput compatibility // legacy - kept for formInput compatibility
name: `system.skills.${skillKey}.rank`, name: `system.skills.${skillKey}.rank`,
@@ -232,6 +231,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
break break
case "notes": case "notes":
context.tab = context.tabs.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.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.description ?? "", { async: true })
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes ?? "", { async: true }) context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes ?? "", { async: true })
break break

View File

@@ -1,50 +1,56 @@
import OathHammerActorSheet from "./base-actor-sheet.mjs" 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 { export default class OathHammerNPCSheet extends OathHammerActorSheet {
/** @override */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ["npc"], classes: ["npc"],
position: { position: { width: 720, height: "auto" },
width: 720, window: { contentClasses: ["npc-content"] },
height: "auto",
},
window: {
contentClasses: ["npc-content"],
},
actions: { actions: {
rollInitiative: OathHammerNPCSheet.#onRollInitiative, rollInitiative: OathHammerNPCSheet.#onRollInitiative,
adjustGrit: OathHammerNPCSheet.#onAdjustGrit,
rollSkillNPC: OathHammerNPCSheet.#onRollSkillNPC,
rollArmor: OathHammerNPCSheet.#onRollArmor,
createSpell: OathHammerNPCSheet.#onCreateSpell,
createMiracle: OathHammerNPCSheet.#onCreateMiracle,
castNPCSpell: OathHammerNPCSheet.#onCastNPCSpell,
castNPCMiracle: OathHammerNPCSheet.#onCastNPCMiracle,
createNpcAttack: OathHammerNPCSheet.#onCreateNpcAttack,
rollNpcAttack: OathHammerNPCSheet.#onRollNpcAttack,
}, },
} }
/** @override */
static PARTS = { static PARTS = {
main: { main: { template: "systems/fvtt-oath-hammer/templates/actor/npc-sheet.hbs" },
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" },
tabs: { combat: { template: "systems/fvtt-oath-hammer/templates/actor/npc-combat.hbs" },
template: "templates/generic/tab-navigation.hbs", traits: { template: "systems/fvtt-oath-hammer/templates/actor/npc-traits.hbs" },
}, magic: { template: "systems/fvtt-oath-hammer/templates/actor/npc-magic.hbs" },
combat: { equipment: { template: "systems/fvtt-oath-hammer/templates/actor/npc-equipment.hbs" },
template: "systems/fvtt-oath-hammer/templates/actor/npc-combat.hbs", notes: { template: "systems/fvtt-oath-hammer/templates/actor/npc-notes.hbs" },
},
notes: {
template: "systems/fvtt-oath-hammer/templates/actor/npc-notes.hbs",
},
} }
/** @override */ tabGroups = { sheet: "skills" }
tabGroups = {
sheet: "combat",
}
#getTabs() { #getTabs() {
const isNPC = this.document.system.subtype === "npc"
const hasMagic = this.document.items.some(i => i.type === "spell" || i.type === "miracle")
const tabs = { const tabs = {
combat: { id: "combat", group: "sheet", icon: "fa-solid fa-swords", label: "OATHHAMMER.Tab.Combat" }, skills: { id: "skills", group: "sheet", icon: "fa-solid fa-dice-d6", label: "OATHHAMMER.Tab.Skills" },
notes: { id: "notes", group: "sheet", icon: "fa-solid fa-book", label: "OATHHAMMER.Tab.Notes" }, 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)) { for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : "" v.cssClass = v.active ? "active" : ""
} }
return tabs return tabs
@@ -54,6 +60,23 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
async _prepareContext() { async _prepareContext() {
const context = await super._prepareContext() const context = await super._prepareContext()
context.tabs = this.#getTabs() 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 return context
} }
@@ -61,29 +84,265 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
async _preparePartContext(partId, context) { async _preparePartContext(partId, context) {
const doc = this.document const doc = this.document
switch (partId) { 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 break
case "combat": case "combat":
context.tab = context.tabs.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 context.combatantInitiative = game.combat?.combatants.find(c => c.actor?.id === doc.id)?.initiative ?? null
break 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": case "notes":
context.tab = context.tabs.notes context.tab = context.tabs.notes
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.description, { 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 }) context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes, { async: true })
break break
} }
return context return context
} }
/** @override */
async _onDrop(event) { async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return if (!this.isEditable || !this.isEditMode) return
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event) const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data.type === "Item") { if (data.type !== "Item") return
const item = await fromUuid(data.uuid) const item = await fromUuid(data.uuid)
return this._onDropItem(item) 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() { static async #onRollInitiative() {
@@ -95,4 +354,53 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
await rollInitiativeCheck(actor) 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" 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 { export default class OathHammerSettlementSheet extends OathHammerActorSheet {
/** @override */ /** @override */
@@ -14,9 +14,11 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
contentClasses: ["settlement-content"], contentClasses: ["settlement-content"],
}, },
actions: { actions: {
adjustCurrency: OathHammerSettlementSheet.#onAdjustCurrency, adjustCurrency: OathHammerSettlementSheet.#onAdjustCurrency,
adjustQty: OathHammerSettlementSheet.#onAdjustQty, adjustQty: OathHammerSettlementSheet.#onAdjustQty,
toggleConstructed: OathHammerSettlementSheet.#onToggleConstructed, toggleConstructed: OathHammerSettlementSheet.#onToggleConstructed,
createRegiment: OathHammerSettlementSheet.#onCreateRegiment,
collectTaxes: OathHammerSettlementSheet.#onCollectTaxes,
}, },
} }
@@ -37,6 +39,9 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
inventory: { inventory: {
template: "systems/fvtt-oath-hammer/templates/actor/settlement-inventory.hbs", template: "systems/fvtt-oath-hammer/templates/actor/settlement-inventory.hbs",
}, },
garrison: {
template: "systems/fvtt-oath-hammer/templates/actor/settlement-garrison.hbs",
},
} }
/** @override */ /** @override */
@@ -46,9 +51,10 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
#getTabs() { #getTabs() {
const tabs = { const tabs = {
overview: { id: "overview", group: "sheet", icon: "fa-solid fa-city", label: "OATHHAMMER.Tab.Overview" }, 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" }, 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" }, 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)) { for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id v.active = this.tabGroups[v.group] === v.id
@@ -81,7 +87,11 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
break break
case "buildings": case "buildings":
context.tab = context.tabs.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 break
case "inventory": { case "inventory": {
context.tab = context.tabs.inventory context.tab = context.tabs.inventory
@@ -90,6 +100,10 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
context.equipments = doc.itemTypes.equipment context.equipments = doc.itemTypes.equipment
break break
} }
case "garrison":
context.tab = context.tabs.garrison
context.regiments = doc.itemTypes.regiment ?? []
break
} }
return context return context
} }
@@ -129,4 +143,59 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
if (!item) return if (!item) return
await item.update({ "system.constructed": !item.system.constructed }) 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 actorSys = actor.system
const isRanged = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing" 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 skillDef = SYSTEM.SKILLS[skillKey]
const defaultAttr = skillDef.attribute const defaultAttr = skillDef.attribute
const attrRank = actorSys.attributes[defaultAttr].rank const attrRank = actorSys.attributes[defaultAttr].rank
@@ -53,7 +53,8 @@ export default class OathHammerWeaponDialog {
const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4 const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4
const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜" const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
const mightRank = actorSys.attributes.might.rank 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 traitLabels = [...sys.traits].map(t => {
const key = SYSTEM.WEAPON_TRAITS[t] const key = SYSTEM.WEAPON_TRAITS[t]
@@ -329,8 +330,11 @@ export default class OathHammerWeaponDialog {
const damageColorType = hasDeadly ? "black" : hasBrutal ? "red" : "white" const damageColorType = hasDeadly ? "black" : hasBrutal ? "red" : "white"
const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4 const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4
const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜" const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
const mightRank = actorSys.attributes.might.rank const isRanged = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing"
const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1) 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 // Auto-bonuses from special properties
let autoDamageBonus = 0 let autoDamageBonus = 0

View File

@@ -177,6 +177,19 @@ export const MAGIC_QUALITY_CHOICES = {
legendary: "OATHHAMMER.MagicQuality.Legendary" 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 = { export const RARITY_CHOICES = {
always: "OATHHAMMER.Rarity.Always", always: "OATHHAMMER.Rarity.Always",
common: "OATHHAMMER.Rarity.Common", common: "OATHHAMMER.Rarity.Common",
@@ -198,9 +211,22 @@ export const RARITY_DV = {
// Two types of trait per the rulebook terminology // Two types of trait per the rulebook terminology
export const TRAIT_TYPE_CHOICES = { export const TRAIT_TYPE_CHOICES = {
"special-trait": "OATHHAMMER.TraitType.SpecialTrait", "special-trait": "OATHHAMMER.TraitType.SpecialTrait",
"class-trait": "OATHHAMMER.TraitType.ClassTrait", "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) // When a trait's uses reset (none = passive/always on)
@@ -395,10 +421,13 @@ export const SYSTEM = {
EQUIPMENT_TYPE_CHOICES, EQUIPMENT_TYPE_CHOICES,
MAGIC_ITEM_TYPE_CHOICES, MAGIC_ITEM_TYPE_CHOICES,
MAGIC_QUALITY_CHOICES, MAGIC_QUALITY_CHOICES,
CLASS_RESTRICTION_CHOICES,
RARITY_CHOICES, RARITY_CHOICES,
RARITY_DV, RARITY_DV,
TRAIT_TYPE_CHOICES, TRAIT_TYPE_CHOICES,
TRAIT_USAGE_PERIOD, TRAIT_USAGE_PERIOD,
NPC_SUBTYPES,
DICE_COLOR_TYPES,
BUILDING_SKILL_CHOICES, BUILDING_SKILL_CHOICES,
SETTLEMENT_ARCHETYPES, SETTLEMENT_ARCHETYPES,
STATUS_EFFECTS, 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 OathHammerClass } from "./class.mjs"
export { default as OathHammerBuilding } from "./building.mjs" export { default as OathHammerBuilding } from "./building.mjs"
export { default as OathHammerSettlement } from "./settlement.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. // 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. // Color dice: type (white 4+, red 3+, black 2+) + count of colored dice in the pool.
const skillField = () => new fields.SchemaField({ 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 }), modifier: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
colorDiceType: new fields.StringField({ required: true, nullable: false, initial: "white", colorDiceType: new fields.StringField({ required: true, nullable: false, initial: "white",
choices: { white: "OATHHAMMER.ColorDice.White", red: "OATHHAMMER.ColorDice.Red", black: "OATHHAMMER.ColorDice.Black" } }), 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.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: true, textSearch: true }) schema.notes = new fields.HTMLField({ required: true, textSearch: true })
const attributeField = () => new fields.SchemaField({ // NPC (humanoid, needs light) vs Creature (monster, darkvision)
rank: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1, max: 4 }) schema.subtype = new fields.StringField({
}) required: true, initial: "creature", choices: SYSTEM.NPC_SUBTYPES
schema.attributes = new fields.SchemaField({
might: attributeField(),
toughness: attributeField(),
agility: attributeField(),
willpower: attributeField(),
intelligence: attributeField(),
fate: attributeField()
}) })
schema.grit = new fields.SchemaField({ 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 }) 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({ 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({ schema.movement = new fields.SchemaField({
@@ -37,7 +37,7 @@ export default class OathHammerNPC extends foundry.abstract.TypeDataModel {
schema.attackBonus = new fields.NumberField({ ...requiredInteger, initial: 0 }) schema.attackBonus = new fields.NumberField({ ...requiredInteger, initial: 0 })
schema.damageBonus = new fields.NumberField({ ...requiredInteger, initial: 0 }) schema.damageBonus = new fields.NumberField({ ...requiredInteger, initial: 0 })
schema.initiativeBonus = new fields.NumberField({ ...requiredInteger, initial: 0 }) schema.initiativeBonus = new fields.NumberField({ ...requiredInteger, initial: 0 })
schema.challengeRating = new fields.StringField({ required: true, nullable: false, initial: "1" }) schema.challengeRating = new fields.StringField({ required: true, nullable: false, initial: "1" })
return schema return schema
} }
@@ -46,6 +46,5 @@ export default class OathHammerNPC extends foundry.abstract.TypeDataModel {
prepareDerivedData() { prepareDerivedData() {
super.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) // Enchantment description (displayed when isMagic is true)
schema.magicEffect = new fields.HTMLField({ required: false, textSearch: true }) schema.magicEffect = new fields.HTMLField({ required: false, textSearch: true })
// Class/lineage restriction, e.g. "Dwarves only" (empty = no restriction) // 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 return schema
} }

View File

@@ -242,7 +242,7 @@ export async function rollWeaponAttack(actor, weapon, options = {}) {
const actorSys = actor.system const actorSys = actor.system
const isRanged = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing" 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 skillDef = SYSTEM.SKILLS[skillKey]
const defaultAttr = skillDef.attribute const defaultAttr = skillDef.attribute
@@ -339,8 +339,11 @@ export async function rollWeaponDamage(actor, weapon, options = {}) {
const colorEmoji = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜" const colorEmoji = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
const colorLabel = hasDeadly ? "Black" : hasBrutal ? "Red" : "White" const colorLabel = hasDeadly ? "Black" : hasBrutal ? "Red" : "White"
const mightRank = actorSys.attributes.might.rank const isRangedDmg = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing"
const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1) 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 totalDice = Math.max(baseDamageDice + sv + damageBonus + autoDamageBonus, 1)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold) 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 } 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, trait: models.OathHammerTrait,
oath: models.OathHammerOath, oath: models.OathHammerOath,
"class": models.OathHammerClass, "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) 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.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.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.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 CONFIG.statusEffects = STATUS_EFFECTS

View File

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

View File

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

View File

@@ -1,4 +1,8 @@
<section data-tab="notes" data-group="{{tab.group}}" class="tab {{tab.cssClass}}"> <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> <fieldset>
<legend>{{localize "OATHHAMMER.Label.Description"}}</legend> <legend>{{localize "OATHHAMMER.Label.Description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} {{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"> <div class="initiative-bar">
<a data-action="rollInitiative" class="initiative-roll-btn" data-tooltip="{{localize 'OATHHAMMER.Roll.InitiativeHint'}}"> <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"}} <i class="fa-solid fa-swords"></i> {{localize "OATHHAMMER.Dialog.RollInitiative"}}
</a> </a>
{{#if combatantInitiative}}<span class="initiative-score" data-tooltip="{{localize 'OATHHAMMER.Label.Initiative'}}">⚔ {{combatantInitiative}}</span>{{/if}} {{#if combatantInitiative}}<span class="initiative-score" data-tooltip="{{localize 'OATHHAMMER.Label.Initiative'}}">⚔ {{combatantInitiative}}</span>{{/if}}
</div> </div>
<fieldset> <fieldset>
<legend>{{localize "OATHHAMMER.Label.Weapons"}}</legend> <legend>{{localize "OATHHAMMER.Label.Attacks"}}
{{#if weapons.length}} {{#unless isPlayMode}}<a data-action="createNpcAttack" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
<ul class="item-list"> </legend>
{{#each weapons as |weapon|}} {{#if npcAttacks.length}}
<li class="item-entry flexrow" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"> <ul class="item-list item-list--npc-attack">
<img src="{{weapon.img}}" class="item-img" /> <li class="item-list-header">
<span class="item-name">{{weapon.name}}</span> <span></span>
<span class="item-detail">{{weapon.system.damageLabel}}</span> <span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span class="item-detail">AP: {{weapon.system.ap}}</span> <span>{{localize "OATHHAMMER.Label.Damage"}}</span>
{{#unless ../isPlayMode}} <span title="Armor Penetration">AP</span>
<a data-action="edit" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-edit"></i></a> <span></span>
<a data-action="delete" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-trash"></i></a> </li>
{{/unless}} {{#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> </li>
{{/each}} {{/each}}
</ul> </ul>
{{else}} {{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoWeapons"}}</p> <p class="no-items">{{localize "OATHHAMMER.Label.NoAttacks"}}</p>
{{/if}} {{/if}}
</fieldset> </fieldset>
</section> </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> <fieldset>
<legend>{{localize "OATHHAMMER.Label.Description"}}</legend> <legend>{{localize "OATHHAMMER.Label.Description"}}</legend>
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} {{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'}}"> <section class="npc-main npc-main-{{ifThen isPlayMode 'play' 'edit'}}">
<fieldset> <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"> <div class="npc-pc flexrow">
<!-- LEFT: portrait only -->
<div class="npc-left"> <div class="npc-left">
<img class="actor-img" src="{{actor.img}}" data-edit="img" data-action="editImage" data-tooltip="{{actor.name}}" /> <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> </div>
<!-- RIGHT: name + vitals grid + stats -->
<div class="npc-right"> <div class="npc-right">
<div class="character-name"> <div class="character-name">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}} {{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> <i class="fa-solid fa-user-{{ifThen isPlayMode 'lock' 'pen'}}"></i>
</a> </a>
</div> </div>
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Stats"}}</legend> <!-- Vitals: 2×2 grid -->
<div class="flexrow"> <div class="npc-vitals-grid">
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.CR"}}</label> <div class="npc-vital npc-vital-grit">
{{formInput systemFields.challengeRating value=system.challengeRating name="system.challengeRating" disabled=isPlayMode}} <span class="vital-label">{{localize "OATHHAMMER.Label.Grit"}}</span>
</div> <span class="vital-value">
<div class="form-group"> <a class="grit-btn" data-action="adjustGrit" data-delta="-1" data-tooltip="1"></a>
<label>{{localize "OATHHAMMER.Label.AttackBonus"}}</label> <input type="number" class="npc-num-input" name="system.grit.value" value="{{system.grit.value}}" min="0" />
{{formInput systemFields.attackBonus value=system.attackBonus name="system.attackBonus" disabled=isPlayMode}} <span class="res-sep">/</span>
</div> {{formInput systemFields.grit.fields.max value=system.grit.max name="system.grit.max" disabled=isPlayMode}}
<div class="form-group"> <a class="grit-btn" data-action="adjustGrit" data-delta="1" data-tooltip="+1">+</a>
<label>{{localize "OATHHAMMER.Label.DamageBonus"}}</label> </span>
{{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> </div>
</fieldset>
<fieldset> <div class="npc-vital">
<legend>{{localize "OATHHAMMER.Label.Attributes"}}</legend> <span class="vital-label{{#if isPlayMode}} vital-roll-label{{/if}}"
<div class="attributes-grid"> {{#if isPlayMode}}data-action="rollArmor" data-tooltip="OATHHAMMER.Tooltip.RollArmor"{{/if}}>
{{#each system.attributes as |attr key|}} {{#if isPlayMode}}<i class="fa-solid fa-dice-d6"></i>{{/if}}
<div class="attribute-box"> {{localize "OATHHAMMER.Label.ArmorDice"}}
<label>{{localize (concat "OATHHAMMER.Attribute." (capitalize key))}}</label> </span>
{{formInput (lookup ../systemFields.attributes.fields key).fields.rank value=attr.rank name=(concat "system.attributes." key ".rank") disabled=../isPlayMode}} <span class="vital-value">
</div> <input type="number" class="npc-num-input" name="system.armorDice.value" value="{{system.armorDice.value}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
{{/each}} {{#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>
</fieldset>
</div> <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> </div>
</fieldset> </fieldset>
</section> </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}}"> <section data-tab="buildings" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<p class="settlement-hint">{{localize "OATHHAMMER.Settlement.BuildingHint"}}</p> <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}} {{#if buildings.length}}
<ul class="item-list item-list--buildings"> <ul class="item-list item-list--buildings">
@@ -14,7 +21,7 @@
{{#each buildings as |building|}} {{#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}}"> <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" /> <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"> <span class="item-constructed">
{{#unless ../isPlayMode}} {{#unless ../isPlayMode}}
<a data-action="toggleConstructed" data-item-id="{{building.id}}" class="construct-toggle"> <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> <span class="item-cost">{{building.system.cost}} gp</span>
<div class="item-actions"> <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> <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> </div>
</li> </li>
{{/each}} {{/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> <span class="item-type">{{weapon.system.weaponType}}</span>
<div class="item-actions"> <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> <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> </div>
</li> </li>
{{/each}} {{/each}}
@@ -43,7 +43,7 @@
<span class="item-type">{{localize armor.system.armorType}}</span> <span class="item-type">{{localize armor.system.armorType}}</span>
<div class="item-actions"> <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> <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> </div>
</li> </li>
{{/each}} {{/each}}
@@ -78,7 +78,7 @@
</span> </span>
<div class="item-actions"> <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> <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> </div>
</li> </li>
{{/each}} {{/each}}

View File

@@ -11,22 +11,6 @@
<a data-action="adjustCurrency" data-field="system.currency.gold" data-delta="1" class="qty-btn">+</a> <a data-action="adjustCurrency" data-field="system.currency.gold" data-delta="1" class="qty-btn">+</a>
</div> </div>
</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> </div>
</fieldset> </fieldset>

View File

@@ -14,10 +14,6 @@
</a> </a>
</div> </div>
<div class="settlement-stats flexrow"> <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"> <div class="stat-item">
<label>{{localize "OATHHAMMER.Label.Territory"}}</label> <label>{{localize "OATHHAMMER.Label.Territory"}}</label>
{{formInput systemFields.territory value=system.territory name="system.territory" disabled=isPlayMode}} {{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="flexrow">
<div class="align-top"> <div class="align-top">
{{formField systemFields.proficiencyGroup value=system.proficiencyGroup name="system.proficiencyGroup" localize=true}} {{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"> <div class="form-group">
<label>{{localize "OATHHAMMER.Weapon.FIELDS.damageMod.label"}}</label> <label>{{localize "OATHHAMMER.Weapon.FIELDS.damageMod.label"}}</label>
<div class="form-fields"> <div class="form-fields">
@@ -43,12 +51,29 @@
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}} {{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</fieldset> </fieldset>
{{#if system.isMagic}} {{#if system.isMagic}}
<fieldset> <fieldset class="enchantment-fieldset">
<legend>{{localize "OATHHAMMER.Label.Enchantment"}}</legend> <legend>{{localize "OATHHAMMER.Label.Enchantment"}}</legend>
<div class="flexrow"> <div class="form-group">
{{formField systemFields.magicQuality value=system.magicQuality name="system.magicQuality" localize=true}} <label>{{localize "OATHHAMMER.Label.MagicQuality"}}</label>
{{formField systemFields.isCursed value=system.isCursed name="system.isCursed"}} <div class="form-fields">
{{formField systemFields.classRestriction value=system.classRestriction name="system.classRestriction"}} <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> </div>
{{formInput systemFields.magicEffect enriched=enrichedMagicEffect value=system.magicEffect name="system.magicEffect" toggled=true}} {{formInput systemFields.magicEffect enriched=enrichedMagicEffect value=system.magicEffect name="system.magicEffect" toggled=true}}
</fieldset> </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>