Various fixes and changes based on tester feedback

This commit is contained in:
2026-03-17 13:50:32 +01:00
parent 92ba9c3501
commit 000bf348a6
29 changed files with 1450 additions and 192 deletions

View File

@@ -30,7 +30,7 @@
.oathhammer .npc-content { .oathhammer .npc-content {
font-family: "Calibri", "Segoe UI", sans-serif; font-family: "Calibri", "Segoe UI", sans-serif;
font-size: 0.86rem; font-size: 0.86rem;
color: var(--color-dark-1); color: #2a1a0a;
background-image: var(--oh-background-image); background-image: var(--oh-background-image);
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
@@ -51,7 +51,7 @@
.oathhammer .npc-content select:disabled { .oathhammer .npc-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: var(--color-dark-3); color: #2a1a0a;
} }
.oathhammer .character-content input, .oathhammer .character-content input,
.oathhammer .npc-content input, .oathhammer .npc-content input,
@@ -226,7 +226,8 @@
margin-bottom: 2px; margin-bottom: 2px;
} }
.oathhammer .skills-container .skill-row label.skill-name-col { .oathhammer .skills-container .skill-row label.skill-name-col {
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
font-weight: bold;
} }
.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 {
@@ -409,7 +410,7 @@
border: 1px solid #535128; border: 1px solid #535128;
border-radius: 3px; border-radius: 3px;
background: rgba(0, 0, 0, 0.15); background: rgba(0, 0, 0, 0.15);
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
min-width: 6rem; min-width: 6rem;
min-height: 1.6rem; min-height: 1.6rem;
} }
@@ -429,18 +430,18 @@
.oathhammer .character-main .character-identity-bar .identity-slot .identity-name { .oathhammer .character-main .character-identity-bar .identity-slot .identity-name {
flex: 1; flex: 1;
font-family: "BlueDragon", "Palatino Linotype", serif; font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
} }
.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.85); font-size: calc(0.86rem * 0.9);
opacity: 0.6; opacity: 0.8;
} }
.oathhammer .character-main .character-identity-bar .identity-slot .slot-placeholder { .oathhammer .character-main .character-identity-bar .identity-slot .slot-placeholder {
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
} }
.oathhammer .character-main .character-identity-bar .identity-slot a { .oathhammer .character-main .character-identity-bar .identity-slot a {
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
opacity: 0.7; opacity: 0.85;
} }
.oathhammer .character-main .character-identity-bar .identity-slot a:hover { .oathhammer .character-main .character-identity-bar .identity-slot a:hover {
opacity: 1; opacity: 1;
@@ -450,21 +451,22 @@
align-items: center; align-items: center;
gap: 3px; gap: 3px;
margin-left: auto; margin-left: auto;
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
} }
.oathhammer .character-main .character-identity-bar .identity-xp .xp-label { .oathhammer .character-main .character-identity-bar .identity-xp .xp-label {
font-family: "BlueDragon", "Palatino Linotype", serif; font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
color: #535128; font-weight: bold;
color: #2a1a0a;
margin-left: 4px; margin-left: 4px;
} }
.oathhammer .character-main .character-identity-bar .identity-xp .xp-sep { .oathhammer .character-main .character-identity-bar .identity-xp .xp-sep {
opacity: 0.6; opacity: 0.8;
} }
.oathhammer .character-main .character-identity-bar .identity-xp input { .oathhammer .character-main .character-identity-bar .identity-xp input {
width: 3rem; width: 3rem;
text-align: center; text-align: center;
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
padding: 1px 2px; padding: 1px 2px;
} }
.oathhammer .character-main .character-stats-band { .oathhammer .character-main .character-stats-band {
@@ -496,17 +498,18 @@
.oathhammer .character-main .character-stats-band .character-resources .character-resource input { .oathhammer .character-main .character-stats-band .character-resources .character-resource input {
width: 2.4rem; width: 2.4rem;
text-align: center; text-align: center;
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
padding: 1px 2px; padding: 1px 2px;
} }
.oathhammer .character-main .character-stats-band .character-resources .character-resource .res-sep { .oathhammer .character-main .character-stats-band .character-resources .character-resource .res-sep {
opacity: 0.5; opacity: 0.7;
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
} }
.oathhammer .character-main .character-stats-band .character-resources .resource-label { .oathhammer .character-main .character-stats-band .character-resources .resource-label {
min-width: 3.8rem; min-width: 4.2rem;
font-family: "BlueDragon", "Palatino Linotype", serif; font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.9); font-size: 0.86rem;
font-weight: bold;
color: #2a1a0a; color: #2a1a0a;
} }
.oathhammer .character-main .character-stats-band .character-attributes { .oathhammer .character-main .character-stats-band .character-attributes {
@@ -529,7 +532,8 @@
} }
.oathhammer .attribute-box label { .oathhammer .attribute-box label {
font-family: "BlueDragon", "Palatino Linotype", serif; font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.9);
font-weight: bold;
color: #2a1a0a; color: #2a1a0a;
text-align: center; text-align: center;
} }
@@ -622,7 +626,7 @@
text-align: center; text-align: center;
} }
.oathhammer .character-arcane-stress span { .oathhammer .character-arcane-stress span {
opacity: 0.6; opacity: 0.8;
} }
.oathhammer .defense-display { .oathhammer .defense-display {
min-width: 3rem; min-width: 3rem;
@@ -697,7 +701,7 @@
} }
.oathhammer .item-entry .item-detail { .oathhammer .item-entry .item-detail {
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
color: #535128; color: #2a1a0a;
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -705,7 +709,7 @@
} }
.oathhammer .item-entry .item-group { .oathhammer .item-entry .item-group {
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
color: #535128; color: #2a1a0a;
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -742,7 +746,7 @@
gap: 4px; gap: 4px;
} }
.oathhammer .item-entry .item-actions a { .oathhammer .item-entry .item-actions a {
opacity: 0.6; opacity: 0.85;
transition: opacity 0.2s; transition: opacity 0.2s;
font-size: calc(0.86rem * 0.85); font-size: calc(0.86rem * 0.85);
} }
@@ -754,11 +758,11 @@
} }
.oathhammer .item-list--weapon .item-list-header, .oathhammer .item-list--weapon .item-list-header,
.oathhammer .item-list--weapon .item-entry { .oathhammer .item-list--weapon .item-entry {
grid-template-columns: 24px 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 5.5rem; grid-template-columns: 24px 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 9rem;
} }
.oathhammer .item-list--armor .item-list-header, .oathhammer .item-list--armor .item-list-header,
.oathhammer .item-list--armor .item-entry { .oathhammer .item-list--armor .item-entry {
grid-template-columns: 24px 1fr 3.5rem 2.5rem 3.5rem 1.5rem 1.8rem 3.5rem; grid-template-columns: 24px 1fr 3.5rem 2.5rem 3.5rem 1.5rem 1.8rem 5.5rem;
} }
.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 {
@@ -772,6 +776,10 @@
.oathhammer .item-list--miracle .item-entry { .oathhammer .item-list--miracle .item-entry {
grid-template-columns: 24px 1fr 4.5rem 5.5rem; grid-template-columns: 24px 1fr 4.5rem 5.5rem;
} }
.oathhammer .miracles-blocked {
opacity: 0.45;
pointer-events: none;
}
.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 3rem 3.5rem; grid-template-columns: 24px 1fr 5rem 3rem 3.5rem;
@@ -794,7 +802,7 @@
} }
.oathhammer .item-usage { .oathhammer .item-usage {
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
color: #535128; color: #2a1a0a;
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -817,7 +825,7 @@
color: #c0392b; color: #c0392b;
} }
.oathhammer .no-items { .oathhammer .no-items {
color: var(--color-dark-5); color: #535128;
font-style: italic; font-style: italic;
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
padding: 4px; padding: 4px;
@@ -825,12 +833,70 @@
.oathhammer .create-btn { .oathhammer .create-btn {
margin-left: 6px; margin-left: 6px;
color: #084a74; color: #084a74;
opacity: 0.6; opacity: 0.85;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.oathhammer .create-btn:hover { .oathhammer .create-btn:hover {
opacity: 1; opacity: 1;
} }
.miracle-blocked-banner {
display: flex;
align-items: center;
gap: 6px;
background: rgba(192, 57, 43, 0.1);
border: 1px solid rgba(192, 57, 43, 0.45);
border-radius: 4px;
padding: 4px 10px;
margin-bottom: 4px;
color: #c0392b;
font-size: calc(0.86rem * 0.9);
font-weight: bold;
}
.miracle-blocked-banner span {
flex: 1;
}
.miracle-blocked-banner a {
color: #2a1a0a;
font-weight: normal;
font-size: calc(0.86rem * 0.85);
opacity: 0.85;
transition: opacity 0.2s;
white-space: nowrap;
}
.miracle-blocked-banner a:hover {
opacity: 1;
}
.miracle-blocked-banner a i {
color: #c8a84b;
}
.slots-counter {
display: flex;
gap: 6px;
padding: 2px 6px 4px;
}
.slots-counter .slots-label {
font-size: calc(0.86rem * 0.9);
font-weight: bold;
color: #2a1a0a;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.slots-counter .slots-value {
font-size: calc(0.86rem * 0.9);
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: 1px 8px;
min-width: 3.5rem;
text-align: center;
}
.slots-counter .slots-value.slots-over {
color: #c0392b;
background: rgba(192, 57, 43, 0.1);
border-color: rgba(192, 57, 43, 0.4);
}
.oathhammer .item-sheet-common { .oathhammer .item-sheet-common {
overflow: auto; overflow: auto;
padding: 10px 20px; padding: 10px 20px;
@@ -1025,6 +1091,10 @@
background: rgba(231, 76, 60, 0.15); background: rgba(231, 76, 60, 0.15);
color: #c0392b; color: #c0392b;
} }
.oh-roll-card .roll-opposed {
background: rgba(52, 152, 219, 0.15);
color: #1a6fa8;
}
.oathhammer .rarity-roll-btn { .oathhammer .rarity-roll-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -1032,7 +1102,7 @@
cursor: pointer; cursor: pointer;
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
color: #084a74; color: #084a74;
opacity: 0.6; opacity: 0.85;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.oathhammer .rarity-roll-btn:hover { .oathhammer .rarity-roll-btn:hover {
@@ -1155,25 +1225,27 @@
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
color: #2a1a0a; color: #2a1a0a;
} }
.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row { .fvtt-oath-hammer .oh-roll-dialog .roll-option-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
align-items: center; align-items: center;
gap: 4px 8px; gap: 4px 8px;
padding: 5px 0; padding: 5px 0;
} }
.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row:not(:last-child) { .fvtt-oath-hammer .oh-roll-dialog .roll-option-row:not(:last-child) {
border-bottom: 1px solid rgba(83, 81, 40, 0.1); border-bottom: 1px solid rgba(83, 81, 40, 0.1);
} }
.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row label { .fvtt-oath-hammer .oh-roll-dialog .roll-option-row label {
flex: 1 1 120px; flex: 0 0 140px;
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
color: #2a1a0a; color: #2a1a0a;
white-space: nowrap; white-space: nowrap;
font-family: "Calibri", "Segoe UI", sans-serif; font-family: "Calibri", "Segoe UI", sans-serif;
} }
.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row select { .fvtt-oath-hammer .oh-roll-dialog .roll-option-row select,
flex: 0 0 90px; .fvtt-oath-hammer .oh-roll-dialog .roll-option-row input[type="text"],
.fvtt-oath-hammer .oh-roll-dialog .roll-option-row input[type="number"] {
flex: 0 0 110px;
padding: 3px 6px; padding: 3px 6px;
border: 1px solid rgba(49, 47, 23, 0.2); border: 1px solid rgba(49, 47, 23, 0.2);
border-radius: 3px; border-radius: 3px;
@@ -1182,27 +1254,38 @@
font-family: "Calibri", "Segoe UI", sans-serif; font-family: "Calibri", "Segoe UI", sans-serif;
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
} }
.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-row .roll-option-hint { .fvtt-oath-hammer .oh-roll-dialog .roll-option-row .roll-option-hint {
flex: 1 1 100%; flex: 1 1 auto;
font-size: calc(calc(0.86rem * 0.9) * 0.85); font-size: calc(calc(0.86rem * 0.9) * 0.85);
color: #535128; color: #535128;
font-style: italic; font-style: italic;
padding-left: 4px; padding-left: 6px;
white-space: normal; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-luck label { .fvtt-oath-hammer .oh-roll-dialog .roll-option-luck label {
color: #c8a84b; color: #c8a84b;
font-weight: bold; font-weight: bold;
} }
.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-luck select { .fvtt-oath-hammer .oh-roll-dialog .roll-option-luck select {
border-color: #c8a84b; border-color: #c8a84b;
} }
.fvtt-oath-hammer .oh-roll-dialog .roll-options-block .roll-option-luck .luck-icon { .fvtt-oath-hammer .oh-roll-dialog .roll-option-luck .luck-icon {
color: #c8a84b; color: #c8a84b;
font-size: 0.8em; font-size: 0.8em;
} }
.fvtt-oath-hammer .oh-roll-dialog .roll-option-check input[type="checkbox"] {
flex: 0 0 auto;
width: 16px;
height: 16px;
accent-color: #084a74;
cursor: pointer;
}
.fvtt-oath-hammer .oh-roll-dialog .roll-option-check label {
cursor: pointer;
flex: 1 1 auto;
}
.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;
@@ -1220,7 +1303,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
opacity: 0.6; opacity: 0.85;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.fvtt-oath-hammer .skills-list a.skill-name-col:hover { .fvtt-oath-hammer .skills-list a.skill-name-col:hover {
@@ -1373,7 +1456,7 @@
font-family: "Calibri", "Segoe UI", sans-serif; font-family: "Calibri", "Segoe UI", sans-serif;
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
cursor: pointer; cursor: pointer;
opacity: 0.6; opacity: 0.85;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.oh-weapon-card .oh-roll-damage-btn:hover, .oh-weapon-card .oh-roll-damage-btn:hover,
@@ -1395,29 +1478,38 @@
font-weight: bold; font-weight: bold;
margin-left: 8px; margin-left: 8px;
} }
.item-list--weapon .item-actions { .item-list--weapon .item-entry .item-actions {
gap: 8px; gap: 14px;
} }
.item-list--weapon .item-actions a[data-action="attackWeapon"] { .item-list--weapon .item-entry .item-actions a[data-action="attackWeapon"] {
color: #084a74; color: #084a74;
font-size: 1.1em; font-size: 1.1em;
opacity: 0.6; opacity: 0.85;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.item-list--weapon .item-actions a[data-action="attackWeapon"]:hover { .item-list--weapon .item-entry .item-actions a[data-action="attackWeapon"]:hover {
opacity: 1; opacity: 1;
} }
.item-list--weapon .item-actions a[data-action="damageWeapon"] { .item-list--weapon .item-entry .item-actions a[data-action="defendWeapon"] {
color: #535128;
font-size: 1.1em;
opacity: 0.85;
transition: opacity 0.2s;
}
.item-list--weapon .item-entry .item-actions a[data-action="defendWeapon"]:hover {
opacity: 1;
}
.item-list--weapon .item-entry .item-actions a[data-action="damageWeapon"] {
color: #8b0000; color: #8b0000;
font-size: 1.1em; font-size: 1.1em;
opacity: 0.6; opacity: 0.85;
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.item-list--weapon .item-actions a[data-action="damageWeapon"]:hover { .item-list--weapon .item-entry .item-actions a[data-action="damageWeapon"]:hover {
opacity: 1; opacity: 1;
} }
.item-list--weapon .item-actions a[data-action="edit"] { .item-list--weapon .item-entry .item-actions a[data-action="edit"] {
margin-left: 6px; margin-left: 4px;
} }
.oh-spell-dialog .dv-badge, .oh-spell-dialog .dv-badge,
.oh-miracle-dialog .dv-badge { .oh-miracle-dialog .dv-badge {
@@ -1565,3 +1657,84 @@
.item-list--miracle .item-actions a[data-action="edit"] { .item-list--miracle .item-actions a[data-action="edit"] {
margin-left: 6px; margin-left: 6px;
} }
.oh-defense-dialog .oh-trait-info,
.oh-armor-dialog .oh-trait-info {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
margin: 4px 0;
background: rgba(8, 74, 116, 0.08);
border: 1px solid rgba(8, 74, 116, 0.25);
border-radius: 4px;
font-size: 0.84em;
color: #052c44;
}
.oh-defense-dialog .oh-trait-info i,
.oh-armor-dialog .oh-trait-info i {
flex-shrink: 0;
}
.oh-defense-card .oh-roll-header,
.oh-armor-card .oh-roll-header {
display: flex;
align-items: center;
gap: 8px;
}
.oh-defense-card .oh-defense-icon,
.oh-armor-card .oh-defense-icon {
font-size: 1.1em;
color: #084a74;
flex-shrink: 0;
}
.oh-defense-card .oh-card-weapon-img,
.oh-armor-card .oh-card-weapon-img {
width: 28px;
height: 28px;
-o-object-fit: contain;
object-fit: contain;
border-radius: 3px;
border: 1px solid rgba(83, 81, 40, 0.2);
flex-shrink: 0;
}
.defense-row {
align-items: flex-end;
}
.defense-row .defense-roll-group {
flex: 0 0 auto;
}
.defense-roll-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: #084a74;
color: #fff;
border-radius: 4px;
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85);
cursor: pointer;
text-decoration: none;
opacity: 0.85;
transition: opacity 0.2s;
}
.defense-roll-btn:hover {
opacity: 1;
}
.defense-roll-btn i {
font-size: 0.9em;
}
.defense-roll-btn:hover {
opacity: 0.85;
color: #fff;
text-decoration: none;
}
.item-list--armor .item-actions {
gap: 8px;
}
.item-list--armor .item-actions a[data-action="rollArmorSave"] .armor-roll-icon {
color: #084a74;
font-size: 1.05em;
}
.item-list--armor .item-actions a[data-action="edit"] {
margin-left: 6px;
}

View File

@@ -60,6 +60,14 @@
"Survival": "Survival", "Survival": "Survival",
"Tracking": "Tracking" "Tracking": "Tracking"
}, },
"Lineage": {
"Dwarf": "Dwarf",
"Firbolg": "Firbolg",
"Halfling": "Halfling",
"HighElf": "High Elf",
"Human": "Human",
"WoodElf": "Wood Elf"
},
"Class": { "Class": {
"Berserker": "Berserker", "Berserker": "Berserker",
"Champion": "Champion", "Champion": "Champion",
@@ -211,6 +219,7 @@
"Weapons": "Weapons", "Weapons": "Weapons",
"Armor": "Armor & Shields", "Armor": "Armor & Shields",
"Ammunition": "Ammunition", "Ammunition": "Ammunition",
"ItemSlots": "Item Slots",
"Spells": "Spells", "Spells": "Spells",
"Miracles": "Miracles", "Miracles": "Miracles",
"Equipment": "Equipment", "Equipment": "Equipment",
@@ -231,7 +240,9 @@
"NoArmor": "No armor or shields.", "NoArmor": "No armor or shields.",
"NoSpells": "No spells known.", "NoSpells": "No spells known.",
"NoMiracles": "No miracles known.", "NoMiracles": "No miracles known.",
"MiracleBlocked": "Divine favour lost — no more miracles today.",
"NoEquipment": "No equipment.", "NoEquipment": "No equipment.",
"NoTraits": "Drop traits here.",
"Enchantment": "Enchantment", "Enchantment": "Enchantment",
"Tenet": "Tenet", "Tenet": "Tenet",
"Boon": "Boon", "Boon": "Boon",
@@ -276,12 +287,15 @@
"Spell": "New Spell", "Spell": "New Spell",
"Miracle": "New Miracle", "Miracle": "New Miracle",
"Equipment": "New Equipment", "Equipment": "New Equipment",
"Building": "New Building" "Building": "New Building",
"Trait": "New Trait"
}, },
"ToggleSheet": "Toggle Edit/Play Mode", "ToggleSheet": "Toggle Edit/Play Mode",
"Action": { "Action": {
"CastSpell": "Cast Spell", "CastSpell": "Cast Spell",
"InvokeMiracle": "Invoke Miracle" "InvokeMiracle": "Invoke Miracle",
"ResetMiracleBlocked": "Restore divine favour (new day)",
"NewDay": "New day"
}, },
"Dialog": { "Dialog": {
"SkillCheckTitle": "Skill Check: {skill}", "SkillCheckTitle": "Skill Check: {skill}",
@@ -300,6 +314,8 @@
"Visibility": "Visibility", "Visibility": "Visibility",
"Attribute": "Attribute", "Attribute": "Attribute",
"RollSkill": "Click to roll skill check", "RollSkill": "Click to roll skill check",
"ExplodeOn5": "Explode on 5+",
"ExplodeOn5Hint": "trait bonus — 5s & 6s explode",
"AttackTitle": "Attack: {weapon}", "AttackTitle": "Attack: {weapon}",
"DamageTitle": "Damage: {weapon}", "DamageTitle": "Damage: {weapon}",
"Attack": "Attack", "Attack": "Attack",
@@ -335,7 +351,30 @@
"MiracleDVNote": "miracle # today", "MiracleDVNote": "miracle # today",
"MiracleCount": "Miracle # Today", "MiracleCount": "Miracle # Today",
"MiracleCountHint": "1st = DV 1, 2nd = DV 2...", "MiracleCountHint": "1st = DV 1, 2nd = DV 2...",
"MiracleFailWarning": "Failure blocks ALL miracles for the rest of the day." "MiracleFailWarning": "Failure blocks ALL miracles for the rest of the day.",
"DefenseTitle": "Defense Roll: {actor}",
"RollDefense": "Roll Defense",
"DefenseOptions": "Defense Options",
"AttackType": "Attack Type",
"DefenseMelee": "Melee Attack",
"DefenseRanged": "Ranged Attack",
"GoverningAttr": "Attribute",
"MightMeleeHint": "melee only: may use Might",
"ParryActive": "Parry — red defense dice vs melee",
"BlockActive": "Block — red defense dice vs ranged (+1)",
"ArmorPenalty": "armor penalty",
"WeaponDefenseTitle": "Weapon Defense: {weapon}",
"DiminishingDefense": "Diminishing Defense",
"DiminishingDefenseHint": "-2 penalty per additional defense this turn",
"DiminishFirst": "1st defense (no penalty)",
"DiminishSecond": "2nd defense (2)",
"DiminishThird": "3rd+ defense (4)",
"ArmorRollTitle": "Armor Roll: {armor}",
"RollArmor": "Roll Armor",
"ArmorRollOptions": "Armor Roll Options",
"APPenalty": "AP (Attacker)",
"APHint": "attacker's Armor Piercing value",
"ReinforcedHint": "Reinforced — rolling red dice"
}, },
"Enhancement": { "Enhancement": {
"None": "None", "None": "None",
@@ -352,6 +391,7 @@
"Check": "Check", "Check": "Check",
"Success": "Success!", "Success": "Success!",
"Failure": "Failure", "Failure": "Failure",
"Exploded": "exploded dice",
"AutoSuccess": "Automatically Available", "AutoSuccess": "Automatically Available",
"RarityCheck": "Rarity Check", "RarityCheck": "Rarity Check",
"NoActor": "No character selected — assign a character to your user first.", "NoActor": "No character selected — assign a character to your user first.",
@@ -362,6 +402,11 @@
"MiracleCast": "Miracle Invocation", "MiracleCast": "Miracle Invocation",
"StressGained": "Arcane Stress Gained", "StressGained": "Arcane Stress Gained",
"MiracleBlocked": "You are now blocked from casting miracles for the rest of the day!", "MiracleBlocked": "You are now blocked from casting miracles for the rest of the day!",
"Defense": "Defense Roll",
"DefenseResult": "defense successes",
"ArmorRoll": "Armor Roll",
"ArmorBypassed": "Armor bypassed (0 dice — AP ≥ AV)",
"Successes": "successes",
"DualAttr": { "DualAttr": {
"DefenseMelee": "melee defense", "DefenseMelee": "melee defense",
"FightingNimble": "nimble weapon", "FightingNimble": "nimble weapon",
@@ -893,6 +938,9 @@
"Actor": { "Actor": {
"character": "Character", "character": "Character",
"npc": "NPC" "npc": "NPC"
},
"Warning": {
"MiracleBlocked": "Divine favour has been lost — you cannot invoke miracles until a new day."
} }
} }
} }

View File

@@ -76,7 +76,7 @@
border: 1px solid @color-olive; border: 1px solid @color-olive;
border-radius: 3px; border-radius: 3px;
background: rgba(0,0,0,0.15); background: rgba(0,0,0,0.15);
font-size: @font-size-sm; font-size: @font-size-xs;
min-width: 6rem; min-width: 6rem;
min-height: 1.6rem; min-height: 1.6rem;
@@ -97,13 +97,13 @@
.identity-name { .identity-name {
flex: 1; flex: 1;
font-family: @font-secondary; font-family: @font-secondary;
font-size: @font-size-sm; font-size: @font-size-xs;
} }
.slot-icon { font-size: @font-size-sm; opacity: 0.6; } .slot-icon { font-size: @font-size-xs; opacity: 0.8; }
.slot-placeholder { font-size: @font-size-sm; } .slot-placeholder { font-size: @font-size-xs; }
a { font-size: @font-size-sm; opacity: 0.7; &:hover { opacity: 1; } } a { font-size: @font-size-xs; opacity: 0.85; &:hover { opacity: 1; } }
} }
.identity-xp { .identity-xp {
@@ -111,21 +111,22 @@
align-items: center; align-items: center;
gap: 3px; gap: 3px;
margin-left: auto; margin-left: auto;
font-size: @font-size-sm; font-size: @font-size-xs;
.xp-label { .xp-label {
font-family: @font-secondary; font-family: @font-secondary;
font-size: @font-size-sm; font-size: @font-size-xs;
color: @color-olive; font-weight: bold;
color: @color-dark;
margin-left: 4px; margin-left: 4px;
} }
.xp-sep { opacity: 0.6; } .xp-sep { opacity: 0.8; }
input { input {
width: 3rem; width: 3rem;
text-align: center; text-align: center;
font-size: @font-size-sm; font-size: @font-size-xs;
padding: 1px 2px; padding: 1px 2px;
} }
} }
@@ -161,17 +162,18 @@
input { input {
width: 2.4rem; width: 2.4rem;
text-align: center; text-align: center;
font-size: @font-size-sm; font-size: @font-size-xs;
padding: 1px 2px; padding: 1px 2px;
} }
.res-sep { opacity: 0.5; font-size: @font-size-xs; } .res-sep { opacity: 0.7; font-size: @font-size-xs; }
} }
.resource-label { .resource-label {
min-width: 3.8rem; min-width: 4.2rem;
font-family: @font-secondary; font-family: @font-secondary;
font-size: @font-size-xs; font-size: @font-size-base;
font-weight: bold;
color: @color-dark; color: @color-dark;
} }
} }
@@ -204,7 +206,8 @@
label { label {
font-family: @font-secondary; font-family: @font-secondary;
font-size: @font-size-sm; font-size: @font-size-xs;
font-weight: bold;
color: @color-dark; color: @color-dark;
text-align: center; text-align: center;
} }
@@ -311,7 +314,7 @@
width: 2.8rem; width: 2.8rem;
text-align: center; text-align: center;
} }
span { opacity: 0.6; } span { opacity: 0.8; }
} }
// Defense display // Defense display

