Add party an army sheeets

This commit is contained in:
2026-03-25 18:02:39 +01:00
parent b46c6d804c
commit f1dda301d7
37 changed files with 2024 additions and 254 deletions

View File

@@ -28,7 +28,10 @@
} }
.oathhammer .character-content, .oathhammer .character-content,
.oathhammer .npc-content, .oathhammer .npc-content,
.oathhammer .settlement-content { .oathhammer .settlement-content,
.oathhammer .regiment-content,
.oathhammer .party-content,
.oathhammer .army-content {
font-family: "Calibri", "Segoe UI", sans-serif; font-family: "Calibri", "Segoe UI", sans-serif;
font-size: 0.86rem; font-size: 0.86rem;
color: #2a1a0a; color: #2a1a0a;
@@ -40,20 +43,32 @@
} }
.oathhammer .character-content nav.tabs [data-tab], .oathhammer .character-content nav.tabs [data-tab],
.oathhammer .npc-content nav.tabs [data-tab], .oathhammer .npc-content nav.tabs [data-tab],
.oathhammer .settlement-content nav.tabs [data-tab] { .oathhammer .settlement-content nav.tabs [data-tab],
.oathhammer .regiment-content nav.tabs [data-tab],
.oathhammer .party-content nav.tabs [data-tab],
.oathhammer .army-content nav.tabs [data-tab] {
color: #535128; color: #535128;
} }
.oathhammer .character-content nav.tabs [data-tab].active, .oathhammer .character-content nav.tabs [data-tab].active,
.oathhammer .npc-content nav.tabs [data-tab].active, .oathhammer .npc-content nav.tabs [data-tab].active,
.oathhammer .settlement-content nav.tabs [data-tab].active { .oathhammer .settlement-content nav.tabs [data-tab].active,
.oathhammer .regiment-content nav.tabs [data-tab].active,
.oathhammer .party-content nav.tabs [data-tab].active,
.oathhammer .army-content nav.tabs [data-tab].active {
color: #084a74; color: #084a74;
} }
.oathhammer .character-content input:disabled, .oathhammer .character-content input:disabled,
.oathhammer .npc-content input:disabled, .oathhammer .npc-content input:disabled,
.oathhammer .settlement-content input:disabled, .oathhammer .settlement-content input:disabled,
.oathhammer .regiment-content input:disabled,
.oathhammer .party-content input:disabled,
.oathhammer .army-content input:disabled,
.oathhammer .character-content select:disabled, .oathhammer .character-content select:disabled,
.oathhammer .npc-content select:disabled, .oathhammer .npc-content select:disabled,
.oathhammer .settlement-content select:disabled { .oathhammer .settlement-content select:disabled,
.oathhammer .regiment-content select:disabled,
.oathhammer .party-content select:disabled,
.oathhammer .army-content select:disabled {
background-color: rgba(0, 0, 0, 0.08); background-color: rgba(0, 0, 0, 0.08);
border-color: transparent; border-color: transparent;
color: #2a1a0a; color: #2a1a0a;
@@ -61,9 +76,15 @@
.oathhammer .character-content input, .oathhammer .character-content input,
.oathhammer .npc-content input, .oathhammer .npc-content input,
.oathhammer .settlement-content input, .oathhammer .settlement-content input,
.oathhammer .regiment-content input,
.oathhammer .party-content input,
.oathhammer .army-content input,
.oathhammer .character-content select, .oathhammer .character-content select,
.oathhammer .npc-content select, .oathhammer .npc-content select,
.oathhammer .settlement-content select { .oathhammer .settlement-content select,
.oathhammer .regiment-content select,
.oathhammer .party-content select,
.oathhammer .army-content select {
height: 1.5rem; height: 1.5rem;
background-color: rgba(255, 255, 255, 0.3); background-color: rgba(255, 255, 255, 0.3);
border-color: #084a74; border-color: #084a74;
@@ -71,7 +92,10 @@
} }
.oathhammer .character-content input[name="name"], .oathhammer .character-content input[name="name"],
.oathhammer .npc-content input[name="name"], .oathhammer .npc-content input[name="name"],
.oathhammer .settlement-content input[name="name"] { .oathhammer .settlement-content input[name="name"],
.oathhammer .regiment-content input[name="name"],
.oathhammer .party-content input[name="name"],
.oathhammer .army-content input[name="name"] {
height: 2.5rem; height: 2.5rem;
font-family: "Sherwood", "Palatino Linotype", serif; font-family: "Sherwood", "Palatino Linotype", serif;
font-size: calc(0.86rem * 1.2); font-size: calc(0.86rem * 1.2);
@@ -82,14 +106,20 @@
} }
.oathhammer .character-content fieldset, .oathhammer .character-content fieldset,
.oathhammer .npc-content fieldset, .oathhammer .npc-content fieldset,
.oathhammer .settlement-content fieldset { .oathhammer .settlement-content fieldset,
.oathhammer .regiment-content fieldset,
.oathhammer .party-content fieldset,
.oathhammer .army-content fieldset {
margin-bottom: 4px; margin-bottom: 4px;
border-radius: 4px; border-radius: 4px;
border-color: #535128; border-color: #535128;
} }
.oathhammer .character-content legend, .oathhammer .character-content legend,
.oathhammer .npc-content legend, .oathhammer .npc-content legend,
.oathhammer .settlement-content legend { .oathhammer .settlement-content legend,
.oathhammer .regiment-content legend,
.oathhammer .party-content legend,
.oathhammer .army-content legend {
font-family: "BlueDragon", "Palatino Linotype", serif; font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 1.1); font-size: calc(0.86rem * 1.1);
font-weight: bold; font-weight: bold;
@@ -98,7 +128,10 @@
} }
.oathhammer .character-content label, .oathhammer .character-content label,
.oathhammer .npc-content label, .oathhammer .npc-content label,
.oathhammer .settlement-content label { .oathhammer .settlement-content label,
.oathhammer .regiment-content label,
.oathhammer .party-content label,
.oathhammer .army-content label {
font-family: "BlueDragon", "Palatino Linotype", serif; font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: 0.86rem; font-size: 0.86rem;
color: #2a1a0a; color: #2a1a0a;
@@ -882,6 +915,93 @@
gap: 8px; gap: 8px;
padding: 4px 0; padding: 4px 0;
} }
.oathhammer .npc-main .regiment-vitals-grid.regiment-row1 {
grid-template-columns: 1fr 1fr 1fr;
}
.oathhammer .npc-main .regiment-vitals-grid.regiment-row2 {
grid-template-columns: 1fr 1fr;
border-top: none;
margin-top: 4px;
padding-top: 4px;
border-top: 1px dashed rgba(83, 81, 40, 0.5);
}
.oathhammer .regiment-content .npc-left {
min-width: 94px;
max-width: 94px;
}
.oathhammer .regiment-content .npc-left .actor-img {
width: 94px;
height: 110px;
-o-object-fit: cover;
object-fit: cover;
-o-object-position: center top;
object-position: center top;
border: 2px solid rgba(42, 26, 10, 0.4);
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
flex-grow: 0;
}
.oathhammer .regiment-fieldset {
border: none;
padding: 0;
margin: 0;
}
.oathhammer .regiment-leader-row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 5px;
padding: 4px 6px;
border: 1px dashed rgba(83, 81, 40, 0.6);
border-radius: 3px;
background: rgba(0, 0, 0, 0.04);
min-height: 28px;
}
.oathhammer .regiment-leader-row .regiment-leader-label {
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85);
font-weight: bold;
color: #2a1a0a;
white-space: nowrap;
min-width: 5.5rem;
}
.oathhammer .regiment-leader-row .regiment-leader-img {
width: 22px;
height: 22px;
border-radius: 3px;
border: 1px solid #535128;
-o-object-fit: cover;
object-fit: cover;
flex-shrink: 0;
}
.oathhammer .regiment-leader-row .regiment-leader-name {
flex: 1;
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85);
color: #084a74;
font-weight: 600;
cursor: pointer;
}
.oathhammer .regiment-leader-row .regiment-leader-name:hover {
text-decoration: underline;
color: #c8a84b;
}
.oathhammer .regiment-leader-row .regiment-leader-empty {
flex: 1;
font-size: calc(0.86rem * 0.9);
color: rgba(42, 26, 10, 0.45);
font-style: italic;
}
.oathhammer .regiment-leader-row .regiment-leader-clear {
color: rgba(42, 26, 10, 0.4);
font-size: calc(0.86rem * 0.85);
cursor: pointer;
flex-shrink: 0;
}
.oathhammer .regiment-leader-row .regiment-leader-clear:hover {
color: #cc3333;
}
.oathhammer .item-list { .oathhammer .item-list {
list-style: none; list-style: none;
margin: 0; margin: 0;
@@ -1012,11 +1132,11 @@
} }
.oathhammer .item-list--spell .item-list-header, .oathhammer .item-list--spell .item-list-header,
.oathhammer .item-list--spell .item-entry { .oathhammer .item-list--spell .item-entry {
grid-template-columns: 24px 1fr 3rem 6rem 3rem 5.5rem; grid-template-columns: 24px 1fr 2.5rem 5.5rem 3.5rem 4.5rem 3.5rem 5rem;
} }
.oathhammer .item-list--miracle .item-list-header, .oathhammer .item-list--miracle .item-list-header,
.oathhammer .item-list--miracle .item-entry { .oathhammer .item-list--miracle .item-entry {
grid-template-columns: 24px 1fr 4.5rem 5.5rem; grid-template-columns: 24px 1fr 6rem 5rem;
} }
.oathhammer .miracles-blocked { .oathhammer .miracles-blocked {
opacity: 0.45; opacity: 0.45;
@@ -1333,7 +1453,7 @@
} }
.oathhammer .regiment-sheet .regiment-stats-row { .oathhammer .regiment-sheet .regiment-stats-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 4px; gap: 4px;
} }
.oathhammer .regiment-sheet .regiment-stats-row .form-group > label { .oathhammer .regiment-sheet .regiment-stats-row .form-group > label {
@@ -2328,3 +2448,307 @@
.oathhammer .settlement-buildings-header .collect-taxes-btn i { .oathhammer .settlement-buildings-header .collect-taxes-btn i {
color: #c8a84b; color: #c8a84b;
} }
.oathhammer .party-main .party-header {
display: flex;
flex-direction: row;
gap: 8px;
align-items: stretch;
}
.oathhammer .party-main .party-portrait-wrap {
flex-shrink: 0;
}
.oathhammer .party-main .party-portrait-wrap .party-portrait {
width: 94px;
height: 110px;
-o-object-fit: cover;
object-fit: cover;
-o-object-position: center top;
object-position: center top;
border: 2px solid rgba(42, 26, 10, 0.4);
border-radius: 4px;
cursor: pointer;
}
.oathhammer .party-main .party-header-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 6px;
min-width: 0;
}
.oathhammer .party-main .party-header-body .character-name {
display: flex;
align-items: center;
gap: 4px;
border-bottom: 1px solid #535128;
padding-bottom: 4px;
}
.oathhammer .party-main .party-header-body .character-name input,
.oathhammer .party-main .party-header-body .character-name span {
flex: 1;
min-width: 0;
font-family: "Sherwood", "Palatino Linotype", serif;
font-size: calc(0.86rem * 1.1);
}
.oathhammer .party-main .party-header-body .character-name > .control {
flex-shrink: 0;
}
.oathhammer .party-main .party-header-fieldset {
border: none;
padding: 0;
margin: 0;
}
.oathhammer .party-main .party-treasury {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 6px;
border: 1px solid #535128;
border-radius: 3px;
background: rgba(0, 0, 0, 0.08);
flex-wrap: wrap;
}
.oathhammer .party-main .party-treasury .party-treasury-label {
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85);
font-weight: bold;
color: #2a1a0a;
white-space: nowrap;
min-width: 4rem;
}
.oathhammer .party-main .party-treasury .party-currency {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 7rem;
}
.oathhammer .party-main .party-treasury .party-currency .currency-label {
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.9);
font-weight: bold;
color: #2a1a0a;
white-space: nowrap;
min-width: 1.8rem;
}
.oathhammer .party-main .party-treasury .party-currency .currency-stepper {
display: flex;
align-items: center;
gap: 2px;
}
.oathhammer .party-main .party-treasury .party-currency .currency-stepper input {
width: 3.5rem;
text-align: center;
font-size: calc(0.86rem * 0.85);
padding: 1px 2px;
}
.oathhammer .party-main .party-treasury .party-currency .currency-stepper .currency-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.1rem;
height: 1.1rem;
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 .party-main .party-treasury .party-currency .currency-stepper .currency-btn:hover {
background: #c8a84b;
border-color: #c8a84b;
}
.oathhammer .party-main .party-treasury .party-currency-gp .currency-label {
color: #987d2e;
}
.oathhammer .party-main .party-treasury .party-currency-sp .currency-label {
color: #888;
}
.oathhammer .party-main .party-treasury .party-currency-cp .currency-label {
color: #aa6633;
}
.oathhammer .item-list--party-member .item-list-header,
.oathhammer .item-list--party-member .item-entry {
grid-template-columns: 1.8rem 24px 1fr 7rem 3rem 5rem 5.5rem;
}
.oathhammer .item-list--party-member .party-member-order {
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.9);
font-weight: bold;
color: #535128;
text-align: center;
align-self: center;
}
.oathhammer .item-list--party-loot .item-list-header,
.oathhammer .item-list--party-loot .item-entry {
grid-template-columns: 24px 1fr 6rem 5.5rem 5rem;
}
.oathhammer .item-list--party-loot .item-qty {
display: flex;
align-items: center;
gap: 3px;
font-size: calc(0.86rem * 0.85);
}
.oathhammer .item-list--party-loot .item-qty span {
min-width: 1.5rem;
text-align: center;
}
.oathhammer .item-list--party-loot .item-qty .qty-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
font-size: 0.8rem;
font-weight: bold;
line-height: 1;
border: 1px solid #535128;
border-radius: 2px;
background: rgba(83, 81, 40, 0.2);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.oathhammer .item-list--party-loot .item-qty .qty-btn:hover {
background: #c8a84b;
border-color: #c8a84b;
}
.oathhammer .army-main .army-header {
display: flex;
flex-direction: row;
gap: 8px;
align-items: stretch;
}
.oathhammer .army-main .army-portrait-wrap {
flex-shrink: 0;
}
.oathhammer .army-main .army-portrait-wrap .army-portrait {
width: 94px;
height: 110px;
-o-object-fit: cover;
object-fit: cover;
-o-object-position: center top;
object-position: center top;
border: 2px solid rgba(42, 26, 10, 0.4);
border-radius: 4px;
cursor: pointer;
}
.oathhammer .army-main .army-header-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 5px;
min-width: 0;
}
.oathhammer .army-main .army-header-body .character-name {
display: flex;
align-items: center;
gap: 4px;
border-bottom: 1px solid #535128;
padding-bottom: 4px;
}
.oathhammer .army-main .army-header-body .character-name input,
.oathhammer .army-main .army-header-body .character-name span {
flex: 1;
min-width: 0;
font-family: "Sherwood", "Palatino Linotype", serif;
font-size: calc(0.86rem * 1.1);
}
.oathhammer .army-main .army-header-body .character-name > .control {
flex-shrink: 0;
}
.oathhammer .army-main .army-header-fieldset {
border: none;
padding: 0;
margin: 0;
}
.oathhammer .army-main .army-leader-row {
display: flex;
align-items: center;
gap: 5px;
padding: 3px 5px;
border: 1px dashed #535128;
border-radius: 3px;
min-height: 2rem;
background: rgba(83, 81, 40, 0.05);
}
.oathhammer .army-main .army-leader-row .army-field-label {
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85);
font-weight: bold;
color: #2a1a0a;
white-space: nowrap;
min-width: 6rem;
}
.oathhammer .army-main .army-leader-row .army-leader-img {
width: 24px;
height: 24px;
-o-object-fit: cover;
object-fit: cover;
border-radius: 3px;
border: 1px solid #535128;
}
.oathhammer .army-main .army-leader-row .army-leader-name {
flex: 1;
font-size: calc(0.86rem * 0.85);
color: #2a1a0a;
}
.oathhammer .army-main .army-leader-row .army-leader-name:hover {
color: #535128;
text-decoration: underline;
}
.oathhammer .army-main .army-leader-row .army-leader-clear {
color: #535128;
}
.oathhammer .army-main .army-leader-row .army-leader-clear:hover {
color: #aa3333;
}
.oathhammer .army-main .army-leader-row .army-field-empty {
flex: 1;
font-size: calc(0.86rem * 0.9);
color: rgba(42, 26, 10, 0.5);
font-style: italic;
}
.oathhammer .army-main .army-location-row {
display: flex;
align-items: center;
gap: 5px;
}
.oathhammer .army-main .army-location-row .army-field-label {
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85);
font-weight: bold;
color: #2a1a0a;
white-space: nowrap;
min-width: 6rem;
}
.oathhammer .army-main .army-location-row input {
flex: 1;
font-size: calc(0.86rem * 0.85);
}
.oathhammer .item-list--army-regiment .item-list-header,
.oathhammer .item-list--army-regiment .item-entry {
grid-template-columns: 24px 1fr 4.5rem 4.5rem 4.5rem 4.5rem 3rem;
}
.oathhammer .item-list--army-regiment .army-total-row {
border-top: 2px solid #535128;
font-weight: bold;
}
.oathhammer .item-list--army-regiment .army-total-row .army-total-label {
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85);
color: #2a1a0a;
}
.oathhammer .item-list--army-regiment .army-total-row .army-total-value {
color: #987d2e;
font-family: "BlueDragon", "Palatino Linotype", serif;
}

