Add rules-based squad tactics checks

This commit is contained in:
2026-06-29 14:00:19 +02:00
parent 3aebcf8f0e
commit b6cff53b8e
5 changed files with 152 additions and 18 deletions

View File

@@ -2472,20 +2472,31 @@
"id": "cr-skills-tactics",
"rule_id": "cr-skills-tactics",
"title": "Tactics",
"summary": "Advanced / Intelligence",
"content": "Type: Advanced / Intelligence\nUse: Plan battlefield action, identify doctrine, set ambushes, read deployments, or exploit enemy mistakes.\nRoll: May be an Extended Test for long, risky, or complex work.\nAction: Varies; planning efforts may be Extended Tests.",
"summary": "Advanced / Intelligence / Skill Groups",
"content": "Type: Advanced / Intelligence\nGroups: Air Combat, Armoured Tactics, Assault Doctrine, Defensive Doctrine, Orbital Drop Procedures, Recon and Stealth.\nUse: Apply military knowledge to on-the-ground manoeuvres, deployments, ambushes, approaches, and battle doctrine.\nRoll: This is not Command and does not grant free Cohesion or Squad Mode abilities; success gives tactical information or positioning.\nAction: Varies by situation; planning may be an Extended Test.",
"category": "skills",
"tags": [
"",
"advanced",
"skill"
"skill",
"tactics",
"squad tactics",
"assault doctrine",
"defensive doctrine",
"recon and stealth"
],
"aliases": [
"Intelligence",
"Tactics Skill"
"Tactics Skill",
"Squad Tactics",
"Recon and Stealth",
"Assault Doctrine",
"Defensive Doctrine"
],
"relatedRules": [
"Test Difficulty"
"Test Difficulty",
"Squad Mode",
"Cohesion",
"Command"
],
"page": 94,
"pageEnd": 107,
@@ -2493,8 +2504,8 @@
"sourceAbbr": "CR",
"sourceMethod": "curated",
"confidence": 0.98,
"midgamePriority": 55,
"examples": "The team identifies the best chokepoint before Tyrant Horrors reach the shrine."
"midgamePriority": 85,
"examples": "Before breaching the engine shrine, a marine rolls Tactics to identify the best approach and priority threat; success gives direction, not free Cohesion."
},
{
"id": "cr-skills-tech-use",

View File

@@ -0,0 +1,57 @@
const { missionHelpers, pool } = require('../mariadb');
const SQUAD_TACTICS_CHECK = {
name: 'Squad Tactics',
skill: 'Tactics',
characteristic: 'Int',
target: 45,
modifier: 0,
rulesNote: 'Core: Tactics is an Advanced Intelligence Skill with groups such as Assault Doctrine, Defensive Doctrine, Orbital Drop Procedures, and Recon and Stealth. It gives tactical reading/positioning; it does not grant free Cohesion or Squad Mode abilities.',
reward: 'On success, the Kill-team identifies the best tactical approach for this scene; extra Degrees of Success may reveal a safer route, priority threat, ambush vector, or better position.',
};
function hasSquadTactics(scene) {
return (scene.checks || []).some(check => String(check.name || '').toLowerCase() === 'squad tactics');
}
function addSquadTactics(scene) {
if (hasSquadTactics(scene)) return scene;
return {
...scene,
checks: [SQUAD_TACTICS_CHECK, ...(Array.isArray(scene.checks) ? scene.checks : [])],
};
}
(async function main() {
const missions = await missionHelpers.getAll();
const targets = missions.filter(mission => mission.name === 'Vesalius Echo: Theta-92' || mission.is_active);
const results = [];
for (const mission of targets) {
const scenes = Array.isArray(mission.scenes) ? mission.scenes : [];
const nextScenes = scenes.map(addSquadTactics);
const changed = JSON.stringify(scenes) !== JSON.stringify(nextScenes);
if (!changed) {
results.push({ id: mission.id, name: mission.name, changed: false });
continue;
}
const ok = await missionHelpers.update(mission.id, {
name: mission.name,
theme: mission.theme,
sceneCount: nextScenes.length,
enemyCount: mission.enemy_count || 0,
threatLevel: mission.threat_level || 'Medium',
playerCount: mission.player_count || 4,
scenes: nextScenes,
});
results.push({ id: mission.id, name: mission.name, changed: ok });
}
console.log(JSON.stringify({ updated: results }, null, 2));
await pool.end();
})().catch(async error => {
console.error(error);
try { await pool.end(); } catch {}
process.exit(1);
});

View File

@@ -1,14 +1,30 @@
const { missionHelpers, pool } = require('../mariadb');
const squadTacticsCheck = {
name: 'Squad Tactics',
skill: 'Tactics',
characteristic: 'Int',
target: 45,
modifier: 0,
rulesNote: 'Core: Tactics is an Advanced Intelligence Skill with groups such as Assault Doctrine, Defensive Doctrine, Orbital Drop Procedures, and Recon and Stealth. It gives tactical reading/positioning; it does not grant free Cohesion or Squad Mode abilities.',
reward: 'On success, the Kill-team identifies the best tactical approach for this scene; extra Degrees of Success may reveal a safer route, priority threat, ambush vector, or better position.',
};
function withSquadTactics(checks) {
const list = Array.isArray(checks) ? checks : [];
if (list.some(check => String(check.name || '').toLowerCase() === 'squad tactics')) return list;
return [squadTacticsCheck, ...list];
}
function rebalanceScene(scene) {
if (scene.title === 'The Living Vault') {
return {
...scene,
checks: [
checks: withSquadTactics([
{ name: 'Anchor the psychic lock', skill: 'Psyniscience', characteristic: 'Wp', target: 45, modifier: 10, reward: 'Malachiel steadies the vault before the signal tears free.' },
{ name: 'Read psychic carrier wave', skill: 'Psyniscience', characteristic: 'Per', target: 45, modifier: 10, reward: 'Confirm the signal is warp-amplified.' },
{ name: 'Copy datapulse checksum', skill: 'Tech-Use', characteristic: 'Int', target: 45, modifier: 0, reward: 'Recover THETA-92 coordinates without needing a perfect decode.' }
],
]),
complications: [
'2 successes gives full coordinates; 1 success gives partial coordinates with a bad omen.',
'On 2+ failures, the vault tags one marine with a bio-psychic echo.',
@@ -20,11 +36,11 @@ function rebalanceScene(scene) {
if (scene.title === 'Prisoner Signal') {
return {
...scene,
checks: [
checks: withSquadTactics([
{ name: 'Open stasis cages safely', skill: 'Tech-Use', characteristic: 'Int', target: 45, modifier: 10, reward: 'Release prisoners without killing support systems.' },
{ name: 'Triage movable prisoners', skill: 'Medicae', characteristic: 'Int', target: 45, modifier: 10, reward: 'Save the prisoners who can survive extraction.' },
{ name: 'Spot biomorphic trap', skill: 'Awareness', characteristic: 'Per', target: 45, modifier: 10, reward: 'Detect the altered prisoner before it wakes.' }
],
]),
enemies: [
{ name: 'Gene-Warped Prisoner', type: 'mutant trap', count: 1, wounds: 12, ws: 35, bs: 0, ap: 2, damage: '1d10+4', agBonus: 4 }
]
@@ -34,12 +50,12 @@ function rebalanceScene(scene) {
if (scene.title === 'Engine Shrine Sabotage') {
return {
...scene,
checks: [
checks: withSquadTactics([
{ name: 'Place demolition charges', skill: 'Demolitions', characteristic: 'Int', target: 45, modifier: 10, reward: 'Arm the charges in the right sequence.' },
{ name: 'Sense the living machine-spirit', skill: 'Psyniscience', characteristic: 'Wp', target: 45, modifier: 0, reward: 'Malachiel predicts the reactor pulse before it vents.' },
{ name: 'Rip open armour panels', skill: 'Strength', characteristic: 'S', target: 45, modifier: 10, reward: 'Expose the core without losing time.' },
{ name: 'Avoid plasma venting', skill: 'Dodge', characteristic: 'Ag', target: 45, modifier: 10, reward: 'Avoid 1d10+4 Energy Pen 3.' }
],
]),
enemies: [
{ name: 'Gland Hound', type: 'bio-hound', count: 1, wounds: 14, ws: 40, bs: 0, ap: 2, damage: '1d10+5', agBonus: 5 }
]
@@ -50,11 +66,11 @@ function rebalanceScene(scene) {
return {
...scene,
type: 'briefing',
checks: [
checks: withSquadTactics([
{ name: 'Hold the transmission in memory', skill: 'Willpower', characteristic: 'Wp', target: 45, modifier: 10, reward: 'Preserve the coordinates despite Biles psychic static.' },
{ name: 'Lock coordinates', skill: 'Navigation', characteristic: 'Int', target: 45, modifier: 10, reward: '+10 to first infiltration/navigation test next mission.' },
{ name: 'Frame strategic report', skill: 'Command', characteristic: 'Fel', target: 45, modifier: 10, reward: 'Turn the discovery into actionable Deathwatch orders.' }
],
]),
complications: [
'Intel priority choices: enemy intel, facility map, sample data, or prisoner testimony.',
'If Sample extracted, quarantine becomes a next-session pressure.',
@@ -64,7 +80,7 @@ function rebalanceScene(scene) {
};
}
return scene;
return { ...scene, checks: withSquadTactics(scene.checks) };
}
(async function main() {

View File

@@ -34,6 +34,22 @@ const canon = {
}
};
const squadTacticsCheck = {
name: 'Squad Tactics',
skill: 'Tactics',
characteristic: 'Int',
target: 45,
modifier: 0,
rulesNote: 'Core: Tactics is an Advanced Intelligence Skill with groups such as Assault Doctrine, Defensive Doctrine, Orbital Drop Procedures, and Recon and Stealth. It gives tactical reading/positioning; it does not grant free Cohesion or Squad Mode abilities.',
reward: 'On success, the Kill-team identifies the best tactical approach for this scene; extra Degrees of Success may reveal a safer route, priority threat, ambush vector, or better position.',
};
function withSquadTactics(scene) {
const checks = Array.isArray(scene.checks) ? scene.checks : [];
if (checks.some(check => String(check.name || '').toLowerCase() === 'squad tactics')) return scene;
return { ...scene, checks: [squadTacticsCheck, ...checks] };
}
const scenes = [
{
title: 'The Living Vault',
@@ -417,7 +433,7 @@ const scenes = [
enemyCount: 4,
threatLevel: 'High',
playerCount: existing.player_count || 4,
scenes: scenes.map(scene => ({ ...scene, missionCanon: canon, missionStoryFlags: storyFlags }))
scenes: scenes.map(withSquadTactics).map(scene => ({ ...scene, missionCanon: canon, missionStoryFlags: storyFlags }))
});
if (!ok) throw new Error(`Failed to update mission ${existing.id}`);

View File

@@ -62,25 +62,38 @@ function isCombatScene(scene) {
function defaultChecksForScene(scene) {
const type = String(scene?.type || '').toLowerCase();
const squadTacticsCheck = {
name: 'Squad Tactics',
skill: 'Tactics',
characteristic: 'Int',
target: 45,
modifier: 0,
rulesNote: 'Core: Tactics is an Advanced Intelligence Skill. Use it to read battlefield doctrine, manoeuvres, ambushes, deployments, or the best approach. It does not grant free Squad Mode abilities or Cohesion.',
reward: 'On success, the team identifies the best tactical approach for this scene; extra Degrees of Success may reveal a better position, priority threat, or safer route.',
};
if (type.includes('combat')) {
return [
squadTacticsCheck,
{ name: 'Identify priority target', skill: 'Awareness', characteristic: 'Per', target: 45, modifier: 0, reward: 'Learn the most dangerous enemy or ambush vector.' },
{ name: 'Hold firing position', skill: 'Dodge', characteristic: 'Ag', target: 45, modifier: -10, reward: 'Avoid being driven out of cover this round.' },
];
}
if (type.includes('investigation') || type.includes('puzzle')) {
return [
squadTacticsCheck,
{ name: 'Read the spoor and auspex ghosts', skill: 'Awareness', characteristic: 'Per', target: 45, modifier: 0, reward: 'Reveal the next clue or hidden movement.' },
{ name: 'Decode corrupted systems', skill: 'Tech-Use', characteristic: 'Int', target: 45, modifier: -10, reward: 'Recover data before the trail degrades.' },
];
}
if (type.includes('social')) {
return [
squadTacticsCheck,
{ name: 'Stabilise frightened survivors', skill: 'Command', characteristic: 'Fel', target: 45, modifier: 0, reward: 'Gain cooperation and a useful witness detail.' },
{ name: 'Interrogate conflicting accounts', skill: 'Scrutiny', characteristic: 'Per', target: 45, modifier: -10, reward: 'Separate panic from truth.' },
];
}
return [
squadTacticsCheck,
{ name: 'Maintain tactical awareness', skill: 'Awareness', characteristic: 'Per', target: 45, modifier: 0, reward: 'Spot the next useful detail.' },
{ name: 'Coordinate the kill-team', skill: 'Command', characteristic: 'Fel', target: 45, modifier: 0, reward: 'Grant a clear tactical direction.' },
];
@@ -1129,6 +1142,21 @@ export default function MissionTab({ authedPlayer }) {
>
{checks.map(check => <option key={check.name} value={check.name}>{check.name}</option>)}
</select>
{selectedCheck && (
<div className="rounded border border-slate-800 bg-slate-900/70 px-2 py-2 text-xs text-slate-300">
<div>
<span className="font-semibold text-cyan-200">{selectedCheck.skill || 'Skill'}</span>
{' '}({selectedCheck.characteristic || 'Characteristic'})
{selectedCheck.modifier ? ` · ${selectedCheck.modifier > 0 ? '+' : ''}${selectedCheck.modifier}` : ''}
</div>
{selectedCheck.rulesNote && (
<div className="mt-1 text-slate-400">{selectedCheck.rulesNote}</div>
)}
{selectedCheck.reward && (
<div className="mt-1 text-amber-200">Success: {selectedCheck.reward}</div>
)}
</div>
)}
<button
onClick={() => rollMiniSceneCheck(sceneIndex, scene)}
disabled={!selectedCheck || !state.player || skillLocked}
@@ -1845,6 +1873,12 @@ export default function MissionTab({ authedPlayer }) {
{check.skill || 'Skill'} ({check.characteristic || 'Characteristic'}) target {target}
{check.modifier ? ` (${check.modifier > 0 ? '+' : ''}${check.modifier})` : ''}
</div>
{check.rulesNote && (
<div className="mt-1 text-xs text-slate-500">{check.rulesNote}</div>
)}
{check.reward && (
<div className="mt-1 text-xs text-amber-200">Success: {check.reward}</div>
)}
</div>
<button
onClick={openSceneInRoller}