View File

@@ -39,7 +39,7 @@
.oathhammer .npc-content { .oathhammer .npc-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: var(--color-dark-1); color: @color-dark;
.sheet-background(); .sheet-background();
overflow: auto; overflow: auto;
padding: 10px 20px; // Inner margin so content clears the parchment border padding: 10px 20px; // Inner margin so content clears the parchment border
@@ -53,7 +53,7 @@
select:disabled { select:disabled {
background-color: @color-disabled-bg; background-color: @color-disabled-bg;
border-color: transparent; border-color: transparent;
color: var(--color-dark-3); color: @color-dark;
} }
input, input,
@@ -208,7 +208,8 @@
margin-bottom: 2px; margin-bottom: 2px;
label.skill-name-col { label.skill-name-col {
font-size: @font-size-sm; font-size: @font-size-xs;
font-weight: bold;
} }
.skill-rank-col select, .skill-rank-col select,

View File

@@ -56,7 +56,7 @@
.item-detail { .item-detail {
font-size: @font-size-xs; font-size: @font-size-xs;
color: @color-olive; color: @color-dark;
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -65,7 +65,7 @@
.item-group { .item-group {
font-size: @font-size-xs; font-size: @font-size-xs;
color: @color-olive; color: @color-dark;
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -117,13 +117,13 @@
.item-list--weapon { .item-list--weapon {
.item-list-header, .item-entry { .item-list-header, .item-entry {
grid-template-columns: @item-img-size 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 5.5rem; grid-template-columns: @item-img-size 1fr 4.5rem 3.5rem 2rem 1.5rem 1.8rem 9rem;
} }
} }
.item-list--armor { .item-list--armor {
.item-list-header, .item-entry { .item-list-header, .item-entry {
grid-template-columns: @item-img-size 1fr 3.5rem 2.5rem 3.5rem 1.5rem 1.8rem 3.5rem; grid-template-columns: @item-img-size 1fr 3.5rem 2.5rem 3.5rem 1.5rem 1.8rem 5.5rem;
} }
} }
@@ -145,6 +145,11 @@
} }
} }
.miracles-blocked {
opacity: 0.45;
pointer-events: none;
}
.item-list--equipment { .item-list--equipment {
.item-list-header, .item-entry { .item-list-header, .item-entry {
grid-template-columns: @item-img-size 1fr 5rem 3rem 3.5rem; grid-template-columns: @item-img-size 1fr 5rem 3rem 3.5rem;
@@ -177,7 +182,7 @@
.item-usage { .item-usage {
font-size: @font-size-xs; font-size: @font-size-xs;
color: @color-olive; color: @color-dark;
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -197,7 +202,7 @@
} }
.no-items { .no-items {
color: var(--color-dark-5); color: @color-olive;
font-style: italic; font-style: italic;
font-size: @font-size-xs; font-size: @font-size-xs;
padding: 4px; padding: 4px;
@@ -209,3 +214,62 @@
.transition-opacity(); .transition-opacity();
} }
} }
// Miracle blocked banner on the Magic tab
.miracle-blocked-banner {
display: flex;
align-items: center;
gap: 6px;
background: fade(#c0392b, 10%);
border: 1px solid fade(#c0392b, 45%);
border-radius: 4px;
padding: 4px 10px;
margin-bottom: 4px;
color: #c0392b;
font-size: @font-size-xs;
font-weight: bold;
span { flex: 1; }
a {
color: @color-dark;
font-weight: normal;
font-size: @font-size-sm;
.transition-opacity();
white-space: nowrap;
i { color: @color-gold; }
}
}
// Slots counter on the Combat tab
.slots-counter {
display: flex;
gap: 6px;
padding: 2px 6px 4px;
.slots-label {
font-size: @font-size-xs;
font-weight: bold;
color: @color-dark;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.slots-value {
font-size: @font-size-xs;
font-weight: bold;
color: @color-dark;
background: fade(@color-gold, 15%);
border: 1px solid fade(@color-gold, 40%);
border-radius: 4px;
padding: 1px 8px;
min-width: 3.5rem;
text-align: center;
&.slots-over {
color: #c0392b;
background: fade(#c0392b, 10%);
border-color: fade(#c0392b, 40%);
}
}
}

View File

@@ -133,55 +133,72 @@
} }
} }
// ——— Options block ——— // ——— Option rows — applies to all fieldsets in any roll dialog ———
.roll-option-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 4px 8px;
padding: 5px 0;
&:not(:last-child) {
border-bottom: 1px solid fade(@color-olive, 10%);
}
label {
flex: 0 0 140px;
font-size: @font-size-xs;
color: @color-dark;
white-space: nowrap;
font-family: @font-body;
}
select, input[type="text"], input[type="number"] {
flex: 0 0 110px;
padding: 3px 6px;
border: 1px solid darken(@color-olive-faint, 10%);
border-radius: 3px;
background: rgba(255, 255, 255, 0.85);
color: @color-dark;
font-family: @font-body;
font-size: @font-size-xs;
}
.roll-option-hint {
flex: 1 1 auto;
font-size: calc(@font-size-xs * 0.85);
color: @color-olive;
font-style: italic;
padding-left: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.roll-option-luck {
label { color: @color-gold; font-weight: bold; }
select { border-color: @color-gold; }
.luck-icon { color: @color-gold; font-size: 0.8em; }
}
.roll-option-check {
input[type="checkbox"] {
flex: 0 0 auto;
width: 16px;
height: 16px;
accent-color: @color-blue;
cursor: pointer;
}
label {
cursor: pointer;
flex: 1 1 auto;
}
}
// ——— Options block (legacy scope kept for skill roll dialog) ———
.roll-options-block { .roll-options-block {
.roll-option-row { // inherits .roll-option-row styles from parent scope
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 8px;
padding: 5px 0;
&:not(:last-child) {
border-bottom: 1px solid fade(@color-olive, 10%);
}
label {
flex: 1 1 120px;
font-size: @font-size-xs;
color: @color-dark;
white-space: nowrap;
font-family: @font-body;
}
select {
flex: 0 0 90px;
padding: 3px 6px;
border: 1px solid darken(@color-olive-faint, 10%);
border-radius: 3px;
background: rgba(255, 255, 255, 0.85);
color: @color-dark;
font-family: @font-body;
font-size: @font-size-xs;
}
.roll-option-hint {
flex: 1 1 100%;
font-size: calc(@font-size-xs * 0.85);
color: @color-olive;
font-style: italic;
padding-left: 4px;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
}
}
.roll-option-luck {
label { color: @color-gold; font-weight: bold; }
select { border-color: @color-gold; }
.luck-icon { color: @color-gold; font-size: 0.8em; }
}
} }
// ——— Visibility block ——— // ——— Visibility block ———
@@ -390,19 +407,24 @@
} }
// Attack/damage buttons in weapon list // Attack/damage buttons in weapon list
.item-list--weapon .item-actions { .item-list--weapon .item-entry .item-actions {
gap: 8px; gap: 14px;
a[data-action="attackWeapon"] { a[data-action="attackWeapon"] {
color: @color-blue; color: @color-blue;
font-size: 1.1em; font-size: 1.1em;
.transition-opacity(); .transition-opacity();
} }
a[data-action="defendWeapon"] {
color: @color-olive;
font-size: 1.1em;
.transition-opacity();
}
a[data-action="damageWeapon"] { a[data-action="damageWeapon"] {
color: #8b0000; color: #8b0000;
font-size: 1.1em; font-size: 1.1em;
.transition-opacity(); .transition-opacity();
} }
a[data-action="edit"] { margin-left: 6px; } a[data-action="edit"] { margin-left: 4px; }
} }
// ============================================================ // ============================================================
@@ -550,3 +572,83 @@
a[data-action="castMiracle"] .miracle-cast-icon { color: #5a3000; font-size: 1.05em; } a[data-action="castMiracle"] .miracle-cast-icon { color: #5a3000; font-size: 1.05em; }
a[data-action="edit"] { margin-left: 6px; } a[data-action="edit"] { margin-left: 6px; }
} }
// ============================================================
// DEFENSE / ARMOR DIALOG STYLES
// ============================================================
.oh-defense-dialog,
.oh-armor-dialog {
.oh-trait-info {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
margin: 4px 0;
background: fade(@color-blue, 8%);
border: 1px solid fade(@color-blue, 25%);
border-radius: 4px;
font-size: 0.84em;
color: darken(@color-blue, 10%);
i { flex-shrink: 0; }
}
}
// Defense card in chat
.oh-defense-card,
.oh-armor-card {
.oh-roll-header {
display: flex;
align-items: center;
gap: 8px;
}
.oh-defense-icon {
font-size: 1.1em;
color: @color-blue;
flex-shrink: 0;
}
.oh-card-weapon-img {
width: 28px;
height: 28px;
object-fit: contain;
border-radius: 3px;
border: 1px solid @color-olive-faint;
flex-shrink: 0;
}
}
// Defense roll button in combat tab
.defense-row {
align-items: flex-end;
.defense-roll-group {
flex: 0 0 auto;
}
}
.defense-roll-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: @color-blue;
color: #fff;
border-radius: 4px;
font-family: @font-secondary;
font-size: @font-size-sm;
cursor: pointer;
text-decoration: none;
.transition-opacity();
i { font-size: 0.9em; }
&:hover { opacity: 0.85; color: #fff; text-decoration: none; }
}
// Armor roll icon in armor list
.item-list--armor .item-actions {
gap: 8px;
a[data-action="rollArmorSave"] .armor-roll-icon {
color: @color-blue;
font-size: 1.05em;
}
a[data-action="edit"] { margin-left: 6px; }
}

View File

@@ -77,6 +77,11 @@
background: fade(#e74c3c, 15%); background: fade(#e74c3c, 15%);
color: #c0392b; color: #c0392b;
} }
.roll-opposed {
background: fade(#3498db, 15%);
color: #1a6fa8;
}
} }
// Rollable rarity button on item sheets // Rollable rarity button on item sheets

