feat: add Settlement actor type with Overview/Buildings/Inventory tabs

- New TypeDataModel: archetype, territory, renown, currency (gp/sp/cp),
  garrison, underSiege, isCapital, founded, taxNotes, description, notes
- 3-tab ApplicationV2 sheet with drag & drop for building/weapon/armor/equipment
- Currency steppers (+/−), building constructed toggle, qty controls
- LESS-based CSS (settlement-sheet.less) + base.less updated for shared styles
- Full i18n keys in lang/en.json (8 settlement archetypes)
- system.json: registered settlement actor type

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-20 17:01:38 +01:00
parent b67d85c6be
commit b3fd7e1aa1
28 changed files with 966 additions and 270 deletions

View File

@@ -27,7 +27,8 @@
background-size: 100% 100%; background-size: 100% 100%;
} }
.oathhammer .character-content, .oathhammer .character-content,
.oathhammer .npc-content { .oathhammer .npc-content,
.oathhammer .settlement-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;
@@ -38,32 +39,39 @@
padding: 10px 20px; padding: 10px 20px;
} }
.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] {
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 {
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 .character-content select:disabled, .oathhammer .character-content select:disabled,
.oathhammer .npc-content select:disabled { .oathhammer .npc-content select:disabled,
.oathhammer .settlement-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;
} }
.oathhammer .character-content input, .oathhammer .character-content input,
.oathhammer .npc-content input, .oathhammer .npc-content input,
.oathhammer .settlement-content input,
.oathhammer .character-content select, .oathhammer .character-content select,
.oathhammer .npc-content select { .oathhammer .npc-content select,
.oathhammer .settlement-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;
color: #2a1a0a; color: #2a1a0a;
} }
.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"] {
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);
@@ -73,13 +81,15 @@
background: transparent; background: transparent;
} }
.oathhammer .character-content fieldset, .oathhammer .character-content fieldset,
.oathhammer .npc-content fieldset { .oathhammer .npc-content fieldset,
.oathhammer .settlement-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 {
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;
@@ -87,7 +97,8 @@
color: #084a74; color: #084a74;
} }
.oathhammer .character-content label, .oathhammer .character-content label,
.oathhammer .npc-content label { .oathhammer .npc-content label,
.oathhammer .settlement-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;
@@ -229,11 +240,6 @@
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
font-weight: bold; font-weight: bold;
} }
.oathhammer .skills-container .skill-row a.skill-name-col {
display: inline-flex;
align-items: center;
gap: 5px;
}
.oathhammer .skills-container .skill-row .skill-rank-col select, .oathhammer .skills-container .skill-row .skill-rank-col select,
.oathhammer .skills-container .skill-row .skill-modifier-col input { .oathhammer .skills-container .skill-row .skill-modifier-col input {
width: 100%; width: 100%;
@@ -438,19 +444,6 @@
font-size: 0.86rem; font-size: 0.86rem;
font-weight: bold; font-weight: bold;
} }
.oathhammer .character-main .character-identity-bar .identity-slot a.class-open-link {
flex: 1;
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: 0.86rem;
font-weight: bold;
opacity: 1;
text-decoration: none;
cursor: pointer;
}
.oathhammer .character-main .character-identity-bar .identity-slot a.class-open-link:hover {
text-decoration: underline;
opacity: 1;
}
.oathhammer .character-main .character-identity-bar .identity-slot .slot-icon { .oathhammer .character-main .character-identity-bar .identity-slot .slot-icon {
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
opacity: 0.8; opacity: 0.8;
@@ -575,51 +568,6 @@
width: 4rem; width: 4rem;
text-align: center; text-align: center;
} }
.oathhammer .currency-stepper {
display: flex;
align-items: center;
gap: 2px;
}
.oathhammer .currency-stepper input {
width: 3.5rem;
text-align: center;
}
/* Shared +/- button style */
.oathhammer .qty-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
font-size: 0.9rem;
font-weight: bold;
line-height: 1;
color: #2a1a0a;
background: rgba(200, 168, 75, 0.2);
border: 1px solid rgba(200, 168, 75, 0.5);
border-radius: 3px;
cursor: pointer;
user-select: none;
flex-shrink: 0;
text-decoration: none;
}
.oathhammer .qty-btn:hover {
background: rgba(200, 168, 75, 0.45);
color: #2a1a0a;
}
/* Quantity stepper in item rows */
.oathhammer .item-qty-stepper {
display: flex;
align-items: center;
gap: 3px;
justify-content: center;
}
.oathhammer .item-qty-stepper .qty-value {
min-width: 1.6rem;
text-align: center;
font-size: calc(0.86rem * 0.9);
font-weight: bold;
}
.oathhammer .identity-lineage-class { .oathhammer .identity-lineage-class {
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
@@ -734,9 +682,6 @@
.oathhammer .item-list-header .col-name { .oathhammer .item-list-header .col-name {
text-align: left; text-align: left;
} }
.oathhammer .item-list-header .col-oath-effect {
text-align: left;
}
.oathhammer .item-entry { .oathhammer .item-entry {
display: grid; display: grid;
align-items: center; align-items: center;
@@ -833,7 +778,7 @@
} }
.oathhammer .item-list--ammo .item-list-header, .oathhammer .item-list--ammo .item-list-header,
.oathhammer .item-list--ammo .item-entry { .oathhammer .item-list--ammo .item-entry {
grid-template-columns: 24px 1fr 5.5rem 3.5rem; grid-template-columns: 24px 1fr 4rem 3.5rem;
} }
.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 {
@@ -849,7 +794,7 @@
} }
.oathhammer .item-list--equipment .item-list-header, .oathhammer .item-list--equipment .item-list-header,
.oathhammer .item-list--equipment .item-entry { .oathhammer .item-list--equipment .item-entry {
grid-template-columns: 24px 1fr 5rem 5.5rem 3.5rem; grid-template-columns: 24px 1fr 5rem 3rem 3.5rem;
} }
.oathhammer .item-list--magic-item .item-list-header, .oathhammer .item-list--magic-item .item-list-header,
.oathhammer .item-list--magic-item .item-entry { .oathhammer .item-list--magic-item .item-entry {
@@ -865,21 +810,7 @@
} }
.oathhammer .item-list--oath .item-list-header, .oathhammer .item-list--oath .item-list-header,
.oathhammer .item-list--oath .item-entry { .oathhammer .item-list--oath .item-entry {
grid-template-columns: 24px 1fr 10rem 3.5rem 3.5rem; grid-template-columns: 24px 1fr 7rem 3.5rem 3.5rem;
}
.oathhammer .item-oath-effect {
font-size: calc(0.86rem * 0.88);
color: #2a1a0a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.oathhammer .item-oath-effect.oath-boon {
color: #1a5c2a;
}
.oathhammer .item-oath-effect.oath-bane {
color: #c0392b;
font-style: italic;
} }
.oathhammer .item-usage { .oathhammer .item-usage {
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
@@ -978,44 +909,6 @@
background: rgba(192, 57, 43, 0.1); background: rgba(192, 57, 43, 0.1);
border-color: rgba(192, 57, 43, 0.4); border-color: rgba(192, 57, 43, 0.4);
} }
/* Initiative bar */
.initiative-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 6px 6px;
}
.initiative-roll-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
font-size: calc(0.86rem * 0.9);
font-weight: bold;
color: #2a1a0a;
background: rgba(200, 168, 75, 0.2);
border: 1px solid rgba(200, 168, 75, 0.5);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.04em;
transition: background 0.15s;
}
.initiative-roll-btn:hover {
background: rgba(200, 168, 75, 0.4);
color: #2a1a0a;
}
.initiative-score {
font-size: calc(0.86rem * 0.95);
font-weight: bold;
color: #2a1a0a;
background: rgba(200, 168, 75, 0.15);
border: 1px solid rgba(200, 168, 75, 0.4);
border-radius: 4px;
padding: 2px 10px;
}
.oathhammer .item-sheet-common { .oathhammer .item-sheet-common {
overflow: auto; overflow: auto;
padding: 10px 20px; padding: 10px 20px;
@@ -1237,14 +1130,6 @@
.fvtt-oath-hammer .window-content { .fvtt-oath-hammer .window-content {
background: #f5ead0; background: #f5ead0;
padding: 6px 8px; padding: 6px 8px;
/* iOS Safari flex-overflow fix: prevent flex child collapse */
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* iOS Safari: ensure dialog content is not hidden due to flex-height collapse */
.fvtt-oath-hammer.dialog .window-content {
min-height: 80px;
height: auto;
} }
.fvtt-oath-hammer .oh-roll-dialog { .fvtt-oath-hammer .oh-roll-dialog {
font-family: "Calibri", "Segoe UI", sans-serif; font-family: "Calibri", "Segoe UI", sans-serif;
@@ -1413,27 +1298,6 @@
cursor: pointer; cursor: pointer;
flex: 1 1 auto; flex: 1 1 auto;
} }
/* Range conditions stacked checkbox block */
.fvtt-oath-hammer .oh-roll-dialog .roll-option-block {
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px 0;
border-top: 1px solid rgba(83, 81, 40, 0.15);
}
.fvtt-oath-hammer .oh-roll-dialog .roll-option-block-label {
font-size: calc(0.86rem * 0.85);
font-weight: bold;
color: #5a3e1b;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.fvtt-oath-hammer .oh-roll-dialog .range-conditions {
display: flex;
flex-direction: column;
gap: 2px;
padding-left: 4px;
}
.fvtt-oath-hammer .oh-roll-dialog .roll-visibility-block select { .fvtt-oath-hammer .oh-roll-dialog .roll-visibility-block select {
width: 100%; width: 100%;
padding: 4px 6px; padding: 4px 6px;
@@ -1886,98 +1750,150 @@
.item-list--armor .item-actions a[data-action="edit"] { .item-list--armor .item-actions a[data-action="edit"] {
margin-left: 6px; margin-left: 6px;
} }
.oathhammer .settlement-main {
/* ============================================================ padding: 0 0 8px;
FREE ROLL BAR — Chat sidebar widget border-bottom: 1px solid rgba(42, 26, 10, 0.25);
============================================================ */ margin-bottom: 4px;
.oh-free-roll-bar {
pointer-events: all;
display: flex;
align-items: center;
gap: 6px;
padding: 5px 8px;
background: linear-gradient(135deg, #2a1a0a 0%, #3d2812 100%);
border-top: 1px solid rgba(255, 200, 80, 0.25);
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
flex-wrap: wrap;
} }
.oh-free-roll-bar .oh-frb-label { .oathhammer .settlement-header {
color: #f5d78e; align-items: flex-start;
font-family: "BlueDragon", "Palatino Linotype", serif; gap: 10px;
font-size: calc(0.86rem * 0.9); }
.oathhammer .settlement-header .actor-img {
width: 72px;
height: 72px;
-o-object-fit: cover;
object-fit: cover;
border: 2px solid rgba(42, 26, 10, 0.4);
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
}
.oathhammer .settlement-header-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.oathhammer .settlement-name-row {
align-items: center;
gap: 8px;
}
.oathhammer .settlement-archetype-badge {
font-size: calc(0.86rem * 0.85);
color: #cd9d2c;
background: rgba(200, 168, 75, 0.15);
border: 1px solid rgba(200, 168, 75, 0.4);
border-radius: 3px;
padding: 1px 6px;
white-space: nowrap; white-space: nowrap;
}
.oathhammer .settlement-stats {
gap: 8px;
flex-wrap: wrap;
align-items: flex-start;
}
.oathhammer .settlement-stats .stat-item {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 80px;
}
.oathhammer .settlement-stats .stat-item label {
font-size: 0.72rem;
color: #2a1a0a;
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.oathhammer .settlement-stats .stat-item input {
padding: 2px 4px;
}
.oathhammer .settlement-stats .stat-item--flags {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
flex-wrap: wrap;
min-width: unset;
}
.oathhammer .settlement-stats .stat-item--flags label {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
opacity: 0.9;
}
.oh-free-roll-bar .oh-frb-controls {
display: flex;
align-items: center;
gap: 5px;
flex: 1;
flex-wrap: wrap;
}
.oh-free-roll-bar .oh-frb-pool {
height: 1.6rem;
font-size: 0.8rem;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 200, 80, 0.3);
border-radius: 3px;
color: #f5ead0;
cursor: pointer;
padding: 0 3px;
}
.oh-free-roll-bar .oh-frb-pool option {
background: #2a1a0a;
color: #f5ead0;
}
.oh-free-roll-bar .oh-frb-color {
height: 1.6rem;
font-size: 0.8rem;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 200, 80, 0.3);
border-radius: 3px;
color: #f5ead0;
cursor: pointer;
padding: 0 3px;
}
.oh-free-roll-bar .oh-frb-color option {
background: #2a1a0a;
color: #f5ead0;
}
.oh-free-roll-bar .oh-frb-explode-label {
display: flex;
align-items: center;
gap: 3px;
color: #f5d78e;
font-size: 0.78rem;
cursor: pointer;
white-space: nowrap;
opacity: 0.85;
}
.oh-free-roll-bar .oh-frb-explode-label input[type="checkbox"] {
cursor: pointer;
accent-color: #c9a227;
}
.oh-free-roll-bar .oh-frb-roll-btn {
margin-left: auto;
height: 1.7rem;
padding: 0 10px;
background: linear-gradient(135deg, #c9a227 0%, #8a6a10 100%);
border: 1px solid rgba(255, 200, 80, 0.4);
border-radius: 4px;
color: #1a0e00;
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.85);
font-weight: bold; text-transform: none;
letter-spacing: 0;
cursor: pointer; cursor: pointer;
white-space: nowrap;
transition: filter 0.15s;
} }
.oh-free-roll-bar .oh-frb-roll-btn:hover { .oathhammer .settlement-badge {
filter: brightness(1.15); font-size: 0.75rem;
font-weight: bold;
border-radius: 3px;
padding: 2px 7px;
} }
.oh-free-roll-bar .oh-frb-roll-btn:active { .oathhammer .settlement-badge--capital {
filter: brightness(0.9); background: rgba(200, 168, 75, 0.2);
border: 1px solid rgba(200, 168, 75, 0.5);
color: #5d4d1d;
}
.oathhammer .settlement-badge--siege {
background: rgba(180, 40, 40, 0.12);
border: 1px solid rgba(180, 40, 40, 0.4);
color: #8a1a1a;
}
.oathhammer .settlement-content .tab {
padding: 8px 0;
}
.oathhammer .settlement-overview-grid {
display: grid;
grid-template-columns: 1fr 1fr 2fr;
gap: 8px;
margin-bottom: 8px;
}
.oathhammer .settlement-hint {
font-size: 0.78rem;
color: #2a1a0a;
opacity: 0.7;
margin: 0 0 8px;
font-style: italic;
}
.oathhammer .item-list--buildings .item-list-header,
.oathhammer .item-list--buildings .item-entry {
display: grid;
grid-template-columns: 28px 1fr 3rem 5rem 4rem 3rem;
align-items: center;
gap: 4px;
}
.oathhammer .item-entry.building-constructed {
background: rgba(60, 140, 60, 0.08);
}
.oathhammer .item-entry.building-constructed .item-name {
font-weight: 600;
}
.oathhammer .item-constructed {
text-align: center;
font-size: 1rem;
color: #3a7a3a;
}
.oathhammer .item-tax {
font-size: calc(0.86rem * 0.85);
color: #312f17;
text-align: center;
}
.oathhammer .item-tax--inactive {
opacity: 0.4;
}
.oathhammer .item-cost {
font-size: calc(0.86rem * 0.85);
color: #856d28;
text-align: right;
}
.oathhammer .construct-toggle {
color: #3a7a3a;
cursor: pointer;
font-size: 1rem;
}
.oathhammer .construct-toggle:hover {
color: #214621;
} }

View File

@@ -14,7 +14,8 @@
"Oath": "Oath Hammer Oath Sheet", "Oath": "Oath Hammer Oath Sheet",
"Condition": "Oath Hammer Condition Sheet", "Condition": "Oath Hammer Condition Sheet",
"Class": "Oath Hammer Class Sheet", "Class": "Oath Hammer Class Sheet",
"Building": "Oath Hammer Building Sheet" "Building": "Oath Hammer Building Sheet",
"Settlement": "Oath Hammer Settlement Sheet"
}, },
"Tab": { "Tab": {
"Identity": "Identity", "Identity": "Identity",
@@ -22,7 +23,10 @@
"Combat": "Combat", "Combat": "Combat",
"Magic": "Magic", "Magic": "Magic",
"Equipment": "Equipment", "Equipment": "Equipment",
"Notes": "Notes" "Notes": "Notes",
"Overview": "Overview",
"Buildings": "Buildings",
"Inventory": "Inventory"
}, },
"Attribute": { "Attribute": {
"Might": "Might", "Might": "Might",
@@ -208,6 +212,7 @@
"Movement": "Movement", "Movement": "Movement",
"ArcaneStress": "Arcane Stress", "ArcaneStress": "Arcane Stress",
"StressValue": "Stress", "StressValue": "Stress",
"ThresholdBonus": "Threshold Bonus",
"Attributes": "Attributes", "Attributes": "Attributes",
"Biodata": "Background", "Biodata": "Background",
"Background": "Background", "Background": "Background",
@@ -277,10 +282,25 @@
"Ritual": "Ritual", "Ritual": "Ritual",
"MagicMissile": "Magic Missile", "MagicMissile": "Magic Missile",
"SpellSave": "Save", "SpellSave": "Save",
"Range": "Range",
"Duration": "Duration",
"StressBlocked": "BLOCKED — over stress threshold!", "StressBlocked": "BLOCKED — over stress threshold!",
"ArcaneStressShort": "AS",
"InitiativeBonus": "Initiative Bonus", "InitiativeBonus": "Initiative Bonus",
"Initiative": "Initiative" "Initiative": "Initiative",
"Treasury": "Treasury",
"Renown": "Renown",
"Territory": "Territory",
"Population": "Population",
"Garrison": "Garrison",
"UnderSiege": "Under Siege",
"IsCapital": "Capital",
"Capital": "Capital",
"Founded": "Founded",
"TaxNotes": "Tax Notes",
"TaxRevenue": "Tax Revenue",
"Cost": "Cost",
"Built": "Built",
"Armors": "Armors & Shields"
}, },
"ColorDice": { "ColorDice": {
"White": "White (4+)", "White": "White (4+)",
@@ -301,7 +321,8 @@
"CastSpell": "Cast Spell", "CastSpell": "Cast Spell",
"InvokeMiracle": "Invoke Miracle", "InvokeMiracle": "Invoke Miracle",
"ResetMiracleBlocked": "Restore divine favour (new day)", "ResetMiracleBlocked": "Restore divine favour (new day)",
"NewDay": "New day" "NewDay": "New day",
"ClearStress": "Clear Stress (full rest)"
}, },
"Dialog": { "Dialog": {
"SkillCheckTitle": "Skill Check: {skill}", "SkillCheckTitle": "Skill Check: {skill}",
@@ -355,6 +376,9 @@
"Grimoire": "Grimoire", "Grimoire": "Grimoire",
"GrimoireHas": "Has Grimoire", "GrimoireHas": "Has Grimoire",
"GrimoireNo": "No Grimoire (2)", "GrimoireNo": "No Grimoire (2)",
"PoolSize": "Pool Size",
"PoolSizeHint": "Roll fewer dice to reduce Arcane Stress risk (p.101)",
"PoolSizeReduced": "reduced pool",
"MiracleCastTitle": "Invoke Miracle: {miracle}", "MiracleCastTitle": "Invoke Miracle: {miracle}",
"InvokeMiracle": "Invoke Miracle", "InvokeMiracle": "Invoke Miracle",
"InvokeOptions": "Invoke Options", "InvokeOptions": "Invoke Options",
@@ -947,6 +971,25 @@
"label": "Violated" "label": "Violated"
} }
} }
},
"Warning": {
"MiracleBlocked": "Divine favour has been lost — you cannot invoke miracles until a new day."
},
"SettlementArchetype": {
"CenterOfLearning": "Center of Learning",
"DwarvenBorough": "Dwarven Borough",
"FreeCity": "Free City",
"GuildMunicipality": "Guild Municipality",
"NocklanderOutpost": "Nocklander Outpost",
"PilgrimMission": "Pilgrim Mission",
"PortTown": "Port Town",
"VelathiColony": "Velathi Colony"
},
"Settlement": {
"BuildingHint": "Drag & drop building items here. Toggle the checkbox to mark them as constructed.",
"InventoryHint": "Drag & drop weapons, armor, or equipment here to store them in the settlement reserve.",
"NoBuildings": "No buildings yet. Drag building items here.",
"NoInventory": "No stored inventory. Drag weapons, armor, or equipment here."
} }
}, },
"TYPES": { "TYPES": {
@@ -965,10 +1008,8 @@
}, },
"Actor": { "Actor": {
"character": "Character", "character": "Character",
"npc": "NPC" "npc": "NPC",
}, "settlement": "Settlement"
"Warning": {
"MiracleBlocked": "Divine favour has been lost — you cannot invoke miracles until a new day."
} }
} }
} }

