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
+35
View File
@@ -421,6 +421,41 @@ app.post('/api/combat/attack', (req, res) => {
res.json({ combatState: dbApi.combat.get(), result });
});
app.post('/api/combat/stuck-weapon-removal', (req, res) => {
const { attackerCombatantId, defenderCombatantId, weaponName, kind, removalType, firstAidSkillPercent } = req.body || {};
const current = dbApi.combat.get();
if (!current) return res.status(400).json({ error: 'No active combat' });
const state = current.state;
const attacker = (state.combatants || []).find((c) => c.id === attackerCombatantId);
const defender = (state.combatants || []).find((c) => c.id === defenderCombatantId);
if (!attacker || !defender) return res.status(400).json({ error: 'Unknown attacker or defender combatant id' });
if (kind !== 'impale' && kind !== 'slash') return res.status(400).json({ error: 'kind must be impale or slash' });
let result;
if (removalType === 'attacker') {
const weaponEntry = attacker.weapons.find((w) => w.weapon_name.toLowerCase() === (weaponName || '').toLowerCase());
if (!weaponEntry) return res.status(400).json({ error: `Attacker has no weapon named "${weaponName}"` });
result = rq3.attemptWeaponRemoval(weaponEntry.skill_percent, kind);
result.removalType = 'attacker';
} else if (removalType === 'self') {
result = { success: rq3.removeStuckWeaponFromSelf(defender.str + defender.con), weaponBreaks: false, removalType: 'self' };
} else if (removalType === 'first-aid') {
const pct = Number(firstAidSkillPercent) || 0;
result = { success: rq3.removeStuckWeaponWithFirstAid(pct), weaponBreaks: false, removalType: 'first-aid' };
} else {
return res.status(400).json({ error: 'removalType must be attacker, self, or first-aid' });
}
const outcome = result.weaponBreaks ? 'weapon breaks' : result.success ? 'weapon removed' : 'weapon stays stuck';
dbApi.log.append({
type: 'combat',
summary: `Stuck-weapon removal (${removalType}, ${kind}): ${outcome}`,
details: result,
});
res.json({ combatState: dbApi.combat.get(), result });
});
app.post('/api/combat/cast-spell', (req, res) => {
const { casterCombatantId, targetCombatantId, mechanicId, mpSpent } = req.body || {};
const current = dbApi.combat.get();