View File

@@ -47,7 +47,7 @@
} }
.transition-opacity() { .transition-opacity() {
opacity: 0.6; opacity: 0.85;
transition: opacity 0.2s; transition: opacity 0.2s;
&:hover { opacity: 1; } &:hover { opacity: 1; }
} }

View File

@@ -15,3 +15,5 @@ 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"
export { default as OathHammerMiracleDialog } from "./miracle-dialog.mjs" export { default as OathHammerMiracleDialog } from "./miracle-dialog.mjs"
export { default as OathHammerDefenseDialog } from "./defense-dialog.mjs"
export { default as OathHammerArmorDialog } from "./armor-dialog.mjs"

View File

@@ -0,0 +1,70 @@
/**
* Armor roll dialog.
*
* Pool = Armor Value (AV) AP penalty + manual bonus (can go to 0, unlike other pools)
* Reinforced trait on the armor → red dice (3+)
* Each success on the roll reduces incoming damage by 1.
*/
export default class OathHammerArmorDialog {
static async prompt(actor, armor) {
const sys = armor.system
const av = sys.armorValue ?? 0
const isReinforced = [...(sys.traits ?? [])].includes("reinforced")
// AP options — entered by the user based on the attacker's weapon
const apOptions = Array.from({ length: 9 }, (_, i) => ({
value: -i,
label: i === 0 ? "0" : `${i}`,
selected: i === 0,
}))
const bonusOptions = Array.from({ length: 7 }, (_, i) => {
const v = i - 3
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
})
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
const context = {
actorName: actor.name,
armorName: armor.name,
armorImg: armor.img,
av,
isReinforced,
apOptions,
bonusOptions,
rollModes,
visibility: game.settings.get("core", "rollMode"),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-oath-hammer/templates/armor-roll-dialog.hbs",
context
)
const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.ArmorRollTitle", { armor: armor.name }) },
classes: ["fvtt-oath-hammer"],
content,
rejectClose: false,
buttons: [{
label: game.i18n.localize("OATHHAMMER.Dialog.RollArmor"),
callback: (_ev, btn) => Object.fromEntries(
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
),
}],
})
if (!result) return null
return {
av,
isReinforced,
apPenalty: parseInt(result.ap) || 0,
bonus: parseInt(result.bonus) || 0,
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
}
}
}