View File

@@ -18,7 +18,9 @@
"Settlement": "Oath Hammer Settlement Sheet", "Settlement": "Oath Hammer Settlement Sheet",
"SkillNPC": "Oath Hammer NPC Skill Sheet", "SkillNPC": "Oath Hammer NPC Skill Sheet",
"NpcAttack": "Oath Hammer NPC Attack Sheet", "NpcAttack": "Oath Hammer NPC Attack Sheet",
"Regiment": "Oath Hammer Regiment Sheet" "Regiment": "Oath Hammer Regiment Sheet",
"Party": "Party Sheet",
"Army": "Army Sheet"
}, },
"Tab": { "Tab": {
"Identity": "Oaths / Traits", "Identity": "Oaths / Traits",
@@ -31,7 +33,9 @@
"Buildings": "Buildings", "Buildings": "Buildings",
"Inventory": "Inventory", "Inventory": "Inventory",
"Traits": "Traits", "Traits": "Traits",
"Garrison": "Garrison" "Garrison": "Garrison",
"Members": "Members",
"Loot": "Loot"
}, },
"Attribute": { "Attribute": {
"Might": "Might", "Might": "Might",
@@ -229,7 +233,7 @@
"DefenseValue": "Defense Value", "DefenseValue": "Defense Value",
"ArmorRating": "Armor Rating", "ArmorRating": "Armor Rating",
"DefenseBonus": "Defense Bonus", "DefenseBonus": "Defense Bonus",
"Movement": "Movement", "Movement": "Move",
"ArcaneStress": "Arcane Stress", "ArcaneStress": "Arcane Stress",
"StressValue": "Stress", "StressValue": "Stress",
"ThresholdBonus": "Threshold Bonus", "ThresholdBonus": "Threshold Bonus",
@@ -254,7 +258,7 @@
"Conditions": "Conditions", "Conditions": "Conditions",
"Description": "Description", "Description": "Description",
"Notes": "Notes", "Notes": "Notes",
"Stats": "Statistics", "Stats": "Stats",
"CR": "Challenge Rating", "CR": "Challenge Rating",
"AttackBonus": "Attack Bonus", "AttackBonus": "Attack Bonus",
"DamageBonus": "Damage Bonus", "DamageBonus": "Damage Bonus",
@@ -337,15 +341,34 @@
"Dice": "Dice", "Dice": "Dice",
"DiceColor": "Color", "DiceColor": "Color",
"Special": "Special", "Special": "Special",
"Movement": "Move", "NoRegiments": "No regiments yet — drag regiment actors here.",
"Stats": "Stats", "SupplyCost": "Supply Cost",
"NoRegiments": "No regiments. Add one with the + button.",
"SkillName": "Skill name", "SkillName": "Skill name",
"AttackName": "Attack name", "AttackName": "Attack name",
"TraitName": "Trait name", "TraitName": "Trait name",
"Edit": "Edit", "Edit": "Edit",
"Delete": "Delete", "Delete": "Delete",
"Rank": "Rank" "Rank": "Rank",
"RemoveFromGarrison": "Remove from Garrison",
"RecruitmentCost": "Recruitment Cost",
"UnitLeader": "Unit Leader",
"DropLeaderHint": "Drop a linked actor here",
"MarchingOrder": "Marching Order",
"NoMembers": "No members yet — drag characters here.",
"DropMemberHint": "Drag a character actor here to add them to the party.",
"Loot": "Loot",
"NoLoot": "No loot yet — drag items here.",
"DropLootHint": "Drag weapons, armor or equipment here to add party loot.",
"Qty": "Qty",
"GP": "GP",
"SP": "SP",
"CP": "CP",
"Class": "Class",
"Commander": "Commander",
"Location": "Location",
"Regiments": "Regiments",
"DropRegimentHint": "Drag a regiment actor (must be token-linked) to add it to this army.",
"TotalSupply": "Total Supply"
}, },
"ColorDice": { "ColorDice": {
"White": "White (4+)", "White": "White (4+)",
@@ -364,11 +387,18 @@
"Regiment": "New Regiment", "Regiment": "New Regiment",
"RegimentSkill": "Add Skill", "RegimentSkill": "Add Skill",
"RegimentAttack": "Add Attack", "RegimentAttack": "Add Attack",
"RegimentTrait": "Add Trait" "RegimentTrait": "Add Trait",
"SkillNPC": "New Skill"
}, },
"ToggleSheet": "Toggle Edit/Play Mode", "ToggleSheet": "Toggle Edit/Play Mode",
"Tooltip": { "Tooltip": {
"RollArmor": "Roll Armor Dice" "RollArmor": "Roll Armor Dice",
"OpenLeader": "Open leader sheet",
"ClearLeader": "Remove unit leader",
"MoveUp": "Move up (march forward)",
"MoveDown": "Move down (march back)",
"RemoveMember": "Remove from party",
"RemoveRegiment": "Remove regiment from army"
}, },
"Action": { "Action": {
"CastSpell": "Cast Spell", "CastSpell": "Cast Spell",
@@ -462,7 +492,9 @@
"APPenalty": "AP (Attacker)", "APPenalty": "AP (Attacker)",
"APHint": "attacker's Armor Piercing value", "APHint": "attacker's Armor Piercing value",
"ReinforcedHint": "Reinforced — rolling red dice", "ReinforcedHint": "Reinforced — rolling red dice",
"RollInitiative": "Roll Initiative" "RollInitiative": "Roll Initiative",
"Default": "Default",
"DicePool": "Dice Pool"
}, },
"Enhancement": { "Enhancement": {
"None": "None", "None": "None",
@@ -1056,7 +1088,9 @@
} }
}, },
"Warning": { "Warning": {
"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.",
"LeaderNotLinked": "This actor's token is not linked. Only actors with linked tokens can be unit leaders.",
"RegimentNotLinked": "This regiment actor is not linked to its token. Only token-linked regiment actors can be added to an army."
}, },
"SettlementArchetype": { "SettlementArchetype": {
"CenterOfLearning": "Center of Learning", "CenterOfLearning": "Center of Learning",
@@ -1076,7 +1110,8 @@
"CollectTaxes": "Collect Taxes", "CollectTaxes": "Collect Taxes",
"CollectTaxesTooltip": "Roll tax revenue for all constructed buildings and total the result.", "CollectTaxesTooltip": "Roll tax revenue for all constructed buildings and total the result.",
"NoTaxRevenue": "No constructed buildings with tax revenue defined.", "NoTaxRevenue": "No constructed buildings with tax revenue defined.",
"TotalRevenue": "Total Revenue" "TotalRevenue": "Total Revenue",
"GarrisonHint": "Drag a regiment actor here to add it to the garrison."
}, },
"SkillNPC": { "SkillNPC": {
"FIELDS": { "FIELDS": {
@@ -1124,7 +1159,8 @@
"NpcSubtype": { "NpcSubtype": {
"Creature": "Creature", "Creature": "Creature",
"Npc": "NPC" "Npc": "NPC"
} },
"Party": {}
}, },
"TYPES": { "TYPES": {
"Item": { "Item": {
@@ -1141,13 +1177,15 @@
"class": "Class", "class": "Class",
"skillnpc": "NPC Skill", "skillnpc": "NPC Skill",
"npcattack": "NPC Attack", "npcattack": "NPC Attack",
"regiment": "Regiment",
"building": "Building" "building": "Building"
}, },
"Actor": { "Actor": {
"character": "Character", "character": "Character",
"npc": "NPC / Creature", "npc": "NPC / Creature",
"settlement": "Settlement" "settlement": "Settlement",
"regiment": "Regiment",
"party": "Party",
"army": "Army"
} }
} }
} }

