diff --git a/lang/en.json b/lang/en.json index 6bf4459..44be259 100644 --- a/lang/en.json +++ b/lang/en.json @@ -22,6 +22,8 @@ "AWEMMY.Condition.Edge": "Edge", "AWEMMY.Condition.Hampered": "Hampered", "AWEMMY.Condition.Inhibited": "Inhibited", + "AWEMMY.Condition.Penalty": "Penalty", + "AWEMMY.Condition.DCPenalty": "DC Penalty", "AWEMMY.Condition.Jumbled": "Jumbled", "AWEMMY.Condition.Mishap": "Mishap", "AWEMMY.Condition.Prone": "Prone", @@ -31,6 +33,7 @@ "AWEMMY.Condition.Panel": "Conditions", "AWEMMY.Roll.ConditionBonus": "Condition", "AWEMMY.Item.Description": "Description", + "AWEMMY.Item.RollBonus": "Roll Bonus", "AWEMMY.Error.TraitPasteFailed": "Failed to update traits — please try again.", "AWEMMY.Kit.Use": "Use Kit", "AWEMMY.Kit.Used": "{name} used", @@ -59,9 +62,13 @@ "AWEMMY.Roll.Check": "Check", "AWEMMY.Roll.Roll": "Roll", "AWEMMY.Roll.DialogTitle": "Roll: {name}", - "AWEMMY.Roll.SituationalBonus": "Situational Bonus", + "AWEMMY.Roll.RollTwice": "Roll Twice", + "AWEMMY.Roll.Normal": "Normal", + "AWEMMY.Roll.TakeHigher": "Take Higher", + "AWEMMY.Roll.TakeLower": "Take Lower", + "AWEMMY.Roll.SituationalBonus": "Bonus or Penalty", "AWEMMY.Roll.AttributeBonus": "Attribute Bonus", - "AWEMMY.Roll.KnowledgeBonus": "Knowledge Bonus", + "AWEMMY.Roll.KnowledgeBonus": "Item Bonus", "AWEMMY.Roll.Formula": "Formula", "AWEMMY.Roll.DC": "DC", "AWEMMY.Roll.Visibility": "Visibility", @@ -79,7 +86,8 @@ "AWEMMY.Character.HPTemp": "Temp HP", "AWEMMY.Character.FlowPoints": "Flow Points", "AWEMMY.Character.FlowPointsTemp": "Temp FP", - "AWEMMY.Character.BoostLevel": "Boost Level", + "AWEMMY.Character.BoostLevel": "Boost", + "AWEMMY.Character.Repertoire": "Repertoire", "AWEMMY.Character.Mod": "MOD", "AWEMMY.Character.DC": "DC", "AWEMMY.Character.Bonus": "+/−", diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index be2a5b8..03ceb20 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -100,6 +100,11 @@ export default class AwECharacterSheet extends AwEActorSheet { img: `systems/fvtt-adventures-with-emmy/assets/conditions/${c.id}.svg`, active: doc.statuses.has(c.id) })) + context.inhibitedActive = doc.statuses.has("inhibited") + context.vulnerableActive = doc.statuses.has("vulnerable") + context.inhibitedPenalty = doc.system.inhibitedPenalty + context.vulnerablePenalty = doc.system.vulnerablePenalty + context.hasConditionPenalties = context.inhibitedActive || context.vulnerableActive break case "biography": context.tab = context.tabs.biography diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 8b1064a..03c5049 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -44,11 +44,7 @@ export default class AwEActor extends Actor { const attribute = this.system.attributes[attrId] if (!attribute) return null - const knowledgeBonuses = this.itemTypes.field?.map(f => ({ - label: f.name, - bonus: f.system.knowledgeBonus ?? "" - })).filter(f => f.bonus !== "") ?? [] - + const knowledgeBonuses = this.#buildItemBonuses() const { conditionBonus, conditionLabels } = this.#buildConditionOptions() const roll = await AwERoll.prompt({ @@ -101,12 +97,7 @@ export default class AwEActor extends Actor { const attribute = this.system.attributes[attributeId] if (!attribute) return null - // Collect knowledge bonuses from embedded Field items - const knowledgeBonuses = this.itemTypes.field?.map(f => ({ - label: f.name, - bonus: f.system.knowledgeBonus ?? "" - })).filter(f => f.bonus !== "") ?? [] - + const knowledgeBonuses = this.#buildItemBonuses() const { conditionBonus, conditionLabels } = this.#buildConditionOptions() const roll = await AwERoll.prompt({ @@ -127,6 +118,19 @@ export default class AwEActor extends Actor { return roll } + /** Collect roll bonuses from all item types that declare a rollBonus or knowledgeBonus. */ + #buildItemBonuses() { + const entries = [ + // Field items use the original knowledgeBonus field + ...(this.itemTypes.field ?? []).map(i => ({ label: i.name, bonus: i.system.knowledgeBonus ?? "" })), + // Ability, equipment, specialization use rollBonus + ...(this.itemTypes.ability ?? []).map(i => ({ label: i.name, bonus: i.system.rollBonus ?? "" })), + ...(this.itemTypes.equipment ?? []).map(i => ({ label: i.name, bonus: i.system.rollBonus ?? "" })), + ...(this.itemTypes.specialization ?? []).map(i => ({ label: i.name, bonus: i.system.rollBonus ?? "" })), + ] + return entries.filter(e => e.bonus !== "") + } + #buildConditionOptions() { let conditionBonus = 0 const conditionLabels = [] @@ -142,6 +146,11 @@ export default class AwEActor extends Actor { conditionBonus -= 2 conditionLabels.push({ label: game.i18n.localize("AWEMMY.Condition.Jumbled"), bonus: -2 }) } + if (this.statuses.has("inhibited")) { + const penalty = this.system.inhibitedPenalty ?? 2 + conditionBonus -= penalty + conditionLabels.push({ label: game.i18n.localize("AWEMMY.Condition.Inhibited"), bonus: -penalty }) + } return { conditionBonus, conditionLabels } } diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs index 6cb1bc6..a4a5cf6 100644 --- a/module/documents/roll.mjs +++ b/module/documents/roll.mjs @@ -25,6 +25,7 @@ export default class AwERoll extends Roll { get damageCritical() { return this.options.damageCritical ?? false } get conditionBonus() { return this.options.conditionBonus ?? 0 } get conditionLabels() { return this.options.conditionLabels ?? [] } + get rollTwice() { return this.options.rollTwice ?? "" } // --- Outcome calculation --- @@ -116,17 +117,23 @@ export default class AwERoll extends Roll { const el = dialog.element const bonusSelect = el.querySelector('#awe-bonus') const knowledgeSel = el.querySelector('#awe-knowledge') + const rollTwiceSel = el.querySelector('#awe-roll-twice') const preview = el.querySelector('#awe-formula-preview') + const diceExprEl = el.querySelector('#awe-dice-expr') function updatePreview() { const sit = parseInt(bonusSelect?.value) || 0 const kn = parseInt(knowledgeSel?.value) || 0 const total = baseMod + sit + kn const sign = total >= 0 ? '+' : '−' const abs = Math.abs(total) - preview.textContent = total === 0 ? '1d20' : `1d20 ${sign} ${abs}` + const mode = rollTwiceSel?.value + const dice = mode === 'higher' ? '2d20kh1' : mode === 'lower' ? '2d20kl1' : '1d20' + if (diceExprEl) diceExprEl.textContent = dice + preview.textContent = total === 0 ? dice : `${dice} ${sign} ${abs}` } bonusSelect?.addEventListener('change', updatePreview) knowledgeSel?.addEventListener('change', updatePreview) + rollTwiceSel?.addEventListener('change', updatePreview) updatePreview() }, buttons: [{ @@ -146,10 +153,16 @@ export default class AwERoll extends Roll { const knowledgeBonus = parseInt(result.knowledgeBonus) || 0 const dc = result.dc !== "" ? parseInt(result.dc) : undefined const rollMode = result.visibility ?? game.settings.get("core", "rollMode") + const rollTwice = result.rollTwice ?? "" - // Formula: 1d20 + (mod + attrBonus) [± bonus] [± knowledgeBonus] [± conditionBonus] + // Dice expression based on roll-twice mode + const diceExpr = rollTwice === 'higher' ? '2d20kh1' + : rollTwice === 'lower' ? '2d20kl1' + : '1d20' + + // Formula: {diceExpr} + (mod + attrBonus) [± bonus] [± knowledgeBonus] [± conditionBonus] const totalMod = mod + attrBonus + bonus + knowledgeBonus + (options.conditionBonus ?? 0) - let formula = `1d20` + let formula = diceExpr if (totalMod > 0) formula += ` + ${totalMod}` else if (totalMod < 0) formula += ` - ${Math.abs(totalMod)}` @@ -162,6 +175,7 @@ export default class AwERoll extends Roll { knowledgeBonus, conditionBonus: options.conditionBonus ?? 0, conditionLabels: options.conditionLabels ?? [], + rollTwice, dc, actorId: options.actorId, actorName: options.actorName, @@ -175,8 +189,9 @@ export default class AwERoll extends Roll { await roll.evaluate() // Compute degree of success when a DC is known + // Use the *kept* die result (active:true) for nat-20/nat-1 adjustment if (dc !== undefined) { - const d20Value = roll.dice[0]?.results[0]?.result ?? 0 + const d20Value = roll.dice[0]?.results.find(r => r.active)?.result ?? 0 roll.options.outcome = AwERoll.computeOutcome(roll.total, dc, d20Value) } @@ -220,6 +235,7 @@ export default class AwERoll extends Roll { conditionBonus: isPrivate ? null : this.conditionBonus, conditionLabels: this.conditionLabels, dice: this.dice, + rollTwice: this.rollTwice, outcome: isPrivate ? null : this.outcome, dc: this.dc, actorId: this.actorId, diff --git a/module/models/ability.mjs b/module/models/ability.mjs index 2779b0c..fdcea2f 100644 --- a/module/models/ability.mjs +++ b/module/models/ability.mjs @@ -28,6 +28,7 @@ export default class AwEAbility extends foundry.abstract.TypeDataModel { schema.isDaily = new fields.BooleanField({ required: true, initial: false }) schema.flowPointCost = new fields.NumberField({ required: true, nullable: false, initial: 0, min: 0, integer: true }) schema.usedToday = new fields.BooleanField({ required: true, initial: false }) + schema.rollBonus = new fields.StringField({ initial: "", required: false, nullable: true }) return schema } diff --git a/module/models/character.mjs b/module/models/character.mjs index 4a881fc..1e7aad6 100644 --- a/module/models/character.mjs +++ b/module/models/character.mjs @@ -51,6 +51,10 @@ export default class AwECharacter extends foundry.abstract.TypeDataModel { }, {}) ) + // Condition penalty magnitudes (used when the respective condition is active) + schema.inhibitedPenalty = new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2, min: 0 }) + schema.vulnerablePenalty = new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2, min: 0 }) + return schema } diff --git a/module/models/equipment.mjs b/module/models/equipment.mjs index 0522c7d..0f5218f 100644 --- a/module/models/equipment.mjs +++ b/module/models/equipment.mjs @@ -7,6 +7,7 @@ export default class AwEEquipment extends foundry.abstract.TypeDataModel { schema.description = new fields.HTMLField({ required: true, textSearch: true }) schema.quantity = new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }) schema.weight = new fields.NumberField({ required: true, nullable: false, initial: 0, min: 0 }) + schema.rollBonus = new fields.StringField({ initial: "", required: false, nullable: true }) return schema } diff --git a/module/models/specialization.mjs b/module/models/specialization.mjs index 53ad326..3bc44b6 100644 --- a/module/models/specialization.mjs +++ b/module/models/specialization.mjs @@ -25,6 +25,7 @@ export default class AwESpecialization extends foundry.abstract.TypeDataModel { schema.description = new fields.HTMLField({ required: true, textSearch: true }) schema.traits = new fields.ArrayField(new fields.StringField()) + schema.rollBonus = new fields.StringField({ initial: "", required: false, nullable: true }) return schema } diff --git a/templates/ability.hbs b/templates/ability.hbs index 25f420e..e3f3749 100644 --- a/templates/ability.hbs +++ b/templates/ability.hbs @@ -57,6 +57,11 @@ +