View File

@@ -0,0 +1,112 @@
/**
* Defense roll dialog.
*
* Pool = governing attribute (Agility default; Might option for melee) + Defense skill
* + armorPenalty (auto from equipped armor, always ≤ 0)
* + parryBonus / blockBonus (from equipped weapon traits)
* + manual bonus
*
* Parry trait on equipped weapon → red dice (3+) vs melee; +1 if two Parry weapons
* Block trait on equipped weapon → red dice (3+) vs ranged; +1 to ranged defense
*/
export default class OathHammerDefenseDialog {
static async prompt(actor) {
const actorSys = actor.system
// ── Attributes & skill ──────────────────────────────────────────────
const agiRank = actorSys.attributes.agility.rank
const mightRank = actorSys.attributes.might.rank
const defRank = actorSys.skills.defense.rank
// ── Equipped weapons ────────────────────────────────────────────────
const equipped = actor.items.filter(i => i.type === "weapon" && i.system.equipped)
const parryCount = equipped.filter(w => w.system.traits?.has?.("parry") || [...(w.system.traits ?? [])].includes("parry")).length
const blockCount = equipped.filter(w => w.system.traits?.has?.("block") || [...(w.system.traits ?? [])].includes("block")).length
// ── Equipped armor penalty (sum) ────────────────────────────────────
const armorPenalty = actor.items
.filter(i => i.type === "armor" && i.system.equipped)
.reduce((sum, a) => sum + (a.system.penalty ?? 0), 0)
// ── Build option lists ───────────────────────────────────────────────
const attackTypeOptions = [
{ value: "melee", label: game.i18n.localize("OATHHAMMER.Dialog.DefenseMelee"), selected: true },
{ value: "ranged", label: game.i18n.localize("OATHHAMMER.Dialog.DefenseRanged"), selected: false },
]
const attrOptions = [
{ value: "agility", label: `${game.i18n.localize("OATHHAMMER.Attribute.Agility")} (${agiRank})`, selected: true },
{ value: "might", label: `${game.i18n.localize("OATHHAMMER.Attribute.Might")} (${mightRank})`, selected: false },
]
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
const v = i - 6
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
})
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
const context = {
actorName: actor.name,
agiRank,
mightRank,
defRank,
parryCount,
blockCount,
armorPenalty,
attackTypeOptions,
attrOptions,
bonusOptions,
rollModes,
visibility: game.settings.get("core", "rollMode"),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-oath-hammer/templates/defense-roll-dialog.hbs",
context
)
const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.DefenseTitle", { actor: actor.name }) },
classes: ["fvtt-oath-hammer"],
content,
rejectClose: false,
buttons: [{
label: game.i18n.localize("OATHHAMMER.Dialog.RollDefense"),
callback: (_ev, btn) => Object.fromEntries(
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
),
}],
})
if (!result) return null
const attackType = result.attackType ?? "melee"
const attrChoice = result.attribute ?? "agility"
const attrRank = attrChoice === "might" ? mightRank : agiRank
const bonus = parseInt(result.bonus) || 0
// Determine red dice and trait bonus from equipped weapons
let redDice = false
let traitBonus = 0
if (attackType === "melee" && parryCount > 0) {
redDice = true
if (parryCount >= 2) traitBonus = 1
} else if (attackType === "ranged" && blockCount > 0) {
redDice = true
traitBonus = 1
}
return {
attackType,
attrRank,
attrChoice,
redDice,
traitBonus,
armorPenalty,
bonus,
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
}
}
}

View File

