Add standard scene actions and fix sim fire modes
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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 => <option key={check.name} value={check.name}>{check.name}</option>)}
|
||||
</select>
|
||||
<label className="grid gap-1 text-xs text-slate-400">
|
||||
Standard actions
|
||||
<select
|
||||
value={STANDARD_ACTIONS.some(action => action.name === state.checkName) ? state.checkName : ''}
|
||||
onChange={e => setMiniState(sceneIndex, { checkName: e.target.value || checks[0]?.name || '' })}
|
||||
className="rounded border border-slate-600 bg-slate-900 px-2 py-2 text-sm text-slate-100"
|
||||
>
|
||||
<option value="">Use scene skill</option>
|
||||
{STANDARD_ACTIONS.map(action => (
|
||||
<option key={action.name} value={action.name}>{action.name} · {action.skill}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{isGM && (
|
||||
<div className="grid gap-2 rounded border border-slate-800 bg-slate-900/50 p-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
|
||||
@@ -75,6 +75,7 @@ function SceneRow({ scene, idx }) {
|
||||
{ev.type === 'crit' && <span className="text-yellow-300">⚡ {ev.player} — Righteous Fury on {ev.target} ({ev.dos} DoS)</span>}
|
||||
{ev.type === 'kill' && <span className="text-green-300">☠ {ev.player} slew {ev.target}</span>}
|
||||
{ev.type === 'down' && <span className="text-red-400">💀 {ev.player} went down (round {ev.round})</span>}
|
||||
{ev.type === 'checkRoll' && <span className={ev.success ? 'text-green-300' : 'text-red-300'}>{ev.player} — {ev.check} [{ev.stat}] {ev.roll}/{ev.tn} {ev.success ? `${ev.dos} DoS` : `${ev.dof} DoF`}</span>}
|
||||
{ev.type === 'heroCheck' && <span className="text-cyan-300">★ {ev.player} — {ev.check} ({ev.dos} DoS)</span>}
|
||||
{ev.type === 'complication'&& <span className="text-orange-300">⚡ Complication ({ev.player}): {ev.text?.slice(0, 80)}</span>}
|
||||
{ev.type === 'wound' && <span className="text-red-300">🩸 {ev.player} took {ev.net} wounds from complication</span>}
|
||||
|
||||
Reference in New Issue
Block a user