152
less/army-sheet.less Normal file
View File

@@ -0,0 +1,152 @@
// ============================================================
// ARMY ACTOR SHEET
// ============================================================
.oathhammer .army-main {
.army-header {
display: flex;
flex-direction: row;
gap: 8px;
align-items: stretch;
}
.army-portrait-wrap {
flex-shrink: 0;
.army-portrait {
width: 94px;
height: 110px;
object-fit: cover;
object-position: center top;
border: 2px solid fade(@color-dark, 40%);
border-radius: 4px;
cursor: pointer;
}
}
.army-header-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 5px;
min-width: 0;
.character-name {
display: flex;
align-items: center;
gap: 4px;
border-bottom: 1px solid @color-olive;
padding-bottom: 4px;
input, span {
flex: 1;
min-width: 0;
font-family: @font-primary;
font-size: @font-size-lg;
}
> .control { flex-shrink: 0; }
}
}
.army-header-fieldset {
border: none;
padding: 0;
margin: 0;
}
// ── Leader row ─────────────────────────────────────────────
.army-leader-row {
display: flex;
align-items: center;
gap: 5px;
padding: 3px 5px;
border: 1px dashed @color-olive;
border-radius: 3px;
min-height: 2rem;
background: fade(@color-olive, 5%);
.army-field-label {
font-family: @font-secondary;
font-size: @font-size-sm;
font-weight: bold;
color: @color-dark;
white-space: nowrap;
min-width: 6rem;
}
.army-leader-img {
width: 24px;
height: 24px;
object-fit: cover;
border-radius: 3px;
border: 1px solid @color-olive;
}
.army-leader-name {
flex: 1;
font-size: @font-size-sm;
color: @color-dark;
&:hover { color: @color-olive; text-decoration: underline; }
}
.army-leader-clear {
color: @color-olive;
&:hover { color: #aa3333; }
}
.army-field-empty {
flex: 1;
font-size: @font-size-xs;
color: fade(@color-dark, 50%);
font-style: italic;
}
}
// ── Location row ───────────────────────────────────────────
.army-location-row {
display: flex;
align-items: center;
gap: 5px;
.army-field-label {
font-family: @font-secondary;
font-size: @font-size-sm;
font-weight: bold;
color: @color-dark;
white-space: nowrap;
min-width: 6rem;
}
input {
flex: 1;
font-size: @font-size-sm;
}
}
}
// ── Regiment list ──────────────────────────────────────────────
.oathhammer .item-list--army-regiment {
.item-list-header, .item-entry {
// img | name | grit | armor | movement | supply | actions
grid-template-columns: @item-img-size 1fr 4.5rem 4.5rem 4.5rem 4.5rem 3rem;
}
.army-total-row {
border-top: 2px solid @color-olive;
font-weight: bold;
.army-total-label {
font-family: @font-secondary;
font-size: @font-size-sm;
color: @color-dark;
}
.army-total-value {
color: darken(@color-gold, 15%);
font-family: @font-secondary;
}
}
}

View File

@@ -37,7 +37,10 @@
// Shared actor content base // Shared actor content base
.oathhammer .character-content, .oathhammer .character-content,
.oathhammer .npc-content, .oathhammer .npc-content,
.oathhammer .settlement-content { .oathhammer .settlement-content,
.oathhammer .regiment-content,
.oathhammer .party-content,
.oathhammer .army-content {
font-family: @font-body; // Calibri — standard text per design_rules.md font-family: @font-body; // Calibri — standard text per design_rules.md
font-size: @font-size-base; font-size: @font-size-base;
color: @color-dark; color: @color-dark;

View File

@@ -12,3 +12,5 @@
@import "rolls"; @import "rolls";
@import "roll-dialog"; @import "roll-dialog";
@import "settlement-sheet"; @import "settlement-sheet";
@import "party-sheet";
@import "army-sheet";

View File

@@ -148,13 +148,15 @@
.item-list--spell { .item-list--spell {
.item-list-header, .item-entry { .item-list-header, .item-entry {
grid-template-columns: @item-img-size 1fr 3rem 6rem 3rem 5.5rem; // img | name | DV | Tradition | Range | Duration | SpellSave | actions
grid-template-columns: @item-img-size 1fr 2.5rem 5.5rem 3.5rem 4.5rem 3.5rem 5rem;
} }
} }
.item-list--miracle { .item-list--miracle {
.item-list-header, .item-entry { .item-list-header, .item-entry {
grid-template-columns: @item-img-size 1fr 4.5rem 5.5rem; // img | name | DivineTradition | actions
grid-template-columns: @item-img-size 1fr 6rem 5rem;
} }
} }

View File

@@ -207,7 +207,7 @@
.regiment-stats-row { .regiment-stats-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 4px; gap: 4px;
.form-group > label { flex: 0 0 6rem; } .form-group > label { flex: 0 0 6rem; }

View File

@@ -207,3 +207,100 @@
padding: 4px 0; padding: 4px 0;
} }
// ============================================================
// REGIMENT ACTOR SHEET overrides
// ============================================================
// Regiment uses the same .npc-main structure but split into 2 rows
.oathhammer .npc-main .regiment-vitals-grid {
&.regiment-row1 { grid-template-columns: 1fr 1fr 1fr; }
&.regiment-row2 {
grid-template-columns: 1fr 1fr;
border-top: none;
margin-top: 4px;
padding-top: 4px;
border-top: 1px dashed fade(@color-olive, 50%);
}
}
// Regiment portrait — same dimensions as settlement sheet
.oathhammer .regiment-content .npc-left {
min-width: 94px;
max-width: 94px;
.actor-img {
width: 94px;
height: 110px;
object-fit: cover;
object-position: center top;
border: 2px solid fade(@color-dark, 40%);
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
flex-grow: 0;
}
}
// Regiment fieldset — remove default fieldset border for clean header look
.oathhammer .regiment-fieldset {
border: none;
padding: 0;
margin: 0;
}
// ── Regiment leader row ───────────────────────────────────────
.oathhammer .regiment-leader-row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 5px;
padding: 4px 6px;
border: 1px dashed fade(@color-olive, 60%);
border-radius: 3px;
background: rgba(0,0,0,0.04);
min-height: 28px;
.regiment-leader-label {
font-family: @font-secondary;
font-size: @font-size-sm;
font-weight: bold;
color: @color-dark;
white-space: nowrap;
min-width: 5.5rem;
}
.regiment-leader-img {
width: 22px;
height: 22px;
border-radius: 3px;
border: 1px solid @color-olive;
object-fit: cover;
flex-shrink: 0;
}
.regiment-leader-name {
flex: 1;
font-family: @font-secondary;
font-size: @font-size-sm;
color: @color-blue;
font-weight: 600;
cursor: pointer;
&:hover { text-decoration: underline; color: @color-gold; }
}
.regiment-leader-empty {
flex: 1;
font-size: @font-size-xs;
color: fade(@color-dark, 45%);
font-style: italic;
}
.regiment-leader-clear {
color: fade(@color-dark, 40%);
font-size: @font-size-sm;
cursor: pointer;
flex-shrink: 0;
&:hover { color: #cc3333; }
}
}

186
less/party-sheet.less Normal file
View File

@@ -0,0 +1,186 @@
// ============================================================
// PARTY ACTOR SHEET
// ============================================================
.oathhammer .party-main {
.party-header {
display: flex;
flex-direction: row;
gap: 8px;
align-items: stretch;
}
.party-portrait-wrap {
flex-shrink: 0;
.party-portrait {
width: 94px;
height: 110px;
object-fit: cover;
object-position: center top;
border: 2px solid fade(@color-dark, 40%);
border-radius: 4px;
cursor: pointer;
}
}
.party-header-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 6px;
min-width: 0;
.character-name {
display: flex;
align-items: center;
gap: 4px;
border-bottom: 1px solid @color-olive;
padding-bottom: 4px;
input, span {
flex: 1;
min-width: 0;
font-family: @font-primary;
font-size: @font-size-lg;
}
> .control {
flex-shrink: 0;
}
}
}
.party-header-fieldset {
border: none;
padding: 0;
margin: 0;
}
// ── Treasury ───────────────────────────────────────────────
.party-treasury {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 6px;
border: 1px solid @color-olive;
border-radius: 3px;
background: rgba(0,0,0,0.08);
flex-wrap: wrap;
.party-treasury-label {
font-family: @font-secondary;
font-size: @font-size-sm;
font-weight: bold;
color: @color-dark;
white-space: nowrap;
min-width: 4rem;
}
.party-currency {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 7rem;
.currency-label {
font-family: @font-secondary;
font-size: @font-size-xs;
font-weight: bold;
color: @color-dark;
white-space: nowrap;
min-width: 1.8rem;
}
.currency-stepper {
display: flex;
align-items: center;
gap: 2px;
input {
width: 3.5rem;
text-align: center;
font-size: @font-size-sm;
padding: 1px 2px;
}
.currency-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.1rem;
height: 1.1rem;
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; }
}
}
}
.party-currency-gp .currency-label { color: darken(@color-gold, 15%); }
.party-currency-sp .currency-label { color: #888; }
.party-currency-cp .currency-label { color: #aa6633; }
}
}
// ── Member list ────────────────────────────────────────────────
.oathhammer .item-list--party-member {
.item-list-header, .item-entry {
// order# | img | name | class | level | grit | actions
grid-template-columns: 1.8rem @item-img-size 1fr 7rem 3rem 5rem 5.5rem;
}
.party-member-order {
font-family: @font-secondary;
font-size: @font-size-xs;
font-weight: bold;
color: @color-olive;
text-align: center;
align-self: center;
}
}
// ── Loot list ──────────────────────────────────────────────────
.oathhammer .item-list--party-loot {
.item-list-header, .item-entry {
// img | name | type | qty | actions
grid-template-columns: @item-img-size 1fr 6rem 5.5rem 5rem;
}
.item-qty {
display: flex;
align-items: center;
gap: 3px;
font-size: @font-size-sm;
span { min-width: 1.5rem; text-align: center; }
.qty-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
font-size: 0.8rem;
font-weight: bold;
line-height: 1;
border: 1px solid @color-olive;
border-radius: 2px;
background: @color-olive-faint;
cursor: pointer;
user-select: none;
&:hover { background: @color-gold; border-color: @color-gold; }
}
}
}