@@ -68,9 +68,10 @@ export default class OathHammerRollDialog {
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
// Build select option arrays // Build select option arrays
const dvOptions = Array.from({ length: 10 }, (_, i) => { const dvOptions = Array.from({ length: 11 }, (_, i) => {
const v = i + 1 const v = i // 0..10
return { value: v, label: String(v), selected: v === 2 } const label = v === 0 ? "0 (opposed)" : String(v)
return { value: v, label, selected: v === 2 }
}) })
const bonusOptions = Array.from({ length: 13 }, (_, i) => { const bonusOptions = Array.from({ length: 13 }, (_, i) => {
@@ -130,7 +131,8 @@ export default class OathHammerRollDialog {
callback: (_event, button) => { callback: (_event, button) => {
const out = {} const out = {}
for (const el of button.form.elements) { for (const el of button.form.elements) {
if (el.name) out[el.name] = el.value if (!el.name) continue
out[el.name] = el.type === "checkbox" ? String(el.checked) : el.value
} }
return out return out
}, },
@@ -143,10 +145,11 @@ export default class OathHammerRollDialog {
const attrOverride = result.attrOverride || defaultAttrKey const attrOverride = result.attrOverride || defaultAttrKey
return { return {
dv: Math.max(1, parseInt(result.dv) || 2), dv: Math.max(0, parseInt(result.dv) ?? 2),
bonus: parseInt(result.bonus) || 0, bonus: parseInt(result.bonus) || 0,
luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck), luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck),
supporters: Math.max(0, parseInt(result.supporters) || 0), supporters: Math.max(0, parseInt(result.supporters) || 0),
explodeOn5: result.explodeOn5 === "true",
attrOverride, attrOverride,
visibility: result.visibility ?? game.settings.get("core", "rollMode"), visibility: result.visibility ?? game.settings.get("core", "rollMode"),
} }

View File

@@ -60,6 +60,16 @@ export default class OathHammerActorSheet extends HandlebarsApplicationMixin(fou
/** @override */ /** @override */
_onRender(context, options) { _onRender(context, options) {
this.#dragDrop.forEach((d) => d.bind(this.element)) this.#dragDrop.forEach((d) => d.bind(this.element))
// ProseMirror "Save" dispatches a change event before committing its .value
// to the element, so FormDataExtended may read stale HTML. Instead we
// intercept the event here, stop it from bubbling to the submitOnChange
// handler, and update the document directly with the current editor value.
for (const pm of this.element.querySelectorAll("prose-mirror[name]")) {
pm.addEventListener("change", async (event) => {
event.stopPropagation()
await this.document.update({ [pm.name]: pm.value ?? "" })
})
}
} }
#createDragDropHandlers() { #createDragDropHandlers() {
@@ -89,6 +99,10 @@ export default class OathHammerActorSheet extends HandlebarsApplicationMixin(fou
_onDragStart(event) { _onDragStart(event) {
if ("link" in event.target.dataset) return if ("link" in event.target.dataset) return
const li = event.target.closest("[data-item-uuid]")
if (!li) return
const dragData = { type: "Item", uuid: li.dataset.itemUuid }
event.dataTransfer.setData("text/plain", JSON.stringify(dragData))
} }
_onDragOver(event) {} _onDragOver(event) {}

View File

@@ -81,6 +81,12 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
_onRender(context, options) { _onRender(context, options) {
super._onRender(context, options) super._onRender(context, options)
this.#dragDrop.forEach((d) => d.bind(this.element)) this.#dragDrop.forEach((d) => d.bind(this.element))
for (const pm of this.element.querySelectorAll("prose-mirror[name]")) {
pm.addEventListener("change", async (event) => {
event.stopPropagation()
await this.document.update({ [pm.name]: pm.value ?? "" })
})
}
} }
#createDragDropHandlers() { #createDragDropHandlers() {

View File

@@ -4,7 +4,9 @@ import OathHammerRollDialog from "../roll-dialog.mjs"
import OathHammerWeaponDialog from "../weapon-dialog.mjs" import OathHammerWeaponDialog from "../weapon-dialog.mjs"
import OathHammerSpellDialog from "../spell-dialog.mjs" import OathHammerSpellDialog from "../spell-dialog.mjs"
import OathHammerMiracleDialog from "../miracle-dialog.mjs" import OathHammerMiracleDialog from "../miracle-dialog.mjs"
import { rollSkillCheck, rollWeaponAttack, rollWeaponDamage, rollSpellCast, rollMiracleCast } from "../../rolls.mjs" import OathHammerDefenseDialog from "../defense-dialog.mjs"
import OathHammerArmorDialog from "../armor-dialog.mjs"
import { rollSkillCheck, rollWeaponAttack, rollWeaponDamage, rollSpellCast, rollMiracleCast, rollArmorSave, rollWeaponDefense } from "../../rolls.mjs"
export default class OathHammerCharacterSheet extends OathHammerActorSheet { export default class OathHammerCharacterSheet extends OathHammerActorSheet {
/** @override */ /** @override */
@@ -22,11 +24,15 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
createSpell: OathHammerCharacterSheet.#onCreateSpell, createSpell: OathHammerCharacterSheet.#onCreateSpell,
createMiracle: OathHammerCharacterSheet.#onCreateMiracle, createMiracle: OathHammerCharacterSheet.#onCreateMiracle,
createEquipment: OathHammerCharacterSheet.#onCreateEquipment, createEquipment: OathHammerCharacterSheet.#onCreateEquipment,
createTrait: OathHammerCharacterSheet.#onCreateTrait,
rollSkill: OathHammerCharacterSheet.#onRollSkill, rollSkill: OathHammerCharacterSheet.#onRollSkill,
attackWeapon: OathHammerCharacterSheet.#onAttackWeapon, attackWeapon: OathHammerCharacterSheet.#onAttackWeapon,
defendWeapon: OathHammerCharacterSheet.#onDefendWeapon,
damageWeapon: OathHammerCharacterSheet.#onDamageWeapon, damageWeapon: OathHammerCharacterSheet.#onDamageWeapon,
castSpell: OathHammerCharacterSheet.#onCastSpell, castSpell: OathHammerCharacterSheet.#onCastSpell,
castMiracle: OathHammerCharacterSheet.#onCastMiracle, castMiracle: OathHammerCharacterSheet.#onCastMiracle,
rollArmorSave: OathHammerCharacterSheet.#onRollArmorSave,
resetMiracleBlocked: OathHammerCharacterSheet.#onResetMiracleBlocked,
}, },
} }
@@ -101,6 +107,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
_violated: o.system.violated _violated: o.system.violated
} }
}) })
context.enrichedBackground = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.background ?? "", { async: true })
break break
case "skills": { case "skills": {
context.tab = context.tabs.skills context.tab = context.tabs.skills
@@ -170,6 +177,10 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
} }
}) })
context.ammunition = doc.itemTypes.ammunition context.ammunition = doc.itemTypes.ammunition
// Slot tracking: max = 10 + (Might rank × 2); used = sum of all items' slots
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2)
context.slotsUsed = doc.items.reduce((sum, item) => sum + (item.system.slots ?? 0), 0)
context.slotsOver = context.slotsUsed > context.slotsMax
break break
case "magic": case "magic":
context.tab = context.tabs.magic context.tab = context.tabs.magic
@@ -180,12 +191,14 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
context.tab = context.tabs.equipment context.tab = context.tabs.equipment
context.equipment = doc.itemTypes.equipment context.equipment = doc.itemTypes.equipment
context.magicItems = doc.itemTypes["magic-item"] context.magicItems = doc.itemTypes["magic-item"]
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2)
context.slotsUsed = doc.items.reduce((sum, item) => sum + (item.system.slots ?? 0), 0)
context.slotsOver = context.slotsUsed > context.slotsMax
break break
case "notes": case "notes":
context.tab = context.tabs.notes context.tab = context.tabs.notes
context.enrichedBackground = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.background, { async: true }) context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.description ?? "", { async: true })
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.description, { async: true }) context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes ?? "", { async: true })
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.notes, { async: true })
break break
} }
return context return context
@@ -246,6 +259,10 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Equipment"), type: "equipment" }]) this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Equipment"), type: "equipment" }])
} }
static #onCreateTrait(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Trait"), type: "trait" }])
}
static async #onRollSkill(event, target) { static async #onRollSkill(event, target) {
const skillKey = target.dataset.skill const skillKey = target.dataset.skill
if (!skillKey) return if (!skillKey) return
@@ -264,6 +281,16 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
await rollWeaponAttack(this.document, weapon, opts) await rollWeaponAttack(this.document, weapon, opts)
} }
static async #onDefendWeapon(event, target) {
const weaponId = target.dataset.itemId
if (!weaponId) return
const weapon = this.document.items.get(weaponId)
if (!weapon) return
const opts = await OathHammerWeaponDialog.promptDefense(this.document, weapon)
if (!opts) return
await rollWeaponDefense(this.document, weapon, opts)
}
static async #onDamageWeapon(event, target) { static async #onDamageWeapon(event, target) {
const weaponId = target.dataset.itemId const weaponId = target.dataset.itemId
if (!weaponId) return if (!weaponId) return
@@ -289,8 +316,26 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
if (!miracleId) return if (!miracleId) return
const miracle = this.document.items.get(miracleId) const miracle = this.document.items.get(miracleId)
if (!miracle) return if (!miracle) return
if (this.document.system.miracleBlocked) {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Warning.MiracleBlocked"))
return
}
const opts = await OathHammerMiracleDialog.prompt(this.document, miracle) const opts = await OathHammerMiracleDialog.prompt(this.document, miracle)
if (!opts) return if (!opts) return
await rollMiracleCast(this.document, miracle, opts) await rollMiracleCast(this.document, miracle, opts)
} }
static async #onResetMiracleBlocked() {
await this.document.update({ "system.miracleBlocked": false })
}
static async #onRollArmorSave(event, target) {
const armorId = target.dataset.itemId
if (!armorId) return
const armor = this.document.items.get(armorId)
if (!armor) return
const opts = await OathHammerArmorDialog.prompt(this.document, armor)
if (!opts) return
await rollArmorSave(this.document, armor, opts)
}
} }

View File

@@ -28,15 +28,26 @@ export default class OathHammerClassSheet extends OathHammerItemSheet {
return context return context
} }
/** @override — collect checkbox sets explicitly so unchecking all works */ /** @override */
_prepareSubmitData(event, form, formData) { _onRender(context, options) {
const data = super._prepareSubmitData(event, form, formData) super._onRender(context, options)
data["system.armorProficiency"] = Array.from( // Handle proficiency checkboxes directly — FormDataExtended mishandles
form.querySelectorAll('input[name="system.armorProficiency"]:checked') // multiple same-named checkboxes, so we intercept the change event,
).map(el => el.value) // collect all checked values ourselves, and stop propagation to prevent
data["system.weaponProficiency"] = Array.from( // the generic submitOnChange handler from clobbering the data.
form.querySelectorAll('input[name="system.weaponProficiency"]:checked') for (const cb of this.element.querySelectorAll('.proficiency-checkboxes input[type="checkbox"]')) {
).map(el => el.value) cb.addEventListener("change", this.#onProficiencyChange.bind(this))
return data }
}
async #onProficiencyChange(event) {
event.stopPropagation()
const root = this.element
const armorProficiency = [...root.querySelectorAll('input[name="system.armorProficiency"]:checked')].map(e => e.value)
const weaponProficiency = [...root.querySelectorAll('input[name="system.weaponProficiency"]:checked')].map(e => e.value)
await this.document.update({
"system.armorProficiency": armorProficiency,
"system.weaponProficiency": weaponProficiency,
})
} }
} }

View File

