diff --git a/TODO.md b/TODO.md index ac146ca..e91b9af 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,7 @@ ## High priority (mechanics gaps) - [ ] Call `resolveHeadInjury()` in the attack handler when a Head hit penetrates armor -- [ ] Resolve stuck-weapon follow-up after impale/slash special hits (`attemptWeaponRemoval`, `removeStuckWeaponFromSelf`, `removeStuckWeaponWithFirstAid`) +- [x] Resolve stuck-weapon follow-up after impale/slash special hits (`attemptWeaponRemoval`, `removeStuckWeaponFromSelf`, `removeStuckWeaponWithFirstAid`) ## Medium priority (missing features) diff --git a/public/app.js b/public/app.js index d8bf583..3a59301 100644 --- a/public/app.js +++ b/public/app.js @@ -574,6 +574,45 @@ function combatantOptions(selectedId) { return combatants().map((c) => ``).join(''); } +function renderStuckWeaponFollowUp({ attackerCombatantId, defenderCombatantId, weaponName, kind }) { + const wrap = el(`
`); + wrap.appendChild(el(`Weapon stuck (${kind}). Choose removal attempt:
`)); + const removalResult = el(``); + + async function doRemoval(removalType, extra = {}) { + try { + const res = await api('POST', '/api/combat/stuck-weapon-removal', { + attackerCombatantId, defenderCombatantId, weaponName, kind, removalType, ...extra, + }); + state.combat = res.combatState; + const r = res.result; + const outcome = r.weaponBreaks ? 'Weapon breaks!' : r.success ? 'Weapon removed successfully.' : 'Weapon stays stuck.'; + removalResult.innerHTML = `${escapeHtml(outcome)}
`; + await loadLog(); + renderSidebar(); + renderLog(); + } catch (err) { + removalResult.innerHTML = ``; + } + } + + const attackerBtn = el(``); + attackerBtn.addEventListener('click', () => doRemoval('attacker')); + + const selfBtn = el(``); + selfBtn.addEventListener('click', () => doRemoval('self')); + + const faSkill = el(``); + const faBtn = el(``); + faBtn.addEventListener('click', () => doRemoval('first-aid', { firstAidSkillPercent: Number(faSkill.value) || 0 })); + + const btnRow = el(``); + btnRow.append(attackerBtn, selfBtn, faSkill, faBtn); + wrap.appendChild(btnRow); + wrap.appendChild(removalResult); + return wrap; +} + function renderCombatMain() { const wrap = el(``); if (combatants().length < 1) { @@ -632,12 +671,27 @@ function renderCombatMain() { try { const res = await api('POST', '/api/combat/attack', body); state.combat = res.combatState; - resultBox.innerHTML = ` + resultBox.innerHTML = ''; + const summary = el(`${tierTag(res.result.attackCheck.tier)} roll ${res.result.attackCheck.roll} vs ${res.result.effectiveSkillPercent}% (base ${res.result.effectiveSkillPercent - res.result.modifierTotal}${res.result.modifierTotal ? `, modifiers ${res.result.modifierTotal > 0 ? '+' : ''}${res.result.modifierTotal}` : ''})
${res.result.hitLocationRoll ? `Hit location: ${res.result.hitLocationRoll.location}
` : ''} ${res.result.damageThrough != null ? `Damage through: ${res.result.damageThrough}
` : ''} ${res.result.fumble ? `Fumble: ${res.result.fumble.results.map((r) => r.effect).join('; ')}
` : ''} - `; +