View File

@@ -15,6 +15,8 @@ export { default as OathHammerSettlementSheet } from "./sheets/settlement-sheet.
export { default as OathHammerSkillNPCSheet } from "./sheets/skillnpc-sheet.mjs" export { default as OathHammerSkillNPCSheet } from "./sheets/skillnpc-sheet.mjs"
export { default as OathHammerNpcAttackSheet } from "./sheets/npcattack-sheet.mjs" export { default as OathHammerNpcAttackSheet } from "./sheets/npcattack-sheet.mjs"
export { default as OathHammerRegimentSheet } from "./sheets/regiment-sheet.mjs" export { default as OathHammerRegimentSheet } from "./sheets/regiment-sheet.mjs"
export { default as OathHammerPartySheet } from "./sheets/party-sheet.mjs"
export { default as OathHammerArmySheet } from "./sheets/army-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

@@ -0,0 +1,152 @@
import OathHammerActorSheet from "./base-actor-sheet.mjs"
export default class OathHammerArmySheet extends OathHammerActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["army"],
position: { width: 680, height: 560 },
window: { contentClasses: ["army-content"] },
actions: {
openRegiment: OathHammerArmySheet.#onOpenRegiment,
removeRegiment: OathHammerArmySheet.#onRemoveRegiment,
openLeader: OathHammerArmySheet.#onOpenLeader,
clearLeader: OathHammerArmySheet.#onClearLeader,
},
}
/** @override */
static PARTS = {
main: { template: "systems/fvtt-oath-hammer/templates/actor/army-sheet.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
overview: { template: "systems/fvtt-oath-hammer/templates/actor/army-overview.hbs" },
notes: { template: "systems/fvtt-oath-hammer/templates/actor/army-notes.hbs" },
}
tabGroups = { sheet: "overview" }
#getTabs() {
const tabs = {
overview: { id: "overview", group: "sheet", icon: "fa-solid fa-shield-halved", label: "OATHHAMMER.Tab.Overview" },
notes: { id: "notes", group: "sheet", icon: "fa-solid fa-book", label: "OATHHAMMER.Tab.Notes" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
const doc = this.document
context.tabs = this.#getTabs()
// Resolve leader
const leaderUuid = doc.system.leaderUuid
if (leaderUuid) {
const leader = await fromUuid(leaderUuid)
context.leader = leader ? { uuid: leaderUuid, name: leader.name, img: leader.img } : null
} else {
context.leader = null
}
return context
}
/** @override */
async _preparePartContext(partId, context) {
context = await super._preparePartContext(partId, context)
const doc = this.document
switch (partId) {
case "overview": {
context.tab = context.tabs.overview
const refs = doc.system.regimentRefs ?? []
const regiments = []
let totalSupply = 0
for (const id of refs) {
const regiment = game.actors?.get(id)
if (!regiment) continue
totalSupply += regiment.system.supplyCost ?? 0
regiments.push({
id: regiment.id,
name: regiment.name,
img: regiment.img,
grit: regiment.system.grit?.value ?? 0,
gritMax: regiment.system.grit?.max ?? 0,
armor: regiment.system.armorDice?.value ?? 0,
movement: regiment.system.movement ?? 0,
supplyCost: regiment.system.supplyCost ?? 0,
})
}
context.regiments = regiments
context.totalSupply = totalSupply
break
}
case "notes":
context.tab = context.tabs.notes
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.notes ?? "", { async: true }
)
break
}
return context
}
/** @override */
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data.type === "Actor") {
const actor = await fromUuid(data.uuid)
if (!actor) return
// Leader drop (on leader drop zone)
if (event.target.closest(".army-leader-row")) {
if (!actor.prototypeToken?.actorLink) {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Warning.LeaderNotLinked"))
return
}
return this.document.update({ "system.leaderUuid": actor.uuid })
}
// Regiment drop
if (actor.type !== "regiment") return
if (!actor.prototypeToken?.actorLink) {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Warning.RegimentNotLinked"))
return
}
const refs = foundry.utils.deepClone(this.document.system.regimentRefs ?? [])
if (refs.includes(actor.id)) return
refs.push(actor.id)
return this.document.update({ "system.regimentRefs": refs })
}
}
// ── Actions ─────────────────────────────────────────────────────────────────
static async #onOpenRegiment(event, target) {
const actor = game.actors?.get(target.dataset.actorId)
if (actor) actor.sheet.render(true)
}
static async #onRemoveRegiment(event, target) {
const id = target.dataset.actorId
const refs = (this.document.system.regimentRefs ?? []).filter(r => r !== id)
await this.document.update({ "system.regimentRefs": refs })
}
static async #onOpenLeader() {
const uuid = this.document.system.leaderUuid
if (!uuid) return
const leader = await fromUuid(uuid)
if (leader) leader.sheet.render(true)
}
static async #onClearLeader() {
await this.document.update({ "system.leaderUuid": null })
}
}

View File

@@ -205,11 +205,13 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
context.stressBlocked = doc.system.arcaneStress.value >= doc.system.arcaneStress.threshold context.stressBlocked = doc.system.arcaneStress.value >= doc.system.arcaneStress.threshold
context.spells = doc.itemTypes.spell.map(s => ({ context.spells = doc.itemTypes.spell.map(s => ({
id: s.id, uuid: s.uuid, img: s.img, name: s.name, system: s.system, id: s.id, uuid: s.uuid, img: s.img, name: s.name, system: s.system,
_descTooltip: _stripHtml(s.system.effect) _descTooltip: _stripHtml(s.system.effect),
traditionLabel: game.i18n.localize(SYSTEM.SORCEROUS_TRADITIONS[s.system.tradition]?.label ?? s.system.tradition)
})) }))
context.miracles = doc.itemTypes.miracle.map(m => ({ context.miracles = doc.itemTypes.miracle.map(m => ({
id: m.id, uuid: m.uuid, img: m.img, name: m.name, system: m.system, id: m.id, uuid: m.uuid, img: m.img, name: m.name, system: m.system,
_descTooltip: _stripHtml(m.system.effect) _descTooltip: _stripHtml(m.system.effect),
traditionLabel: game.i18n.localize(SYSTEM.DIVINE_TRADITIONS[m.system.divineTradition] ?? m.system.divineTradition)
})) }))
break break
case "equipment": case "equipment":

View File

@@ -107,11 +107,13 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
context.tab = context.tabs.magic context.tab = context.tabs.magic
context.spells = (doc.itemTypes.spell ?? []).map(s => ({ context.spells = (doc.itemTypes.spell ?? []).map(s => ({
id: s.id, uuid: s.uuid, img: s.img, name: s.name, system: s.system, id: s.id, uuid: s.uuid, img: s.img, name: s.name, system: s.system,
_descTooltip: s.system.effect?.replace(/<[^>]+>/g, "").slice(0, 300) ?? "" _descTooltip: s.system.effect?.replace(/<[^>]+>/g, "").slice(0, 300) ?? "",
traditionLabel: game.i18n.localize(SYSTEM.SORCEROUS_TRADITIONS[s.system.tradition]?.label ?? s.system.tradition)
})) }))
context.miracles = (doc.itemTypes.miracle ?? []).map(m => ({ context.miracles = (doc.itemTypes.miracle ?? []).map(m => ({
id: m.id, uuid: m.uuid, img: m.img, name: m.name, system: m.system, id: m.id, uuid: m.uuid, img: m.img, name: m.name, system: m.system,
_descTooltip: m.system.effect?.replace(/<[^>]+>/g, "").slice(0, 300) ?? "" _descTooltip: m.system.effect?.replace(/<[^>]+>/g, "").slice(0, 300) ?? "",
traditionLabel: game.i18n.localize(SYSTEM.DIVINE_TRADITIONS[m.system.divineTradition] ?? m.system.divineTradition)
})) }))
break break
case "equipment": case "equipment":

View File