View File

@@ -36,7 +36,8 @@
// Shared actor content base // Shared actor content base
.oathhammer .character-content, .oathhammer .character-content,
.oathhammer .npc-content { .oathhammer .npc-content,
.oathhammer .settlement-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

@@ -11,3 +11,4 @@
@import "item-sheets"; @import "item-sheets";
@import "rolls"; @import "rolls";
@import "roll-dialog"; @import "roll-dialog";
@import "settlement-sheet";

172
less/settlement-sheet.less Normal file
View File

@@ -0,0 +1,172 @@
// ============================================================
// SETTLEMENT SHEET
// ============================================================
.oathhammer .settlement-main {
padding: 0 0 8px;
border-bottom: 1px solid fade(@color-dark, 25%);
margin-bottom: 4px;
}
.oathhammer .settlement-header {
align-items: flex-start;
gap: 10px;
}
.oathhammer .settlement-header .actor-img {
width: 72px;
height: 72px;
object-fit: cover;
border: 2px solid fade(@color-dark, 40%);
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
}
.oathhammer .settlement-header-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.oathhammer .settlement-name-row {
align-items: center;
gap: 8px;
}
.oathhammer .settlement-archetype-badge {
font-size: @font-size-sm;
color: darken(@color-paper, 40%);
background: fade(@color-gold, 15%);
border: 1px solid fade(@color-gold, 40%);
border-radius: 3px;
padding: 1px 6px;
white-space: nowrap;
}
.oathhammer .settlement-stats {
gap: 8px;
flex-wrap: wrap;
align-items: flex-start;
.stat-item {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 80px;
label {
font-size: 0.72rem;
color: @color-dark;
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 0.03em;
}
input { padding: 2px 4px; }
&--flags {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
flex-wrap: wrap;
min-width: unset;
label {
display: flex;
align-items: center;
gap: 4px;
font-size: @font-size-sm;
text-transform: none;
letter-spacing: 0;
cursor: pointer;
}
}
}
}
.oathhammer .settlement-badge {
font-size: 0.75rem;
font-weight: bold;
border-radius: 3px;
padding: 2px 7px;
&--capital {
background: fade(@color-gold, 20%);
border: 1px solid fade(@color-gold, 50%);
color: darken(@color-gold, 30%);
}
&--siege {
background: rgba(180, 40, 40, 0.12);
border: 1px solid rgba(180, 40, 40, 0.4);
color: #8a1a1a;
}
}
.oathhammer .settlement-content .tab {
padding: 8px 0;
}
// Overview grid: garrison | founded | tax notes
.oathhammer .settlement-overview-grid {
display: grid;
grid-template-columns: 1fr 1fr 2fr;
gap: 8px;
margin-bottom: 8px;
}
.oathhammer .settlement-hint {
font-size: 0.78rem;
color: @color-dark;
opacity: 0.7;
margin: 0 0 8px;
font-style: italic;
}
// Buildings item list
.oathhammer .item-list--buildings {
.item-list-header,
.item-entry {
display: grid;
grid-template-columns: 28px 1fr 3rem 5rem 4rem 3rem;
align-items: center;
gap: 4px;
}
}
.oathhammer .item-entry.building-constructed {
background: rgba(60, 140, 60, 0.08);
.item-name { font-weight: 600; }
}
.oathhammer .item-constructed {
text-align: center;
font-size: 1rem;
color: #3a7a3a;
}
.oathhammer .item-tax {
font-size: @font-size-sm;
color: darken(@color-olive, 10%);
text-align: center;
&--inactive { opacity: 0.4; }
}
.oathhammer .item-cost {
font-size: @font-size-sm;
color: darken(@color-gold, 20%);
text-align: right;
}
.oathhammer .construct-toggle {
color: #3a7a3a;
cursor: pointer;
font-size: 1rem;
&:hover { color: darken(#3a7a3a, 15%); }
}

View File

@@ -11,6 +11,7 @@ export { default as OathHammerTraitSheet } from "./sheets/trait-sheet.mjs"
export { default as OathHammerOathSheet } from "./sheets/oath-sheet.mjs" export { default as OathHammerOathSheet } from "./sheets/oath-sheet.mjs"
export { default as OathHammerClassSheet } from "./sheets/class-sheet.mjs" export { default as OathHammerClassSheet } from "./sheets/class-sheet.mjs"
export { default as OathHammerBuildingSheet } from "./sheets/building-sheet.mjs" export { default as OathHammerBuildingSheet } from "./sheets/building-sheet.mjs"
export { default as OathHammerSettlementSheet } from "./sheets/settlement-sheet.mjs"
export { default as 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

@@ -67,7 +67,7 @@ export default class OathHammerArmorDialog {
) )
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.ArmorRollTitle", { armor: armor.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.ArmorRollTitle", { armor: armor.name }), resizable: true },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 }, position: { width: 420 },
content, content,

