diff --git a/css/fvtt-oath-hammer.css b/css/fvtt-oath-hammer.css index 233ff74..e48de6a 100644 --- a/css/fvtt-oath-hammer.css +++ b/css/fvtt-oath-hammer.css @@ -28,7 +28,10 @@ } .oathhammer .character-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-size: 0.86rem; color: #2a1a0a; @@ -40,20 +43,32 @@ } .oathhammer .character-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; } .oathhammer .character-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; } .oathhammer .character-content input:disabled, .oathhammer .npc-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 .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); border-color: transparent; color: #2a1a0a; @@ -61,9 +76,15 @@ .oathhammer .character-content input, .oathhammer .npc-content input, .oathhammer .settlement-content input, +.oathhammer .regiment-content input, +.oathhammer .party-content input, +.oathhammer .army-content input, .oathhammer .character-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; background-color: rgba(255, 255, 255, 0.3); border-color: #084a74; @@ -71,7 +92,10 @@ } .oathhammer .character-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; font-family: "Sherwood", "Palatino Linotype", serif; font-size: calc(0.86rem * 1.2); @@ -82,14 +106,20 @@ } .oathhammer .character-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; border-radius: 4px; border-color: #535128; } .oathhammer .character-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-size: calc(0.86rem * 1.1); font-weight: bold; @@ -98,7 +128,10 @@ } .oathhammer .character-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-size: 0.86rem; color: #2a1a0a; @@ -882,6 +915,93 @@ gap: 8px; 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 { list-style: none; margin: 0; @@ -1012,11 +1132,11 @@ } .oathhammer .item-list--spell .item-list-header, .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-entry { - grid-template-columns: 24px 1fr 4.5rem 5.5rem; + grid-template-columns: 24px 1fr 6rem 5rem; } .oathhammer .miracles-blocked { opacity: 0.45; @@ -1333,7 +1453,7 @@ } .oathhammer .regiment-sheet .regiment-stats-row { display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr 1fr; gap: 4px; } .oathhammer .regiment-sheet .regiment-stats-row .form-group > label { @@ -2328,3 +2448,307 @@ .oathhammer .settlement-buildings-header .collect-taxes-btn i { 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; +} diff --git a/lang/en.json b/lang/en.json index 5f52ff7..2966300 100644 --- a/lang/en.json +++ b/lang/en.json @@ -18,7 +18,9 @@ "Settlement": "Oath Hammer Settlement Sheet", "SkillNPC": "Oath Hammer NPC Skill Sheet", "NpcAttack": "Oath Hammer NPC Attack Sheet", - "Regiment": "Oath Hammer Regiment Sheet" + "Regiment": "Oath Hammer Regiment Sheet", + "Party": "Party Sheet", + "Army": "Army Sheet" }, "Tab": { "Identity": "Oaths / Traits", @@ -31,7 +33,9 @@ "Buildings": "Buildings", "Inventory": "Inventory", "Traits": "Traits", - "Garrison": "Garrison" + "Garrison": "Garrison", + "Members": "Members", + "Loot": "Loot" }, "Attribute": { "Might": "Might", @@ -229,7 +233,7 @@ "DefenseValue": "Defense Value", "ArmorRating": "Armor Rating", "DefenseBonus": "Defense Bonus", - "Movement": "Movement", + "Movement": "Move", "ArcaneStress": "Arcane Stress", "StressValue": "Stress", "ThresholdBonus": "Threshold Bonus", @@ -254,7 +258,7 @@ "Conditions": "Conditions", "Description": "Description", "Notes": "Notes", - "Stats": "Statistics", + "Stats": "Stats", "CR": "Challenge Rating", "AttackBonus": "Attack Bonus", "DamageBonus": "Damage Bonus", @@ -337,15 +341,34 @@ "Dice": "Dice", "DiceColor": "Color", "Special": "Special", - "Movement": "Move", - "Stats": "Stats", - "NoRegiments": "No regiments. Add one with the + button.", + "NoRegiments": "No regiments yet — drag regiment actors here.", + "SupplyCost": "Supply Cost", "SkillName": "Skill name", "AttackName": "Attack name", "TraitName": "Trait name", "Edit": "Edit", "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": { "White": "White (4+)", @@ -364,11 +387,18 @@ "Regiment": "New Regiment", "RegimentSkill": "Add Skill", "RegimentAttack": "Add Attack", - "RegimentTrait": "Add Trait" + "RegimentTrait": "Add Trait", + "SkillNPC": "New Skill" }, "ToggleSheet": "Toggle Edit/Play Mode", "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": { "CastSpell": "Cast Spell", @@ -462,7 +492,9 @@ "APPenalty": "AP (Attacker)", "APHint": "attacker's Armor Piercing value", "ReinforcedHint": "Reinforced — rolling red dice", - "RollInitiative": "Roll Initiative" + "RollInitiative": "Roll Initiative", + "Default": "Default", + "DicePool": "Dice Pool" }, "Enhancement": { "None": "None", @@ -1056,7 +1088,9 @@ } }, "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": { "CenterOfLearning": "Center of Learning", @@ -1076,7 +1110,8 @@ "CollectTaxes": "Collect Taxes", "CollectTaxesTooltip": "Roll tax revenue for all constructed buildings and total the result.", "NoTaxRevenue": "No constructed buildings with tax revenue defined.", - "TotalRevenue": "Total Revenue" + "TotalRevenue": "Total Revenue", + "GarrisonHint": "Drag a regiment actor here to add it to the garrison." }, "SkillNPC": { "FIELDS": { @@ -1124,7 +1159,8 @@ "NpcSubtype": { "Creature": "Creature", "Npc": "NPC" - } + }, + "Party": {} }, "TYPES": { "Item": { @@ -1141,13 +1177,15 @@ "class": "Class", "skillnpc": "NPC Skill", "npcattack": "NPC Attack", - "regiment": "Regiment", "building": "Building" }, "Actor": { "character": "Character", "npc": "NPC / Creature", - "settlement": "Settlement" + "settlement": "Settlement", + "regiment": "Regiment", + "party": "Party", + "army": "Army" } } } \ No newline at end of file diff --git a/less/army-sheet.less b/less/army-sheet.less new file mode 100644 index 0000000..2c7e4c3 --- /dev/null +++ b/less/army-sheet.less @@ -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; + } + } +} diff --git a/less/base.less b/less/base.less index bf1d77a..2310127 100644 --- a/less/base.less +++ b/less/base.less @@ -37,7 +37,10 @@ // Shared actor content base .oathhammer .character-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-size: @font-size-base; color: @color-dark; diff --git a/less/fvtt-oath-hammer.less b/less/fvtt-oath-hammer.less index dc5d234..8aa4ffd 100644 --- a/less/fvtt-oath-hammer.less +++ b/less/fvtt-oath-hammer.less @@ -12,3 +12,5 @@ @import "rolls"; @import "roll-dialog"; @import "settlement-sheet"; +@import "party-sheet"; +@import "army-sheet"; diff --git a/less/item-list.less b/less/item-list.less index f89a952..74ea3fe 100644 --- a/less/item-list.less +++ b/less/item-list.less @@ -148,13 +148,15 @@ .item-list--spell { .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-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; } } diff --git a/less/item-sheets.less b/less/item-sheets.less index 2751ed1..f7c1852 100644 --- a/less/item-sheets.less +++ b/less/item-sheets.less @@ -207,7 +207,7 @@ .regiment-stats-row { display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr 1fr; gap: 4px; .form-group > label { flex: 0 0 6rem; } diff --git a/less/npc-sheet.less b/less/npc-sheet.less index bcec37f..c14618a 100644 --- a/less/npc-sheet.less +++ b/less/npc-sheet.less @@ -207,3 +207,100 @@ 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; } + } +} diff --git a/less/party-sheet.less b/less/party-sheet.less new file mode 100644 index 0000000..87599bd --- /dev/null +++ b/less/party-sheet.less @@ -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; } + } + } +} diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 9c15b57..170e230 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -15,6 +15,8 @@ export { default as OathHammerSettlementSheet } from "./sheets/settlement-sheet. export { default as OathHammerSkillNPCSheet } from "./sheets/skillnpc-sheet.mjs" export { default as OathHammerNpcAttackSheet } from "./sheets/npcattack-sheet.mjs" export { default as OathHammerRegimentSheet } from "./sheets/regiment-sheet.mjs" +export { default as 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 OathHammerWeaponDialog } from "./weapon-dialog.mjs" export { default as OathHammerSpellDialog } from "./spell-dialog.mjs" diff --git a/module/applications/sheets/army-sheet.mjs b/module/applications/sheets/army-sheet.mjs new file mode 100644 index 0000000..5966868 --- /dev/null +++ b/module/applications/sheets/army-sheet.mjs @@ -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 }) + } +} diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index 9a9762b..c80f7af 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -205,11 +205,13 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { context.stressBlocked = doc.system.arcaneStress.value >= doc.system.arcaneStress.threshold context.spells = doc.itemTypes.spell.map(s => ({ 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 => ({ 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 case "equipment": diff --git a/module/applications/sheets/npc-sheet.mjs b/module/applications/sheets/npc-sheet.mjs index 634b697..42113b7 100644 --- a/module/applications/sheets/npc-sheet.mjs +++ b/module/applications/sheets/npc-sheet.mjs @@ -107,11 +107,13 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { context.tab = context.tabs.magic context.spells = (doc.itemTypes.spell ?? []).map(s => ({ id: s.id, uuid: s.uuid, img: s.img, name: s.name, system: s.system, - _descTooltip: s.system.effect?.replace(/<[^>]+>/g, "").slice(0, 300) ?? "" + _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 => ({ 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 case "equipment": diff --git a/module/applications/sheets/party-sheet.mjs b/module/applications/sheets/party-sheet.mjs new file mode 100644 index 0000000..32fea02 --- /dev/null +++ b/module/applications/sheets/party-sheet.mjs @@ -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 }) + } +} diff --git a/module/applications/sheets/regiment-sheet.mjs b/module/applications/sheets/regiment-sheet.mjs index 3dbb63d..d0a0d0b 100644 --- a/module/applications/sheets/regiment-sheet.mjs +++ b/module/applications/sheets/regiment-sheet.mjs @@ -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" -export default class OathHammerRegimentSheet extends OathHammerItemSheet { +export default class OathHammerRegimentSheet extends OathHammerActorSheet { /** @override */ static DEFAULT_OPTIONS = { classes: ["regiment"], - position: { width: 560, height: "auto" }, + position: { width: 680, height: 620 }, window: { contentClasses: ["regiment-content"] }, actions: { - addSkill: OathHammerRegimentSheet.#onAddSkill, - removeSkill: OathHammerRegimentSheet.#onRemoveSkill, - addAttack: OathHammerRegimentSheet.#onAddAttack, - removeAttack:OathHammerRegimentSheet.#onRemoveAttack, - addTrait: OathHammerRegimentSheet.#onAddTrait, - removeTrait: OathHammerRegimentSheet.#onRemoveTrait, + adjustGrit: OathHammerRegimentSheet.#onAdjustGrit, + rollArmor: OathHammerRegimentSheet.#onRollArmor, + rollSkillNPC: OathHammerRegimentSheet.#onRollSkillNPC, + createNpcAttack: OathHammerRegimentSheet.#onCreateNpcAttack, + rollNpcAttack: OathHammerRegimentSheet.#onRollNpcAttack, + createSkill: OathHammerRegimentSheet.#onCreateSkill, + createTrait: OathHammerRegimentSheet.#onCreateTrait, + openLeader: OathHammerRegimentSheet.#onOpenLeader, + clearLeader: OathHammerRegimentSheet.#onClearLeader, }, } /** @override */ 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 */ async _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( Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) ) - context.dicePoolChoices = Object.fromEntries( - Array.from({ length: 21 }, (_, i) => [i, String(i)]) - ) - context.apChoices = Object.fromEntries( - Array.from({ length: 7 }, (_, i) => [i, String(i)]) - ) + // Resolve leader actor + const leaderUuid = this.document.system.leaderUuid + if (leaderUuid) { + const leader = await fromUuid(leaderUuid) + context.leader = leader ? { id: leader.id, uuid: leader.uuid, name: leader.name, img: leader.img } : null + } else { + context.leader = null + } return context } - // ── Array helpers ──────────────────────────────────────────────────────────── - - static async #onAddSkill() { - const skills = foundry.utils.deepClone(this.document.system.skills ?? []) - skills.push({ name: "", value: 2, colorDiceType: "white" }) - await this.document.update({ "system.skills": skills }) + /** @override */ + async _preparePartContext(partId, context) { + const doc = this.document + switch (partId) { + case "main": + 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) { - const idx = parseInt(target.dataset.idx, 10) - const skills = foundry.utils.deepClone(this.document.system.skills ?? []) - skills.splice(idx, 1) - await this.document.update({ "system.skills": skills }) + /** @override */ + async _onDrop(event) { + if (!this.isEditable || !this.isEditMode) return + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event) + + // 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 }) + } + + if (data.type !== "Item") return + const item = await fromUuid(data.uuid) + if (!item) return + const ALLOWED = new Set(["skillnpc", "npcattack", "trait"]) + if (!ALLOWED.has(item.type)) return + return this._onDropItem(item) } - static async #onAddAttack() { - const attacks = foundry.utils.deepClone(this.document.system.attacks ?? []) - attacks.push({ name: "", damageDice: 6, colorDiceType: "white", ap: 0, special: "" }) - await this.document.update({ "system.attacks": attacks }) + // ── Actions ──────────────────────────────────────────────────────────────── + + static async #onAdjustGrit(event, target) { + const delta = parseInt(target.dataset.delta, 10) + const current = this.document.system.grit?.value ?? 0 + const max = this.document.system.grit?.max ?? current + await this.document.update({ "system.grit.value": Math.max(0, Math.min(max, current + delta)) }) } - static async #onRemoveAttack(event, target) { - const idx = parseInt(target.dataset.idx, 10) - const attacks = foundry.utils.deepClone(this.document.system.attacks ?? []) - attacks.splice(idx, 1) - await this.document.update({ "system.attacks": attacks }) + static async #onRollArmor() { + const doc = this.document + const armorDice = doc.system.armorDice + 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 #onAddTrait() { - const traits = foundry.utils.deepClone(this.document.system.traits ?? []) - traits.push({ name: "", description: "" }) - await this.document.update({ "system.traits": traits }) + static async #onRollSkillNPC(event, target) { + const skill = this.document.items.get(target.dataset.itemId) + if (!skill) 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: 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 async #onRemoveTrait(event, target) { - const idx = parseInt(target.dataset.idx, 10) - const traits = foundry.utils.deepClone(this.document.system.traits ?? []) - traits.splice(idx, 1) - await this.document.update({ "system.traits": traits }) + 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 }) } } diff --git a/module/applications/sheets/settlement-sheet.mjs b/module/applications/sheets/settlement-sheet.mjs index 9763a35..72a6985 100644 --- a/module/applications/sheets/settlement-sheet.mjs +++ b/module/applications/sheets/settlement-sheet.mjs @@ -1,6 +1,6 @@ import OathHammerActorSheet from "./base-actor-sheet.mjs" -const ALLOWED_ITEM_TYPES = new Set(["building", "equipment", "weapon", "armor", "regiment"]) +const ALLOWED_ITEM_TYPES = new Set(["building", "equipment", "weapon", "armor"]) export default class OathHammerSettlementSheet extends OathHammerActorSheet { /** @override */ @@ -17,8 +17,9 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet { adjustCurrency: OathHammerSettlementSheet.#onAdjustCurrency, adjustQty: OathHammerSettlementSheet.#onAdjustQty, toggleConstructed: OathHammerSettlementSheet.#onToggleConstructed, - createRegiment: OathHammerSettlementSheet.#onCreateRegiment, collectTaxes: OathHammerSettlementSheet.#onCollectTaxes, + openRegiment: OathHammerSettlementSheet.#onOpenRegiment, + removeRegiment: OathHammerSettlementSheet.#onRemoveRegiment, }, } @@ -102,7 +103,9 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet { } case "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 } return context @@ -112,6 +115,17 @@ export default class OathHammerSettlementSheet extends OathHammerActorSheet { async _onDrop(event) { if (!this.isEditable || !this.isEditMode) return 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 const item = await fromUuid(data.uuid) 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 }) } - static async #onCreateRegiment() { - await this.document.createEmbeddedDocuments("Item", [{ - name: game.i18n.localize("OATHHAMMER.NewItem.Regiment"), - type: "regiment", - }]) + 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 actorId = target.dataset.actorId + const refs = (this.document.system.garrisonRefs ?? []).filter(id => id !== actorId) + await this.document.update({ "system.garrisonRefs": refs }) } static async #onCollectTaxes() { diff --git a/module/models/_module.mjs b/module/models/_module.mjs index 734ac17..a2a3b33 100644 --- a/module/models/_module.mjs +++ b/module/models/_module.mjs @@ -15,3 +15,5 @@ export { default as OathHammerSettlement } from "./settlement.mjs" export { default as OathHammerSkillNPC } from "./skillnpc.mjs" export { default as OathHammerNpcAttack } from "./npcattack.mjs" export { default as OathHammerRegiment } from "./regiment.mjs" +export { default as OathHammerParty } from "./party.mjs" +export { default as OathHammerArmy } from "./army.mjs" diff --git a/module/models/army.mjs b/module/models/army.mjs new file mode 100644 index 0000000..bdbc40f --- /dev/null +++ b/module/models/army.mjs @@ -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 + } +} diff --git a/module/models/party.mjs b/module/models/party.mjs new file mode 100644 index 0000000..0a686fd --- /dev/null +++ b/module/models/party.mjs @@ -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"] +} diff --git a/module/models/regiment.mjs b/module/models/regiment.mjs index 0ea998e..af707f5 100644 --- a/module/models/regiment.mjs +++ b/module/models/regiment.mjs @@ -7,46 +7,33 @@ export default class OathHammerRegiment extends foundry.abstract.TypeDataModel { const schema = {} 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({ - 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({ - value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0, max: 20 }), + value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0, max: 20 }), colorDiceType: new fields.StringField({ required: true, nullable: false, initial: "white", choices: SYSTEM.DICE_COLOR_TYPES }), }) - schema.movement = new fields.NumberField({ ...requiredInteger, initial: 60, min: 0, max: 500 }) + 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.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: "" }), - })) + schema.leaderUuid = new fields.StringField({ required: false, nullable: true, initial: null }) return schema } static LOCALIZATION_PREFIXES = ["OATHHAMMER.Regiment"] + get threshold() { + return { white: 4, red: 3, black: 2 }[this.armorDice.colorDiceType] ?? 4 + } + get colorEmoji() { return { white: "⬜", red: "🔴", black: "⬛" }[this.armorDice.colorDiceType] ?? "⬜" } diff --git a/module/models/settlement.mjs b/module/models/settlement.mjs index d7c65dc..0792602 100644 --- a/module/models/settlement.mjs +++ b/module/models/settlement.mjs @@ -33,6 +33,11 @@ export default class OathHammerSettlement extends foundry.abstract.TypeDataModel schema.isCapital = new fields.BooleanField({ required: true, initial: false }) 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 } diff --git a/oath-hammer.mjs b/oath-hammer.mjs index 56adf03..6a3e948 100644 --- a/oath-hammer.mjs +++ b/oath-hammer.mjs @@ -23,8 +23,11 @@ Hooks.once("init", function () { CONFIG.Combat.documentClass = OathHammerCombat CONFIG.Actor.dataModels = { character: models.OathHammerCharacter, - npc: models.OathHammerNPC, - settlement: models.OathHammerSettlement + npc: models.OathHammerNPC, + settlement: models.OathHammerSettlement, + regiment: models.OathHammerRegiment, + party: models.OathHammerParty, + army: models.OathHammerArmy, } CONFIG.Item.documentClass = documents.OathHammerItem @@ -42,7 +45,6 @@ Hooks.once("init", function () { building: models.OathHammerBuilding, skillnpc: models.OathHammerSkillNPC, npcattack: models.OathHammerNpcAttack, - regiment: models.OathHammerRegiment, } foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet) @@ -61,6 +63,21 @@ Hooks.once("init", function () { makeDefault: true, 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.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.OathHammerSkillNPCSheet, { types: ["skillnpc"], makeDefault: true, label: "OATHHAMMER.Sheet.SkillNPC" }) foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerNpcAttackSheet, { types: ["npcattack"], makeDefault: true, label: "OATHHAMMER.Sheet.NpcAttack" }) - foundry.documents.collections.Items.registerSheet("fvtt-oath-hammer", applications.OathHammerRegimentSheet, { types: ["regiment"], makeDefault: true, label: "OATHHAMMER.Sheet.Regiment" }) CONFIG.statusEffects = STATUS_EFFECTS diff --git a/system.json b/system.json index aec6905..69d64b2 100644 --- a/system.json +++ b/system.json @@ -42,6 +42,22 @@ "description", "notes" ] + }, + "regiment": { + "htmlFields": [ + "description", + "notes" + ] + }, + "party": { + "htmlFields": [ + "notes" + ] + }, + "army": { + "htmlFields": [ + "notes" + ] } }, "Item": { @@ -107,8 +123,7 @@ ] }, "skillnpc": {}, - "npcattack": {}, - "regiment": {} + "npcattack": {} } }, "grid": { diff --git a/templates/actor/army-notes.hbs b/templates/actor/army-notes.hbs new file mode 100644 index 0000000..e6d0938 --- /dev/null +++ b/templates/actor/army-notes.hbs @@ -0,0 +1,6 @@ +
+
+ + {{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}} +
+
diff --git a/templates/actor/army-overview.hbs b/templates/actor/army-overview.hbs new file mode 100644 index 0000000..220082d --- /dev/null +++ b/templates/actor/army-overview.hbs @@ -0,0 +1,55 @@ +
+ +
+ {{localize "OATHHAMMER.Label.Regiments"}} + + {{#if regiments.length}} + + {{else}} +

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

+ {{/if}} +
+ + {{#unless isPlayMode}} +

{{localize "OATHHAMMER.Label.DropRegimentHint"}}

+ {{/unless}} + +
diff --git a/templates/actor/army-sheet.hbs b/templates/actor/army-sheet.hbs new file mode 100644 index 0000000..16c4742 --- /dev/null +++ b/templates/actor/army-sheet.hbs @@ -0,0 +1,45 @@ +
+
+
+ + +
+ +
+ + +
+ +
+ {{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}} + + + +
+ + +
+ {{localize "OATHHAMMER.Label.Commander"}} + {{#if leader}} + + {{leader.name}} + {{#unless isPlayMode}} + + + + {{/unless}} + {{else}} + {{localize "OATHHAMMER.Label.DropLeaderHint"}} + {{/if}} +
+ + +
+ {{localize "OATHHAMMER.Label.Location"}} + {{formInput systemFields.location value=system.location name="system.location" placeholder="—" disabled=isPlayMode}} +
+ +
+
+
+
diff --git a/templates/actor/character-magic.hbs b/templates/actor/character-magic.hbs index a067807..02fafbb 100644 --- a/templates/actor/character-magic.hbs +++ b/templates/actor/character-magic.hbs @@ -35,7 +35,7 @@ {{spell.name}} {{spell.system.difficultyValue}} - {{localize spell.system.tradition}} + {{spell.traditionLabel}} {{#if spell.system.range}}{{spell.system.range}}{{else}}—{{/if}} {{#if spell.system.duration}}{{spell.system.duration}}{{else}}—{{/if}} {{#if spell.system.spellSave}}{{spell.system.spellSave}}{{else}}—{{/if}} @@ -74,7 +74,7 @@
  • {{miracle.name}} - {{miracle.system.divineTradition}} + {{miracle.traditionLabel}}
    diff --git a/templates/actor/npc-magic.hbs b/templates/actor/npc-magic.hbs index 682bb14..4da5164 100644 --- a/templates/actor/npc-magic.hbs +++ b/templates/actor/npc-magic.hbs @@ -13,6 +13,7 @@ {{localize "OATHHAMMER.Label.Tradition"}} {{localize "OATHHAMMER.Label.Range"}} {{localize "OATHHAMMER.Label.Duration"}} + {{localize "OATHHAMMER.Label.SpellSave"}}
  • {{#each spells as |spell|}} @@ -20,11 +21,12 @@ {{spell.name}} {{spell.system.difficultyValue}} - {{localize spell.system.tradition}} + {{spell.traditionLabel}} {{#if spell.system.range}}{{spell.system.range}}{{else}}—{{/if}} {{#if spell.system.duration}}{{spell.system.duration}}{{else}}—{{/if}} + {{#if spell.system.spellSave}}{{spell.system.spellSave}}{{else}}—{{/if}}
    - +
    @@ -52,9 +54,9 @@
  • {{miracle.name}} - {{miracle.system.divineTradition}} + {{miracle.traditionLabel}}
    - +
    diff --git a/templates/actor/npc-skills.hbs b/templates/actor/npc-skills.hbs index d082a83..7941546 100644 --- a/templates/actor/npc-skills.hbs +++ b/templates/actor/npc-skills.hbs @@ -23,7 +23,7 @@ {{skill.system.threshold}} + data-tooltip="{{localize 'OATHHAMMER.Dialog.RollSkill'}}">
    diff --git a/templates/actor/party-loot.hbs b/templates/actor/party-loot.hbs new file mode 100644 index 0000000..124c54b --- /dev/null +++ b/templates/actor/party-loot.hbs @@ -0,0 +1,39 @@ +
    + +
    + {{localize "OATHHAMMER.Label.Loot"}} + + {{#if lootItems.length}} +
      +
    • + + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.Type"}} + {{localize "OATHHAMMER.Label.Qty"}} + +
    • + {{#each lootItems as |item|}} +
    • + + {{item.name}} + {{item.typeLabel}} +
      + + {{#if item.system.quantity}}{{item.system.quantity}}{{else}}1{{/if}} + + +
      +
      + + +
      +
    • + {{/each}} +
    + {{else}} +

    {{localize "OATHHAMMER.Label.NoLoot"}}

    + {{/if}} +
    + +

    {{localize "OATHHAMMER.Label.DropLootHint"}}

    + +
    diff --git a/templates/actor/party-members.hbs b/templates/actor/party-members.hbs new file mode 100644 index 0000000..9aaa495 --- /dev/null +++ b/templates/actor/party-members.hbs @@ -0,0 +1,46 @@ +
    + +
    + {{localize "OATHHAMMER.Label.MarchingOrder"}} + + {{#if members.length}} +
      +
    • + # + + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.Class"}} + {{localize "OATHHAMMER.Label.Level"}} + {{localize "OATHHAMMER.Label.Grit"}} + +
    • + {{#each members as |member|}} +
    • + {{member.position}} + + + {{member.name}} + + {{member.classLabel}} + {{member.level}} + {{member.grit}} +
      + {{#unless member.isFirst}} + + {{/unless}} + {{#unless member.isLast}} + + {{/unless}} + +
      +
    • + {{/each}} +
    + {{else}} +

    {{localize "OATHHAMMER.Label.NoMembers"}}

    + {{/if}} +
    + +

    {{localize "OATHHAMMER.Label.DropMemberHint"}}

    + +
    diff --git a/templates/actor/party-notes.hbs b/templates/actor/party-notes.hbs new file mode 100644 index 0000000..e6d0938 --- /dev/null +++ b/templates/actor/party-notes.hbs @@ -0,0 +1,6 @@ +
    +
    + + {{formInput systemFields.notes enriched=enrichedNotes value=system.notes name="system.notes" toggled=true}} +
    +
    diff --git a/templates/actor/party-sheet.hbs b/templates/actor/party-sheet.hbs new file mode 100644 index 0000000..88c7906 --- /dev/null +++ b/templates/actor/party-sheet.hbs @@ -0,0 +1,54 @@ +
    +
    +
    + + +
    + +
    + + +
    +
    + {{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}} + + + +
    + + +
    + {{localize "OATHHAMMER.Label.Treasury"}} + +
    + {{localize "OATHHAMMER.Label.GP"}} +
    + + + + +
    +
    + +
    + {{localize "OATHHAMMER.Label.SP"}} +
    + + + + +
    +
    + +
    + {{localize "OATHHAMMER.Label.CP"}} +
    + + + + +
    +
    + +
    +
    +
    +
    +
    diff --git a/templates/actor/regiment-combat.hbs b/templates/actor/regiment-combat.hbs new file mode 100644 index 0000000..98fdc09 --- /dev/null +++ b/templates/actor/regiment-combat.hbs @@ -0,0 +1,33 @@ +
    +
    + {{localize "OATHHAMMER.Label.Attacks"}} + {{#unless isPlayMode}}{{/unless}} + + {{#if npcAttacks.length}} +
      +
    • + + {{localize "OATHHAMMER.Label.Name"}} + {{localize "OATHHAMMER.Label.Damage"}} + AP + +
    • + {{#each npcAttacks as |attack|}} +
    • + + {{attack.name}} + {{attack.system.damageLabel}} + {{#if attack.system.ap}}{{attack.system.ap}}{{else}}—{{/if}} +
      + + + +
      +
    • + {{/each}} +
    + {{else}} +

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

    + {{/if}} +
    +
    diff --git a/templates/actor/regiment-sheet.hbs b/templates/actor/regiment-sheet.hbs new file mode 100644 index 0000000..f62980f --- /dev/null +++ b/templates/actor/regiment-sheet.hbs @@ -0,0 +1,107 @@ +
    +
    + +
    + + +
    + +
    + + +
    +
    + {{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}} + + + +
    + + +
    + + +
    + {{localize "OATHHAMMER.Label.Grit"}} + + + + / + {{formInput systemFields.grit.fields.max value=system.grit.max name="system.grit.max" disabled=isPlayMode}} + + + +
    + + +
    + + {{#if isPlayMode}}{{/if}} + {{localize "OATHHAMMER.Label.ArmorDice"}} + + + + {{#if isPlayMode}} + {{armorDiceEmoji}} + {{else}} + + {{/if}} + +
    + + +
    + {{localize "OATHHAMMER.Label.Movement"}} + + + ft + +
    + +
    + + +
    + + +
    + {{localize "OATHHAMMER.Label.SupplyCost"}} + + + gp / month + +
    + + +
    + {{localize "OATHHAMMER.Label.RecruitmentCost"}} + + + gp + +
    + +
    + + +
    + {{localize "OATHHAMMER.Label.UnitLeader"}} + {{#if leader}} + + {{leader.name}} + {{#unless isPlayMode}} + + + + {{/unless}} + {{else}} + {{localize "OATHHAMMER.Label.DropLeaderHint"}} + {{/if}} +
    + +
    +
    +
    +
    diff --git a/templates/actor/settlement-garrison.hbs b/templates/actor/settlement-garrison.hbs index 99164cd..7643c3b 100644 --- a/templates/actor/settlement-garrison.hbs +++ b/templates/actor/settlement-garrison.hbs @@ -2,7 +2,6 @@
    {{localize "OATHHAMMER.Label.Garrison"}} - {{#unless isPlayMode}}{{/unless}} {{#if regiments.length}}
      @@ -15,15 +14,15 @@ {{#each regiments as |regiment|}} -
    • +
    • - {{regiment.name}} + {{regiment.name}} {{regiment.system.grit.max}} {{regiment.system.armorLabel}} {{regiment.system.movement}} ft
      - - + +
    • {{/each}} @@ -33,4 +32,6 @@ {{/if}}
    +

    {{localize "OATHHAMMER.Settlement.GarrisonHint"}}

    + diff --git a/templates/item/regiment-sheet.hbs b/templates/item/regiment-sheet.hbs deleted file mode 100644 index c513472..0000000 --- a/templates/item/regiment-sheet.hbs +++ /dev/null @@ -1,122 +0,0 @@ -
    - -
    - - {{formInput fields.name value=source.name}} -
    - -
    - {{localize "OATHHAMMER.Label.Stats"}} -
    -
    - -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - ft -
    -
    -
    -
    - -
    - - {{localize "OATHHAMMER.Tab.Skills"}} - - - {{#if system.skills.length}} -
    - {{localize "OATHHAMMER.Label.Name"}} - {{localize "OATHHAMMER.Label.Rank"}} - {{localize "OATHHAMMER.Label.DiceColor"}} - -
    - {{#each system.skills as |skill idx|}} -
    - - - - -
    - {{/each}} - {{else}} -

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

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

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

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

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

    - {{/if}} -
    - -
    - {{localize "OATHHAMMER.Label.Description"}} - - {{{system.description}}} - -
    - -