@@ -0,0 +1,170 @@
import OathHammerActorSheet from "./base-actor-sheet.mjs"
const ALLOWED_LOOT_TYPES = new Set(["weapon", "armor", "ammunition", "equipment", "magic-item"])
export default class OathHammerPartySheet extends OathHammerActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["party"],
position: { width: 780, height: 600 },
window: { contentClasses: ["party-content"] },
actions: {
openMember: OathHammerPartySheet.#onOpenMember,
removeMember: OathHammerPartySheet.#onRemoveMember,
moveMemberUp: OathHammerPartySheet.#onMoveMemberUp,
moveMemberDown: OathHammerPartySheet.#onMoveMemberDown,
adjustCurrency: OathHammerPartySheet.#onAdjustCurrency,
adjustQty: OathHammerPartySheet.#onAdjustQty,
},
}
/** @override */
static PARTS = {
main: { template: "systems/fvtt-oath-hammer/templates/actor/party-sheet.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
members: { template: "systems/fvtt-oath-hammer/templates/actor/party-members.hbs" },
loot: { template: "systems/fvtt-oath-hammer/templates/actor/party-loot.hbs" },
notes: { template: "systems/fvtt-oath-hammer/templates/actor/party-notes.hbs" },
}
tabGroups = { sheet: "members" }
#getTabs() {
const tabs = {
members: { id: "members", group: "sheet", icon: "fa-solid fa-users", label: "OATHHAMMER.Tab.Members" },
loot: { id: "loot", group: "sheet", icon: "fa-solid fa-treasure-chest", label: "OATHHAMMER.Tab.Loot" },
notes: { id: "notes", group: "sheet", icon: "fa-solid fa-book", label: "OATHHAMMER.Tab.Notes" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
return context
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
break
case "members": {
context.tab = context.tabs.members
const refs = doc.system.memberRefs ?? []
context.members = refs.map((id, idx) => {
const actor = game.actors?.get(id)
if (!actor) return null
const sys = actor.system
const classItem = actor.items?.find(i => i.type === "class")
return {
id: actor.id,
name: actor.name,
img: actor.img,
idx,
position: idx + 1,
isFirst: idx === 0,
isLast: idx === refs.length - 1,
classLabel: classItem?.name ?? "—",
level: sys.level ?? "—",
grit: sys.grit ? `${sys.grit.value}/${sys.grit.max}` : "—",
}
}).filter(Boolean)
break
}
case "loot": {
context.tab = context.tabs.loot
const allItems = doc.items.contents.filter(i => ALLOWED_LOOT_TYPES.has(i.type))
context.lootItems = allItems.map(i => ({
id: i.id, uuid: i.uuid, img: i.img, name: i.name,
type: i.type,
typeLabel: game.i18n.localize(`TYPES.Item.${i.type}`),
system: i.system,
}))
break
}
case "notes":
context.tab = context.tabs.notes
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.notes ?? "", { async: true }
)
break
}
return context
}
/** @override */
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data.type === "Actor") {
const actor = await fromUuid(data.uuid)
if (!actor || actor.type !== "character") return
const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? [])
if (refs.includes(actor.id)) return
refs.push(actor.id)
return this.document.update({ "system.memberRefs": refs })
}
if (data.type === "Item") {
const item = await fromUuid(data.uuid)
if (!item || !ALLOWED_LOOT_TYPES.has(item.type)) return
return this._onDropItem(item)
}
}
// ── Actions ─────────────────────────────────────────────────────────────────
static async #onOpenMember(event, target) {
const actor = game.actors?.get(target.dataset.actorId)
if (actor) actor.sheet.render(true)
}
static async #onRemoveMember(event, target) {
const id = target.dataset.actorId
const refs = (this.document.system.memberRefs ?? []).filter(r => r !== id)
await this.document.update({ "system.memberRefs": refs })
}
static async #onMoveMemberUp(event, target) {
const idx = parseInt(target.dataset.idx, 10)
if (idx <= 0) return
const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? []);
[refs[idx - 1], refs[idx]] = [refs[idx], refs[idx - 1]]
await this.document.update({ "system.memberRefs": refs })
}
static async #onMoveMemberDown(event, target) {
const idx = parseInt(target.dataset.idx, 10)
const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? [])
if (idx >= refs.length - 1) return;
[refs[idx], refs[idx + 1]] = [refs[idx + 1], refs[idx]]
await this.document.update({ "system.memberRefs": refs })
}
static async #onAdjustCurrency(event, target) {
const field = target.dataset.field
const delta = parseInt(target.dataset.delta, 10)
const cur = foundry.utils.getProperty(this.document, field) ?? 0
await this.document.update({ [field]: Math.max(0, cur + delta) })
}
static async #onAdjustQty(event, target) {
const item = this.document.items.get(target.dataset.itemId)
const delta = parseInt(target.dataset.delta, 10)
if (!item) return
const qty = (item.system.quantity ?? 1) + delta
if (qty <= 0) return item.delete()
await item.update({ "system.quantity": qty })
}
}

View File