View File

@@ -68,7 +68,7 @@ export default class OathHammerDefenseDialog {
) )
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.DefenseTitle", { actor: actor.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.DefenseTitle", { actor: actor.name }), resizable: true },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 }, position: { width: 420 },
content, content,

View File

@@ -30,6 +30,15 @@ export default class OathHammerMiracleDialog {
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
}) })
const availableLuck = actorSys.luck?.value ?? 0
const isHuman = (actorSys.lineage?.name ?? "").toLowerCase() === "human"
const luckDicePerPoint = isHuman ? 3 : 2
const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({
value: i,
label: i === 0 ? "0" : `${i} (+${i * luckDicePerPoint}d)`,
selected: i === 0,
}))
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
const context = { const context = {
@@ -47,6 +56,9 @@ export default class OathHammerMiracleDialog {
basePool, basePool,
miracleCountOptions, miracleCountOptions,
bonusOptions, bonusOptions,
availableLuck,
isHuman,
luckOptions,
rollModes, rollModes,
visibility: game.settings.get("core", "rollMode"), visibility: game.settings.get("core", "rollMode"),
} }
@@ -84,6 +96,8 @@ export default class OathHammerMiracleDialog {
bonus: parseInt(result.bonus) || 0, bonus: parseInt(result.bonus) || 0,
visibility: result.visibility ?? game.settings.get("core", "rollMode"), visibility: result.visibility ?? game.settings.get("core", "rollMode"),
explodeOn5: result.explodeOn5 === "true", explodeOn5: result.explodeOn5 === "true",
luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck),
luckIsHuman: result.luckIsHuman === "true",
} }
} }
} }

