diff --git a/database/routes/simulationRoutes.js b/database/routes/simulationRoutes.js index 3bce954..952db21 100644 --- a/database/routes/simulationRoutes.js +++ b/database/routes/simulationRoutes.js @@ -162,13 +162,14 @@ function parseRof(weapon) { if (weaponIsGrenade(weapon)) return { single: true, semi: null, full: null }; const rof = String(weapon?.rof || '').trim(); const parts = rof.split('/').map(part => part.trim()); + const single = value => /^s$/i.test(value || '') || /^single$/i.test(value || ''); const numeric = value => { if (!value || value === '-' || /^s$/i.test(value) || /^single$/i.test(value)) return null; const n = Number(String(value).match(/\d+/)?.[0] || 0); return n > 0 ? n : null; }; return { - single: rof ? true : weaponUsesAmmo(weapon), + single: rof ? single(parts[0]) : weaponUsesAmmo(weapon), semi: numeric(parts[1]), full: numeric(parts[2]), }; @@ -224,7 +225,7 @@ function chooseAttackWeapon(player, enemies = []) { const weapons = Array.isArray(player.weapons) ? player.weapons : []; const profile = player.combatProfile || 'balanced'; const availableGrenades = weapons.filter(w => weaponIsGrenade(w) && weaponUsesAmmo(w) && Number(w.ammoRemaining || 0) > 0); - const grenadeChance = profile === 'ranged' ? 0.24 : (profile === 'melee' ? 0.08 : 0.18); + const grenadeChance = profile === 'ranged' ? 0.32 : (profile === 'melee' ? 0.12 : 0.24); if (availableGrenades.length && enemies.filter(e => e.wounds > 0).length > 1 && Math.random() < grenadeChance) { return availableGrenades.find(w => /frag/i.test(w.name || '')) || availableGrenades[0]; } @@ -434,7 +435,18 @@ function combatRound(roundNum, players, enemies, checks, sceneTitle, rollFeed, e const diffCheckMod = (diff && diff.checkMod) || 0; if (checks.length > 0 && Math.random() < 0.45) { const chk = checks[Math.floor(Math.random() * checks.length)]; - performCheck(player, chk.name, chk.stat, (chk.modifier || 0) + diffCheckMod, rollFeed, sceneTitle); + const r = performCheck(player, chk.name, chk.stat, (chk.modifier || 0) + diffCheckMod, rollFeed, sceneTitle); + events.push({ + type: 'checkRoll', + player: player.name, + check: chk.name, + stat: chk.stat, + roll: r.result, + tn: r.effective, + success: r.success, + dos: r.dos, + dof: r.dof, + }); } else { const weapon = chooseAttackWeapon(player, enemies); const fireMode = chooseFireMode(weapon); @@ -635,6 +647,17 @@ function runSimulation(mission, playerStates, diff, options = {}) { const player = playerStates.find(p => p.name === a.player); if (!player || !isAlive(player)) { sceneFailures++; continue; } const r = performCheck(player, a.check.name, a.stat, a.modifier + d.checkMod, rollFeed, title); + sceneEvents.push({ + type: 'checkRoll', + player: player.name, + check: a.check.name, + stat: a.stat, + roll: r.result, + tn: r.effective, + success: r.success, + dos: r.dos, + dof: r.dof, + }); if (r.success) { sceneSuccesses++; if (r.dos >= 3) sceneEvents.push({ type: 'heroCheck', player: player.name, check: a.check.name, dos: r.dos }); diff --git a/src/components/MissionTab.jsx b/src/components/MissionTab.jsx index 1a73a57..5898c39 100644 --- a/src/components/MissionTab.jsx +++ b/src/components/MissionTab.jsx @@ -99,6 +99,19 @@ function defaultChecksForScene(scene) { ]; } +const STANDARD_ACTIONS = [ + { name: 'Scan for threats', skill: 'Awareness', characteristic: 'Per', target: 45, modifier: 0, reward: 'Spot hidden enemies, traps, movement, or useful battlefield detail.' }, + { name: 'Read tactical situation', skill: 'Tactics', characteristic: 'Int', target: 45, modifier: 0, rulesNote: 'Use Tactics to assess doctrine, priority targets, ambush risk, or best approach. This is not a free Squad Mode ability.', reward: 'Identify a tactical edge or priority threat.' }, + { name: 'Coordinate squad action', skill: 'Command', characteristic: 'Fel', target: 45, modifier: 0, reward: 'Give the kill-team a clear direction or steady an ally under pressure.' }, + { name: 'Operate or bypass machine', skill: 'Tech-Use', characteristic: 'Int', target: 45, modifier: 0, reward: 'Open, disable, decode, stabilize, or exploit a machine system.' }, + { name: 'Diagnose biological threat', skill: 'Medicae', characteristic: 'Int', target: 45, modifier: 0, reward: 'Identify injury, mutation, toxin, infection, or usable biological data.' }, + { name: 'Scrutinise motive or lie', skill: 'Scrutiny', characteristic: 'Per', target: 45, modifier: 0, reward: 'Read fear, deception, corruption, or hidden intent.' }, + { name: 'Force open or hold position', skill: 'Strength', characteristic: 'S', target: 45, modifier: 0, reward: 'Move an obstacle, hold a door, brace a collapse, or force a path.' }, + { name: 'Move under pressure', skill: 'Agility', characteristic: 'Ag', target: 45, modifier: 0, reward: 'Cross a gap, dodge debris, move fast, or reach cover.' }, + { name: 'Set or disarm charges', skill: 'Demolition', characteristic: 'Int', target: 45, modifier: 0, reward: 'Place explosives, disarm charges, or identify weak structural points.' }, + { name: 'Resist fear or warp pressure', skill: 'Willpower', characteristic: 'Wp', target: 45, modifier: 0, reward: 'Hold discipline against fear, psychic pressure, or corruption.' }, +]; + function combatForScene(scene) { if (scene?.combat && typeof scene.combat === 'object') return scene.combat; const enemies = sceneEnemies(scene); @@ -881,6 +894,7 @@ export default function MissionTab({ authedPlayer }) { return { player: current.player || defaultPlayer, checkName: current.checkName || checks[0]?.name || '', + standardActionName: current.standardActionName || '', skillDifficultyModifier: current.skillDifficultyModifier ?? scene?.skillDifficultyModifier ?? 0, skillSituationModifiers: Array.isArray(current.skillSituationModifiers) ? current.skillSituationModifiers @@ -977,7 +991,8 @@ export default function MissionTab({ authedPlayer }) { async function rollMiniSceneCheck(sceneIndex, scene) { const state = miniState(sceneIndex, scene); const checks = listForScene(scene, 'checks').length ? listForScene(scene, 'checks') : defaultChecksForScene(scene); - const check = checks.find(c => c.name === state.checkName) || checks[0]; + const skillChoices = [...checks, ...STANDARD_ACTIONS]; + const check = skillChoices.find(c => c.name === state.checkName) || checks[0] || STANDARD_ACTIONS[0]; if (!check || !state.player) return; if (!scene?.rollsUnlocked && hasMiniSkillRolled(sceneIndex, scene, state.player, check)) return; const feedSceneIndex = isGM ? sceneIndex : activeSceneIndex; @@ -1154,11 +1169,12 @@ export default function MissionTab({ authedPlayer }) { function renderMiniSceneRoller(scene, sceneIndex, opts = {}) { if (!scene) return null; const checks = listForScene(scene, 'checks').length ? listForScene(scene, 'checks') : defaultChecksForScene(scene); + const skillChoices = [...checks, ...STANDARD_ACTIONS]; const enemies = sceneEnemyObjects(scene); const state = miniState(sceneIndex, scene); const combatStateForScene = scene.combatState || {}; const initiatives = Array.isArray(combatStateForScene.initiatives) ? combatStateForScene.initiatives : []; - const selectedCheck = checks.find(check => check.name === state.checkName) || checks[0]; + const selectedCheck = skillChoices.find(check => check.name === state.checkName) || checks[0] || STANDARD_ACTIONS[0]; const selectedPlayer = state.player || (isGM ? playerList[0] : authedPlayer); const skillModifiersVisible = playerCanSeeSkillConditions(scene); const skillBreakdown = selectedCheck && selectedPlayer @@ -1232,6 +1248,19 @@ export default function MissionTab({ authedPlayer }) { > {checks.map(check => )} + {isGM && (