@@ -1,80 +1,266 @@
import OathHammerItemSheet from "./base-item-sheet.mjs" import OathHammerActorSheet from "./base-actor-sheet.mjs"
import { rollNPCSkill, rollNPCArmor, rollNPCAttackDamage } from "../../rolls.mjs"
import { SYSTEM } from "../../config/system.mjs" import { SYSTEM } from "../../config/system.mjs"
export default class OathHammerRegimentSheet extends OathHammerItemSheet { export default class OathHammerRegimentSheet extends OathHammerActorSheet {
/** @override */ /** @override */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ["regiment"], classes: ["regiment"],
position: { width: 560, height: "auto" }, position: { width: 680, height: 620 },
window: { contentClasses: ["regiment-content"] }, window: { contentClasses: ["regiment-content"] },
actions: { actions: {
addSkill: OathHammerRegimentSheet.#onAddSkill, adjustGrit: OathHammerRegimentSheet.#onAdjustGrit,
removeSkill: OathHammerRegimentSheet.#onRemoveSkill, rollArmor: OathHammerRegimentSheet.#onRollArmor,
addAttack: OathHammerRegimentSheet.#onAddAttack, rollSkillNPC: OathHammerRegimentSheet.#onRollSkillNPC,
removeAttack:OathHammerRegimentSheet.#onRemoveAttack, createNpcAttack: OathHammerRegimentSheet.#onCreateNpcAttack,
addTrait: OathHammerRegimentSheet.#onAddTrait, rollNpcAttack: OathHammerRegimentSheet.#onRollNpcAttack,
removeTrait: OathHammerRegimentSheet.#onRemoveTrait, createSkill: OathHammerRegimentSheet.#onCreateSkill,
createTrait: OathHammerRegimentSheet.#onCreateTrait,
openLeader: OathHammerRegimentSheet.#onOpenLeader,
clearLeader: OathHammerRegimentSheet.#onClearLeader,
}, },
} }
/** @override */ /** @override */
static PARTS = { static PARTS = {
main: { template: "systems/fvtt-oath-hammer/templates/item/regiment-sheet.hbs" }, main: { template: "systems/fvtt-oath-hammer/templates/actor/regiment-sheet.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
skills: { template: "systems/fvtt-oath-hammer/templates/actor/npc-skills.hbs" },
combat: { template: "systems/fvtt-oath-hammer/templates/actor/regiment-combat.hbs" },
traits: { template: "systems/fvtt-oath-hammer/templates/actor/npc-traits.hbs" },
notes: { template: "systems/fvtt-oath-hammer/templates/actor/npc-notes.hbs" },
}
tabGroups = { sheet: "skills" }
#getTabs() {
const tabs = {
skills: { id: "skills", group: "sheet", icon: "fa-solid fa-dice-d6", label: "OATHHAMMER.Tab.Skills" },
combat: { id: "combat", group: "sheet", icon: "fa-solid fa-swords", label: "OATHHAMMER.Tab.Combat" },
traits: { id: "traits", group: "sheet", icon: "fa-solid fa-star", label: "OATHHAMMER.Tab.Traits" },
notes: { id: "notes", group: "sheet", icon: "fa-solid fa-book", label: "OATHHAMMER.Tab.Notes" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
} }
/** @override */ /** @override */
async _prepareContext() { async _prepareContext() {
const context = await super._prepareContext() const context = await super._prepareContext()
context.tabs = this.#getTabs()
const armorColor = this.document.system.armorDice?.colorDiceType ?? "white"
context.armorDiceEmoji = armorColor === "black" ? "⬛" : armorColor === "red" ? "🔴" : "⬜"
context.colorChoices = Object.fromEntries( context.colorChoices = Object.fromEntries(
Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)])
) )
context.dicePoolChoices = Object.fromEntries( // Resolve leader actor
Array.from({ length: 21 }, (_, i) => [i, String(i)]) const leaderUuid = this.document.system.leaderUuid
) if (leaderUuid) {
context.apChoices = Object.fromEntries( const leader = await fromUuid(leaderUuid)
Array.from({ length: 7 }, (_, i) => [i, String(i)]) context.leader = leader ? { id: leader.id, uuid: leader.uuid, name: leader.name, img: leader.img } : null
) } else {
context.leader = null
}
return context return context
} }
// ── Array helpers ──────────────────────────────────────────────────────────── /** @override */
async _preparePartContext(partId, context) {
static async #onAddSkill() { const doc = this.document
const skills = foundry.utils.deepClone(this.document.system.skills ?? []) switch (partId) {
skills.push({ name: "", value: 2, colorDiceType: "white" }) case "main":
await this.document.update({ "system.skills": skills }) break
case "skills":
context.tab = context.tabs.skills
context.skills = doc.itemTypes.skillnpc ?? []
break
case "combat":
context.tab = context.tabs.combat
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) ?? ""
}))
break
case "traits":
context.tab = context.tabs.traits
context.traits = (doc.itemTypes.trait ?? []).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 "notes":
context.tab = context.tabs.notes
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.description ?? "", { async: true }
)
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.notes ?? "", { async: true }
)
break
}
return context
} }
static async #onRemoveSkill(event, target) { /** @override */
const idx = parseInt(target.dataset.idx, 10) async _onDrop(event) {
const skills = foundry.utils.deepClone(this.document.system.skills ?? []) if (!this.isEditable || !this.isEditMode) return
skills.splice(idx, 1) const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
await this.document.update({ "system.skills": skills })
// Actor drop → set as unit leader (must be token-linked)
if (data.type === "Actor") {
const actor = await fromUuid(data.uuid)
if (!actor) return
if (!actor.prototypeToken?.actorLink) {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Warning.LeaderNotLinked"))
return
}
return this.document.update({ "system.leaderUuid": actor.uuid })
} }
static async #onAddAttack() { if (data.type !== "Item") return
const attacks = foundry.utils.deepClone(this.document.system.attacks ?? []) const item = await fromUuid(data.uuid)
attacks.push({ name: "", damageDice: 6, colorDiceType: "white", ap: 0, special: "" }) if (!item) return
await this.document.update({ "system.attacks": attacks }) const ALLOWED = new Set(["skillnpc", "npcattack", "trait"])
if (!ALLOWED.has(item.type)) return
return this._onDropItem(item)
} }
static async #onRemoveAttack(event, target) { // ── Actions ────────────────────────────────────────────────────────────────
const idx = parseInt(target.dataset.idx, 10)
const attacks = foundry.utils.deepClone(this.document.system.attacks ?? []) static async #onAdjustGrit(event, target) {
attacks.splice(idx, 1) const delta = parseInt(target.dataset.delta, 10)
await this.document.update({ "system.attacks": attacks }) 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 async #onAddTrait() { static async #onRollArmor() {
const traits = foundry.utils.deepClone(this.document.system.traits ?? []) const doc = this.document
traits.push({ name: "", description: "" }) const armorDice = doc.system.armorDice
await this.document.update({ "system.traits": traits }) if (!armorDice?.value) return ui.notifications.info("No armor dice to roll.")
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: doc.img, basePool: armorDice.value, bonusOptions,
colorChoices: { white: "⬜ White (4+)", red: "🔴 Red (3+)", black: "⬛ Black (2+)" },
selectedColor: armorDice.colorDiceType,
rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes),
visibility: game.settings.get("core", "rollMode")
}
)
const result = await foundry.applications.api.DialogV2.prompt({
window: { title: `${doc.name}${game.i18n.localize("OATHHAMMER.Roll.ArmorRoll")}`, 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 = n => form.querySelector(`[name="${n}"]`)?.value
await rollNPCArmor(doc, {
bonus: parseInt(getValue("bonus")) || 0,
colorOverride: getValue("colorOverride") || null,
visibility: getValue("visibility"),
})
} }
static async #onRemoveTrait(event, target) { static async #onRollSkillNPC(event, target) {
const idx = parseInt(target.dataset.idx, 10) const skill = this.document.items.get(target.dataset.itemId)
const traits = foundry.utils.deepClone(this.document.system.traits ?? []) if (!skill) return
traits.splice(idx, 1) const bonusOptions = Array.from({ length: 13 }, (_, i) => {
await this.document.update({ "system.traits": traits }) 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: skill.name, skillImg: skill.img, basePool: skill.system.dicePool, bonusOptions,
colorChoices: { white: "⬜ White (4+)", red: "🔴 Red (3+)", black: "⬛ Black (2+)" },
selectedColor: skill.system.colorDiceType,
rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes),
visibility: game.settings.get("core", "rollMode")
}
)
const result = await foundry.applications.api.DialogV2.prompt({
window: { title: `${skill.name}${game.i18n.localize("OATHHAMMER.Tab.Skills")}`, 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 = n => form.querySelector(`[name="${n}"]`)?.value
await rollNPCSkill(this.document, skill, {
bonus: parseInt(getValue("bonus")) || 0,
colorOverride: getValue("colorOverride") || null,
visibility: getValue("visibility"),
})
}
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, basePool: attack.system.damageDice, bonusOptions,
colorChoices: { white: "⬜ White (4+)", red: "🔴 Red (3+)", black: "⬛ Black (2+)" },
selectedColor: attack.system.colorDiceType,
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}${game.i18n.localize("OATHHAMMER.Dialog.Damage")}`, 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 = n => form.querySelector(`[name="${n}"]`)?.value
await rollNPCAttackDamage(this.document, attack, {
bonus: parseInt(getValue("bonus")) || 0,
colorOverride: getValue("colorOverride") || null,
visibility: getValue("visibility"),
})
}
static #onCreateSkill() {
this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("OATHHAMMER.NewItem.SkillNPC"), type: "skillnpc"
}])
}
static #onCreateTrait() {
this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("OATHHAMMER.NewItem.Trait"), type: "trait"
}])
}
static async #onOpenLeader() {
const leaderUuid = this.document.system.leaderUuid
if (!leaderUuid) return
const leader = await fromUuid(leaderUuid)
if (leader) leader.sheet.render(true)
}
static async #onClearLeader() {
await this.document.update({ "system.leaderUuid": null })
} }
} }

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", "regiment"]) const ALLOWED_ITEM_TYPES = new Set(["building", "equipment", "weapon", "armor"])
export default class OathHammerSettlementSheet extends OathHammerActorSheet { export default class OathHammerSettlementSheet extends OathHammerActorSheet {
/** @override */ /** @override */
@@ -17,8 +17,9 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
adjustCurrency: OathHammerSettlementSheet.#onAdjustCurrency, adjustCurrency: OathHammerSettlementSheet.#onAdjustCurrency,
adjustQty: OathHammerSettlementSheet.#onAdjustQty, adjustQty: OathHammerSettlementSheet.#onAdjustQty,
toggleConstructed: OathHammerSettlementSheet.#onToggleConstructed, toggleConstructed: OathHammerSettlementSheet.#onToggleConstructed,
createRegiment: OathHammerSettlementSheet.#onCreateRegiment,
collectTaxes: OathHammerSettlementSheet.#onCollectTaxes, collectTaxes: OathHammerSettlementSheet.#onCollectTaxes,
openRegiment: OathHammerSettlementSheet.#onOpenRegiment,
removeRegiment: OathHammerSettlementSheet.#onRemoveRegiment,
}, },
} }
@@ -102,7 +103,9 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
} }
case "garrison": case "garrison":
context.tab = context.tabs.garrison context.tab = context.tabs.garrison
context.regiments = doc.itemTypes.regiment ?? [] context.regiments = (doc.system.garrisonRefs ?? [])
.map(id => game.actors?.get(id))
.filter(Boolean)
break break
} }
return context return context
@@ -112,6 +115,17 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
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)
// Regiment actors dropped onto garrison tab
if (data.type === "Actor") {
const actor = await fromUuid(data.uuid)
if (!actor || actor.type !== "regiment") return
const refs = foundry.utils.deepClone(this.document.system.garrisonRefs ?? [])
if (refs.includes(actor.id)) return // already linked
refs.push(actor.id)
return this.document.update({ "system.garrisonRefs": refs })
}
if (data.type !== "Item") return if (data.type !== "Item") return
const item = await fromUuid(data.uuid) const item = await fromUuid(data.uuid)
if (!item || !ALLOWED_ITEM_TYPES.has(item.type)) return if (!item || !ALLOWED_ITEM_TYPES.has(item.type)) return
@@ -144,11 +158,15 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet {
await item.update({ "system.constructed": !item.system.constructed }) await item.update({ "system.constructed": !item.system.constructed })
} }
static async #onCreateRegiment() { static async #onOpenRegiment(event, target) {
await this.document.createEmbeddedDocuments("Item", [{ const actor = game.actors?.get(target.dataset.actorId)
name: game.i18n.localize("OATHHAMMER.NewItem.Regiment"), if (actor) actor.sheet.render(true)
type: "regiment", }
}])
static async #onRemoveRegiment(event, target) {
const actorId = target.dataset.actorId
const refs = (this.document.system.garrisonRefs ?? []).filter(id => id !== actorId)
await this.document.update({ "system.garrisonRefs": refs })
} }
static async #onCollectTaxes() { static async #onCollectTaxes() {

View File

@@ -15,3 +15,5 @@ export { default as OathHammerSettlement } from "./settlement.mjs"
export { default as OathHammerSkillNPC } from "./skillnpc.mjs" export { default as OathHammerSkillNPC } from "./skillnpc.mjs"
export { default as OathHammerNpcAttack } from "./npcattack.mjs" export { default as OathHammerNpcAttack } from "./npcattack.mjs"
export { default as OathHammerRegiment } from "./regiment.mjs" export { default as OathHammerRegiment } from "./regiment.mjs"
export { default as OathHammerParty } from "./party.mjs"
export { default as OathHammerArmy } from "./army.mjs"

13
module/models/army.mjs Normal file
View File

@@ -0,0 +1,13 @@
export default class OathHammerArmy extends foundry.abstract.TypeDataModel {
static defineSchema() {
const { fields } = foundry.data
const schema = {}
schema.regimentRefs = new fields.ArrayField(new fields.StringField({ required: true, blank: false }))
schema.leaderUuid = new fields.StringField({ required: false, nullable: true, initial: null })
schema.location = new fields.StringField({ required: false, nullable: true, initial: "" })
schema.notes = new fields.HTMLField({ required: false, nullable: true, initial: "" })
return schema
}
}

24
module/models/party.mjs Normal file
View File

@@ -0,0 +1,24 @@
export default class OathHammerParty extends foundry.abstract.TypeDataModel {
static defineSchema() {
const { fields } = foundry.data
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.notes = new fields.HTMLField({ required: false, nullable: true, initial: "" })
// Ordered list of character actor IDs — position = marching order
schema.memberRefs = new fields.ArrayField(
new fields.StringField({ required: true, nullable: false, blank: false })
)
schema.treasury = new fields.SchemaField({
gp: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
sp: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
cp: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
})
return schema
}
static LOCALIZATION_PREFIXES = ["OATHHAMMER.Party"]
}

View File

@@ -7,10 +7,11 @@ export default class OathHammerRegiment extends foundry.abstract.TypeDataModel {
const schema = {} const schema = {}
schema.description = new fields.HTMLField({ required: false, nullable: true, initial: "" }) schema.description = new fields.HTMLField({ required: false, nullable: true, initial: "" })
schema.notes = new fields.StringField({ required: false, nullable: true, initial: "" }) schema.notes = new fields.HTMLField({ required: false, nullable: true, initial: "" })
schema.grit = new fields.SchemaField({ schema.grit = new fields.SchemaField({
max: new fields.NumberField({ ...requiredInteger, initial: 20, min: 0, max: 200 }), value: new fields.NumberField({ ...requiredInteger, initial: 20, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 20, min: 0 }),
}) })
schema.armorDice = new fields.SchemaField({ schema.armorDice = new fields.SchemaField({
@@ -19,34 +20,20 @@ export default class OathHammerRegiment extends foundry.abstract.TypeDataModel {
}) })
schema.movement = new fields.NumberField({ ...requiredInteger, initial: 60, min: 0, max: 500 }) schema.movement = new fields.NumberField({ ...requiredInteger, initial: 60, min: 0, max: 500 })
schema.supplyCost = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.recruitmentCost = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
// Embedded skill rows: [{name, value, colorDiceType}] schema.leaderUuid = new fields.StringField({ required: false, nullable: true, initial: null })
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 return schema
} }
static LOCALIZATION_PREFIXES = ["OATHHAMMER.Regiment"] static LOCALIZATION_PREFIXES = ["OATHHAMMER.Regiment"]
get threshold() {
return { white: 4, red: 3, black: 2 }[this.armorDice.colorDiceType] ?? 4
}
get colorEmoji() { get colorEmoji() {
return { white: "⬜", red: "🔴", black: "⬛" }[this.armorDice.colorDiceType] ?? "⬜" return { white: "⬜", red: "🔴", black: "⬛" }[this.armorDice.colorDiceType] ?? "⬜"
} }

View File

@@ -33,6 +33,11 @@ export default class OathHammerSettlement extends foundry.abstract.TypeDataModel
schema.isCapital = new fields.BooleanField({ required: true, initial: false }) schema.isCapital = new fields.BooleanField({ required: true, initial: false })
schema.taxNotes = new fields.StringField({ required: true, nullable: false, initial: "" }) schema.taxNotes = new fields.StringField({ required: true, nullable: false, initial: "" })
// Linked regiment actor IDs
schema.garrisonRefs = new fields.ArrayField(
new fields.StringField({ required: true, nullable: false, blank: false })
)
return schema return schema
} }

View File

@@ -24,7 +24,10 @@ Hooks.once("init", function () {
CONFIG.Actor.dataModels = { CONFIG.Actor.dataModels = {
character: models.OathHammerCharacter, character: models.OathHammerCharacter,
npc: models.OathHammerNPC, npc: models.OathHammerNPC,
settlement: models.OathHammerSettlement settlement: models.OathHammerSettlement,
regiment: models.OathHammerRegiment,
party: models.OathHammerParty,
army: models.OathHammerArmy,
} }
CONFIG.Item.documentClass = documents.OathHammerItem CONFIG.Item.documentClass = documents.OathHammerItem
@@ -42,7 +45,6 @@ Hooks.once("init", function () {
building: models.OathHammerBuilding, building: models.OathHammerBuilding,
skillnpc: models.OathHammerSkillNPC, skillnpc: models.OathHammerSkillNPC,
npcattack: models.OathHammerNpcAttack, 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)
@@ -61,6 +63,21 @@ Hooks.once("init", function () {
makeDefault: true, makeDefault: true,
label: "OATHHAMMER.Sheet.Settlement" label: "OATHHAMMER.Sheet.Settlement"
}) })
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerRegimentSheet, {
types: ["regiment"],
makeDefault: true,
label: "OATHHAMMER.Sheet.Regiment"
})
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerPartySheet, {
types: ["party"],
makeDefault: true,
label: "OATHHAMMER.Sheet.Party"
})
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerArmySheet, {
types: ["army"],
makeDefault: true,
label: "OATHHAMMER.Sheet.Army"
})
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet) foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerWeaponSheet, { types: ["weapon"], makeDefault: true, label: "OATHHAMMER.Sheet.Weapon" }) foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerWeaponSheet, { types: ["weapon"], makeDefault: true, label: "OATHHAMMER.Sheet.Weapon" })
@@ -76,7 +93,6 @@ Hooks.once("init", function () {
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.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.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

@@ -42,6 +42,22 @@
"description", "description",
"notes" "notes"
] ]
},
"regiment": {
"htmlFields": [
"description",
"notes"
]
},
"party": {
"htmlFields": [
"notes"
]
},
"army": {
"htmlFields": [
"notes"
]
} }
}, },
"Item": { "Item": {
@@ -107,8 +123,7 @@
] ]
}, },
"skillnpc": {}, "skillnpc": {},
"npcattack": {}, "npcattack": {}
"regiment": {}
} }
}, },
"grid": { "grid": {

View File

@@ -0,0 +1,6 @@
<section data-tab="notes" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.Notes"}}</label>
{{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}}
</div>
</section>

View File

@@ -0,0 +1,55 @@
<section data-tab="overview" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset class="army-regiments-fieldset">
<legend>{{localize "OATHHAMMER.Label.Regiments"}}</legend>
{{#if regiments.length}}
<ul class="item-list item-list--army-regiment">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Grit"}}</span>
<span>{{localize "OATHHAMMER.Label.ArmorDice"}}</span>
<span>{{localize "OATHHAMMER.Label.Movement"}}</span>
<span>{{localize "OATHHAMMER.Label.SupplyCost"}}</span>
<span></span>
</li>
{{#each regiments as |regiment|}}
<li class="item-entry" data-actor-id="{{regiment.id}}">
<img src="{{regiment.img}}" class="item-img" />
<span class="item-name">
<a data-action="openRegiment" data-actor-id="{{regiment.id}}">{{regiment.name}}</a>
</span>
<span>{{regiment.grit}}/{{regiment.gritMax}}</span>
<span>{{regiment.armor}}d6</span>
<span>{{regiment.movement}}</span>
<span>{{regiment.supplyCost}} GP</span>
<div class="item-actions">
{{#unless ../isPlayMode}}
<a data-action="removeRegiment" data-actor-id="{{regiment.id}}" data-tooltip="{{localize 'OATHHAMMER.Tooltip.RemoveRegiment'}}">
<i class="fa-solid fa-xmark"></i>
</a>
{{/unless}}
</div>
</li>
{{/each}}
<li class="item-entry army-total-row">
<span></span>
<span class="col-name army-total-label">{{localize "OATHHAMMER.Label.TotalSupply"}}</span>
<span></span>
<span></span>
<span></span>
<span class="army-total-value">{{totalSupply}} GP</span>
<span></span>
</li>
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoRegiments"}}</p>
{{/if}}
</fieldset>
{{#unless isPlayMode}}
<p class="settlement-hint">{{localize "OATHHAMMER.Label.DropRegimentHint"}}</p>
{{/unless}}
</section>

View File

@@ -0,0 +1,45 @@
<section class="army-main army-main-{{ifThen isPlayMode 'play' 'edit'}}">
<fieldset class="army-header-fieldset">
<div class="army-header">
<!-- Portrait -->
<div class="army-portrait-wrap">
<img class="actor-img army-portrait" src="{{actor.img}}" data-edit="img" data-action="editImage" data-tooltip="{{actor.name}}" />
</div>
<!-- Name + Leader + Location -->
<div class="army-header-body">
<div class="character-name">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}}
<a class="control" data-action="toggleSheet" data-tooltip="OATHHAMMER.ToggleSheet" data-tooltip-direction="UP">
<i class="fa-solid {{#if isPlayMode}}fa-shield-halved{{else}}fa-user-pen{{/if}}"></i>
</a>
</div>
<!-- Leader -->
<div class="army-leader-row" data-drop-target="leader">
<span class="army-field-label">{{localize "OATHHAMMER.Label.Commander"}}</span>
{{#if leader}}
<img src="{{leader.img}}" class="army-leader-img" />
<a class="army-leader-name" data-action="openLeader" data-tooltip="{{localize 'OATHHAMMER.Tooltip.OpenLeader'}}">{{leader.name}}</a>
{{#unless isPlayMode}}
<a class="army-leader-clear" data-action="clearLeader" data-tooltip="{{localize 'OATHHAMMER.Tooltip.ClearLeader'}}">
<i class="fa-solid fa-times"></i>
</a>
{{/unless}}
{{else}}
<span class="army-field-empty">{{localize "OATHHAMMER.Label.DropLeaderHint"}}</span>
{{/if}}
</div>
<!-- Location -->
<div class="army-location-row">
<span class="army-field-label">{{localize "OATHHAMMER.Label.Location"}}</span>
{{formInput systemFields.location value=system.location name="system.location" placeholder="—" disabled=isPlayMode}}
</div>
</div><!-- /army-header-body -->
</div>
</fieldset>
</section>

View File

@@ -35,7 +35,7 @@
<img src="{{spell.img}}" class="item-img" /> <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-name" {{#if spell._descTooltip}}data-tooltip="{{spell._descTooltip}}"{{/if}}>{{spell.name}}</span>
<span class="item-detail">{{spell.system.difficultyValue}}</span> <span class="item-detail">{{spell.system.difficultyValue}}</span>
<span class="item-type">{{localize spell.system.tradition}}</span> <span class="item-type">{{spell.traditionLabel}}</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.range}}{{spell.system.range}}{{else}}{{/if}}</span>
<span class="item-detail item-detail--small">{{#if spell.system.duration}}{{spell.system.duration}}{{else}}{{/if}}</span> <span class="item-detail item-detail--small">{{#if spell.system.duration}}{{spell.system.duration}}{{else}}{{/if}}</span>
<span class="item-detail item-detail--small">{{#if spell.system.spellSave}}{{spell.system.spellSave}}{{else}}{{/if}}</span> <span class="item-detail item-detail--small">{{#if spell.system.spellSave}}{{spell.system.spellSave}}{{else}}{{/if}}</span>
@@ -74,7 +74,7 @@
<li class="item-entry" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}"> <li class="item-entry" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}">
<img src="{{miracle.img}}" class="item-img" /> <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-name" {{#if miracle._descTooltip}}data-tooltip="{{miracle._descTooltip}}"{{/if}}>{{miracle.name}}</span>
<span class="item-detail">{{miracle.system.divineTradition}}</span> <span class="item-detail">{{miracle.traditionLabel}}</span>
<div class="item-actions"> <div class="item-actions">
<a data-action="castMiracle" 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="castMiracle" 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="edit" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}"><i class="fa-solid fa-edit"></i></a>

View File

@@ -13,6 +13,7 @@
<span>{{localize "OATHHAMMER.Label.Tradition"}}</span> <span>{{localize "OATHHAMMER.Label.Tradition"}}</span>
<span>{{localize "OATHHAMMER.Label.Range"}}</span> <span>{{localize "OATHHAMMER.Label.Range"}}</span>
<span>{{localize "OATHHAMMER.Label.Duration"}}</span> <span>{{localize "OATHHAMMER.Label.Duration"}}</span>
<span>{{localize "OATHHAMMER.Label.SpellSave"}}</span>
<span></span> <span></span>
</li> </li>
{{#each spells as |spell|}} {{#each spells as |spell|}}
@@ -20,11 +21,12 @@
<img src="{{spell.img}}" class="item-img" /> <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-name" {{#if spell._descTooltip}}data-tooltip="{{spell._descTooltip}}"{{/if}}>{{spell.name}}</span>
<span class="item-detail">{{spell.system.difficultyValue}}</span> <span class="item-detail">{{spell.system.difficultyValue}}</span>
<span class="item-type">{{localize spell.system.tradition}}</span> <span class="item-type">{{spell.traditionLabel}}</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.range}}{{spell.system.range}}{{else}}{{/if}}</span>
<span class="item-detail item-detail--small">{{#if spell.system.duration}}{{spell.system.duration}}{{else}}{{/if}}</span> <span class="item-detail item-detail--small">{{#if spell.system.duration}}{{spell.system.duration}}{{else}}{{/if}}</span>
<span class="item-detail item-detail--small">{{#if spell.system.spellSave}}{{spell.system.spellSave}}{{else}}{{/if}}</span>
<div class="item-actions"> <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="castNPCSpell" data-item-id="{{spell.id}}" data-tooltip="{{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="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> <a data-action="delete" data-item-id="{{spell.id}}" data-item-uuid="{{spell.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div> </div>
@@ -52,9 +54,9 @@
<li class="item-entry" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}"> <li class="item-entry" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}">
<img src="{{miracle.img}}" class="item-img" /> <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-name" {{#if miracle._descTooltip}}data-tooltip="{{miracle._descTooltip}}"{{/if}}>{{miracle.name}}</span>
<span class="item-detail">{{miracle.system.divineTradition}}</span> <span class="item-detail">{{miracle.traditionLabel}}</span>
<div class="item-actions"> <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="castNPCMiracle" data-item-id="{{miracle.id}}" data-tooltip="{{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="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> <a data-action="delete" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div> </div>

View File

@@ -23,7 +23,7 @@
<span class="item-detail">{{skill.system.threshold}}</span> <span class="item-detail">{{skill.system.threshold}}</span>
<a class="npc-skill-roll-btn" data-action="rollSkillNPC" <a class="npc-skill-roll-btn" data-action="rollSkillNPC"
data-item-id="{{skill.id}}" data-item-uuid="{{skill.uuid}}" data-item-id="{{skill.id}}" data-item-uuid="{{skill.uuid}}"
data-tooltip="{{localize 'OATHHAMMER.Roll.RollSkill'}}"> data-tooltip="{{localize 'OATHHAMMER.Dialog.RollSkill'}}">
<i class="fa-solid fa-dice-d6"></i> <i class="fa-solid fa-dice-d6"></i>
</a> </a>
<div class="item-actions"> <div class="item-actions">

View File

@@ -0,0 +1,39 @@
<section data-tab="loot" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Loot"}}</legend>
{{#if lootItems.length}}
<ul class="item-list item-list--party-loot">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Type"}}</span>
<span>{{localize "OATHHAMMER.Label.Qty"}}</span>
<span></span>
</li>
{{#each lootItems 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>
<span class="item-type">{{item.typeLabel}}</span>
<div class="item-qty">
<a data-action="adjustQty" data-item-id="{{item.id}}" data-delta="-1" class="qty-btn"></a>
<span>{{#if item.system.quantity}}{{item.system.quantity}}{{else}}1{{/if}}</span>
<a data-action="adjustQty" data-item-id="{{item.id}}" data-delta="1" class="qty-btn">+</a>
</div>
<div class="item-actions">
<a data-action="edit" data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}" data-tooltip="{{localize 'OATHHAMMER.Label.Edit'}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{item.id}}" data-item-uuid="{{item.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.NoLoot"}}</p>
{{/if}}
</fieldset>
<p class="settlement-hint">{{localize "OATHHAMMER.Label.DropLootHint"}}</p>
</section>

View File

@@ -0,0 +1,46 @@
<section data-tab="members" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.MarchingOrder"}}</legend>
{{#if members.length}}
<ul class="item-list item-list--party-member">
<li class="item-list-header">
<span class="col-order">#</span>
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Class"}}</span>
<span>{{localize "OATHHAMMER.Label.Level"}}</span>
<span>{{localize "OATHHAMMER.Label.Grit"}}</span>
<span></span>
</li>
{{#each members as |member|}}
<li class="item-entry" data-actor-id="{{member.id}}">
<span class="party-member-order">{{member.position}}</span>
<img src="{{member.img}}" class="item-img" />
<span class="item-name">
<a data-action="openMember" data-actor-id="{{member.id}}">{{member.name}}</a>
</span>
<span class="item-detail item-detail--small">{{member.classLabel}}</span>
<span class="item-detail">{{member.level}}</span>
<span class="item-detail">{{member.grit}}</span>
<div class="item-actions">
{{#unless member.isFirst}}
<a data-action="moveMemberUp" data-idx="{{member.idx}}" data-tooltip="{{localize 'OATHHAMMER.Tooltip.MoveUp'}}"><i class="fa-solid fa-chevron-up"></i></a>
{{/unless}}
{{#unless member.isLast}}
<a data-action="moveMemberDown" data-idx="{{member.idx}}" data-tooltip="{{localize 'OATHHAMMER.Tooltip.MoveDown'}}"><i class="fa-solid fa-chevron-down"></i></a>
{{/unless}}
<a data-action="removeMember" data-actor-id="{{member.id}}" data-tooltip="{{localize 'OATHHAMMER.Tooltip.RemoveMember'}}"><i class="fa-solid fa-times"></i></a>
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoMembers"}}</p>
{{/if}}
</fieldset>
<p class="settlement-hint">{{localize "OATHHAMMER.Label.DropMemberHint"}}</p>
</section>

View File

@@ -0,0 +1,6 @@
<section data-tab="notes" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.Notes"}}</label>
{{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}}
</div>
</section>

View File

@@ -0,0 +1,54 @@
<section class="party-main party-main-{{ifThen isPlayMode 'play' 'edit'}}">
<fieldset class="party-header-fieldset">
<div class="party-header">
<!-- Portrait -->
<div class="party-portrait-wrap">
<img class="actor-img party-portrait" src="{{actor.img}}" data-edit="img" data-action="editImage" data-tooltip="{{actor.name}}" />
</div>
<!-- Name + Treasury -->
<div class="party-header-body">
<div class="character-name">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}}
<a class="control" data-action="toggleSheet" data-tooltip="OATHHAMMER.ToggleSheet" data-tooltip-direction="UP">
<i class="fa-solid {{#if isPlayMode}}fa-users-viewfinder{{else}}fa-user-pen{{/if}}"></i>
</a>
</div>
<!-- Treasury -->
<div class="party-treasury">
<span class="party-treasury-label">{{localize "OATHHAMMER.Label.Treasury"}}</span>
<div class="party-currency party-currency-gp">
<span class="currency-label">{{localize "OATHHAMMER.Label.GP"}}</span>
<div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.treasury.gp" data-delta="-1" class="currency-btn"></a>
<input type="number" name="system.treasury.gp" value="{{system.treasury.gp}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
<a data-action="adjustCurrency" data-field="system.treasury.gp" data-delta="1" class="currency-btn">+</a>
</div>
</div>
<div class="party-currency party-currency-sp">
<span class="currency-label">{{localize "OATHHAMMER.Label.SP"}}</span>
<div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.treasury.sp" data-delta="-1" class="currency-btn"></a>
<input type="number" name="system.treasury.sp" value="{{system.treasury.sp}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
<a data-action="adjustCurrency" data-field="system.treasury.sp" data-delta="1" class="currency-btn">+</a>
</div>
</div>
<div class="party-currency party-currency-cp">
<span class="currency-label">{{localize "OATHHAMMER.Label.CP"}}</span>
<div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.treasury.cp" data-delta="-1" class="currency-btn"></a>
<input type="number" name="system.treasury.cp" value="{{system.treasury.cp}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
<a data-action="adjustCurrency" data-field="system.treasury.cp" data-delta="1" class="currency-btn">+</a>
</div>
</div>
</div><!-- /party-treasury -->
</div><!-- /party-header-body -->
</div>
</fieldset>
</section>

View File

@@ -0,0 +1,33 @@
<section data-tab="combat" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Attacks"}}
{{#unless isPlayMode}}<a data-action="createNpcAttack" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
</legend>
{{#if npcAttacks.length}}
<ul class="item-list item-list--npc-attack">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Damage"}}</span>
<span title="Armor Penetration">AP</span>
<span></span>
</li>
{{#each npcAttacks as |attack|}}
<li class="item-entry" data-item-id="{{attack.id}}" data-item-uuid="{{attack.uuid}}">
<img src="{{attack.img}}" class="item-img" />
<span class="item-name" {{#if attack._descTooltip}}data-tooltip="{{attack._descTooltip}}"{{/if}}>{{attack.name}}</span>
<span class="item-detail">{{attack.system.damageLabel}}</span>
<span class="item-detail">{{#if attack.system.ap}}{{attack.system.ap}}{{else}}{{/if}}</span>
<div class="item-actions">
<a data-action="rollNpcAttack" data-item-id="{{attack.id}}" data-tooltip="{{localize 'OATHHAMMER.Dialog.Damage'}}"><i class="fa-solid fa-burst"></i></a>
<a data-action="edit" data-item-id="{{attack.id}}" data-item-uuid="{{attack.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{attack.id}}" data-item-uuid="{{attack.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoAttacks"}}</p>
{{/if}}
</fieldset>
</section>

View File

@@ -0,0 +1,107 @@
<section class="npc-main npc-main-{{ifThen isPlayMode 'play' 'edit'}}">
<fieldset class="regiment-fieldset">
<div class="npc-pc flexrow">
<!-- LEFT: portrait -->
<div class="npc-left">
<img class="actor-img" src="{{actor.img}}" data-edit="img" data-action="editImage" data-tooltip="{{actor.name}}" />
</div>
<!-- RIGHT: name + vitals -->
<div class="npc-right">
<div class="character-name">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}}
<a class="control" data-action="toggleSheet" data-tooltip="OATHHAMMER.ToggleSheet" data-tooltip-direction="UP">
<i class="fa-solid fa-user-{{ifThen isPlayMode 'lock' 'pen'}}"></i>
</a>
</div>
<!-- Row 1: combat stats -->
<div class="npc-vitals-grid regiment-vitals-grid regiment-row1">
<!-- Grit -->
<div class="npc-vital npc-vital-grit">
<span class="vital-label">{{localize "OATHHAMMER.Label.Grit"}}</span>
<span class="vital-value">
<a class="grit-btn" data-action="adjustGrit" data-delta="-1" data-tooltip="1"></a>
<input type="number" class="npc-num-input" name="system.grit.value" value="{{system.grit.value}}" min="0" />
<span class="res-sep">/</span>
{{formInput systemFields.grit.fields.max value=system.grit.max name="system.grit.max" disabled=isPlayMode}}
<a class="grit-btn" data-action="adjustGrit" data-delta="1" data-tooltip="+1">+</a>
</span>
</div>
<!-- Armor Dice -->
<div class="npc-vital">
<span class="vital-label{{#if isPlayMode}} vital-roll-label{{/if}}"
{{#if isPlayMode}}data-action="rollArmor" data-tooltip="OATHHAMMER.Tooltip.RollArmor"{{/if}}>
{{#if isPlayMode}}<i class="fa-solid fa-dice-d6"></i>{{/if}}
{{localize "OATHHAMMER.Label.ArmorDice"}}
</span>
<span class="vital-value">
<input type="number" class="npc-num-input" name="system.armorDice.value" value="{{system.armorDice.value}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
{{#if isPlayMode}}
<span class="npc-color-badge">{{armorDiceEmoji}}</span>
{{else}}
<select name="system.armorDice.colorDiceType" class="npc-color-select">
{{selectOptions colorChoices selected=system.armorDice.colorDiceType}}
</select>
{{/if}}
</span>
</div>
<!-- Movement -->
<div class="npc-vital">
<span class="vital-label">{{localize "OATHHAMMER.Label.Movement"}}</span>
<span class="vital-value">
<input type="number" class="npc-num-input" name="system.movement" value="{{system.movement}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
<span class="res-sep">ft</span>
</span>
</div>
</div><!-- /row1 -->
<!-- Row 2: cost stats -->
<div class="npc-vitals-grid regiment-vitals-grid regiment-row2">
<!-- Supply Cost -->
<div class="npc-vital">
<span class="vital-label">{{localize "OATHHAMMER.Label.SupplyCost"}}</span>
<span class="vital-value">
<input type="number" class="npc-num-input" name="system.supplyCost" value="{{system.supplyCost}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
<span class="res-sep">gp / month</span>
</span>
</div>
<!-- Recruitment Cost -->
<div class="npc-vital">
<span class="vital-label">{{localize "OATHHAMMER.Label.RecruitmentCost"}}</span>
<span class="vital-value">
<input type="number" class="npc-num-input" name="system.recruitmentCost" value="{{system.recruitmentCost}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
<span class="res-sep">gp</span>
</span>
</div>
</div><!-- /row2 -->
<!-- Leader -->
<div class="regiment-leader-row" data-drop-target="leader">
<span class="regiment-leader-label">{{localize "OATHHAMMER.Label.UnitLeader"}}</span>
{{#if leader}}
<img src="{{leader.img}}" class="regiment-leader-img" />
<a class="regiment-leader-name" data-action="openLeader" data-tooltip="{{localize 'OATHHAMMER.Tooltip.OpenLeader'}}">{{leader.name}}</a>
{{#unless isPlayMode}}
<a class="regiment-leader-clear" data-action="clearLeader" data-tooltip="{{localize 'OATHHAMMER.Tooltip.ClearLeader'}}">
<i class="fa-solid fa-times"></i>
</a>
{{/unless}}
{{else}}
<span class="regiment-leader-empty">{{localize "OATHHAMMER.Label.DropLeaderHint"}}</span>
{{/if}}
</div>
</div><!-- /npc-right -->
</div>
</fieldset>
</section>

View File

@@ -2,7 +2,6 @@
<fieldset> <fieldset>
<legend>{{localize "OATHHAMMER.Label.Garrison"}} <legend>{{localize "OATHHAMMER.Label.Garrison"}}
{{#unless isPlayMode}}<a data-action="createRegiment" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
</legend> </legend>
{{#if regiments.length}} {{#if regiments.length}}
<ul class="item-list item-list--regiment"> <ul class="item-list item-list--regiment">
@@ -15,15 +14,15 @@
<span></span> <span></span>
</li> </li>
{{#each regiments as |regiment|}} {{#each regiments as |regiment|}}
<li class="item-entry" data-item-id="{{regiment.id}}" data-item-uuid="{{regiment.uuid}}"> <li class="item-entry" data-actor-id="{{regiment.id}}">
<img src="{{regiment.img}}" class="item-img" /> <img src="{{regiment.img}}" class="item-img" />
<span class="item-name">{{regiment.name}}</span> <span class="item-name"><a data-action="openRegiment" data-actor-id="{{regiment.id}}">{{regiment.name}}</a></span>
<span class="item-detail">{{regiment.system.grit.max}}</span> <span class="item-detail">{{regiment.system.grit.max}}</span>
<span class="item-detail">{{regiment.system.armorLabel}}</span> <span class="item-detail">{{regiment.system.armorLabel}}</span>
<span class="item-detail">{{regiment.system.movement}} ft</span> <span class="item-detail">{{regiment.system.movement}} ft</span>
<div class="item-actions"> <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="openRegiment" data-actor-id="{{regiment.id}}" 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> <a data-action="removeRegiment" data-actor-id="{{regiment.id}}" data-tooltip="{{localize 'OATHHAMMER.Label.RemoveFromGarrison'}}"><i class="fa-solid fa-minus-circle"></i></a>
</div> </div>
</li> </li>
{{/each}} {{/each}}
@@ -33,4 +32,6 @@
{{/if}} {{/if}}
</fieldset> </fieldset>
<p class="settlement-hint">{{localize "OATHHAMMER.Settlement.GarrisonHint"}}</p>
</section> </section>

View File

@@ -1,122 +0,0 @@
<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>