View File

@@ -124,7 +124,7 @@ export default class OathHammerRollDialog {
const title = game.i18n.format("OATHHAMMER.Dialog.SkillCheckTitle", { skill: context.skillLabel }) const title = game.i18n.format("OATHHAMMER.Dialog.SkillCheckTitle", { skill: context.skillLabel })
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title }, window: { title, resizable: true },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 }, position: { width: 420 },
content, content,

View File

@@ -37,6 +37,8 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
rollInitiative: OathHammerCharacterSheet.#onRollInitiative, rollInitiative: OathHammerCharacterSheet.#onRollInitiative,
adjustQty: OathHammerCharacterSheet.#onAdjustQty, adjustQty: OathHammerCharacterSheet.#onAdjustQty,
adjustCurrency: OathHammerCharacterSheet.#onAdjustCurrency, adjustCurrency: OathHammerCharacterSheet.#onAdjustCurrency,
adjustStress: OathHammerCharacterSheet.#onAdjustStress,
clearStress: OathHammerCharacterSheet.#onClearStress,
}, },
} }
@@ -201,6 +203,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
break break
case "magic": case "magic":
context.tab = context.tabs.magic context.tab = context.tabs.magic
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)
@@ -404,6 +407,17 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
const current = foundry.utils.getProperty(this.document, field) ?? 0 const current = foundry.utils.getProperty(this.document, field) ?? 0
await this.document.update({ [field]: Math.max(0, current + delta) }) await this.document.update({ [field]: Math.max(0, current + delta) })
} }
static async #onAdjustStress(event, target) {
const delta = parseInt(target.dataset.delta, 10)
const current = this.document.system.arcaneStress.value ?? 0
const max = this.document.system.arcaneStress.threshold
await this.document.update({ "system.arcaneStress.value": Math.max(0, Math.min(max, current + delta)) })
}
static async #onClearStress(_event, _target) {
await this.document.update({ "system.arcaneStress.value": 0 })
}
} }
/** Strip HTML tags and collapse whitespace for use in data-tooltip attributes. */ /** Strip HTML tags and collapse whitespace for use in data-tooltip attributes. */

View File

