feat: wire up stuck-weapon follow-up for impale/slash specials

Add POST /api/combat/stuck-weapon-removal endpoint supporting three
removal types: attacker (weapon skill roll), self (STR+CON roll), and
first-aid (First Aid skill roll). After a special hit with damage
through, the attack result UI shows a stuck-weapon card with buttons
for each removal option.
This commit is contained in:
2026-07-01 08:19:01 +10:00
parent 4742a7d876
commit c223912915
3 changed files with 92 additions and 3 deletions
+56 -2
View File
@@ -574,6 +574,45 @@ function combatantOptions(selectedId) {
return combatants().map((c) => `<option value="${c.id}" ${c.id === selectedId ? 'selected' : ''}>${escapeHtml(c.name)}</option>`).join('');
}
function renderStuckWeaponFollowUp({ attackerCombatantId, defenderCombatantId, weaponName, kind }) {
const wrap = el(`<div class="card" style="margin-top:0.75rem"></div>`);
wrap.appendChild(el(`<p><strong>Weapon stuck (${kind}).</strong> Choose removal attempt:</p>`));
const removalResult = el(`<div></div>`);
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 = `<p>${escapeHtml(outcome)}</p>`;
await loadLog();
renderSidebar();
renderLog();
} catch (err) {
removalResult.innerHTML = `<p class="error-message">${escapeHtml(err.message)}</p>`;
}
}
const attackerBtn = el(`<button class="button">Attacker removes weapon</button>`);
attackerBtn.addEventListener('click', () => doRemoval('attacker'));
const selfBtn = el(`<button class="button">Target removes from self</button>`);
selfBtn.addEventListener('click', () => doRemoval('self'));
const faSkill = el(`<input type="number" placeholder="First Aid %" style="width:7rem">`);
const faBtn = el(`<button class="button">Remove with First Aid</button>`);
faBtn.addEventListener('click', () => doRemoval('first-aid', { firstAidSkillPercent: Number(faSkill.value) || 0 }));
const btnRow = el(`<div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;margin-top:0.5rem"></div>`);
btnRow.append(attackerBtn, selfBtn, faSkill, faBtn);
wrap.appendChild(btnRow);
wrap.appendChild(removalResult);
return wrap;
}
function renderCombatMain() {
const wrap = el(`<div></div>`);
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(`<div>
<p>${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}` : ''})</p>
${res.result.hitLocationRoll ? `<p>Hit location: ${res.result.hitLocationRoll.location}</p>` : ''}
${res.result.damageThrough != null ? `<p>Damage through: ${res.result.damageThrough}</p>` : ''}
${res.result.fumble ? `<p>Fumble: ${res.result.fumble.results.map((r) => r.effect).join('; ')}</p>` : ''}
`;
</div>`);
resultBox.appendChild(summary);
const special = res.result.damageResult?.special;
const weaponStuck = (special === 'impale' || special === 'slash') && res.result.damageThrough > 0;
if (weaponStuck) {
const stuckBox = renderStuckWeaponFollowUp({
attackerCombatantId: body.attackerCombatantId,
defenderCombatantId: body.defenderCombatantId,
weaponName: body.weaponName,
kind: special,
});
resultBox.appendChild(stuckBox);
}
await loadLog();
renderSidebar();
renderLog();