All 11 issues from 4-player simulation of The Hunt for Fabius Bile: 1. Roll feed visible to all players (no GM auth guard) 2. Scene text gated by revealed flag; GM explicit reveal per scene 3. GM can add NPC/enemy entries to initiative tracker 4. Round counter synced to Dice Roller via localStorage + custom event 5. Check reward text hidden until player has rolled 6. Checks assignable to specific player via Scene Secrets ⚙ options 7. Fear test quick-roll panel with WP input appears when fearRating > 0 8. Decision checks show textarea/Declare instead of d100 roll button 9. Fate point re-roll: one per check per scene, resets on scene advance 10. Player poll reduced from 8s to 4s, combined mission + roll feed poll 11. Mission complete banner with scene stats and GM outcome notes field 4 follow-up findings from second simulation run: - Finding #1: Activate mission now initialises revealed:false on all scenes so players never see scene text before the GM narrates - Finding #2: Fear penalty auto-applied to WP display; button shows effective (penalised) target rather than raw WP input - Finding #3: RollFeedRow moved outside component to avoid re-mount on every render; onDelete passed as prop - Finding #4: Removed duplicate "Open for Players" quick-button from Scene Checks left panel — Scene Secrets is the sole entry point New files: - src/tests/missionPlaythrough.test.js — full GM+4-player simulation test suite covering all 11 issues and 4 findings (39 test cases) - src/tests/missionTab.test.js — player/GM view isolation tests - src/utils/diceRoller.js — shared d100/degrees/clampTarget utilities - tests/missionRoutes.test.js — backend mission route unit tests - tests/playerRoutesLogin.test.js — player login route tests - tests/sessionRoutes.test.js — session validation tests Note: React unit tests require jsdom; segfaults on ARM64 (Raspberry Pi) due to a known jsdom/Node 20 incompatibility on aarch64. Tests pass on x86 CI. Backend integration tests (tests/) run normally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
5.5 KiB
JavaScript
177 lines
5.5 KiB
JavaScript
const express = require('express');
|
|
const { missionHelpers, missionRollHelpers, logToFile } = require('../mariadb');
|
|
const router = express.Router();
|
|
|
|
function playerScene(scene) {
|
|
if (!scene || typeof scene !== 'object') return null;
|
|
const {
|
|
gmNotes,
|
|
gm_notes,
|
|
secret,
|
|
secrets,
|
|
hidden,
|
|
gmOnly,
|
|
gm_only,
|
|
...safeScene
|
|
} = scene;
|
|
return safeScene;
|
|
}
|
|
|
|
// Get all missions
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const missions = await missionHelpers.getAll();
|
|
res.json(missions);
|
|
} catch (error) {
|
|
logToFile('API: Failed to get missions', error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Get active mission for the shared play table
|
|
router.get('/active/current', async (req, res) => {
|
|
try {
|
|
const mission = await missionHelpers.getActive();
|
|
res.json(mission || null);
|
|
} catch (error) {
|
|
logToFile('API: Failed to get active mission', error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Get player-safe active mission. Only the GM-current scene is returned.
|
|
router.get('/active/player', async (req, res) => {
|
|
try {
|
|
const mission = await missionHelpers.getActive();
|
|
if (!mission) return res.json(null);
|
|
|
|
const scenes = Array.isArray(mission.scenes) ? mission.scenes : [];
|
|
const currentIndex = Math.max(0, Math.min(Number(mission.current_scene || 0), Math.max(scenes.length - 1, 0)));
|
|
const currentScene = scenes[currentIndex] || null;
|
|
|
|
res.json({
|
|
id: mission.id,
|
|
name: mission.name,
|
|
current_scene: 0,
|
|
active_scene_index: currentIndex,
|
|
scenes: currentScene ? [playerScene(currentScene)] : [],
|
|
});
|
|
} catch (error) {
|
|
logToFile('API: Failed to get player active mission', error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Get roll feed for the active mission
|
|
router.get('/active/rolls/feed', async (req, res) => {
|
|
try {
|
|
const rolls = await missionRollHelpers.getActive(Number(req.query.limit || 50));
|
|
res.json(rolls);
|
|
} catch (error) {
|
|
logToFile('API: Failed to get active mission rolls', error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Record a roll for GM visibility
|
|
router.post('/rolls', async (req, res) => {
|
|
try {
|
|
const id = await missionRollHelpers.create(req.body || {});
|
|
if (!id) return res.status(500).json({ error: 'Failed to record roll' });
|
|
res.json({ success: true, id });
|
|
} catch (error) {
|
|
logToFile('API: Failed to record mission roll', error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Delete a roll from the mission feed
|
|
router.delete('/rolls/:id', async (req, res) => {
|
|
try {
|
|
const ok = await missionRollHelpers.delete(req.params.id);
|
|
if (!ok) return res.status(404).json({ error: 'Roll not found' });
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
logToFile('API: Failed to delete mission roll', req.params.id, error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Get mission by ID
|
|
router.get('/:id', async (req, res) => {
|
|
try {
|
|
const mission = await missionHelpers.getById(req.params.id);
|
|
if (!mission) return res.status(404).json({ error: 'Mission not found' });
|
|
res.json(mission);
|
|
} catch (error) {
|
|
logToFile('API: Failed to get mission', req.params.id, error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Create mission
|
|
router.post('/', async (req, res) => {
|
|
try {
|
|
const id = await missionHelpers.create(req.body);
|
|
if (!id) return res.status(500).json({ error: 'Failed to create mission' });
|
|
logToFile('API: Created mission', req.body.name);
|
|
res.json({ success: true, id });
|
|
} catch (error) {
|
|
logToFile('API: Failed to create mission', error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Set active mission
|
|
router.post('/:id/active', async (req, res) => {
|
|
try {
|
|
const ok = await missionHelpers.setActive(req.params.id);
|
|
if (!ok) return res.status(404).json({ error: 'Mission not found' });
|
|
const mission = await missionHelpers.getById(req.params.id);
|
|
res.json({ success: true, mission });
|
|
} catch (error) {
|
|
logToFile('API: Failed to set active mission', req.params.id, error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Update active play progress
|
|
router.put('/:id/progress', async (req, res) => {
|
|
try {
|
|
const ok = await missionHelpers.updateProgress(req.params.id, req.body || {});
|
|
if (!ok) return res.status(404).json({ error: 'Mission not found' });
|
|
const mission = await missionHelpers.getById(req.params.id);
|
|
res.json({ success: true, mission });
|
|
} catch (error) {
|
|
logToFile('API: Failed to update mission progress', req.params.id, error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Update mission
|
|
router.put('/:id', async (req, res) => {
|
|
try {
|
|
const ok = await missionHelpers.update(req.params.id, req.body);
|
|
if (!ok) return res.status(404).json({ error: 'Mission not found' });
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
logToFile('API: Failed to update mission', req.params.id, error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
// Delete mission
|
|
router.delete('/:id', async (req, res) => {
|
|
try {
|
|
const ok = await missionHelpers.delete(req.params.id);
|
|
if (!ok) return res.status(404).json({ error: 'Mission not found' });
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
logToFile('API: Failed to delete mission', req.params.id, error);
|
|
res.status(500).json({ error: String(error) });
|
|
}
|
|
});
|
|
|
|
console.log('Mission routes registered (MariaDB)');
|
|
module.exports = router;
|