@@ -0,0 +1,132 @@
import OathHammerActorSheet from "./base-actor-sheet.mjs"
const ALLOWED_ITEM_TYPES = new Set(["building", "equipment", "weapon", "armor"])
export default class OathHammerSettlementSheet extends OathHammerActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["settlement"],
position: {
width: 840,
height: "auto",
},
window: {
contentClasses: ["settlement-content"],
},
actions: {
adjustCurrency: OathHammerSettlementSheet.#onAdjustCurrency,
adjustQty: OathHammerSettlementSheet.#onAdjustQty,
toggleConstructed: OathHammerSettlementSheet.#onToggleConstructed,
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-oath-hammer/templates/actor/settlement-sheet.hbs",
},
tabs: {
template: "templates/generic/tab-navigation.hbs",
},
overview: {
template: "systems/fvtt-oath-hammer/templates/actor/settlement-overview.hbs",
},
buildings: {
template: "systems/fvtt-oath-hammer/templates/actor/settlement-buildings.hbs",
},
inventory: {
template: "systems/fvtt-oath-hammer/templates/actor/settlement-inventory.hbs",
},
}
/** @override */
tabGroups = {
sheet: "overview",
}
#getTabs() {
const tabs = {
overview: { id: "overview", group: "sheet", icon: "fa-solid fa-city", label: "OATHHAMMER.Tab.Overview" },
buildings: { id: "buildings", group: "sheet", icon: "fa-solid fa-building", label: "OATHHAMMER.Tab.Buildings" },
inventory: { id: "inventory", group: "sheet", icon: "fa-solid fa-boxes-stacked", label: "OATHHAMMER.Tab.Inventory" },
}
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()
context.archetypeChoices = Object.fromEntries(
Object.entries(SYSTEM.SETTLEMENT_ARCHETYPES).map(([k, v]) => [k, game.i18n.localize(v)])
)
return context
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
break
case "overview":
context.tab = context.tabs.overview
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.description, { async: true }
)
break
case "buildings":
context.tab = context.tabs.buildings
context.buildings = doc.itemTypes.building
break
case "inventory": {
context.tab = context.tabs.inventory
context.weapons = doc.itemTypes.weapon
context.armors = doc.itemTypes.armor
context.equipments = doc.itemTypes.equipment
break
}
}
return context
}
/** @override */
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event)
if (data.type !== "Item") return
const item = await fromUuid(data.uuid)
if (!item || !ALLOWED_ITEM_TYPES.has(item.type)) return
return this._onDropItem(item)
}
static async #onAdjustCurrency(event, target) {
const field = target.dataset.field
const delta = parseInt(target.dataset.delta, 10)
if (!field || isNaN(delta)) return
const current = foundry.utils.getProperty(this.document, field) ?? 0
await this.document.update({ [field]: Math.max(0, current + delta) })
}
static async #onAdjustQty(event, target) {
const itemId = target.dataset.itemId
const delta = parseInt(target.dataset.delta, 10)
if (!itemId || isNaN(delta)) return
const item = this.document.items.get(itemId)
if (!item) return
const current = item.system.quantity ?? 0
await item.update({ "system.quantity": Math.max(0, current + delta) })
}
static async #onToggleConstructed(event, target) {
const itemId = target.dataset.itemId
if (!itemId) return
const item = this.document.items.get(itemId)
if (!item) return
await item.update({ "system.constructed": !item.system.constructed })
}
}

View File

@@ -52,6 +52,22 @@ export default class OathHammerSpellDialog {
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
}) })
const availableLuck = actorSys.luck?.value ?? 0
const isHuman = (actorSys.lineage?.name ?? "").toLowerCase() === "human"
const luckDicePerPoint = isHuman ? 3 : 2
const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({
value: i,
label: i === 0 ? "0" : `${i} (+${i * luckDicePerPoint}d)`,
selected: i === 0,
}))
// Pool size selector: casters may roll fewer dice to reduce Arcane Stress (p.101)
const poolSizeOptions = Array.from({ length: basePool }, (_, i) => ({
value: i + 1,
label: `${i + 1}d`,
selected: i + 1 === basePool,
}))
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
const context = { const context = {
@@ -70,11 +86,15 @@ export default class OathHammerSpellDialog {
intRank, intRank,
magicRank, magicRank,
basePool, basePool,
poolSizeOptions,
currentStress, currentStress,
stressThreshold, stressThreshold,
isOverThreshold, isOverThreshold,
enhancementOptions, enhancementOptions,
bonusOptions, bonusOptions,
availableLuck,
isHuman,
luckOptions,
rollModes, rollModes,
visibility: game.settings.get("core", "rollMode"), visibility: game.settings.get("core", "rollMode"),
} }
@@ -116,9 +136,12 @@ export default class OathHammerSpellDialog {
noStress: enh.noStress, noStress: enh.noStress,
elementalBonus: parseInt(result.elementalBonus) || 0, elementalBonus: parseInt(result.elementalBonus) || 0,
bonus: parseInt(result.bonus) || 0, bonus: parseInt(result.bonus) || 0,
poolSize: Math.min(Math.max(1, parseInt(result.poolSize) || basePool), basePool),
grimPenalty: parseInt(result.noGrimoire) || 0, grimPenalty: parseInt(result.noGrimoire) || 0,
visibility: result.visibility ?? game.settings.get("core", "rollMode"), visibility: result.visibility ?? game.settings.get("core", "rollMode"),
explodeOn5: result.explodeOn5 === "true", explodeOn5: result.explodeOn5 === "true",
luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck),
luckIsHuman: result.luckIsHuman === "true",
} }
} }
} }

View File

@@ -120,7 +120,7 @@ export default class OathHammerWeaponDialog {
) )
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.AttackTitle", { weapon: weapon.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.AttackTitle", { weapon: weapon.name }), resizable: true },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 }, position: { width: 420 },
content, content,
@@ -268,7 +268,7 @@ export default class OathHammerWeaponDialog {
) )
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.WeaponDefenseTitle", { weapon: weapon.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.WeaponDefenseTitle", { weapon: weapon.name }), resizable: true },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 }, position: { width: 420 },
content, content,
@@ -375,7 +375,7 @@ export default class OathHammerWeaponDialog {
) )
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.DamageTitle", { weapon: weapon.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.DamageTitle", { weapon: weapon.name }), resizable: true },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 }, position: { width: 420 },
content, content,

View File

@@ -362,6 +362,17 @@ export const BUILDING_SKILL_CHOICES = {
masonry: "OATHHAMMER.BuildingSkill.Masonry" masonry: "OATHHAMMER.BuildingSkill.Masonry"
} }
export const SETTLEMENT_ARCHETYPES = {
"center-of-learning": "OATHHAMMER.SettlementArchetype.CenterOfLearning",
"dwarven-borough": "OATHHAMMER.SettlementArchetype.DwarvenBorough",
"free-city": "OATHHAMMER.SettlementArchetype.FreeCity",
"guild-municipality": "OATHHAMMER.SettlementArchetype.GuildMunicipality",
"nocklander-outpost": "OATHHAMMER.SettlementArchetype.NocklanderOutpost",
"pilgrim-mission": "OATHHAMMER.SettlementArchetype.PilgrimMission",
"port-town": "OATHHAMMER.SettlementArchetype.PortTown",
"velathi-colony": "OATHHAMMER.SettlementArchetype.VelathiColony"
}
export const SYSTEM = { export const SYSTEM = {
id: SYSTEM_ID, id: SYSTEM_ID,
ATTRIBUTES, ATTRIBUTES,
@@ -389,6 +400,7 @@ export const SYSTEM = {
TRAIT_TYPE_CHOICES, TRAIT_TYPE_CHOICES,
TRAIT_USAGE_PERIOD, TRAIT_USAGE_PERIOD,
BUILDING_SKILL_CHOICES, BUILDING_SKILL_CHOICES,
SETTLEMENT_ARCHETYPES,
STATUS_EFFECTS, STATUS_EFFECTS,
ATTRIBUTE_RANK_CHOICES, ATTRIBUTE_RANK_CHOICES,
ASCII ASCII

View File

@@ -11,3 +11,4 @@ export { default as OathHammerTrait } from "./trait.mjs"
export { default as OathHammerOath } from "./oath.mjs" export { default as OathHammerOath } from "./oath.mjs"
export { default as OathHammerClass } from "./class.mjs" export { default as OathHammerClass } from "./class.mjs"
export { default as OathHammerBuilding } from "./building.mjs" export { default as OathHammerBuilding } from "./building.mjs"
export { default as OathHammerSettlement } from "./settlement.mjs"

View File

@@ -79,7 +79,8 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
schema.arcaneStress = new fields.SchemaField({ schema.arcaneStress = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
threshold: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }) threshold: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
thresholdBonus: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
}) })
schema.miracleBlocked = new fields.BooleanField({ required: true, initial: false }) schema.miracleBlocked = new fields.BooleanField({ required: true, initial: false })
@@ -130,5 +131,7 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
this.luck.max = this.attributes.fate.rank this.luck.max = this.attributes.fate.rank
// Defense score = 10 + Agility + Armor Rating + bonus // Defense score = 10 + Agility + Armor Rating + bonus
this.defense.value = 10 + this.attributes.agility.rank + this.defense.armorRating + this.defense.bonus this.defense.value = 10 + this.attributes.agility.rank + this.defense.armorRating + this.defense.bonus
// Stress Threshold = Willpower rank + Magic rank + bonus (rulebook p.101)
this.arcaneStress.threshold = this.attributes.willpower.rank + this.skills.magic.rank + this.arcaneStress.thresholdBonus
} }
} }