@@ -129,6 +129,143 @@ export default class OathHammerWeaponDialog {
} }
} }
// ------------------------------------------------------------------ //
// DEFENSE DIALOG
// ------------------------------------------------------------------ //
/**
* Show the weapon defense dialog and return resolved options.
*
* Defense pool = Agility (or Might) + Defense skill + trait bonuses + armor penalty + diminish penalty + bonus
*
* Parry trait → red dice vs melee; +1 if two Parry weapons equipped
* Block trait → red dice vs ranged; +1 bonus always
* Diminishing defense: -2 per additional defense after the first in a turn
*/
static async promptDefense(actor, weapon) {
const sys = weapon.system
const actorSys = actor.system
const agiRank = actorSys.attributes.agility.rank
const mightRank = actorSys.attributes.might.rank
const defRank = actorSys.skills.defense.rank
// Detect this weapon's defense-relevant traits
const hasParry = sys.traits.has("parry")
const hasBlock = sys.traits.has("block")
// Count all equipped parry/block weapons (for +1 with two Parry weapons)
const equipped = actor.items.filter(i => i.type === "weapon" && i.system.equipped)
const parryCount = equipped.filter(w => w.system.traits?.has?.("parry") || [...(w.system.traits ?? [])].includes("parry")).length
const blockCount = equipped.filter(w => w.system.traits?.has?.("block") || [...(w.system.traits ?? [])].includes("block")).length
// Armor penalty from all equipped armors
const armorPenalty = actor.items
.filter(i => i.type === "armor" && i.system.equipped)
.reduce((sum, a) => sum + (a.system.penalty ?? 0), 0)
// Pre-select attack type: block weapons default to ranged, parry to melee
const defaultAttackType = hasBlock && !hasParry ? "ranged" : "melee"
const traitLabels = [...sys.traits].map(t => {
const key = SYSTEM.WEAPON_TRAITS[t]
return key ? game.i18n.localize(key) : t
})
const attackTypeOptions = [
{ value: "melee", label: game.i18n.localize("OATHHAMMER.Dialog.DefenseMelee"), selected: defaultAttackType === "melee" },
{ value: "ranged", label: game.i18n.localize("OATHHAMMER.Dialog.DefenseRanged"), selected: defaultAttackType === "ranged" },
]
const attrOptions = [
{ value: "agility", label: `${game.i18n.localize("OATHHAMMER.Attribute.Agility")} (${agiRank})`, selected: true },
{ value: "might", label: `${game.i18n.localize("OATHHAMMER.Attribute.Might")} (${mightRank})`, selected: false },
]
const diminishOptions = [
{ value: 0, label: game.i18n.localize("OATHHAMMER.Dialog.DiminishFirst"), selected: true },
{ value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.DiminishSecond"), selected: false },
{ value: -4, label: game.i18n.localize("OATHHAMMER.Dialog.DiminishThird"), selected: false },
]
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
const v = i - 6
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
})
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
const context = {
actorName: actor.name,
weaponName: weapon.name,
weaponImg: weapon.img,
agiRank,
mightRank,
defRank,
hasParry,
hasBlock,
parryCount,
blockCount,
armorPenalty,
traits: traitLabels,
attackTypeOptions,
attrOptions,
diminishOptions,
bonusOptions,
rollModes,
visibility: game.settings.get("core", "rollMode"),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-oath-hammer/templates/weapon-defense-dialog.hbs",
context
)
const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.WeaponDefenseTitle", { weapon: weapon.name }) },
classes: ["fvtt-oath-hammer"],
content,
rejectClose: false,
buttons: [{
label: game.i18n.localize("OATHHAMMER.Dialog.RollDefense"),
callback: (_ev, btn) => Object.fromEntries(
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
),
}],
})
if (!result) return null
const attackType = result.attackType ?? defaultAttackType
const attrChoice = result.attribute ?? "agility"
const attrRank = attrChoice === "might" ? mightRank : agiRank
const diminishPenalty = parseInt(result.diminish) || 0
const bonus = parseInt(result.bonus) || 0
// Resolve red dice and trait bonus based on selected attack type
let redDice = false
let traitBonus = 0
if (attackType === "melee" && hasParry) {
redDice = true
traitBonus = parryCount >= 2 ? 1 : 0
} else if (attackType === "ranged" && hasBlock) {
redDice = true
traitBonus = 1
}
return {
attackType,
attrRank,
attrChoice,
redDice,
traitBonus,
armorPenalty,
diminishPenalty,
bonus,
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
}
}
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //
// DAMAGE DIALOG // DAMAGE DIALOG
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //

View File

@@ -82,6 +82,8 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
threshold: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 }) threshold: new fields.NumberField({ ...requiredInteger, initial: 10, min: 0 })
}) })
schema.miracleBlocked = new fields.BooleanField({ required: true, initial: false })
schema.movement = new fields.SchemaField({ schema.movement = new fields.SchemaField({
base: new fields.NumberField({ ...requiredInteger, initial: 30, min: 0 }), base: new fields.NumberField({ ...requiredInteger, initial: 30, min: 0 }),
adjusted: new fields.NumberField({ ...requiredInteger, initial: 30, min: 0 }) adjusted: new fields.NumberField({ ...requiredInteger, initial: 30, min: 0 })

View File

@@ -24,7 +24,7 @@ import { SYSTEM } from "./config/system.mjs"
* @returns {Promise<{successes: number, dv: number, isSuccess: boolean}>} * @returns {Promise<{successes: number, dv: number, isSuccess: boolean}>}
*/ */
export async function rollSkillCheck(actor, skillKey, dv, options = {}) { export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
const { bonus = 0, luckSpend = 0, supporters = 0, attrOverride, visibility, flavor } = options const { bonus = 0, luckSpend = 0, supporters = 0, attrOverride, visibility, flavor, explodeOn5 = false } = options
const sys = actor.system const sys = actor.system
const skillDef = SYSTEM.SKILLS[skillKey] const skillDef = SYSTEM.SKILLS[skillKey]
@@ -52,7 +52,8 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
// Roll the dice pool // Roll the dice pool
const roll = await new Roll(`${totalDice}d6`).evaluate() const roll = await new Roll(`${totalDice}d6`).evaluate()
// Count successes — exploding 6s produce additional dice // Count successes — exploding dice produce additional dice
const explodeThreshold = explodeOn5 ? 5 : 6
let successes = 0 let successes = 0
const diceResults = [] const diceResults = []
let extraDice = 0 let extraDice = 0
@@ -60,7 +61,7 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
for (const r of roll.dice[0].results) { for (const r of roll.dice[0].results) {
const val = r.result const val = r.result
if (val >= threshold) successes++ if (val >= threshold) successes++
if (val === 6) extraDice++ if (val >= explodeThreshold) extraDice++
diceResults.push({ val, exploded: false }) diceResults.push({ val, exploded: false })
} }
@@ -70,12 +71,13 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
for (const r of xRoll.dice[0].results) { for (const r of xRoll.dice[0].results) {
const val = r.result const val = r.result
if (val >= threshold) successes++ if (val >= threshold) successes++
if (val === 6) extraDice++ if (val >= explodeThreshold) extraDice++
diceResults.push({ val, exploded: true }) diceResults.push({ val, exploded: true })
} }
} }
const isSuccess = successes >= dv const isOpposed = dv === 0
const isSuccess = isOpposed ? null : successes >= dv
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
const skillLabel = game.i18n.localize(skillDef.label) const skillLabel = game.i18n.localize(skillDef.label)
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrKey.charAt(0).toUpperCase()}${attrKey.slice(1)}`) const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrKey.charAt(0).toUpperCase()}${attrKey.slice(1)}`)
@@ -89,18 +91,26 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
}).join(" ") }).join(" ")
// Build modifier summary // Build modifier summary
const explodedCount = diceResults.filter(d => d.exploded).length
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 * 2} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP)`) if (luckSpend > 0) modParts.push(`+${luckSpend * 2} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP)`)
if (supporters > 0) modParts.push(`+${supporters} ${game.i18n.localize("OATHHAMMER.Dialog.Supporters")}`) if (supporters > 0) modParts.push(`+${supporters} ${game.i18n.localize("OATHHAMMER.Dialog.Supporters")}`)
if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${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>` : ""
const resultClass = isSuccess ? "roll-success" : "roll-failure" const resultClass = isOpposed ? "roll-opposed" : isSuccess ? "roll-success" : "roll-failure"
const resultLabel = isSuccess const resultLabel = isOpposed
? game.i18n.localize("OATHHAMMER.Roll.Success") ? game.i18n.localize("OATHHAMMER.Roll.Opposed")
: game.i18n.localize("OATHHAMMER.Roll.Failure") : isSuccess
? game.i18n.localize("OATHHAMMER.Roll.Success")
: game.i18n.localize("OATHHAMMER.Roll.Failure")
const cardFlavor = flavor ?? `${skillLabel} ${game.i18n.localize("OATHHAMMER.Roll.Check")} (DV ${dv})` const cardFlavor = flavor ?? (isOpposed
? `${skillLabel} ${game.i18n.localize("OATHHAMMER.Roll.Check")} (${game.i18n.localize("OATHHAMMER.Roll.Opposed")})`
: `${skillLabel} ${game.i18n.localize("OATHHAMMER.Roll.Check")} (DV ${dv})`)
const successDisplay = isOpposed ? String(successes) : `${successes} / ${dv}`
const content = ` const content = `
<div class="oh-roll-card"> <div class="oh-roll-card">
@@ -112,7 +122,7 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
${modLine} ${modLine}
<div class="oh-roll-dice">${diceHtml}</div> <div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result ${resultClass}"> <div class="oh-roll-result ${resultClass}">
<span class="oh-roll-successes">${successes} / ${dv}</span> <span class="oh-roll-successes">${successDisplay}</span>
<span class="oh-roll-verdict">${resultLabel}</span> <span class="oh-roll-verdict">${resultLabel}</span>
</div> </div>
</div> </div>
@@ -549,7 +559,247 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
ChatMessage.applyRollMode(msgData, rollMode) ChatMessage.applyRollMode(msgData, rollMode)
await ChatMessage.create(msgData) await ChatMessage.create(msgData)
if (!isSuccess) {
await actor.update({ "system.miracleBlocked": true })
}
return { successes, dv, isSuccess } return { successes, dv, isSuccess }
} }
function _cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : "" } function _cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : "" }
// ============================================================
// DEFENSE ROLL
// ============================================================
/**
* Roll a defense check (Agility/Might + Defense skill) and post to chat.
*
* @param {Actor} actor
* @param {object} options From OathHammerDefenseDialog.prompt()
*/
export async function rollDefense(actor, options = {}) {
const {
attackType = "melee",
attrRank = 0,
attrChoice = "agility",
redDice = false,
traitBonus = 0,
armorPenalty = 0,
bonus = 0,
visibility,
} = options
const defRank = actor.system.skills.defense.rank
const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + bonus, 1)
const threshold = redDice ? 3 : 4
const colorEmoji = redDice ? "🔴" : "⬜"
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
const diceHtml = _diceHtml(diceResults, threshold)
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrChoice.charAt(0).toUpperCase() + attrChoice.slice(1)}`)
const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Defense")
const typeLabel = game.i18n.localize(attackType === "ranged" ? "OATHHAMMER.Dialog.DefenseRanged" : "OATHHAMMER.Dialog.DefenseMelee")
const modParts = []
if (traitBonus > 0) modParts.push(`+${traitBonus} ${game.i18n.localize(attackType === "melee" ? "OATHHAMMER.WeaponTrait.Parry" : "OATHHAMMER.WeaponTrait.Block")}`)
if (armorPenalty < 0) modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`)
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
<div class="oh-roll-card oh-defense-card">
<div class="oh-roll-header">
<i class="fa-solid fa-shield-halved oh-defense-icon"></i>
<span>${game.i18n.localize("OATHHAMMER.Roll.Defense")}${typeLabel}</span>
</div>
<div class="oh-roll-info">
<span>${attrLabel} ${attrRank} + ${skillLabel} ${defRank}</span>
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result">
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
<span class="oh-roll-verdict">${game.i18n.localize("OATHHAMMER.Roll.DefenseResult")}</span>
</div>
</div>
`
const rollMode = visibility ?? game.settings.get("core", "rollMode")
const msgData = {
speaker: ChatMessage.getSpeaker({ actor }),
content,
rolls: [roll],
sound: CONFIG.sounds.dice,
}
ChatMessage.applyRollMode(msgData, rollMode)
await ChatMessage.create(msgData)
return { successes }
}
// ============================================================
// WEAPON DEFENSE ROLL
// ============================================================
/**
* Roll a defense check triggered from a specific weapon, applying the
* weapon's Parry / Block traits, and post to chat.
*
* Pool = (Agility or Might) + Defense skill
* + traitBonus (Parry: +1 if two Parry weapons; Block: +1 vs ranged)
* + armorPenalty (≤ 0)
* + diminishPenalty (0 / 2 / 4 for 1st / 2nd / 3rd+ defense)
* + bonus
*
* Parry trait → red dice (3+) when defending vs melee attacks
* Block trait → red dice (3+) + +1 bonus when defending vs ranged attacks
*
* @param {Actor} actor
* @param {Item} weapon The weapon used to defend
* @param {object} options From OathHammerWeaponDialog.promptDefense()
*/
export async function rollWeaponDefense(actor, weapon, options = {}) {
const {
attackType = "melee",
attrRank = 0,
attrChoice = "agility",
redDice = false,
traitBonus = 0,
armorPenalty = 0,
diminishPenalty = 0,
bonus = 0,
visibility,
} = options
const defRank = actor.system.skills.defense.rank
const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus, 1)
const threshold = redDice ? 3 : 4
const colorEmoji = redDice ? "🔴" : "⬜"
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
const diceHtml = _diceHtml(diceResults, threshold)
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${_cap(attrChoice)}`)
const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Defense")
const typeLabel = game.i18n.localize(attackType === "ranged" ? "OATHHAMMER.Dialog.DefenseRanged" : "OATHHAMMER.Dialog.DefenseMelee")
const modParts = []
if (traitBonus > 0) modParts.push(`+${traitBonus} ${game.i18n.localize(attackType === "melee" ? "OATHHAMMER.WeaponTrait.Parry" : "OATHHAMMER.WeaponTrait.Block")}`)
if (armorPenalty < 0) modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`)
if (diminishPenalty < 0) modParts.push(`${diminishPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.DiminishingDefense")}`)
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
<div class="oh-roll-card oh-defense-card">
<div class="oh-roll-header">
<img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
<span>${weapon.name}${game.i18n.localize("OATHHAMMER.Roll.Defense")} (${typeLabel})</span>
</div>
<div class="oh-roll-info">
<span>${attrLabel} ${attrRank} + ${skillLabel} ${defRank}</span>
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result">
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
<span class="oh-roll-verdict">${game.i18n.localize("OATHHAMMER.Roll.DefenseResult")}</span>
</div>
</div>
`
const rollMode = visibility ?? game.settings.get("core", "rollMode")
const msgData = {
speaker: ChatMessage.getSpeaker({ actor }),
content,
rolls: [roll],
sound: CONFIG.sounds.dice,
}
ChatMessage.applyRollMode(msgData, rollMode)
await ChatMessage.create(msgData)
return { successes }
}
// ============================================================
// ARMOR ROLL
// ============================================================
/**
* Roll an armor saving roll (AV dice AP) and post to chat.
* Unlike other rolls, AP can reduce the pool to 0 (armor bypassed).
* Each success reduces incoming damage by 1.
*
* @param {Actor} actor
* @param {Item} armor
* @param {object} options From OathHammerArmorDialog.prompt()
*/
export async function rollArmorSave(actor, armor, options = {}) {
const {
av = armor.system.armorValue ?? 0,
isReinforced = false,
apPenalty = 0,
bonus = 0,
visibility,
} = options
// Armor CAN be reduced to 0 dice (fully bypassed by AP)
const totalDice = Math.max(av + apPenalty + bonus, 0)
const threshold = isReinforced ? 3 : 4
const colorEmoji = isReinforced ? "🔴" : "⬜"
let successes = 0
let diceHtml = `<em>${game.i18n.localize("OATHHAMMER.Roll.ArmorBypassed")}</em>`
let roll
if (totalDice > 0) {
const result = await _rollPool(totalDice, threshold)
roll = result.roll
successes = result.successes
diceHtml = _diceHtml(result.diceResults, threshold)
} else {
// Zero dice — create a dummy roll with no results so Foundry can still attach it
roll = new Roll("0d6")
await roll.evaluate()
}
const modParts = []
if (apPenalty < 0) modParts.push(`AP ${apPenalty}`)
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
<div class="oh-roll-card oh-armor-card">
<div class="oh-roll-header">
<img src="${armor.img}" class="oh-card-weapon-img" alt="${armor.name}" />
<span>${armor.name} (AV ${av})</span>
</div>
<div class="oh-roll-info">
<span>${game.i18n.localize("OATHHAMMER.Roll.ArmorRoll")}</span>
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result ${successes > 0 ? "roll-success" : ""}">
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
<span class="oh-roll-verdict">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Damage")}</span>
</div>
</div>
`
const rollMode = visibility ?? game.settings.get("core", "rollMode")
const msgData = {
speaker: ChatMessage.getSpeaker({ actor }),
content,
rolls: [roll],
sound: CONFIG.sounds.dice,
}
ChatMessage.applyRollMode(msgData, rollMode)
await ChatMessage.create(msgData)
return { successes, totalDice }
}

