Add initiative rolls
This commit is contained in:
@@ -430,7 +430,8 @@
|
|||||||
.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.9);
|
font-size: 0.86rem;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.oathhammer .character-main .character-identity-bar .identity-slot .slot-icon {
|
.oathhammer .character-main .character-identity-bar .identity-slot .slot-icon {
|
||||||
font-size: calc(0.86rem * 0.9);
|
font-size: calc(0.86rem * 0.9);
|
||||||
@@ -897,6 +898,44 @@
|
|||||||
background: rgba(192, 57, 43, 0.1);
|
background: rgba(192, 57, 43, 0.1);
|
||||||
border-color: rgba(192, 57, 43, 0.4);
|
border-color: rgba(192, 57, 43, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Initiative bar */
|
||||||
|
.initiative-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 4px 6px 6px;
|
||||||
|
}
|
||||||
|
.initiative-roll-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
font-size: calc(0.86rem * 0.9);
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2a1a0a;
|
||||||
|
background: rgba(200, 168, 75, 0.2);
|
||||||
|
border: 1px solid rgba(200, 168, 75, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.initiative-roll-btn:hover {
|
||||||
|
background: rgba(200, 168, 75, 0.4);
|
||||||
|
color: #2a1a0a;
|
||||||
|
}
|
||||||
|
.initiative-score {
|
||||||
|
font-size: calc(0.86rem * 0.95);
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2a1a0a;
|
||||||
|
background: rgba(200, 168, 75, 0.15);
|
||||||
|
border: 1px solid rgba(200, 168, 75, 0.4);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
}
|
||||||
.oathhammer .item-sheet-common {
|
.oathhammer .item-sheet-common {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
|||||||
24
lang/en.json
24
lang/en.json
@@ -161,7 +161,8 @@
|
|||||||
},
|
},
|
||||||
"TraitType": {
|
"TraitType": {
|
||||||
"SpecialTrait": "Special Trait",
|
"SpecialTrait": "Special Trait",
|
||||||
"ClassTrait": "Class Trait"
|
"ClassTrait": "Class Trait",
|
||||||
|
"LineageTrait": "Lineage Trait"
|
||||||
},
|
},
|
||||||
"Condition": {
|
"Condition": {
|
||||||
"Blinded": "Blinded",
|
"Blinded": "Blinded",
|
||||||
@@ -243,6 +244,7 @@
|
|||||||
"MiracleBlocked": "Divine favour lost — no more miracles today.",
|
"MiracleBlocked": "Divine favour lost — no more miracles today.",
|
||||||
"NoEquipment": "No equipment.",
|
"NoEquipment": "No equipment.",
|
||||||
"NoTraits": "Drop traits here.",
|
"NoTraits": "Drop traits here.",
|
||||||
|
"NoOaths": "No oaths yet.",
|
||||||
"Enchantment": "Enchantment",
|
"Enchantment": "Enchantment",
|
||||||
"Tenet": "Tenet",
|
"Tenet": "Tenet",
|
||||||
"Boon": "Boon",
|
"Boon": "Boon",
|
||||||
@@ -275,7 +277,9 @@
|
|||||||
"MagicMissile": "Magic Missile",
|
"MagicMissile": "Magic Missile",
|
||||||
"SpellSave": "Save",
|
"SpellSave": "Save",
|
||||||
"StressBlocked": "BLOCKED — over stress threshold!",
|
"StressBlocked": "BLOCKED — over stress threshold!",
|
||||||
"ArcaneStressShort": "AS"
|
"ArcaneStressShort": "AS",
|
||||||
|
"InitiativeBonus": "Initiative Bonus",
|
||||||
|
"Initiative": "Initiative"
|
||||||
},
|
},
|
||||||
"ColorDice": {
|
"ColorDice": {
|
||||||
"White": "White (4+)",
|
"White": "White (4+)",
|
||||||
@@ -288,7 +292,8 @@
|
|||||||
"Miracle": "New Miracle",
|
"Miracle": "New Miracle",
|
||||||
"Equipment": "New Equipment",
|
"Equipment": "New Equipment",
|
||||||
"Building": "New Building",
|
"Building": "New Building",
|
||||||
"Trait": "New Trait"
|
"Trait": "New Trait",
|
||||||
|
"Oath": "New Oath"
|
||||||
},
|
},
|
||||||
"ToggleSheet": "Toggle Edit/Play Mode",
|
"ToggleSheet": "Toggle Edit/Play Mode",
|
||||||
"Action": {
|
"Action": {
|
||||||
@@ -310,12 +315,15 @@
|
|||||||
"SupportersHint": "+1 die each",
|
"SupportersHint": "+1 die each",
|
||||||
"LuckSpend": "Luck Points",
|
"LuckSpend": "Luck Points",
|
||||||
"LuckHint": "+2 dice each",
|
"LuckHint": "+2 dice each",
|
||||||
|
"LuckHuman": "Human (+3d)",
|
||||||
"Available": "available",
|
"Available": "available",
|
||||||
"Visibility": "Visibility",
|
"Visibility": "Visibility",
|
||||||
"Attribute": "Attribute",
|
"Attribute": "Attribute",
|
||||||
"RollSkill": "Click to roll skill check",
|
"RollSkill": "Click to roll skill check",
|
||||||
"ExplodeOn5": "Explode on 5+",
|
"ExplodeOn5": "Explode on 5+",
|
||||||
"ExplodeOn5Hint": "trait bonus — 5s & 6s explode",
|
"ExplodeOn5Hint": "trait bonus — 5s & 6s explode",
|
||||||
|
"DiceColor": "Dice Color",
|
||||||
|
"DiceColorHint": "equipment traits can upgrade dice",
|
||||||
"AttackTitle": "Attack: {weapon}",
|
"AttackTitle": "Attack: {weapon}",
|
||||||
"DamageTitle": "Damage: {weapon}",
|
"DamageTitle": "Damage: {weapon}",
|
||||||
"Attack": "Attack",
|
"Attack": "Attack",
|
||||||
@@ -374,7 +382,8 @@
|
|||||||
"ArmorRollOptions": "Armor Roll Options",
|
"ArmorRollOptions": "Armor Roll Options",
|
||||||
"APPenalty": "AP (Attacker)",
|
"APPenalty": "AP (Attacker)",
|
||||||
"APHint": "attacker's Armor Piercing value",
|
"APHint": "attacker's Armor Piercing value",
|
||||||
"ReinforcedHint": "Reinforced — rolling red dice"
|
"ReinforcedHint": "Reinforced — rolling red dice",
|
||||||
|
"RollInitiative": "Roll Initiative"
|
||||||
},
|
},
|
||||||
"Enhancement": {
|
"Enhancement": {
|
||||||
"None": "None",
|
"None": "None",
|
||||||
@@ -406,7 +415,9 @@
|
|||||||
"DefenseResult": "defense successes",
|
"DefenseResult": "defense successes",
|
||||||
"ArmorRoll": "Armor Roll",
|
"ArmorRoll": "Armor Roll",
|
||||||
"ArmorBypassed": "Armor bypassed (0 dice — AP ≥ AV)",
|
"ArmorBypassed": "Armor bypassed (0 dice — AP ≥ AV)",
|
||||||
"Successes": "successes",
|
"Initiative": "Initiative",
|
||||||
|
"InitiativeHint": "Opposed Leadership check — winner chooses company order",
|
||||||
|
"Opposed": "Opposed",
|
||||||
"DualAttr": {
|
"DualAttr": {
|
||||||
"DefenseMelee": "melee defense",
|
"DefenseMelee": "melee defense",
|
||||||
"FightingNimble": "nimble weapon",
|
"FightingNimble": "nimble weapon",
|
||||||
@@ -564,6 +575,9 @@
|
|||||||
"rarity": {
|
"rarity": {
|
||||||
"label": "Rarity"
|
"label": "Rarity"
|
||||||
},
|
},
|
||||||
|
"slots": {
|
||||||
|
"label": "Slots"
|
||||||
|
},
|
||||||
"equipped": {
|
"equipped": {
|
||||||
"label": "Equipped"
|
"label": "Equipped"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -97,7 +97,8 @@
|
|||||||
.identity-name {
|
.identity-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: @font-secondary;
|
font-family: @font-secondary;
|
||||||
font-size: @font-size-xs;
|
font-size: @font-size-base;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-icon { font-size: @font-size-xs; opacity: 0.8; }
|
.slot-icon { font-size: @font-size-xs; opacity: 0.8; }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Armor roll dialog.
|
* Armor roll dialog.
|
||||||
*
|
*
|
||||||
* Pool = Armor Value (AV) − AP penalty + manual bonus (can go to 0, unlike other pools)
|
* Pool = Armor Value (AV) − AP penalty + manual bonus (can go to 0, unlike other pools)
|
||||||
* Reinforced trait on the armor → red dice (3+)
|
* Reinforced trait on the armor → red dice (3+) by default
|
||||||
* Each success on the roll reduces incoming damage by 1.
|
* Each success on the roll reduces incoming damage by 1.
|
||||||
*/
|
*/
|
||||||
export default class OathHammerArmorDialog {
|
export default class OathHammerArmorDialog {
|
||||||
@@ -11,7 +11,8 @@ export default class OathHammerArmorDialog {
|
|||||||
const sys = armor.system
|
const sys = armor.system
|
||||||
const av = sys.armorValue ?? 0
|
const av = sys.armorValue ?? 0
|
||||||
|
|
||||||
const isReinforced = [...(sys.traits ?? [])].includes("reinforced")
|
const isReinforced = [...(sys.traits ?? [])].includes("reinforced")
|
||||||
|
const defaultColor = isReinforced ? "red" : "white"
|
||||||
|
|
||||||
// AP options — entered by the user based on the attacker's weapon
|
// AP options — entered by the user based on the attacker's weapon
|
||||||
const apOptions = Array.from({ length: 9 }, (_, i) => ({
|
const apOptions = Array.from({ length: 9 }, (_, i) => ({
|
||||||
@@ -25,6 +26,12 @@ export default class OathHammerArmorDialog {
|
|||||||
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const colorOptions = [
|
||||||
|
{ value: "white", label: "⬜ White (4+)", selected: defaultColor === "white" },
|
||||||
|
{ value: "red", label: "🔴 Red (3+)", selected: defaultColor === "red" },
|
||||||
|
{ value: "black", label: "⬛ Black (2+)", selected: defaultColor === "black" },
|
||||||
|
]
|
||||||
|
|
||||||
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
@@ -35,6 +42,7 @@ export default class OathHammerArmorDialog {
|
|||||||
isReinforced,
|
isReinforced,
|
||||||
apOptions,
|
apOptions,
|
||||||
bonusOptions,
|
bonusOptions,
|
||||||
|
colorOptions,
|
||||||
rollModes,
|
rollModes,
|
||||||
visibility: game.settings.get("core", "rollMode"),
|
visibility: game.settings.get("core", "rollMode"),
|
||||||
}
|
}
|
||||||
@@ -51,9 +59,14 @@ export default class OathHammerArmorDialog {
|
|||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
buttons: [{
|
buttons: [{
|
||||||
label: game.i18n.localize("OATHHAMMER.Dialog.RollArmor"),
|
label: game.i18n.localize("OATHHAMMER.Dialog.RollArmor"),
|
||||||
callback: (_ev, btn) => Object.fromEntries(
|
callback: (_ev, btn) => {
|
||||||
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
|
const out = {}
|
||||||
),
|
for (const el of btn.form.elements) {
|
||||||
|
if (!el.name) continue
|
||||||
|
out[el.name] = el.type === "checkbox" ? String(el.checked) : el.value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -62,9 +75,11 @@ export default class OathHammerArmorDialog {
|
|||||||
return {
|
return {
|
||||||
av,
|
av,
|
||||||
isReinforced,
|
isReinforced,
|
||||||
apPenalty: parseInt(result.ap) || 0,
|
colorOverride: result.colorOverride || defaultColor,
|
||||||
bonus: parseInt(result.bonus) || 0,
|
apPenalty: parseInt(result.ap) || 0,
|
||||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
bonus: parseInt(result.bonus) || 0,
|
||||||
|
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||||
|
explodeOn5: result.explodeOn5 === "true",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,9 +63,14 @@ export default class OathHammerMiracleDialog {
|
|||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
buttons: [{
|
buttons: [{
|
||||||
label: game.i18n.localize("OATHHAMMER.Dialog.InvokeMiracle"),
|
label: game.i18n.localize("OATHHAMMER.Dialog.InvokeMiracle"),
|
||||||
callback: (_ev, btn) => Object.fromEntries(
|
callback: (_ev, btn) => {
|
||||||
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
|
const out = {}
|
||||||
),
|
for (const el of btn.form.elements) {
|
||||||
|
if (!el.name) continue
|
||||||
|
out[el.name] = el.type === "checkbox" ? String(el.checked) : el.value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -78,6 +83,7 @@ export default class OathHammerMiracleDialog {
|
|||||||
isRitual,
|
isRitual,
|
||||||
bonus: parseInt(result.bonus) || 0,
|
bonus: parseInt(result.bonus) || 0,
|
||||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||||
|
explodeOn5: result.explodeOn5 === "true",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export default class OathHammerRollDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const availableLuck = sys.luck?.value ?? 0
|
const availableLuck = sys.luck?.value ?? 0
|
||||||
|
const isHuman = (sys.lineage?.name ?? "").toLowerCase() === "human"
|
||||||
|
const luckDicePerPoint = isHuman ? 3 : 2
|
||||||
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
||||||
|
|
||||||
// Build select option arrays
|
// Build select option arrays
|
||||||
@@ -85,7 +87,7 @@ export default class OathHammerRollDialog {
|
|||||||
|
|
||||||
const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({
|
const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({
|
||||||
value: i,
|
value: i,
|
||||||
label: i === 0 ? `0` : `${i} (+${i * 2}d)`,
|
label: i === 0 ? `0` : `${i} (+${i * luckDicePerPoint}d)`,
|
||||||
selected: i === 0,
|
selected: i === 0,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -103,6 +105,7 @@ export default class OathHammerRollDialog {
|
|||||||
colorLabel,
|
colorLabel,
|
||||||
threshold,
|
threshold,
|
||||||
availableLuck,
|
availableLuck,
|
||||||
|
isHuman,
|
||||||
attrOptions,
|
attrOptions,
|
||||||
isDualAttr: !!dualDef,
|
isDualAttr: !!dualDef,
|
||||||
rollModes,
|
rollModes,
|
||||||
@@ -145,13 +148,14 @@ export default class OathHammerRollDialog {
|
|||||||
const attrOverride = result.attrOverride || defaultAttrKey
|
const attrOverride = result.attrOverride || defaultAttrKey
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dv: Math.max(0, 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),
|
luckIsHuman: result.luckIsHuman === "true",
|
||||||
explodeOn5: result.explodeOn5 === "true",
|
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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import OathHammerSpellDialog from "../spell-dialog.mjs"
|
|||||||
import OathHammerMiracleDialog from "../miracle-dialog.mjs"
|
import OathHammerMiracleDialog from "../miracle-dialog.mjs"
|
||||||
import OathHammerDefenseDialog from "../defense-dialog.mjs"
|
import OathHammerDefenseDialog from "../defense-dialog.mjs"
|
||||||
import OathHammerArmorDialog from "../armor-dialog.mjs"
|
import OathHammerArmorDialog from "../armor-dialog.mjs"
|
||||||
import { rollSkillCheck, rollWeaponAttack, rollWeaponDamage, rollSpellCast, rollMiracleCast, rollArmorSave, rollWeaponDefense } from "../../rolls.mjs"
|
import { rollSkillCheck, rollWeaponAttack, rollWeaponDamage, rollSpellCast, rollMiracleCast, rollArmorSave, rollWeaponDefense, rollInitiativeCheck } from "../../rolls.mjs"
|
||||||
|
|
||||||
export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
||||||
/** @override */
|
/** @override */
|
||||||
@@ -25,6 +25,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
|||||||
createMiracle: OathHammerCharacterSheet.#onCreateMiracle,
|
createMiracle: OathHammerCharacterSheet.#onCreateMiracle,
|
||||||
createEquipment: OathHammerCharacterSheet.#onCreateEquipment,
|
createEquipment: OathHammerCharacterSheet.#onCreateEquipment,
|
||||||
createTrait: OathHammerCharacterSheet.#onCreateTrait,
|
createTrait: OathHammerCharacterSheet.#onCreateTrait,
|
||||||
|
createOath: OathHammerCharacterSheet.#onCreateOath,
|
||||||
rollSkill: OathHammerCharacterSheet.#onRollSkill,
|
rollSkill: OathHammerCharacterSheet.#onRollSkill,
|
||||||
attackWeapon: OathHammerCharacterSheet.#onAttackWeapon,
|
attackWeapon: OathHammerCharacterSheet.#onAttackWeapon,
|
||||||
defendWeapon: OathHammerCharacterSheet.#onDefendWeapon,
|
defendWeapon: OathHammerCharacterSheet.#onDefendWeapon,
|
||||||
@@ -33,6 +34,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
|||||||
castMiracle: OathHammerCharacterSheet.#onCastMiracle,
|
castMiracle: OathHammerCharacterSheet.#onCastMiracle,
|
||||||
rollArmorSave: OathHammerCharacterSheet.#onRollArmorSave,
|
rollArmorSave: OathHammerCharacterSheet.#onRollArmorSave,
|
||||||
resetMiracleBlocked: OathHammerCharacterSheet.#onResetMiracleBlocked,
|
resetMiracleBlocked: OathHammerCharacterSheet.#onResetMiracleBlocked,
|
||||||
|
rollInitiative: OathHammerCharacterSheet.#onRollInitiative,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,15 +98,18 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
|||||||
_typeLabel: typeKey ? game.i18n.localize(typeKey) : a.system.traitType,
|
_typeLabel: typeKey ? game.i18n.localize(typeKey) : a.system.traitType,
|
||||||
_usageLabel: isPassive
|
_usageLabel: isPassive
|
||||||
? game.i18n.localize("OATHHAMMER.UsagePeriod.None")
|
? game.i18n.localize("OATHHAMMER.UsagePeriod.None")
|
||||||
: `${a.system.maxUses > 0 ? a.system.maxUses + " / " : ""}${game.i18n.localize(periodKey)}`
|
: `${a.system.maxUses > 0 ? a.system.maxUses + " / " : ""}${game.i18n.localize(periodKey)}`,
|
||||||
|
_descTooltip: _stripHtml(a.system.description)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
context.oaths = doc.itemTypes.oath.map(o => {
|
context.oaths = doc.itemTypes.oath.map(o => {
|
||||||
const typeEntry = SYSTEM.OATH_TYPES[o.system.oathType]
|
const typeEntry = SYSTEM.OATH_TYPES[o.system.oathType]
|
||||||
|
const parts = [o.system.tenet, o.system.boon, o.system.bane].filter(Boolean)
|
||||||
return {
|
return {
|
||||||
id: o.id, uuid: o.uuid, img: o.img, name: o.name, system: o.system,
|
id: o.id, uuid: o.uuid, img: o.img, name: o.name, system: o.system,
|
||||||
_typeLabel: typeEntry ? game.i18n.localize(typeEntry.label) : o.system.oathType,
|
_typeLabel: typeEntry ? game.i18n.localize(typeEntry.label) : o.system.oathType,
|
||||||
_violated: o.system.violated
|
_violated: o.system.violated,
|
||||||
|
_descTooltip: _stripHtml(parts.join(" "))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
context.enrichedBackground = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.background ?? "", { async: true })
|
context.enrichedBackground = await foundry.applications.ux.TextEditor.implementation.enrichHTML(doc.system.background ?? "", { async: true })
|
||||||
@@ -160,6 +165,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
|||||||
id: w.id, uuid: w.uuid, img: w.img, name: w.name, system: w.system,
|
id: w.id, uuid: w.uuid, img: w.img, name: w.name, system: w.system,
|
||||||
_groupLabel: groupKey ? game.i18n.localize(groupKey) : w.system.proficiencyGroup,
|
_groupLabel: groupKey ? game.i18n.localize(groupKey) : w.system.proficiencyGroup,
|
||||||
_traitsTooltip: traitsLabel || null,
|
_traitsTooltip: traitsLabel || null,
|
||||||
|
_descTooltip: _stripHtml(w.system.description),
|
||||||
_isMagic: w.system.isMagic
|
_isMagic: w.system.isMagic
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -173,6 +179,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
|||||||
id: a.id, uuid: a.uuid, img: a.img, name: a.name, system: a.system,
|
id: a.id, uuid: a.uuid, img: a.img, name: a.name, system: a.system,
|
||||||
_typeLabel: typeKey ? game.i18n.localize(typeKey) : a.system.armorType,
|
_typeLabel: typeKey ? game.i18n.localize(typeKey) : a.system.armorType,
|
||||||
_traitsTooltip: traitsLabel || null,
|
_traitsTooltip: traitsLabel || null,
|
||||||
|
_descTooltip: _stripHtml(a.system.description),
|
||||||
_isMagic: a.system.isMagic
|
_isMagic: a.system.isMagic
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -181,16 +188,31 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
|||||||
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2)
|
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2)
|
||||||
context.slotsUsed = doc.items.reduce((sum, item) => sum + (item.system.slots ?? 0), 0)
|
context.slotsUsed = doc.items.reduce((sum, item) => sum + (item.system.slots ?? 0), 0)
|
||||||
context.slotsOver = context.slotsUsed > context.slotsMax
|
context.slotsOver = context.slotsUsed > context.slotsMax
|
||||||
|
// Show current initiative score if actor is in an active combat
|
||||||
|
const combatant = game.combat?.combatants.find(c => c.actor?.id === doc.id)
|
||||||
|
context.combatantInitiative = combatant?.initiative ?? null
|
||||||
break
|
break
|
||||||
case "magic":
|
case "magic":
|
||||||
context.tab = context.tabs.magic
|
context.tab = context.tabs.magic
|
||||||
context.spells = doc.itemTypes.spell
|
context.spells = doc.itemTypes.spell.map(s => ({
|
||||||
context.miracles = doc.itemTypes.miracle
|
id: s.id, uuid: s.uuid, img: s.img, name: s.name, system: s.system,
|
||||||
|
_descTooltip: _stripHtml(s.system.effect)
|
||||||
|
}))
|
||||||
|
context.miracles = doc.itemTypes.miracle.map(m => ({
|
||||||
|
id: m.id, uuid: m.uuid, img: m.img, name: m.name, system: m.system,
|
||||||
|
_descTooltip: _stripHtml(m.system.effect)
|
||||||
|
}))
|
||||||
break
|
break
|
||||||
case "equipment":
|
case "equipment":
|
||||||
context.tab = context.tabs.equipment
|
context.tab = context.tabs.equipment
|
||||||
context.equipment = doc.itemTypes.equipment
|
context.equipment = doc.itemTypes.equipment.map(e => ({
|
||||||
context.magicItems = doc.itemTypes["magic-item"]
|
id: e.id, uuid: e.uuid, img: e.img, name: e.name, system: e.system,
|
||||||
|
_descTooltip: _stripHtml(e.system.description)
|
||||||
|
}))
|
||||||
|
context.magicItems = doc.itemTypes["magic-item"].map(m => ({
|
||||||
|
id: m.id, uuid: m.uuid, img: m.img, name: m.name, system: m.system,
|
||||||
|
_descTooltip: _stripHtml(m.system.description)
|
||||||
|
}))
|
||||||
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2)
|
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2)
|
||||||
context.slotsUsed = doc.items.reduce((sum, item) => sum + (item.system.slots ?? 0), 0)
|
context.slotsUsed = doc.items.reduce((sum, item) => sum + (item.system.slots ?? 0), 0)
|
||||||
context.slotsOver = context.slotsUsed > context.slotsMax
|
context.slotsOver = context.slotsUsed > context.slotsMax
|
||||||
@@ -263,6 +285,10 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
|||||||
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Trait"), type: "trait" }])
|
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Trait"), type: "trait" }])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static #onCreateOath(event, target) {
|
||||||
|
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Oath"), type: "oath" }])
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -338,4 +364,23 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
|||||||
if (!opts) return
|
if (!opts) return
|
||||||
await rollArmorSave(this.document, armor, opts)
|
await rollArmorSave(this.document, armor, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onRollInitiative() {
|
||||||
|
const actor = this.document
|
||||||
|
const combatant = game.combat?.combatants.find(c => c.actor?.id === actor.id)
|
||||||
|
if (combatant) {
|
||||||
|
// Delegate to OathHammerCombat.rollInitiative — posts to chat and updates tracker
|
||||||
|
await game.combat.rollInitiative([combatant.id])
|
||||||
|
} else {
|
||||||
|
// Not in combat — roll for display only
|
||||||
|
await rollInitiativeCheck(actor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strip HTML tags and collapse whitespace for use in data-tooltip attributes. */
|
||||||
|
function _stripHtml(html, maxLen = 300) {
|
||||||
|
if (!html) return ""
|
||||||
|
const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim()
|
||||||
|
return text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import OathHammerActorSheet from "./base-actor-sheet.mjs"
|
import OathHammerActorSheet from "./base-actor-sheet.mjs"
|
||||||
|
import { rollInitiativeCheck } from "../../rolls.mjs"
|
||||||
|
|
||||||
export default class OathHammerNPCSheet extends OathHammerActorSheet {
|
export default class OathHammerNPCSheet extends OathHammerActorSheet {
|
||||||
/** @override */
|
/** @override */
|
||||||
@@ -11,6 +12,9 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
|
|||||||
window: {
|
window: {
|
||||||
contentClasses: ["npc-content"],
|
contentClasses: ["npc-content"],
|
||||||
},
|
},
|
||||||
|
actions: {
|
||||||
|
rollInitiative: OathHammerNPCSheet.#onRollInitiative,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @override */
|
/** @override */
|
||||||
@@ -62,6 +66,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
|
|||||||
case "combat":
|
case "combat":
|
||||||
context.tab = context.tabs.combat
|
context.tab = context.tabs.combat
|
||||||
context.weapons = doc.itemTypes.weapon
|
context.weapons = doc.itemTypes.weapon
|
||||||
|
context.combatantInitiative = game.combat?.combatants.find(c => c.actor?.id === doc.id)?.initiative ?? null
|
||||||
break
|
break
|
||||||
case "notes":
|
case "notes":
|
||||||
context.tab = context.tabs.notes
|
context.tab = context.tabs.notes
|
||||||
@@ -80,4 +85,14 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
|
|||||||
return this._onDropItem(item)
|
return this._onDropItem(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #onRollInitiative() {
|
||||||
|
const actor = this.document
|
||||||
|
const combatant = game.combat?.combatants.find(c => c.actor?.id === actor.id)
|
||||||
|
if (combatant) {
|
||||||
|
await game.combat.rollInitiative([combatant.id])
|
||||||
|
} else {
|
||||||
|
await rollInitiativeCheck(actor)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,9 +91,14 @@ export default class OathHammerSpellDialog {
|
|||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
buttons: [{
|
buttons: [{
|
||||||
label: game.i18n.localize("OATHHAMMER.Dialog.CastSpell"),
|
label: game.i18n.localize("OATHHAMMER.Dialog.CastSpell"),
|
||||||
callback: (_ev, btn) => Object.fromEntries(
|
callback: (_ev, btn) => {
|
||||||
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
|
const out = {}
|
||||||
),
|
for (const el of btn.form.elements) {
|
||||||
|
if (!el.name) continue
|
||||||
|
out[el.name] = el.type === "checkbox" ? String(el.checked) : el.value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -113,6 +118,7 @@ export default class OathHammerSpellDialog {
|
|||||||
bonus: parseInt(result.bonus) || 0,
|
bonus: parseInt(result.bonus) || 0,
|
||||||
grimPenalty: parseInt(result.noGrimoire) || 0,
|
grimPenalty: parseInt(result.noGrimoire) || 0,
|
||||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||||
|
explodeOn5: result.explodeOn5 === "true",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export default class OathHammerWeaponDialog {
|
|||||||
traits: traitLabels,
|
traits: traitLabels,
|
||||||
attackBonusOptions,
|
attackBonusOptions,
|
||||||
rangeOptions,
|
rangeOptions,
|
||||||
|
colorOptions: _colorOptions(skillColor),
|
||||||
rollModes,
|
rollModes,
|
||||||
visibility: game.settings.get("core", "rollMode"),
|
visibility: game.settings.get("core", "rollMode"),
|
||||||
}
|
}
|
||||||
@@ -113,9 +114,14 @@ export default class OathHammerWeaponDialog {
|
|||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
buttons: [{
|
buttons: [{
|
||||||
label: game.i18n.localize("OATHHAMMER.Dialog.RollAttack"),
|
label: game.i18n.localize("OATHHAMMER.Dialog.RollAttack"),
|
||||||
callback: (_ev, btn) => Object.fromEntries(
|
callback: (_ev, btn) => {
|
||||||
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
|
const out = {}
|
||||||
),
|
for (const el of btn.form.elements) {
|
||||||
|
if (!el.name) continue
|
||||||
|
out[el.name] = el.type === "checkbox" ? String(el.checked) : el.value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -124,8 +130,10 @@ export default class OathHammerWeaponDialog {
|
|||||||
attackBonus: parseInt(result.attackBonus) || 0,
|
attackBonus: parseInt(result.attackBonus) || 0,
|
||||||
rangeCondition: parseInt(result.rangeCondition) || 0,
|
rangeCondition: parseInt(result.rangeCondition) || 0,
|
||||||
attrOverride: result.attrOverride || defaultAttr,
|
attrOverride: result.attrOverride || defaultAttr,
|
||||||
|
colorOverride: result.colorOverride || skillColor,
|
||||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||||
autoAttackBonus,
|
autoAttackBonus,
|
||||||
|
explodeOn5: result.explodeOn5 === "true",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +196,12 @@ export default class OathHammerWeaponDialog {
|
|||||||
{ value: -4, label: game.i18n.localize("OATHHAMMER.Dialog.DiminishThird"), selected: false },
|
{ value: -4, label: game.i18n.localize("OATHHAMMER.Dialog.DiminishThird"), selected: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Default dice color: red if parry/block trait matches the default attack type, else white
|
||||||
|
const defaultDefenseColor = (
|
||||||
|
(defaultAttackType === "melee" && hasParry) ||
|
||||||
|
(defaultAttackType === "ranged" && hasBlock)
|
||||||
|
) ? "red" : "white"
|
||||||
|
|
||||||
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
|
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
|
||||||
const v = i - 6
|
const v = i - 6
|
||||||
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
||||||
@@ -212,6 +226,7 @@ export default class OathHammerWeaponDialog {
|
|||||||
attrOptions,
|
attrOptions,
|
||||||
diminishOptions,
|
diminishOptions,
|
||||||
bonusOptions,
|
bonusOptions,
|
||||||
|
colorOptions: _colorOptions(defaultDefenseColor),
|
||||||
rollModes,
|
rollModes,
|
||||||
visibility: game.settings.get("core", "rollMode"),
|
visibility: game.settings.get("core", "rollMode"),
|
||||||
}
|
}
|
||||||
@@ -228,9 +243,14 @@ export default class OathHammerWeaponDialog {
|
|||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
buttons: [{
|
buttons: [{
|
||||||
label: game.i18n.localize("OATHHAMMER.Dialog.RollDefense"),
|
label: game.i18n.localize("OATHHAMMER.Dialog.RollDefense"),
|
||||||
callback: (_ev, btn) => Object.fromEntries(
|
callback: (_ev, btn) => {
|
||||||
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
|
const out = {}
|
||||||
),
|
for (const el of btn.form.elements) {
|
||||||
|
if (!el.name) continue
|
||||||
|
out[el.name] = el.type === "checkbox" ? String(el.checked) : el.value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -241,28 +261,24 @@ export default class OathHammerWeaponDialog {
|
|||||||
const attrRank = attrChoice === "might" ? mightRank : agiRank
|
const attrRank = attrChoice === "might" ? mightRank : agiRank
|
||||||
const diminishPenalty = parseInt(result.diminish) || 0
|
const diminishPenalty = parseInt(result.diminish) || 0
|
||||||
const bonus = parseInt(result.bonus) || 0
|
const bonus = parseInt(result.bonus) || 0
|
||||||
|
const colorOverride = result.colorOverride || defaultDefenseColor
|
||||||
|
|
||||||
// Resolve red dice and trait bonus based on selected attack type
|
// Trait bonus based on selected attack type (color is now user-controlled)
|
||||||
let redDice = false
|
|
||||||
let traitBonus = 0
|
let traitBonus = 0
|
||||||
if (attackType === "melee" && hasParry) {
|
if (attackType === "melee" && hasParry) traitBonus = parryCount >= 2 ? 1 : 0
|
||||||
redDice = true
|
if (attackType === "ranged" && hasBlock) traitBonus = 1
|
||||||
traitBonus = parryCount >= 2 ? 1 : 0
|
|
||||||
} else if (attackType === "ranged" && hasBlock) {
|
|
||||||
redDice = true
|
|
||||||
traitBonus = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attackType,
|
attackType,
|
||||||
attrRank,
|
attrRank,
|
||||||
attrChoice,
|
attrChoice,
|
||||||
redDice,
|
colorOverride,
|
||||||
traitBonus,
|
traitBonus,
|
||||||
armorPenalty,
|
armorPenalty,
|
||||||
diminishPenalty,
|
diminishPenalty,
|
||||||
bonus,
|
bonus,
|
||||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||||
|
explodeOn5: result.explodeOn5 === "true",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,3 +366,11 @@ export default class OathHammerWeaponDialog {
|
|||||||
function _cap(str) {
|
function _cap(str) {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _colorOptions(defaultColor = "white") {
|
||||||
|
return [
|
||||||
|
{ value: "white", label: "⬜ White (4+)", selected: defaultColor === "white" },
|
||||||
|
{ value: "red", label: "🔴 Red (3+)", selected: defaultColor === "red" },
|
||||||
|
{ value: "black", label: "⬛ Black (2+)", selected: defaultColor === "black" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
36
module/combat.mjs
Normal file
36
module/combat.mjs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { rollInitiativeCheck } from "./rolls.mjs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Combat class for Oath Hammer.
|
||||||
|
* Initiative = opposed Leadership check (DV=0).
|
||||||
|
* The number of successes becomes the combatant's initiative score.
|
||||||
|
*/
|
||||||
|
export default class OathHammerCombat extends Combat {
|
||||||
|
/**
|
||||||
|
* Override Foundry's rollInitiative to use Leadership skill checks.
|
||||||
|
* For characters: Leadership (Fate) opposed check, successes = score.
|
||||||
|
* For NPCs: Fate rank + initiative bonus pool.
|
||||||
|
*
|
||||||
|
* @param {string|string[]} ids Combatant IDs to roll for
|
||||||
|
* @param {object} options
|
||||||
|
*/
|
||||||
|
async rollInitiative(ids, { formula = null, updateTurn = true, messageOptions = {} } = {}) {
|
||||||
|
const combatantIds = typeof ids === "string" ? [ids] : ids
|
||||||
|
const updates = []
|
||||||
|
|
||||||
|
for (const id of combatantIds) {
|
||||||
|
const combatant = this.combatants.get(id)
|
||||||
|
if (!combatant?.isOwner) continue
|
||||||
|
const actor = combatant.actor
|
||||||
|
if (!actor) continue
|
||||||
|
|
||||||
|
const { successes } = await rollInitiativeCheck(actor)
|
||||||
|
updates.push({ _id: id, initiative: successes })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updates.length) return this
|
||||||
|
await this.updateEmbeddedDocuments("Combatant", updates)
|
||||||
|
if (updateTurn && this.turn !== null) await this.update({ turn: 0 })
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -198,8 +198,9 @@ export const RARITY_DV = {
|
|||||||
|
|
||||||
// Two types of trait per the rulebook terminology
|
// Two types of trait per the rulebook terminology
|
||||||
export const TRAIT_TYPE_CHOICES = {
|
export const TRAIT_TYPE_CHOICES = {
|
||||||
"special-trait": "OATHHAMMER.TraitType.SpecialTrait",
|
"special-trait": "OATHHAMMER.TraitType.SpecialTrait",
|
||||||
"class-trait": "OATHHAMMER.TraitType.ClassTrait"
|
"class-trait": "OATHHAMMER.TraitType.ClassTrait",
|
||||||
|
"lineage-trait": "OATHHAMMER.TraitType.LineageTrait"
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a trait's uses reset (none = passive/always on)
|
// When a trait's uses reset (none = passive/always on)
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ export default class OathHammerNPC extends foundry.abstract.TypeDataModel {
|
|||||||
base: new fields.NumberField({ ...requiredInteger, initial: 30, min: 0 })
|
base: new fields.NumberField({ ...requiredInteger, initial: 30, min: 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
schema.attackBonus = new fields.NumberField({ ...requiredInteger, initial: 0 })
|
schema.attackBonus = new fields.NumberField({ ...requiredInteger, initial: 0 })
|
||||||
schema.damageBonus = new fields.NumberField({ ...requiredInteger, initial: 0 })
|
schema.damageBonus = new fields.NumberField({ ...requiredInteger, initial: 0 })
|
||||||
|
schema.initiativeBonus = new fields.NumberField({ ...requiredInteger, initial: 0 })
|
||||||
schema.challengeRating = new fields.StringField({ required: true, nullable: false, initial: "1" })
|
schema.challengeRating = new fields.StringField({ required: true, nullable: false, initial: "1" })
|
||||||
|
|
||||||
return schema
|
return schema
|
||||||
|
|||||||
@@ -32,11 +32,10 @@ export default class OathHammerTrait extends foundry.abstract.TypeDataModel {
|
|||||||
static migrateData(source) {
|
static migrateData(source) {
|
||||||
// Migrate old abilityType field → traitType
|
// Migrate old abilityType field → traitType
|
||||||
if (source.abilityType !== undefined && source.traitType === undefined) {
|
if (source.abilityType !== undefined && source.traitType === undefined) {
|
||||||
const map = { "class-ability": "class-trait", "lineage-trait": "special-trait" }
|
const map = { "class-ability": "class-trait" }
|
||||||
source.traitType = map[source.abilityType] ?? "special-trait"
|
source.traitType = map[source.abilityType] ?? "special-trait"
|
||||||
}
|
}
|
||||||
// Migrate old traitType values
|
// Migrate old traitType values
|
||||||
if (source.traitType === "lineage-trait") source.traitType = "special-trait"
|
|
||||||
if (source.traitType === "class-ability") source.traitType = "class-trait"
|
if (source.traitType === "class-ability") source.traitType = "class-trait"
|
||||||
return super.migrateData(source)
|
return super.migrateData(source)
|
||||||
}
|
}
|
||||||
|
|||||||
167
module/rolls.mjs
167
module/rolls.mjs
@@ -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, explodeOn5 = false } = options
|
const { bonus = 0, luckSpend = 0, luckIsHuman = false, 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]
|
||||||
@@ -40,8 +40,10 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
|||||||
const colorType = skill.colorDiceType ?? "white"
|
const colorType = skill.colorDiceType ?? "white"
|
||||||
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
|
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
|
||||||
|
|
||||||
|
const luckDicePerPoint = luckIsHuman ? 3 : 2
|
||||||
|
|
||||||
// Total dice pool (never below 1)
|
// Total dice pool (never below 1)
|
||||||
const totalDice = Math.max(attrRank + skillRank + skillMod + bonus + (luckSpend * 2) + supporters, 1)
|
const totalDice = Math.max(attrRank + skillRank + skillMod + bonus + (luckSpend * luckDicePerPoint) + supporters, 1)
|
||||||
|
|
||||||
// Deduct spent Luck Points from actor
|
// Deduct spent Luck Points from actor
|
||||||
if (luckSpend > 0) {
|
if (luckSpend > 0) {
|
||||||
@@ -51,6 +53,7 @@ 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()
|
||||||
|
const allRolls = [roll]
|
||||||
|
|
||||||
// Count successes — exploding dice produce additional dice
|
// Count successes — exploding dice produce additional dice
|
||||||
const explodeThreshold = explodeOn5 ? 5 : 6
|
const explodeThreshold = explodeOn5 ? 5 : 6
|
||||||
@@ -67,6 +70,7 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
|||||||
|
|
||||||
while (extraDice > 0) {
|
while (extraDice > 0) {
|
||||||
const xRoll = await new Roll(`${extraDice}d6`).evaluate()
|
const xRoll = await new Roll(`${extraDice}d6`).evaluate()
|
||||||
|
allRolls.push(xRoll)
|
||||||
extraDice = 0
|
extraDice = 0
|
||||||
for (const r of xRoll.dice[0].results) {
|
for (const r of xRoll.dice[0].results) {
|
||||||
const val = r.result
|
const val = r.result
|
||||||
@@ -94,7 +98,7 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
|||||||
const explodedCount = diceResults.filter(d => d.exploded).length
|
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 * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
|
||||||
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")}`)
|
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>` : ""
|
||||||
@@ -132,7 +136,7 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
|||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: [roll],
|
rolls: allRolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
@@ -179,8 +183,10 @@ export async function rollRarityCheck(actor, rarityKey, itemName) {
|
|||||||
* @param {number} threshold Minimum value to count as a success
|
* @param {number} threshold Minimum value to count as a success
|
||||||
* @returns {Promise<{roll: Roll, successes: number, diceResults: Array}>}
|
* @returns {Promise<{roll: Roll, successes: number, diceResults: Array}>}
|
||||||
*/
|
*/
|
||||||
async function _rollPool(pool, threshold) {
|
async function _rollPool(pool, threshold, explodeOn5 = false) {
|
||||||
|
const explodeThreshold = explodeOn5 ? 5 : 6
|
||||||
const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate()
|
const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate()
|
||||||
|
const rolls = [roll]
|
||||||
let successes = 0
|
let successes = 0
|
||||||
const diceResults = []
|
const diceResults = []
|
||||||
let extraDice = 0
|
let extraDice = 0
|
||||||
@@ -188,22 +194,23 @@ async function _rollPool(pool, threshold) {
|
|||||||
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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
while (extraDice > 0) {
|
while (extraDice > 0) {
|
||||||
const xRoll = await new Roll(`${extraDice}d6`).evaluate()
|
const xRoll = await new Roll(`${extraDice}d6`).evaluate()
|
||||||
|
rolls.push(xRoll)
|
||||||
extraDice = 0
|
extraDice = 0
|
||||||
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { roll, successes, diceResults }
|
return { roll, rolls, successes, diceResults }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -229,7 +236,7 @@ function _diceHtml(diceResults, threshold) {
|
|||||||
* @param {object} options From OathHammerWeaponDialog.promptAttack()
|
* @param {object} options From OathHammerWeaponDialog.promptAttack()
|
||||||
*/
|
*/
|
||||||
export async function rollWeaponAttack(actor, weapon, options = {}) {
|
export async function rollWeaponAttack(actor, weapon, options = {}) {
|
||||||
const { attackBonus = 0, rangeCondition = 0, attrOverride, visibility, autoAttackBonus = 0 } = options
|
const { attackBonus = 0, rangeCondition = 0, attrOverride, colorOverride, visibility, autoAttackBonus = 0, explodeOn5 = false } = options
|
||||||
|
|
||||||
const sys = weapon.system
|
const sys = weapon.system
|
||||||
const actorSys = actor.system
|
const actorSys = actor.system
|
||||||
@@ -242,23 +249,24 @@ export async function rollWeaponAttack(actor, weapon, options = {}) {
|
|||||||
const attrKey = attrOverride && actorSys.attributes[attrOverride] ? attrOverride : defaultAttr
|
const attrKey = attrOverride && actorSys.attributes[attrOverride] ? attrOverride : defaultAttr
|
||||||
const attrRank = actorSys.attributes[attrKey].rank
|
const attrRank = actorSys.attributes[attrKey].rank
|
||||||
const skillRank = actorSys.skills[skillKey].rank
|
const skillRank = actorSys.skills[skillKey].rank
|
||||||
const colorType = actorSys.skills[skillKey].colorDiceType ?? "white"
|
const baseColorType = actorSys.skills[skillKey].colorDiceType ?? "white"
|
||||||
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
|
const colorType = colorOverride || baseColorType
|
||||||
|
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
|
||||||
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
|
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
|
||||||
|
|
||||||
const totalDice = Math.max(attrRank + skillRank + attackBonus + rangeCondition + autoAttackBonus, 1)
|
const totalDice = Math.max(attrRank + skillRank + attackBonus + rangeCondition + autoAttackBonus, 1)
|
||||||
|
|
||||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
|
||||||
|
|
||||||
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)}`)
|
||||||
const diceHtml = _diceHtml(diceResults, threshold)
|
const diceHtml = _diceHtml(diceResults, threshold)
|
||||||
|
|
||||||
// Modifier summary
|
|
||||||
const modParts = []
|
const modParts = []
|
||||||
if (attackBonus !== 0) modParts.push(`${attackBonus > 0 ? "+" : ""}${attackBonus} ${game.i18n.localize("OATHHAMMER.Dialog.AttackModifier")}`)
|
if (attackBonus !== 0) modParts.push(`${attackBonus > 0 ? "+" : ""}${attackBonus} ${game.i18n.localize("OATHHAMMER.Dialog.AttackModifier")}`)
|
||||||
if (rangeCondition !== 0) modParts.push(`${rangeCondition} ${game.i18n.localize("OATHHAMMER.Dialog.RangeCondition")}`)
|
if (rangeCondition !== 0) modParts.push(`${rangeCondition} ${game.i18n.localize("OATHHAMMER.Dialog.RangeCondition")}`)
|
||||||
if (autoAttackBonus > 0) modParts.push(`+${autoAttackBonus} auto`)
|
if (autoAttackBonus > 0) modParts.push(`+${autoAttackBonus} auto`)
|
||||||
|
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
|
||||||
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 content = `
|
const content = `
|
||||||
@@ -289,7 +297,7 @@ export async function rollWeaponAttack(actor, weapon, options = {}) {
|
|||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: [roll],
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
flags: { "fvtt-oath-hammer": { weaponAttack: flagData } },
|
flags: { "fvtt-oath-hammer": { weaponAttack: flagData } },
|
||||||
}
|
}
|
||||||
@@ -327,7 +335,7 @@ export async function rollWeaponDamage(actor, weapon, options = {}) {
|
|||||||
const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1)
|
const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1)
|
||||||
const totalDice = Math.max(baseDamageDice + sv + damageBonus + autoDamageBonus, 1)
|
const totalDice = Math.max(baseDamageDice + sv + damageBonus + autoDamageBonus, 1)
|
||||||
|
|
||||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold)
|
||||||
const diceHtml = _diceHtml(diceResults, threshold)
|
const diceHtml = _diceHtml(diceResults, threshold)
|
||||||
|
|
||||||
const modParts = []
|
const modParts = []
|
||||||
@@ -361,7 +369,7 @@ export async function rollWeaponDamage(actor, weapon, options = {}) {
|
|||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: [roll],
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
@@ -395,6 +403,7 @@ export async function rollSpellCast(actor, spell, options = {}) {
|
|||||||
bonus = 0,
|
bonus = 0,
|
||||||
grimPenalty = 0,
|
grimPenalty = 0,
|
||||||
visibility,
|
visibility,
|
||||||
|
explodeOn5 = false,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const sys = spell.system
|
const sys = spell.system
|
||||||
@@ -406,7 +415,7 @@ export async function rollSpellCast(actor, spell, options = {}) {
|
|||||||
const threshold = redDice ? 3 : 4
|
const threshold = redDice ? 3 : 4
|
||||||
const colorEmoji = redDice ? "🔴" : "⬜"
|
const colorEmoji = redDice ? "🔴" : "⬜"
|
||||||
|
|
||||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
|
||||||
const diceHtml = _diceHtml(diceResults, threshold)
|
const diceHtml = _diceHtml(diceResults, threshold)
|
||||||
|
|
||||||
// Count 1s for Arcane Stress (unless Safe Spell enhancement)
|
// Count 1s for Arcane Stress (unless Safe Spell enhancement)
|
||||||
@@ -436,6 +445,7 @@ export async function rollSpellCast(actor, spell, options = {}) {
|
|||||||
if (poolPenalty !== 0) modParts.push(`${poolPenalty} ${game.i18n.localize("OATHHAMMER.Enhancement." + _cap(enhancement))}`)
|
if (poolPenalty !== 0) modParts.push(`${poolPenalty} ${game.i18n.localize("OATHHAMMER.Enhancement." + _cap(enhancement))}`)
|
||||||
if (elementalBonus > 0) modParts.push(`+${elementalBonus} ${game.i18n.localize("OATHHAMMER.Dialog.ElementMet")}`)
|
if (elementalBonus > 0) modParts.push(`+${elementalBonus} ${game.i18n.localize("OATHHAMMER.Dialog.ElementMet")}`)
|
||||||
if (grimPenalty < 0) modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`)
|
if (grimPenalty < 0) modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`)
|
||||||
|
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
|
||||||
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 stressLine = `<div class="oh-stress-line${isBlocked ? " stress-blocked" : ""}">
|
const stressLine = `<div class="oh-stress-line${isBlocked ? " stress-blocked" : ""}">
|
||||||
@@ -468,7 +478,7 @@ export async function rollSpellCast(actor, spell, options = {}) {
|
|||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: [roll],
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
@@ -495,6 +505,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
|
|||||||
isRitual = false,
|
isRitual = false,
|
||||||
bonus = 0,
|
bonus = 0,
|
||||||
visibility,
|
visibility,
|
||||||
|
explodeOn5 = false,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const sys = miracle.system
|
const sys = miracle.system
|
||||||
@@ -506,7 +517,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
|
|||||||
const threshold = 4
|
const threshold = 4
|
||||||
const colorEmoji = "⬜"
|
const colorEmoji = "⬜"
|
||||||
|
|
||||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
|
||||||
const diceHtml = _diceHtml(diceResults, threshold)
|
const diceHtml = _diceHtml(diceResults, threshold)
|
||||||
const isSuccess = successes >= dv
|
const isSuccess = successes >= dv
|
||||||
|
|
||||||
@@ -517,9 +528,10 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
|
|||||||
? game.i18n.localize("OATHHAMMER.Roll.Success")
|
? game.i18n.localize("OATHHAMMER.Roll.Success")
|
||||||
: game.i18n.localize("OATHHAMMER.Roll.Failure")
|
: game.i18n.localize("OATHHAMMER.Roll.Failure")
|
||||||
|
|
||||||
const modLine = bonus !== 0
|
const modParts = []
|
||||||
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
|
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
|
||||||
: ""
|
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
|
||||||
|
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
|
||||||
|
|
||||||
const blockedLine = !isSuccess
|
const blockedLine = !isSuccess
|
||||||
? `<div class="oh-miracle-blocked">⚠ ${game.i18n.localize("OATHHAMMER.Roll.MiracleBlocked")}</div>`
|
? `<div class="oh-miracle-blocked">⚠ ${game.i18n.localize("OATHHAMMER.Roll.MiracleBlocked")}</div>`
|
||||||
@@ -553,7 +565,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
|
|||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: [roll],
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
@@ -595,7 +607,7 @@ export async function rollDefense(actor, options = {}) {
|
|||||||
const threshold = redDice ? 3 : 4
|
const threshold = redDice ? 3 : 4
|
||||||
const colorEmoji = redDice ? "🔴" : "⬜"
|
const colorEmoji = redDice ? "🔴" : "⬜"
|
||||||
|
|
||||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold)
|
||||||
const diceHtml = _diceHtml(diceResults, threshold)
|
const diceHtml = _diceHtml(diceResults, threshold)
|
||||||
|
|
||||||
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrChoice.charAt(0).toUpperCase() + attrChoice.slice(1)}`)
|
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrChoice.charAt(0).toUpperCase() + attrChoice.slice(1)}`)
|
||||||
@@ -631,7 +643,7 @@ export async function rollDefense(actor, options = {}) {
|
|||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: [roll],
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
@@ -666,20 +678,21 @@ export async function rollWeaponDefense(actor, weapon, options = {}) {
|
|||||||
attackType = "melee",
|
attackType = "melee",
|
||||||
attrRank = 0,
|
attrRank = 0,
|
||||||
attrChoice = "agility",
|
attrChoice = "agility",
|
||||||
redDice = false,
|
colorOverride = "white",
|
||||||
traitBonus = 0,
|
traitBonus = 0,
|
||||||
armorPenalty = 0,
|
armorPenalty = 0,
|
||||||
diminishPenalty = 0,
|
diminishPenalty = 0,
|
||||||
bonus = 0,
|
bonus = 0,
|
||||||
visibility,
|
visibility,
|
||||||
|
explodeOn5 = false,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const defRank = actor.system.skills.defense.rank
|
const defRank = actor.system.skills.defense.rank
|
||||||
const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus, 1)
|
const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus, 1)
|
||||||
const threshold = redDice ? 3 : 4
|
const threshold = colorOverride === "black" ? 2 : colorOverride === "red" ? 3 : 4
|
||||||
const colorEmoji = redDice ? "🔴" : "⬜"
|
const colorEmoji = colorOverride === "black" ? "⬛" : colorOverride === "red" ? "🔴" : "⬜"
|
||||||
|
|
||||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
|
||||||
const diceHtml = _diceHtml(diceResults, threshold)
|
const diceHtml = _diceHtml(diceResults, threshold)
|
||||||
|
|
||||||
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${_cap(attrChoice)}`)
|
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${_cap(attrChoice)}`)
|
||||||
@@ -691,6 +704,7 @@ export async function rollWeaponDefense(actor, weapon, options = {}) {
|
|||||||
if (armorPenalty < 0) modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`)
|
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 (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")}`)
|
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
|
||||||
|
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
|
||||||
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 content = `
|
const content = `
|
||||||
@@ -716,7 +730,7 @@ export async function rollWeaponDefense(actor, weapon, options = {}) {
|
|||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: [roll],
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
@@ -740,36 +754,43 @@ export async function rollWeaponDefense(actor, weapon, options = {}) {
|
|||||||
*/
|
*/
|
||||||
export async function rollArmorSave(actor, armor, options = {}) {
|
export async function rollArmorSave(actor, armor, options = {}) {
|
||||||
const {
|
const {
|
||||||
av = armor.system.armorValue ?? 0,
|
av = armor.system.armorValue ?? 0,
|
||||||
isReinforced = false,
|
isReinforced = false,
|
||||||
apPenalty = 0,
|
colorOverride = null,
|
||||||
bonus = 0,
|
apPenalty = 0,
|
||||||
|
bonus = 0,
|
||||||
visibility,
|
visibility,
|
||||||
|
explodeOn5 = false,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
// Armor CAN be reduced to 0 dice (fully bypassed by AP)
|
// Armor CAN be reduced to 0 dice (fully bypassed by AP)
|
||||||
const totalDice = Math.max(av + apPenalty + bonus, 0)
|
const totalDice = Math.max(av + apPenalty + bonus, 0)
|
||||||
const threshold = isReinforced ? 3 : 4
|
const colorType = colorOverride || (isReinforced ? "red" : "white")
|
||||||
const colorEmoji = isReinforced ? "🔴" : "⬜"
|
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
|
||||||
|
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
|
||||||
|
|
||||||
let successes = 0
|
let successes = 0
|
||||||
let diceHtml = `<em>${game.i18n.localize("OATHHAMMER.Roll.ArmorBypassed")}</em>`
|
let diceHtml = `<em>${game.i18n.localize("OATHHAMMER.Roll.ArmorBypassed")}</em>`
|
||||||
let roll
|
let roll
|
||||||
|
let rolls = []
|
||||||
|
|
||||||
if (totalDice > 0) {
|
if (totalDice > 0) {
|
||||||
const result = await _rollPool(totalDice, threshold)
|
const result = await _rollPool(totalDice, threshold, explodeOn5)
|
||||||
roll = result.roll
|
roll = result.roll
|
||||||
|
rolls = result.rolls
|
||||||
successes = result.successes
|
successes = result.successes
|
||||||
diceHtml = _diceHtml(result.diceResults, threshold)
|
diceHtml = _diceHtml(result.diceResults, threshold)
|
||||||
} else {
|
} else {
|
||||||
// Zero dice — create a dummy roll with no results so Foundry can still attach it
|
// Zero dice — create a dummy roll with no results so Foundry can still attach it
|
||||||
roll = new Roll("0d6")
|
roll = new Roll("0d6")
|
||||||
await roll.evaluate()
|
await roll.evaluate()
|
||||||
|
rolls = [roll]
|
||||||
}
|
}
|
||||||
|
|
||||||
const modParts = []
|
const modParts = []
|
||||||
if (apPenalty < 0) modParts.push(`AP ${apPenalty}`)
|
if (apPenalty < 0) modParts.push(`AP ${apPenalty}`)
|
||||||
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 (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
|
||||||
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 content = `
|
const content = `
|
||||||
@@ -795,7 +816,7 @@ export async function rollArmorSave(actor, armor, options = {}) {
|
|||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: [roll],
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
@@ -803,3 +824,71 @@ export async function rollArmorSave(actor, armor, options = {}) {
|
|||||||
|
|
||||||
return { successes, totalDice }
|
return { successes, totalDice }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// INITIATIVE ROLL
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll an initiative check for an actor and post the result to chat.
|
||||||
|
*
|
||||||
|
* Characters: opposed Leadership check (Fate + Leadership skill, DV=0).
|
||||||
|
* NPCs: pool of Fate rank + initiativeBonus dice (no skill), threshold 4+.
|
||||||
|
*
|
||||||
|
* @param {Actor} actor
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {number} [options.bonus] Extra dice modifier
|
||||||
|
* @param {string} [options.visibility] Roll mode
|
||||||
|
* @param {boolean} [options.explodeOn5] Explode on 5+
|
||||||
|
* @returns {Promise<{successes: number, dv: number, isSuccess: null}>}
|
||||||
|
*/
|
||||||
|
export async function rollInitiativeCheck(actor, options = {}) {
|
||||||
|
const { bonus = 0, visibility, explodeOn5 = false } = options
|
||||||
|
|
||||||
|
if (actor.type === "character") {
|
||||||
|
return rollSkillCheck(actor, "leadership", 0, {
|
||||||
|
bonus,
|
||||||
|
visibility,
|
||||||
|
explodeOn5,
|
||||||
|
flavor: game.i18n.localize("OATHHAMMER.Roll.Initiative"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NPC: Fate rank + initiativeBonus
|
||||||
|
const sys = actor.system
|
||||||
|
const fateRank = sys.attributes?.fate?.rank ?? 1
|
||||||
|
const initBonus = sys.initiativeBonus ?? 0
|
||||||
|
const pool = Math.max(fateRank + initBonus + bonus, 1)
|
||||||
|
const threshold = 4
|
||||||
|
|
||||||
|
const { roll, rolls, successes, diceResults } = await _rollPool(pool, threshold, explodeOn5)
|
||||||
|
const diceHtml = _diceHtml(diceResults, threshold)
|
||||||
|
|
||||||
|
const modParts = []
|
||||||
|
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
|
||||||
|
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
|
||||||
|
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div class="oh-roll-card">
|
||||||
|
<div class="oh-roll-header">⚔ ${game.i18n.localize("OATHHAMMER.Roll.Initiative")} — ${actor.name}</div>
|
||||||
|
<div class="oh-roll-info">
|
||||||
|
<span>${game.i18n.localize("OATHHAMMER.Attribute.Fate")} ${fateRank}${initBonus ? ` + ${initBonus}` : ""}</span>
|
||||||
|
<span>⬜ ${pool}d6 (4+)</span>
|
||||||
|
</div>
|
||||||
|
${modLine}
|
||||||
|
<div class="oh-roll-dice">${diceHtml}</div>
|
||||||
|
<div class="oh-roll-result roll-opposed">
|
||||||
|
<span class="oh-roll-successes">${successes}</span>
|
||||||
|
<span class="oh-roll-verdict">${game.i18n.localize("OATHHAMMER.Roll.Opposed")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
||||||
|
const msgData = { speaker: ChatMessage.getSpeaker({ actor }), content, rolls: rolls, sound: CONFIG.sounds.dice }
|
||||||
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
|
await ChatMessage.create(msgData)
|
||||||
|
|
||||||
|
return { successes, dv: 0, isSuccess: null }
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as documents from "./module/documents/_module.mjs"
|
|||||||
import * as applications from "./module/applications/_module.mjs"
|
import * as applications from "./module/applications/_module.mjs"
|
||||||
import OathHammerUtils from "./module/utils.mjs"
|
import OathHammerUtils from "./module/utils.mjs"
|
||||||
import OathHammerWeaponDialog from "./module/applications/weapon-dialog.mjs"
|
import OathHammerWeaponDialog from "./module/applications/weapon-dialog.mjs"
|
||||||
|
import OathHammerCombat from "./module/combat.mjs"
|
||||||
import { rollWeaponDamage } from "./module/rolls.mjs"
|
import { rollWeaponDamage } from "./module/rolls.mjs"
|
||||||
|
|
||||||
Hooks.once("init", function () {
|
Hooks.once("init", function () {
|
||||||
@@ -18,6 +19,7 @@ Hooks.once("init", function () {
|
|||||||
game.system.api = { applications, models, documents }
|
game.system.api = { applications, models, documents }
|
||||||
|
|
||||||
CONFIG.Actor.documentClass = documents.OathHammerActor
|
CONFIG.Actor.documentClass = documents.OathHammerActor
|
||||||
|
CONFIG.Combat.documentClass = OathHammerCombat
|
||||||
CONFIG.Actor.dataModels = {
|
CONFIG.Actor.dataModels = {
|
||||||
character: models.OathHammerCharacter,
|
character: models.OathHammerCharacter,
|
||||||
npc: models.OathHammerNPC
|
npc: models.OathHammerNPC
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"distance": 5,
|
"distance": 5,
|
||||||
"units": "ft"
|
"units": "ft"
|
||||||
},
|
},
|
||||||
|
"initiative": "@attributes.fate.rank",
|
||||||
"primaryTokenAttribute": "grit",
|
"primaryTokenAttribute": "grit",
|
||||||
"socket": true,
|
"socket": true,
|
||||||
"background": "systems/fvtt-oath-hammer/assets/images/cover_art.webp",
|
"background": "systems/fvtt-oath-hammer/assets/images/cover_art.webp",
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
<span class="slots-label">{{localize "OATHHAMMER.Label.ItemSlots"}}</span>
|
<span class="slots-label">{{localize "OATHHAMMER.Label.ItemSlots"}}</span>
|
||||||
<span class="slots-value {{#if slotsOver}}slots-over{{/if}}">{{slotsUsed}} / {{slotsMax}}</span>
|
<span class="slots-value {{#if slotsOver}}slots-over{{/if}}">{{slotsUsed}} / {{slotsMax}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="initiative-bar">
|
||||||
|
<a data-action="rollInitiative" class="initiative-roll-btn" data-tooltip="{{localize 'OATHHAMMER.Roll.InitiativeHint'}}">
|
||||||
|
<i class="fa-solid fa-swords"></i> {{localize "OATHHAMMER.Dialog.RollInitiative"}}
|
||||||
|
</a>
|
||||||
|
{{#if combatantInitiative}}<span class="initiative-score" data-tooltip="{{localize 'OATHHAMMER.Label.Initiative'}}">⚔ {{combatantInitiative}}</span>{{/if}}
|
||||||
|
</div>
|
||||||
<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}}
|
||||||
@@ -22,7 +28,7 @@
|
|||||||
{{#each weapons as |weapon|}}
|
{{#each weapons as |weapon|}}
|
||||||
<li class="item-entry" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}">
|
<li class="item-entry" data-item-id="{{weapon.id}}" data-item-uuid="{{weapon.uuid}}">
|
||||||
<img src="{{weapon.img}}" class="item-img" />
|
<img src="{{weapon.img}}" class="item-img" />
|
||||||
<span class="item-name" {{#if weapon._traitsTooltip}}title="{{weapon._traitsTooltip}}"{{/if}}>{{weapon.name}}</span>
|
<span class="item-name" {{#if weapon._descTooltip}}data-tooltip="{{weapon._descTooltip}}"{{/if}}>{{weapon.name}}</span>
|
||||||
<span class="item-group">{{weapon._groupLabel}}</span>
|
<span class="item-group">{{weapon._groupLabel}}</span>
|
||||||
<span class="item-detail">{{weapon.system.damageLabel}}</span>
|
<span class="item-detail">{{weapon.system.damageLabel}}</span>
|
||||||
<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>
|
||||||
@@ -62,7 +68,7 @@
|
|||||||
{{#each armors as |armor|}}
|
{{#each armors as |armor|}}
|
||||||
<li class="item-entry" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}">
|
<li class="item-entry" data-item-id="{{armor.id}}" data-item-uuid="{{armor.uuid}}">
|
||||||
<img src="{{armor.img}}" class="item-img" />
|
<img src="{{armor.img}}" class="item-img" />
|
||||||
<span class="item-name" {{#if armor._traitsTooltip}}title="{{armor._traitsTooltip}}"{{/if}}>{{armor.name}}</span>
|
<span class="item-name" {{#if armor._descTooltip}}data-tooltip="{{armor._descTooltip}}"{{/if}}>{{armor.name}}</span>
|
||||||
<span class="item-type">{{armor._typeLabel}}</span>
|
<span class="item-type">{{armor._typeLabel}}</span>
|
||||||
<span class="item-detail">{{armor.system.armorValue}}</span>
|
<span class="item-detail">{{armor.system.armorValue}}</span>
|
||||||
<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>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
{{#each equipment as |equip|}}
|
{{#each equipment as |equip|}}
|
||||||
<li class="item-entry" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}">
|
<li class="item-entry" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}">
|
||||||
<img src="{{equip.img}}" class="item-img" />
|
<img src="{{equip.img}}" class="item-img" />
|
||||||
<span class="item-name">{{equip.name}}</span>
|
<span class="item-name" {{#if equip._descTooltip}}data-tooltip="{{equip._descTooltip}}"{{/if}}>{{equip.name}}</span>
|
||||||
<span class="item-type">{{localize equip.system.itemType}}</span>
|
<span class="item-type">{{localize equip.system.itemType}}</span>
|
||||||
<span class="item-detail">{{equip.system.quantity}}</span>
|
<span class="item-detail">{{equip.system.quantity}}</span>
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
{{#each magicItems as |mi|}}
|
{{#each magicItems as |mi|}}
|
||||||
<li class="item-entry" data-item-id="{{mi.id}}" data-item-uuid="{{mi.uuid}}">
|
<li class="item-entry" data-item-id="{{mi.id}}" data-item-uuid="{{mi.uuid}}">
|
||||||
<img src="{{mi.img}}" class="item-img" />
|
<img src="{{mi.img}}" class="item-img" />
|
||||||
<span class="item-name">{{mi.name}}</span>
|
<span class="item-name" {{#if mi._descTooltip}}data-tooltip="{{mi._descTooltip}}"{{/if}}>{{mi.name}}</span>
|
||||||
<span class="item-type">{{localize mi.system.rarity}}</span>
|
<span class="item-type">{{localize mi.system.rarity}}</span>
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
<a data-action="edit" data-item-id="{{mi.id}}" data-item-uuid="{{mi.uuid}}"><i class="fa-solid fa-edit"></i></a>
|
<a data-action="edit" data-item-id="{{mi.id}}" data-item-uuid="{{mi.uuid}}"><i class="fa-solid fa-edit"></i></a>
|
||||||
|
|||||||
@@ -1,8 +1,38 @@
|
|||||||
<section data-tab="identity" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
|
<section data-tab="identity" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
|
||||||
|
|
||||||
|
{{!-- Oaths --}}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{localize "OATHHAMMER.Label.Background"}}</legend>
|
<legend>{{localize "OATHHAMMER.Label.Oaths"}}
|
||||||
{{formInput systemFields.background enriched=enrichedBackground value=system.background name="system.background" toggled=true}}
|
{{#unless isPlayMode}}<a data-action="createOath" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
|
||||||
|
</legend>
|
||||||
|
{{#if oaths.length}}
|
||||||
|
<ul class="item-list item-list--oath">
|
||||||
|
<li class="item-list-header">
|
||||||
|
<span></span>
|
||||||
|
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
|
||||||
|
<span>{{localize "OATHHAMMER.Label.Type"}}</span>
|
||||||
|
<span>{{localize "OATHHAMMER.Label.Violated"}}</span>
|
||||||
|
<span></span>
|
||||||
|
</li>
|
||||||
|
{{#each oaths as |oath|}}
|
||||||
|
<li class="item-entry {{#if oath._violated}}oath--violated{{/if}}" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}">
|
||||||
|
<img src="{{oath.img}}" class="item-img" />
|
||||||
|
<span class="item-name" {{#if oath._descTooltip}}data-tooltip="{{oath._descTooltip}}"{{/if}}>{{oath.name}}</span>
|
||||||
|
<span class="item-type">{{oath._typeLabel}}</span>
|
||||||
|
<span class="item-violated">{{#if oath._violated}}<i class="fa-solid fa-circle-xmark"></i>{{else}}<i class="fa-regular fa-circle-check"></i>{{/if}}</span>
|
||||||
|
<div class="item-actions">
|
||||||
|
<a data-action="edit" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}"><i class="fa-solid fa-edit"></i></a>
|
||||||
|
<a data-action="delete" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}"><i class="fa-solid fa-trash"></i></a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{else}}
|
||||||
|
<p class="no-items">{{localize "OATHHAMMER.Label.NoOaths"}}</p>
|
||||||
|
{{/if}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
{{!-- Traits --}}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{localize "OATHHAMMER.Label.Traits"}}
|
<legend>{{localize "OATHHAMMER.Label.Traits"}}
|
||||||
{{#unless isPlayMode}}<a data-action="createTrait" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
|
{{#unless isPlayMode}}<a data-action="createTrait" class="create-btn"><i class="fa-solid fa-plus"></i></a>{{/unless}}
|
||||||
@@ -19,7 +49,7 @@
|
|||||||
{{#each traits as |trait|}}
|
{{#each traits as |trait|}}
|
||||||
<li class="item-entry" data-item-id="{{trait.id}}" data-item-uuid="{{trait.uuid}}" data-drag="true">
|
<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" {{#if trait._descTooltip}}data-tooltip="{{trait._descTooltip}}"{{/if}}>{{trait.name}}</span>
|
||||||
<span class="item-type">{{trait._typeLabel}}</span>
|
<span class="item-type">{{trait._typeLabel}}</span>
|
||||||
<span class="item-usage">{{trait._usageLabel}}</span>
|
<span class="item-usage">{{trait._usageLabel}}</span>
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
@@ -33,30 +63,11 @@
|
|||||||
<p class="no-items">{{localize "OATHHAMMER.Label.NoTraits"}}</p>
|
<p class="no-items">{{localize "OATHHAMMER.Label.NoTraits"}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{{#if oaths.length}}
|
|
||||||
|
{{!-- Background --}}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{localize "OATHHAMMER.Label.Oaths"}}</legend>
|
<legend>{{localize "OATHHAMMER.Label.Background"}}</legend>
|
||||||
<ul class="item-list item-list--oath">
|
{{formInput systemFields.background enriched=enrichedBackground value=system.background name="system.background" toggled=true}}
|
||||||
<li class="item-list-header">
|
|
||||||
<span></span>
|
|
||||||
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
|
|
||||||
<span>{{localize "OATHHAMMER.Label.Type"}}</span>
|
|
||||||
<span>{{localize "OATHHAMMER.Label.Violated"}}</span>
|
|
||||||
<span></span>
|
|
||||||
</li>
|
|
||||||
{{#each oaths as |oath|}}
|
|
||||||
<li class="item-entry {{#if oath._violated}}oath--violated{{/if}}" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}">
|
|
||||||
<img src="{{oath.img}}" class="item-img" />
|
|
||||||
<span class="item-name">{{oath.name}}</span>
|
|
||||||
<span class="item-type">{{oath._typeLabel}}</span>
|
|
||||||
<span class="item-violated">{{#if oath._violated}}<i class="fa-solid fa-circle-xmark"></i>{{else}}<i class="fa-regular fa-circle-check"></i>{{/if}}</span>
|
|
||||||
<div class="item-actions">
|
|
||||||
<a data-action="edit" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}"><i class="fa-solid fa-edit"></i></a>
|
|
||||||
<a data-action="delete" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}"><i class="fa-solid fa-trash"></i></a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{{/if}}
|
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
{{#each spells as |spell|}}
|
{{#each spells as |spell|}}
|
||||||
<li class="item-entry" data-item-id="{{spell.id}}" data-item-uuid="{{spell.uuid}}">
|
<li class="item-entry" data-item-id="{{spell.id}}" data-item-uuid="{{spell.uuid}}">
|
||||||
<img src="{{spell.img}}" class="item-img" />
|
<img src="{{spell.img}}" class="item-img" />
|
||||||
<span class="item-name">{{spell.name}}</span>
|
<span class="item-name" {{#if spell._descTooltip}}data-tooltip="{{spell._descTooltip}}"{{/if}}>{{spell.name}}</span>
|
||||||
<span class="item-detail">{{spell.system.difficultyValue}}</span>
|
<span class="item-detail">{{spell.system.difficultyValue}}</span>
|
||||||
<span class="item-type">{{localize spell.system.tradition}}</span>
|
<span class="item-type">{{localize spell.system.tradition}}</span>
|
||||||
<span class="item-detail">—</span>
|
<span class="item-detail">—</span>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
{{#each miracles as |miracle|}}
|
{{#each miracles as |miracle|}}
|
||||||
<li class="item-entry" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}">
|
<li class="item-entry" data-item-id="{{miracle.id}}" data-item-uuid="{{miracle.uuid}}">
|
||||||
<img src="{{miracle.img}}" class="item-img" />
|
<img src="{{miracle.img}}" class="item-img" />
|
||||||
<span class="item-name">{{miracle.name}}</span>
|
<span class="item-name" {{#if miracle._descTooltip}}data-tooltip="{{miracle._descTooltip}}"{{/if}}>{{miracle.name}}</span>
|
||||||
<span class="item-detail">{{miracle.system.divineTradition}}</span>
|
<span class="item-detail">{{miracle.system.divineTradition}}</span>
|
||||||
<div class="item-actions">
|
<div class="item-actions">
|
||||||
<a data-action="castMiracle" data-item-id="{{miracle.id}}" title="{{localize 'OATHHAMMER.Action.InvokeMiracle'}}"><i class="fa-solid fa-hands-praying miracle-cast-icon"></i></a>
|
<a data-action="castMiracle" data-item-id="{{miracle.id}}" title="{{localize 'OATHHAMMER.Action.InvokeMiracle'}}"><i class="fa-solid fa-hands-praying miracle-cast-icon"></i></a>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{!-- Row 3: Unified stats band: resources | attributes | arcane stress --}}
|
{{!-- Row 3: Unified stats band: resources | attributes --}}
|
||||||
<div class="character-stats-band">
|
<div class="character-stats-band">
|
||||||
<fieldset class="character-resources">
|
<fieldset class="character-resources">
|
||||||
<div class="character-resource">
|
<div class="character-resource">
|
||||||
@@ -63,10 +63,6 @@
|
|||||||
<span class="res-sep">/</span>
|
<span class="res-sep">/</span>
|
||||||
{{formInput systemFields.luck.fields.max value=system.luck.max name="system.luck.max" disabled=isPlayMode}}
|
{{formInput systemFields.luck.fields.max value=system.luck.max name="system.luck.max" disabled=isPlayMode}}
|
||||||
</div>
|
</div>
|
||||||
<div class="character-resource">
|
|
||||||
<span class="resource-label">{{localize "OATHHAMMER.Label.Defense"}}</span>
|
|
||||||
<input type="text" value="{{system.defense.value}}" disabled class="defense-display" />
|
|
||||||
</div>
|
|
||||||
<div class="character-resource">
|
<div class="character-resource">
|
||||||
<span class="resource-label">{{localize "OATHHAMMER.Label.Movement"}}</span>
|
<span class="resource-label">{{localize "OATHHAMMER.Label.Movement"}}</span>
|
||||||
{{formInput systemFields.movement.fields.base value=system.movement.base name="system.movement.base" disabled=isPlayMode}}
|
{{formInput systemFields.movement.fields.base value=system.movement.base name="system.movement.base" disabled=isPlayMode}}
|
||||||
@@ -84,15 +80,6 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="character-arcane-stress">
|
|
||||||
<legend>{{localize "OATHHAMMER.Label.ArcaneStress"}}</legend>
|
|
||||||
<div class="stress-inputs">
|
|
||||||
{{formInput systemFields.arcaneStress.fields.value value=system.arcaneStress.value name="system.arcaneStress.value" disabled=isPlayMode}}
|
|
||||||
<span>/</span>
|
|
||||||
{{formInput systemFields.arcaneStress.fields.threshold value=system.arcaneStress.threshold name="system.arcaneStress.threshold" disabled=isPlayMode}}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
<section data-tab="combat" class="tab">
|
<section data-tab="combat" class="tab">
|
||||||
|
<div class="initiative-bar">
|
||||||
|
<a data-action="rollInitiative" class="initiative-roll-btn" data-tooltip="{{localize 'OATHHAMMER.Roll.InitiativeHint'}}">
|
||||||
|
<i class="fa-solid fa-swords"></i> {{localize "OATHHAMMER.Dialog.RollInitiative"}}
|
||||||
|
</a>
|
||||||
|
{{#if combatantInitiative}}<span class="initiative-score" data-tooltip="{{localize 'OATHHAMMER.Label.Initiative'}}">⚔ {{combatantInitiative}}</span>{{/if}}
|
||||||
|
</div>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{localize "OATHHAMMER.Label.Weapons"}}</legend>
|
<legend>{{localize "OATHHAMMER.Label.Weapons"}}</legend>
|
||||||
{{#if weapons.length}}
|
{{#if weapons.length}}
|
||||||
|
|||||||
@@ -43,6 +43,10 @@
|
|||||||
<label>{{localize "OATHHAMMER.Label.DamageBonus"}}</label>
|
<label>{{localize "OATHHAMMER.Label.DamageBonus"}}</label>
|
||||||
{{formInput systemFields.damageBonus value=system.damageBonus name="system.damageBonus" disabled=isPlayMode}}
|
{{formInput systemFields.damageBonus value=system.damageBonus name="system.damageBonus" disabled=isPlayMode}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{localize "OATHHAMMER.Label.InitiativeBonus"}}</label>
|
||||||
|
{{formInput systemFields.initiativeBonus value=system.initiativeBonus name="system.initiativeBonus" disabled=isPlayMode}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@@ -37,6 +37,20 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-option-row">
|
||||||
|
<label>{{localize "OATHHAMMER.Dialog.DiceColor"}}</label>
|
||||||
|
<select name="colorOverride">
|
||||||
|
{{#each colorOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
|
||||||
|
</select>
|
||||||
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.DiceColorHint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-option-row roll-option-check">
|
||||||
|
<label for="armorExplodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
|
||||||
|
<input type="checkbox" id="armorExplodeOn5" name="explodeOn5" />
|
||||||
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{{!-- Visibility -----------------------------------------------------------}}
|
{{!-- Visibility -----------------------------------------------------------}}
|
||||||
|
|||||||
@@ -53,6 +53,12 @@
|
|||||||
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span>
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-option-row roll-option-check">
|
||||||
|
<label for="miracleExplodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
|
||||||
|
<input type="checkbox" id="miracleExplodeOn5" name="explodeOn5" />
|
||||||
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{{!-- Visibility --}}
|
{{!-- Visibility --}}
|
||||||
|
|||||||
@@ -73,6 +73,8 @@
|
|||||||
<select name="luckSpend">
|
<select name="luckSpend">
|
||||||
{{#each luckOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
|
{{#each luckOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
|
||||||
</select>
|
</select>
|
||||||
|
<label class="luck-human-label" for="luckIsHuman">{{localize "OATHHAMMER.Dialog.LuckHuman"}}</label>
|
||||||
|
<input type="checkbox" id="luckIsHuman" name="luckIsHuman" value="true" {{#if isHuman}}checked{{/if}} />
|
||||||
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}})</span>
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}})</span>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -67,6 +67,12 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-option-row roll-option-check">
|
||||||
|
<label for="spellExplodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
|
||||||
|
<input type="checkbox" id="spellExplodeOn5" name="explodeOn5" />
|
||||||
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{{!-- Visibility --}}
|
{{!-- Visibility --}}
|
||||||
|
|||||||
@@ -47,6 +47,14 @@
|
|||||||
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span>
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-option-row">
|
||||||
|
<label>{{localize "OATHHAMMER.Dialog.DiceColor"}}</label>
|
||||||
|
<select name="colorOverride">
|
||||||
|
{{#each colorOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
|
||||||
|
</select>
|
||||||
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.DiceColorHint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{#if isRanged}}
|
{{#if isRanged}}
|
||||||
<div class="roll-option-row">
|
<div class="roll-option-row">
|
||||||
<label>{{localize "OATHHAMMER.Dialog.RangeCondition"}}</label>
|
<label>{{localize "OATHHAMMER.Dialog.RangeCondition"}}</label>
|
||||||
@@ -56,6 +64,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="roll-option-row roll-option-check">
|
||||||
|
<label for="attackExplodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
|
||||||
|
<input type="checkbox" id="attackExplodeOn5" name="explodeOn5" />
|
||||||
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{{!-- Visibility --}}
|
{{!-- Visibility --}}
|
||||||
|
|||||||
@@ -72,6 +72,20 @@
|
|||||||
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span>
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.AttackModifierHint"}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-option-row">
|
||||||
|
<label>{{localize "OATHHAMMER.Dialog.DiceColor"}}</label>
|
||||||
|
<select name="colorOverride">
|
||||||
|
{{#each colorOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
|
||||||
|
</select>
|
||||||
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.DiceColorHint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-option-row roll-option-check">
|
||||||
|
<label for="defenseExplodeOn5">{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
|
||||||
|
<input type="checkbox" id="defenseExplodeOn5" name="explodeOn5" />
|
||||||
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{{!-- Visibility --}}
|
{{!-- Visibility --}}
|
||||||
|
|||||||
Reference in New Issue
Block a user