View File

@@ -0,0 +1,40 @@
import { SYSTEM } from "../config/system.mjs"
export default class OathHammerSettlement extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: false, textSearch: true })
schema.archetype = new fields.StringField({
required: true,
nullable: false,
initial: "free-city",
choices: SYSTEM.SETTLEMENT_ARCHETYPES
})
schema.territory = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.founded = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.population = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.renown = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.currency = new fields.SchemaField({
gold: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
silver: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
copper: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
schema.garrison = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.underSiege = 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: "" })
return schema
}
static LOCALIZATION_PREFIXES = ["OATHHAMMER.Settlement"]
}

View File

@@ -411,9 +411,12 @@ export async function rollSpellCast(actor, spell, options = {}) {
noStress = false, noStress = false,
elementalBonus = 0, elementalBonus = 0,
bonus = 0, bonus = 0,
poolSize = null,
grimPenalty = 0, grimPenalty = 0,
visibility, visibility,
explodeOn5 = false, explodeOn5 = false,
luckSpend = 0,
luckIsHuman = false,
} = options } = options
const sys = spell.system const sys = spell.system
@@ -421,7 +424,12 @@ export async function rollSpellCast(actor, spell, options = {}) {
const intRank = actorSys.attributes.intelligence.rank const intRank = actorSys.attributes.intelligence.rank
const magicRank = actorSys.skills.magic.rank const magicRank = actorSys.skills.magic.rank
const totalDice = Math.max(intRank + magicRank + bonus + poolPenalty + elementalBonus + grimPenalty, 1) const luckDicePerPoint = luckIsHuman ? 3 : 2
const baseDice = intRank + magicRank + bonus + poolPenalty + elementalBonus + grimPenalty + (luckSpend * luckDicePerPoint)
// poolSize: voluntary reduction (p.101) — clamped to [1, baseDice]
const totalDice = poolSize !== null
? Math.max(1, Math.min(poolSize + bonus + poolPenalty + elementalBonus + grimPenalty + (luckSpend * luckDicePerPoint), baseDice))
: Math.max(baseDice, 1)
const threshold = redDice ? 3 : 4 const threshold = redDice ? 3 : 4
const colorEmoji = redDice ? "🔴" : "⬜" const colorEmoji = redDice ? "🔴" : "⬜"
@@ -439,6 +447,10 @@ export async function rollSpellCast(actor, spell, options = {}) {
await actor.update({ "system.arcaneStress.value": currentStress + totalStressGain }) await actor.update({ "system.arcaneStress.value": currentStress + totalStressGain })
} }
if (luckSpend > 0) {
await actor.update({ "system.luck.value": Math.max(0, (actorSys.luck?.value ?? 0) - luckSpend) })
}
const newStress = (actorSys.arcaneStress.value ?? 0) + totalStressGain const newStress = (actorSys.arcaneStress.value ?? 0) + totalStressGain
const stressMax = actorSys.arcaneStress.threshold const stressMax = actorSys.arcaneStress.threshold
const isBlocked = newStress >= stressMax const isBlocked = newStress >= stressMax
@@ -451,10 +463,12 @@ export async function rollSpellCast(actor, spell, options = {}) {
: game.i18n.localize("OATHHAMMER.Roll.Failure") : game.i18n.localize("OATHHAMMER.Roll.Failure")
const modParts = [] const modParts = []
if (poolSize !== null && poolSize < intRank + magicRank) modParts.push(`🎲 ${poolSize}d ${game.i18n.localize("OATHHAMMER.Dialog.PoolSizeReduced")}`)
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (poolPenalty !== 0) modParts.push(`${poolPenalty} ${game.i18n.localize("OATHHAMMER.Enhancement." + _cap(enhancement))}`) if (poolPenalty !== 0) modParts.push(`${poolPenalty} ${game.i18n.localize("OATHHAMMER.Enhancement." + _cap(enhancement))}`)
if (elementalBonus > 0) modParts.push(`+${elementalBonus} ${game.i18n.localize("OATHHAMMER.Dialog.ElementMet")}`) if (elementalBonus > 0) modParts.push(`+${elementalBonus} ${game.i18n.localize("OATHHAMMER.Dialog.ElementMet")}`)
if (grimPenalty < 0) modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`) if (grimPenalty < 0) modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`)
if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
const explodedCountSpell = diceResults.filter(d => d.exploded).length const explodedCountSpell = diceResults.filter(d => d.exploded).length
if (explodedCountSpell > 0) modParts.push(`💥 ${explodedCountSpell} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) if (explodedCountSpell > 0) modParts.push(`💥 ${explodedCountSpell} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
@@ -517,6 +531,8 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
bonus = 0, bonus = 0,
visibility, visibility,
explodeOn5 = false, explodeOn5 = false,
luckSpend = 0,
luckIsHuman = false,
} = options } = options
const sys = miracle.system const sys = miracle.system
@@ -524,7 +540,8 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
const wpRank = actorSys.attributes.willpower.rank const wpRank = actorSys.attributes.willpower.rank
const magicRank = actorSys.skills.magic.rank const magicRank = actorSys.skills.magic.rank
const totalDice = Math.max(wpRank + magicRank + bonus, 1) const luckDicePerPoint = luckIsHuman ? 3 : 2
const totalDice = Math.max(wpRank + magicRank + bonus + (luckSpend * luckDicePerPoint), 1)
const threshold = 4 const threshold = 4
const colorEmoji = "⬜" const colorEmoji = "⬜"
@@ -532,6 +549,10 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
const diceHtml = _diceHtml(diceResults, threshold) const diceHtml = _diceHtml(diceResults, threshold)
const isSuccess = successes >= dv const isSuccess = successes >= dv
if (luckSpend > 0) {
await actor.update({ "system.luck.value": Math.max(0, (actorSys.luck?.value ?? 0) - luckSpend) })
}
const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Magic") const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Magic")
const attrLabel = game.i18n.localize("OATHHAMMER.Attribute.Willpower") const attrLabel = game.i18n.localize("OATHHAMMER.Attribute.Willpower")
const resultClass = isSuccess ? "roll-success" : "roll-failure" const resultClass = isSuccess ? "roll-success" : "roll-failure"
@@ -541,6 +562,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
const modParts = [] const modParts = []
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
const explodedCountMiracle = diceResults.filter(d => d.exploded).length const explodedCountMiracle = diceResults.filter(d => d.exploded).length
if (explodedCountMiracle > 0) modParts.push(`💥 ${explodedCountMiracle} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) if (explodedCountMiracle > 0) modParts.push(`💥 ${explodedCountMiracle} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""

View File

@@ -23,7 +23,8 @@ Hooks.once("init", function () {
CONFIG.Combat.documentClass = OathHammerCombat CONFIG.Combat.documentClass = OathHammerCombat
CONFIG.Actor.dataModels = { CONFIG.Actor.dataModels = {
character: models.OathHammerCharacter, character: models.OathHammerCharacter,
npc: models.OathHammerNPC npc: models.OathHammerNPC,
settlement: models.OathHammerSettlement
} }
CONFIG.Item.documentClass = documents.OathHammerItem CONFIG.Item.documentClass = documents.OathHammerItem
@@ -52,6 +53,11 @@ Hooks.once("init", function () {
makeDefault: true, makeDefault: true,
label: "OATHHAMMER.Sheet.NPC" label: "OATHHAMMER.Sheet.NPC"
}) })
foundry.documents.collections.Actors.registerSheet("fvtt-oath-hammer", applications.OathHammerSettlementSheet, {
types: ["settlement"],
makeDefault: true,
label: "OATHHAMMER.Sheet.Settlement"
})
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" })

View File

@@ -36,6 +36,12 @@
"background", "background",
"notes" "notes"
] ]
},
"settlement": {
"htmlFields": [
"description",
"notes"
]
} }
}, },
"Item": { "Item": {

View File

@@ -1,9 +1,17 @@
<section data-tab="magic" data-group="{{tab.group}}" class="tab {{tab.cssClass}}"> <section data-tab="magic" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset> <fieldset>
<legend>{{localize "OATHHAMMER.Label.ArcaneStress"}}</legend> <legend>{{localize "OATHHAMMER.Label.ArcaneStress"}}</legend>
<div class="flexrow"> <div class="stress-controls">
<label>{{localize "OATHHAMMER.Label.StressValue"}}</label> <a class="stress-btn" data-action="adjustStress" data-delta="-1" title="1"></a>
<span class="arcane-stress-display">{{system.arcaneStress.value}} / {{system.arcaneStress.threshold}}</span> <span class="arcane-stress-display {{#if stressBlocked}}stress-at-limit{{/if}}">
{{system.arcaneStress.value}} / {{system.arcaneStress.threshold}}
</span>
<a class="stress-btn" data-action="adjustStress" data-delta="1" title="+1">+</a>
<a class="stress-btn stress-btn--clear" data-action="clearStress" title="{{localize 'OATHHAMMER.Action.ClearStress'}}">
<i class="fa-solid fa-rotate-left"></i>
</a>
<span class="stress-bonus-label">{{localize "OATHHAMMER.Label.ThresholdBonus"}}</span>
<input type="number" name="system.arcaneStress.thresholdBonus" value="{{system.arcaneStress.thresholdBonus}}" min="0" class="stress-bonus-input">
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
@@ -17,7 +25,9 @@
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span> <span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>DV</span> <span>DV</span>
<span>{{localize "OATHHAMMER.Label.Tradition"}}</span> <span>{{localize "OATHHAMMER.Label.Tradition"}}</span>
<span>AS</span> <span>{{localize "OATHHAMMER.Label.Range"}}</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|}}
@@ -26,7 +36,9 @@
<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">{{localize spell.system.tradition}}</span>
<span class="item-detail">—</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.spellSave}}{{spell.system.spellSave}}{{else}}{{/if}}</span>
<div class="item-actions"> <div class="item-actions">
<a data-action="castSpell" 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="castSpell" data-item-id="{{spell.id}}" title="{{localize 'OATHHAMMER.Action.CastSpell'}}"><i class="fa-solid fa-wand-sparkles spell-cast-icon"></i></a>
<a data-action="edit" data-item-id="{{spell.id}}" data-item-uuid="{{spell.uuid}}"><i class="fa-solid fa-edit"></i></a> <a data-action="edit" data-item-id="{{spell.id}}" data-item-uuid="{{spell.uuid}}"><i class="fa-solid fa-edit"></i></a>