View File

@@ -25,6 +25,7 @@
"character": { "character": {
"htmlFields": [ "htmlFields": [
"description", "description",
"background",
"notes", "notes",
"lineage.traits" "lineage.traits"
] ]
@@ -32,6 +33,7 @@
"npc": { "npc": {
"htmlFields": [ "htmlFields": [
"description", "description",
"background",
"notes" "notes"
] ]
} }

View File

@@ -1,21 +1,8 @@
<section data-tab="combat" data-group="{{tab.group}}" class="tab {{tab.cssClass}}"> <section data-tab="combat" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset> <div class="slots-counter">
<legend>{{localize "OATHHAMMER.Label.Defense"}}</legend> <span class="slots-label">{{localize "OATHHAMMER.Label.ItemSlots"}}</span>
<div class="flexrow"> <span class="slots-value {{#if slotsOver}}slots-over{{/if}}">{{slotsUsed}} / {{slotsMax}}</span>
<div class="form-group"> </div>
<label>{{localize "OATHHAMMER.Label.DefenseValue"}}</label>
<input type="text" value="{{system.defense.value}}" disabled />
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.ArmorRating"}}</label>
{{formInput systemFields.defense.fields.armorRating value=system.defense.armorRating name="system.defense.armorRating" disabled=isPlayMode}}
</div>
<div class="form-group">
<label>{{localize "OATHHAMMER.Label.DefenseBonus"}}</label>
{{formInput systemFields.defense.fields.bonus value=system.defense.bonus name="system.defense.bonus" disabled=isPlayMode}}
</div>
</div>
</fieldset>
<fieldset> <fieldset>
<legend>{{localize "OATHHAMMER.Label.Weapons"}} <legend>{{localize "OATHHAMMER.Label.Weapons"}}
{{#unless isPlayMode}}<a data-action="createWeapon" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}} {{#unless isPlayMode}}<a data-action="createWeapon" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
@@ -41,10 +28,11 @@
<span class="item-detail">{{#if weapon.system.ap}}{{weapon.system.ap}}{{else}}{{/if}}</span> <span class="item-detail">{{#if weapon.system.ap}}{{weapon.system.ap}}{{else}}{{/if}}</span>
<span class="item-magic">{{#if weapon._isMagic}}<i class="fa-solid fa-wand-sparkles"></i>{{/if}}</span> <span class="item-magic">{{#if weapon._isMagic}}<i class="fa-solid fa-wand-sparkles"></i>{{/if}}</span>
<div class="item-equipped"> <div class="item-equipped">
<input type="checkbox" class="item-equipped-cb" data-item-id="{{weapon.id}}" {{checked weapon.system.equipped}} {{#if ../isPlayMode}}disabled{{/if}}> <input type="checkbox" class="item-equipped-cb" data-item-id="{{weapon.id}}" {{checked weapon.system.equipped}}>
</div> </div>
<div class="item-actions"> <div class="item-actions">
<a data-action="attackWeapon" data-item-id="{{weapon.id}}" title="{{localize "OATHHAMMER.Dialog.Attack"}}"><i class="fa-solid fa-khanda"></i></a> <a data-action="attackWeapon" data-item-id="{{weapon.id}}" title="{{localize "OATHHAMMER.Dialog.Attack"}}"><i class="fa-solid fa-khanda"></i></a>
<a data-action="defendWeapon" data-item-id="{{weapon.id}}" title="{{localize "OATHHAMMER.Dialog.RollDefense"}}"><i class="fa-solid fa-shield-halved"></i></a>
<a data-action="damageWeapon" data-item-id="{{weapon.id}}" title="{{localize "OATHHAMMER.Dialog.Damage"}}"><i class="fa-solid fa-burst"></i></a> <a data-action="damageWeapon" data-item-id="{{weapon.id}}" title="{{localize "OATHHAMMER.Dialog.Damage"}}"><i class="fa-solid fa-burst"></i></a>
<a data-action="edit" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-edit"></i></a> <a data-action="edit" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-trash"></i></a> <a data-action="delete" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}"><i class="fa-solid fa-trash"></i></a>
@@ -80,9 +68,10 @@
<span class="item-detail">{{#if armor.system.penalty}}{{armor.system.penalty}}{{else}}{{/if}}</span> <span class="item-detail">{{#if armor.system.penalty}}{{armor.system.penalty}}{{else}}{{/if}}</span>
<span class="item-magic">{{#if armor._isMagic}}<i class="fa-solid fa-wand-sparkles"></i>{{/if}}</span> <span class="item-magic">{{#if armor._isMagic}}<i class="fa-solid fa-wand-sparkles"></i>{{/if}}</span>
<div class="item-equipped"> <div class="item-equipped">
<input type="checkbox" class="item-equipped-cb" data-item-id="{{armor.id}}" {{checked armor.system.equipped}} {{#if ../isPlayMode}}disabled{{/if}}> <input type="checkbox" class="item-equipped-cb" data-item-id="{{armor.id}}" {{checked armor.system.equipped}}>
</div> </div>
<div class="item-actions"> <div class="item-actions">
<a data-action="rollArmorSave" data-item-id="{{armor.id}}" title="{{localize 'OATHHAMMER.Dialog.RollArmor'}}"><i class="fa-solid fa-shield armor-roll-icon"></i></a>
<a data-action="edit" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-edit"></i></a> <a data-action="edit" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-trash"></i></a> <a data-action="delete" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}"><i class="fa-solid fa-trash"></i></a>
</div> </div>

View File

@@ -1,4 +1,8 @@
<section data-tab="equipment" data-group="{{tab.group}}" class="tab {{tab.cssClass}}"> <section data-tab="equipment" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<div class="slots-counter">
<span class="slots-label">{{localize "OATHHAMMER.Label.ItemSlots"}}</span>
<span class="slots-value {{#if slotsOver}}slots-over{{/if}}">{{slotsUsed}} / {{slotsMax}}</span>
</div>
<fieldset class="currency-bar"> <fieldset class="currency-bar">
<legend>{{localize "OATHHAMMER.Label.Currency"}}</legend> <legend>{{localize "OATHHAMMER.Label.Currency"}}</legend>
<div class="flexrow"> <div class="flexrow">

View File

@@ -3,9 +3,11 @@
<legend>{{localize "OATHHAMMER.Label.Background"}}</legend> <legend>{{localize "OATHHAMMER.Label.Background"}}</legend>
{{formInput systemFields.background enriched=enrichedBackground value=system.background name="system.background" toggled=true}} {{formInput systemFields.background enriched=enrichedBackground value=system.background name="system.background" toggled=true}}
</fieldset> </fieldset>
{{#if traits.length}}
<fieldset> <fieldset>
<legend>{{localize "OATHHAMMER.Label.Traits"}}</legend> <legend>{{localize "OATHHAMMER.Label.Traits"}}
{{#unless isPlayMode}}<a data-action="createTrait" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
</legend>
{{#if traits.length}}
<ul class="item-list item-list--trait"> <ul class="item-list item-list--trait">
<li class="item-list-header"> <li class="item-list-header">
<span></span> <span></span>
@@ -15,7 +17,7 @@
<span></span> <span></span>
</li> </li>
{{#each traits as |trait|}} {{#each traits as |trait|}}
<li class="item-entry" data-item-id="{{trait.id}}" data-item-uuid="{{trait.uuid}}"> <li class="item-entry" data-item-id="{{trait.id}}" data-item-uuid="{{trait.uuid}}" data-drag="true">
<img src="{{trait.img}}" class="item-img" /> <img src="{{trait.img}}" class="item-img" />
<span class="item-name">{{trait.name}}</span> <span class="item-name">{{trait.name}}</span>
<span class="item-type">{{trait._typeLabel}}</span> <span class="item-type">{{trait._typeLabel}}</span>
@@ -27,8 +29,10 @@
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
{{else}}
<p class="no-items">{{localize "OATHHAMMER.Label.NoTraits"}}</p>
{{/if}}
</fieldset> </fieldset>
{{/if}}
{{#if oaths.length}} {{#if oaths.length}}
<fieldset> <fieldset>
<legend>{{localize "OATHHAMMER.Label.Oaths"}}</legend> <legend>{{localize "OATHHAMMER.Label.Oaths"}}</legend>

View File

@@ -43,8 +43,15 @@
<legend>{{localize "OATHHAMMER.Label.Miracles"}} <legend>{{localize "OATHHAMMER.Label.Miracles"}}
{{#unless isPlayMode}}<a data-action="createMiracle" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}} {{#unless isPlayMode}}<a data-action="createMiracle" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
</legend> </legend>
{{#if system.miracleBlocked}}
<div class="miracle-blocked-banner">
<i class="fa-solid fa-ban"></i>
<span>{{localize "OATHHAMMER.Label.MiracleBlocked"}}</span>
<a data-action="resetMiracleBlocked" title="{{localize 'OATHHAMMER.Action.ResetMiracleBlocked'}}"><i class="fa-solid fa-sun"></i> {{localize "OATHHAMMER.Action.NewDay"}}</a>
</div>
{{/if}}
{{#if miracles.length}} {{#if miracles.length}}
<ul class="item-list item-list--miracle"> <ul class="item-list item-list--miracle {{#if system.miracleBlocked}}miracles-blocked{{/if}}">
<li class="item-list-header"> <li class="item-list-header">
<span></span> <span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span> <span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>

View File

@@ -0,0 +1,50 @@
<div class="oh-roll-dialog oh-armor-dialog">
{{!-- Armor header --------------------------------------------------------}}
<div class="spell-header">
<img src="{{armorImg}}" class="weapon-img-sm" alt="{{armorName}}" />
<div class="spell-header-info">
<span class="weapon-name-lg">{{armorName}}</span>
<div class="weapon-badges">
<span class="dv-badge">AV {{av}}</span>
{{#if isReinforced}}<span class="ritual-badge">{{localize "OATHHAMMER.ArmorTrait.Reinforced"}} 🔴</span>{{/if}}
</div>
</div>
</div>
<fieldset class="attack-options-block">
<legend>{{localize "OATHHAMMER.Dialog.ArmorRollOptions"}}</legend>
<div class="pool-info-line">
AV {{av}}
{{#if isReinforced}} · {{localize "OATHHAMMER.Dialog.ReinforcedHint"}}{{/if}}
</div>
{{!-- AP penalty from attacker's weapon ----------------------------------}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.APPenalty"}}</label>
<select name="ap">
{{#each apOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.APHint"}}</span>
</div>
{{!-- Manual bonus -------------------------------------------------------}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.Modifier"}}</label>
<select name="bonus">
{{#each bonusOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
</div>
</fieldset>
{{!-- Visibility -----------------------------------------------------------}}
<fieldset class="roll-visibility-block">
<legend>{{localize "OATHHAMMER.Dialog.Visibility"}}</legend>
<select name="visibility">
{{selectOptions rollModes selected=visibility localize=true}}
</select>
</fieldset>
</div>

View File

@@ -0,0 +1,63 @@
<div class="oh-roll-dialog oh-defense-dialog">
{{!-- Pool preview -------------------------------------------------------}}
<div class="pool-info-line">
{{localize "OATHHAMMER.Skill.Defense"}} ({{localize "OATHHAMMER.Attribute.Agility"}} {{agiRank}})
+ {{localize "OATHHAMMER.Label.SkillRank"}} {{defRank}}
{{#if armorPenalty}} {{armorPenalty}} {{localize "OATHHAMMER.Dialog.ArmorPenalty"}}{{/if}}
</div>
{{!-- Trait summary -------------------------------------------------------}}
{{#if parryCount}}
<div class="oh-trait-info">
<i class="fa-solid fa-shield-halved"></i>
{{localize "OATHHAMMER.Dialog.ParryActive"}}{{#if (gte parryCount 2)}} (+1){{/if}}
</div>
{{/if}}
{{#if blockCount}}
<div class="oh-trait-info">
<i class="fa-solid fa-shield-halved"></i>
{{localize "OATHHAMMER.Dialog.BlockActive"}}
</div>
{{/if}}
<fieldset class="attack-options-block">
<legend>{{localize "OATHHAMMER.Dialog.DefenseOptions"}}</legend>
{{!-- Attack type --------------------------------------------------------}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.AttackType"}}</label>
<select name="attackType">
{{#each attackTypeOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
</div>
{{!-- Governing attribute (melee can use Might) --------------------------}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.GoverningAttr"}}</label>
<select name="attribute">
{{#each attrOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.MightMeleeHint"}}</span>
</div>
{{!-- Manual bonus -------------------------------------------------------}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.Modifier"}}</label>
<select name="bonus">
{{#each bonusOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span>
</div>
</fieldset>
{{!-- Visibility -----------------------------------------------------------}}
<fieldset class="roll-visibility-block">
<legend>{{localize "OATHHAMMER.Dialog.Visibility"}}</legend>
<select name="visibility">
{{selectOptions rollModes selected=visibility localize=true}}
</select>
</fieldset>
</div>

View File

@@ -12,7 +12,7 @@
<label class="proficiency-option"> <label class="proficiency-option">
<input type="checkbox" name="system.armorProficiency" <input type="checkbox" name="system.armorProficiency"
value="{{key}}" value="{{key}}"
{{#if (includes system.armorProficiency key)}}checked{{/if}}> {{#if (includes ../system.armorProficiency key)}}checked{{/if}}>
{{localize label}} {{localize label}}
</label> </label>
{{/each}} {{/each}}
@@ -26,7 +26,7 @@
<label class="proficiency-option"> <label class="proficiency-option">
<input type="checkbox" name="system.weaponProficiency" <input type="checkbox" name="system.weaponProficiency"
value="{{key}}" value="{{key}}"
{{#if (includes system.weaponProficiency key)}}checked{{/if}}> {{#if (includes ../system.weaponProficiency key)}}checked{{/if}}>
{{localize label}} {{localize label}}
</label> </label>
{{/each}} {{/each}}

View File

@@ -61,6 +61,12 @@
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.SupportersHint"}}</span> <span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.SupportersHint"}}</span>
</div> </div>
<div class="roll-option-row roll-option-check">
<label for="explodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
<input type="checkbox" id="explodeOn5" name="explodeOn5" value="true" />
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span>
</div>
{{#if availableLuck}} {{#if availableLuck}}
<div class="roll-option-row roll-option-luck"> <div class="roll-option-row roll-option-luck">
<label>{{localize "OATHHAMMER.Dialog.LuckSpend"}} <i class="fa-solid fa-clover luck-icon"></i></label> <label>{{localize "OATHHAMMER.Dialog.LuckSpend"}} <i class="fa-solid fa-clover luck-icon"></i></label>

View File

@@ -0,0 +1,85 @@
<div class="oh-roll-dialog oh-weapon-dialog">
{{!-- Weapon header --}}
<div class="weapon-header">
<img src="{{weaponImg}}" class="weapon-img-sm" alt="{{weaponName}}" />
<div class="weapon-header-info">
<span class="weapon-name-lg">{{weaponName}}</span>
{{#if traits}}
<div class="weapon-traits-row">
{{#each traits}}<span class="trait-tag-sm">{{this}}</span>{{/each}}
</div>
{{/if}}
</div>
</div>
{{!-- Pool preview --}}
<div class="pool-info-line">
{{localize "OATHHAMMER.Skill.Defense"}} ({{localize "OATHHAMMER.Attribute.Agility"}} {{agiRank}})
+ {{localize "OATHHAMMER.Label.SkillRank"}} {{defRank}}
{{#if armorPenalty}} {{armorPenalty}} {{localize "OATHHAMMER.Dialog.ArmorPenalty"}}{{/if}}
</div>
{{!-- Active trait indicators --}}
{{#if hasParry}}
<div class="oh-trait-info">
<i class="fa-solid fa-shield-halved"></i>
{{localize "OATHHAMMER.Dialog.ParryActive"}}{{#if (gte parryCount 2)}} (+1){{/if}}
</div>
{{/if}}
{{#if hasBlock}}
<div class="oh-trait-info">
<i class="fa-solid fa-shield-halved"></i>
{{localize "OATHHAMMER.Dialog.BlockActive"}}
</div>
{{/if}}
<fieldset class="attack-options-block">
<legend>{{localize "OATHHAMMER.Dialog.DefenseOptions"}}</legend>
{{!-- Attack type --}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.AttackType"}}</label>
<select name="attackType">
{{#each attackTypeOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
</div>
{{!-- Governing attribute (melee can use Might) --}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.GoverningAttr"}}</label>
<select name="attribute">
{{#each attrOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.MightMeleeHint"}}</span>
</div>
{{!-- Diminishing defense penalty --}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.DiminishingDefense"}}</label>
<select name="diminish">
{{#each diminishOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.DiminishingDefenseHint"}}</span>
</div>
{{!-- Manual bonus/penalty --}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.Modifier"}}</label>
<select name="bonus">
{{#each bonusOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span>
</div>
</fieldset>
{{!-- Visibility --}}
<fieldset class="roll-visibility-block">
<legend>{{localize "OATHHAMMER.Dialog.Visibility"}}</legend>
<select name="visibility">
{{selectOptions rollModes selected=visibility localize=true}}
</select>
</fieldset>
</div>