From b6cff53b8e15441e6167f1611290db46408eec69 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 29 Jun 2026 14:00:19 +0200 Subject: [PATCH] Add rules-based squad tactics checks --- database/rules/rules-database.json | 27 ++++++--- database/scripts/add_squad_tactics_checks.js | 57 +++++++++++++++++++ .../rebalance_vesalius_echo_mission.js | 34 ++++++++--- .../scripts/update_vesalius_echo_theta92.js | 18 +++++- src/components/MissionTab.jsx | 34 +++++++++++ 5 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 database/scripts/add_squad_tactics_checks.js diff --git a/database/rules/rules-database.json b/database/rules/rules-database.json index 25707a9..f60c8c3 100644 --- a/database/rules/rules-database.json +++ b/database/rules/rules-database.json @@ -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", diff --git a/database/scripts/add_squad_tactics_checks.js b/database/scripts/add_squad_tactics_checks.js new file mode 100644 index 0000000..fdb8c9d --- /dev/null +++ b/database/scripts/add_squad_tactics_checks.js @@ -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); +}); diff --git a/database/scripts/rebalance_vesalius_echo_mission.js b/database/scripts/rebalance_vesalius_echo_mission.js index 9c7abf0..ce74114 100644 --- a/database/scripts/rebalance_vesalius_echo_mission.js +++ b/database/scripts/rebalance_vesalius_echo_mission.js @@ -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 Bile’s 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() { diff --git a/database/scripts/update_vesalius_echo_theta92.js b/database/scripts/update_vesalius_echo_theta92.js index d536313..5ed8f71 100644 --- a/database/scripts/update_vesalius_echo_theta92.js +++ b/database/scripts/update_vesalius_echo_theta92.js @@ -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}`); diff --git a/src/components/MissionTab.jsx b/src/components/MissionTab.jsx index 42cb157..34edb70 100644 --- a/src/components/MissionTab.jsx +++ b/src/components/MissionTab.jsx @@ -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 => )} + {selectedCheck && ( +
+
+ {selectedCheck.skill || 'Skill'} + {' '}({selectedCheck.characteristic || 'Characteristic'}) + {selectedCheck.modifier ? ` · ${selectedCheck.modifier > 0 ? '+' : ''}${selectedCheck.modifier}` : ''} +
+ {selectedCheck.rulesNote && ( +
{selectedCheck.rulesNote}
+ )} + {selectedCheck.reward && ( +
Success: {selectedCheck.reward}
+ )} +
+ )}