View File

@@ -0,0 +1,41 @@
<section data-tab="buildings" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<p class="settlement-hint">{{localize "OATHHAMMER.Settlement.BuildingHint"}}</p>
{{#if buildings.length}}
<ul class="item-list item-list--buildings">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Built"}}</span>
<span>{{localize "OATHHAMMER.Label.TaxRevenue"}}</span>
<span>{{localize "OATHHAMMER.Label.Cost"}}</span>
<span></span>
</li>
{{#each buildings as |building|}}
<li class="item-entry {{#if building.system.constructed}}building-constructed{{/if}}" data-item-id="{{building.id}}" data-item-uuid="{{building.uuid}}">
<img src="{{building.img}}" class="item-img" />
<span class="item-name" data-tooltip="{{building.name}}">{{building.name}}</span>
<span class="item-constructed">
{{#unless ../isPlayMode}}
<a data-action="toggleConstructed" data-item-id="{{building.id}}" class="construct-toggle">
<i class="fa-{{#if building.system.constructed}}solid{{else}}regular{{/if}} fa-square-check"></i>
</a>
{{else}}
<i class="fa-{{#if building.system.constructed}}solid{{else}}regular{{/if}} fa-square-check"></i>
{{/unless}}
</span>
<span class="item-tax {{#unless building.system.constructed}}item-tax--inactive{{/unless}}">
{{#if building.system.taxRevenue}}{{building.system.taxRevenue}}{{else}}{{/if}}
</span>
<span class="item-cost">{{building.system.cost}} gp</span>
<div class="item-actions">
<a data-action="edit" data-item-id="{{building.id}}" data-item-uuid="{{building.uuid}}"><i class="fa-solid fa-edit"></i></a>
{{#unless ../isPlayMode}}<a data-action="delete" data-item-id="{{building.id}}" data-item-uuid="{{building.uuid}}"><i class="fa-solid fa-trash"></i></a>{{/unless}}
</div>
</li>
{{/each}}
</ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Settlement.NoBuildings"}}</p>
{{/if}}
</section>

View File

@@ -0,0 +1,96 @@
<section data-tab="inventory" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<p class="settlement-hint">{{localize "OATHHAMMER.Settlement.InventoryHint"}}</p>
{{#if weapons.length}}
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Weapons"}}</legend>
<ul class="item-list item-list--weapons">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Type"}}</span>
<span></span>
</li>
{{#each weapons as |weapon|}}
<li class="item-entry" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}">
<img src="{{weapon.img}}" class="item-img" />
<span class="item-name">{{weapon.name}}</span>
<span class="item-type">{{weapon.system.weaponType}}</span>
<div class="item-actions">
<a data-action="edit" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-edit"></i></a>
{{#unless ../isPlayMode}}<a data-action="delete" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-trash"></i></a>{{/unless}}
</div>
</li>
{{/each}}
</ul>
</fieldset>
{{/if}}
{{#if armors.length}}
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Armors"}}</legend>
<ul class="item-list item-list--armors">
<li class="item-list-header">
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Type"}}</span>
<span></span>
</li>
{{#each armors as |armor|}}
<li class="item-entry" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}">
<img src="{{armor.img}}" class="item-img" />
<span class="item-name">{{armor.name}}</span>
<span class="item-type">{{localize armor.system.armorType}}</span>
<div class="item-actions">
<a data-action="edit" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-edit"></i></a>
{{#unless ../isPlayMode}}<a data-action="delete" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-trash"></i></a>{{/unless}}
</div>
</li>
{{/each}}
</ul>
</fieldset>
{{/if}}
{{#if equipments.length}}
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Equipment"}}</legend>
<ul class="item-list item-list--equipment">
<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.Quantity"}}</span>
<span></span>
</li>
{{#each equipments as |equip|}}
<li class="item-entry" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}">
<img src="{{equip.img}}" class="item-img" />
<span class="item-name">{{equip.name}}</span>
<span class="item-type">{{localize equip.system.itemType}}</span>
<span class="item-qty-stepper">
{{#unless ../isPlayMode}}
<a data-action="adjustQty" data-item-id="{{equip.id}}" data-delta="-1" class="qty-btn"></a>
{{/unless}}
<span class="qty-value">{{equip.system.quantity}}</span>
{{#unless ../isPlayMode}}
<a data-action="adjustQty" data-item-id="{{equip.id}}" data-delta="1" class="qty-btn">+</a>
{{/unless}}
</span>
<div class="item-actions">
<a data-action="edit" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}"><i class="fa-solid fa-edit"></i></a>
{{#unless ../isPlayMode}}<a data-action="delete" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}"><i class="fa-solid fa-trash"></i></a>{{/unless}}
</div>
</li>
{{/each}}
</ul>
</fieldset>
{{/if}}
{{#unless weapons.length}}
{{#unless armors.length}}
{{#unless equipments.length}}
<p class="no-items">{{localize "OATHHAMMER.Settlement.NoInventory"}}</p>
{{/unless}}
{{/unless}}
{{/unless}}
</section>

View File

@@ -0,0 +1,61 @@
<section data-tab="overview" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset class="currency-bar">
<legend>{{localize "OATHHAMMER.Label.Treasury"}}</legend>
<div class="flexrow">
<div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.GP"}}</label>
<div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.currency.gold" data-delta="-1" class="qty-btn"></a>
{{formInput systemFields.currency.fields.gold value=system.currency.gold name="system.currency.gold"}}
<a data-action="adjustCurrency" data-field="system.currency.gold" data-delta="1" class="qty-btn">+</a>
</div>
</div>
<div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.SP"}}</label>
<div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.currency.silver" data-delta="-1" class="qty-btn"></a>
{{formInput systemFields.currency.fields.silver value=system.currency.silver name="system.currency.silver"}}
<a data-action="adjustCurrency" data-field="system.currency.silver" data-delta="1" class="qty-btn">+</a>
</div>
</div>
<div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.CP"}}</label>
<div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.currency.copper" data-delta="-1" class="qty-btn"></a>
{{formInput systemFields.currency.fields.copper value=system.currency.copper name="system.currency.copper"}}
<a data-action="adjustCurrency" data-field="system.currency.copper" data-delta="1" class="qty-btn">+</a>
</div>
</div>
</div>
</fieldset>
<div class="settlement-overview-grid">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Garrison"}}</legend>
{{formInput systemFields.garrison value=system.garrison name="system.garrison" disabled=isPlayMode}}
</fieldset>
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Founded"}}</legend>
{{formInput systemFields.founded value=system.founded name="system.founded" disabled=isPlayMode}}
</fieldset>
<fieldset class="tax-notes">
<legend>{{localize "OATHHAMMER.Label.TaxNotes"}}</legend>
{{formInput systemFields.taxNotes value=system.taxNotes name="system.taxNotes" disabled=isPlayMode}}
</fieldset>
</div>
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Description"}}</legend>
{{#if isEditMode}}
<prose-mirror name="system.description" toggled="false" collaborate="false">
{{{system.description}}}
</prose-mirror>
{{else}}
<div class="editor-content">{{{enrichedDescription}}}</div>
{{/if}}
</fieldset>
</section>

View File

@@ -0,0 +1,47 @@
<div class="settlement-main settlement-main-{{ifThen isPlayMode 'play' 'edit'}}">
<header class="settlement-header flexrow">
<img class="actor-img" src="{{actor.img}}" data-edit="img" data-action="editImage" data-tooltip="{{actor.name}}" />
<div class="settlement-header-info">
<div class="settlement-name-row flexrow">
{{formInput fields.name value=source.name rootId=partId disabled=isPlayMode}}
{{#unless isPlayMode}}
{{formInput systemFields.archetype value=system.archetype name="system.archetype" localize=true}}
{{else}}
<span class="settlement-archetype-badge">{{localize (lookup archetypeChoices system.archetype)}}</span>
{{/unless}}
<a class="control" data-action="toggleSheet" data-tooltip="OATHHAMMER.ToggleSheet" data-tooltip-direction="UP">
<i class="fa-solid fa-{{ifThen isPlayMode 'lock' 'pen'}}"></i>
</a>
</div>
<div class="settlement-stats flexrow">
<div class="stat-item">
<label>{{localize "OATHHAMMER.Label.Renown"}}</label>
{{formInput systemFields.renown value=system.renown name="system.renown" disabled=isPlayMode}}
</div>
<div class="stat-item">
<label>{{localize "OATHHAMMER.Label.Territory"}}</label>
{{formInput systemFields.territory value=system.territory name="system.territory" disabled=isPlayMode}}
</div>
<div class="stat-item">
<label>{{localize "OATHHAMMER.Label.Population"}}</label>
{{formInput systemFields.population value=system.population name="system.population" disabled=isPlayMode}}
</div>
<div class="stat-item stat-item--flags">
{{#unless isPlayMode}}
<label>
{{formInput systemFields.isCapital value=system.isCapital name="system.isCapital"}}
{{localize "OATHHAMMER.Label.IsCapital"}}
</label>
<label>
{{formInput systemFields.underSiege value=system.underSiege name="system.underSiege"}}
{{localize "OATHHAMMER.Label.UnderSiege"}}
</label>
{{else}}
{{#if system.isCapital}}<span class="settlement-badge settlement-badge--capital">★ {{localize "OATHHAMMER.Label.Capital"}}</span>{{/if}}
{{#if system.underSiege}}<span class="settlement-badge settlement-badge--siege">⚔ {{localize "OATHHAMMER.Label.UnderSiege"}}</span>{{/if}}
{{/unless}}
</div>
</div>
</div>
</header>
</div>

View File

@@ -53,6 +53,18 @@
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span> <span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span>
</div> </div>
{{#if availableLuck}}
<div class="roll-option-row roll-option-luck">
<label>{{localize "OATHHAMMER.Dialog.LuckSpend"}} <i class="fa-solid fa-clover luck-icon"></i></label>
<select name="luckSpend">
{{#each luckOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<label class="luck-human-label" for="miracleLuckIsHuman">{{localize "OATHHAMMER.Dialog.LuckHuman"}}</label>
<input type="checkbox" id="miracleLuckIsHuman" name="luckIsHuman" value="true" {{#if isHuman}}checked{{/if}} />
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}})</span>
</div>
{{/if}}
<div class="roll-option-row roll-option-check"> <div class="roll-option-row roll-option-check">
<label for="miracleExplodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label> <label for="miracleExplodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
<input type="checkbox" id="miracleExplodeOn5" name="explodeOn5" /> <input type="checkbox" id="miracleExplodeOn5" name="explodeOn5" />

View File

@@ -34,6 +34,16 @@
= <strong>{{basePool}}d6</strong> = <strong>{{basePool}}d6</strong>
</div> </div>
{{#if poolSizeOptions}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.PoolSize"}}</label>
<select name="poolSize">
{{#each poolSizeOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.PoolSizeHint"}}</span>
</div>
{{/if}}
<div class="roll-option-row"> <div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.Enhancement"}}</label> <label>{{localize "OATHHAMMER.Dialog.Enhancement"}}</label>
<select name="enhancement" class="enhancement-select"> <select name="enhancement" class="enhancement-select">
@@ -67,6 +77,18 @@
</select> </select>
</div> </div>
{{#if availableLuck}}
<div class="roll-option-row roll-option-luck">
<label>{{localize "OATHHAMMER.Dialog.LuckSpend"}} <i class="fa-solid fa-clover luck-icon"></i></label>
<select name="luckSpend">
{{#each luckOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<label class="luck-human-label" for="spellLuckIsHuman">{{localize "OATHHAMMER.Dialog.LuckHuman"}}</label>
<input type="checkbox" id="spellLuckIsHuman" name="luckIsHuman" value="true" {{#if isHuman}}checked{{/if}} />
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}})</span>
</div>
{{/if}}
<div class="roll-option-row roll-option-check"> <div class="roll-option-row roll-option-check">
<label for="spellExplodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label> <label for="spellExplodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
<input type="checkbox" id="spellExplodeOn5" name="explodeOn5" /> <input type="checkbox" id="spellExplodeOn5" name="explodeOn5" />