diff --git a/database/mariadb.js b/database/mariadb.js index 35f21c8..9188103 100644 --- a/database/mariadb.js +++ b/database/mariadb.js @@ -802,7 +802,7 @@ const missionRollHelpers = { getForMission: async (missionId, limit = 50) => { try { - const boundedLimit = Math.max(1, Math.min(Number(limit) || 50, 100)); + const boundedLimit = Math.max(1, Math.min(Number(limit) || 50, 500)); const [rows] = await pool.execute( `SELECT * FROM mission_rolls WHERE mission_id = ? ORDER BY created_at DESC LIMIT ${boundedLimit}`, [missionId] diff --git a/src/components/MissionTab.jsx b/src/components/MissionTab.jsx index 52422d0..e48965b 100644 --- a/src/components/MissionTab.jsx +++ b/src/components/MissionTab.jsx @@ -413,7 +413,7 @@ export default function MissionTab({ authedPlayer }) { // #1 — roll feed visible to all (no isGM guard) const loadRollFeed = useCallback(async function loadRollFeed() { try { - const res = await axios.get('/api/missions/active/rolls/feed?limit=50'); + const res = await axios.get('/api/missions/active/rolls/feed?limit=200'); const next = Array.isArray(res.data) ? res.data : []; setRollFeed(prev => JSON.stringify(prev) !== JSON.stringify(next) ? next : prev); } catch (e) { @@ -470,11 +470,20 @@ export default function MissionTab({ authedPlayer }) { const currentSceneIndex = activeMission?.current_scene ?? 0; useEffect(() => { - setRolledChecks(new Set()); - setFateUsedChecks(new Set()); - setDeclarationText(''); - setWpTarget('60'); - }, [currentSceneIndex]); + const sessionState = currentScene?.sessionState || {}; + const playerKey = authedPlayer || 'unknown'; + const playerRolled = sessionState.rolledChecksByPlayer?.[playerKey]; + const playerFate = sessionState.fateUsedChecksByPlayer?.[playerKey]; + const playerResults = sessionState.rollResultsByPlayer?.[playerKey]; + setRolledChecks(new Set(Array.isArray(playerRolled) ? playerRolled : (Array.isArray(sessionState.rolledChecks) ? sessionState.rolledChecks : []))); + setFateUsedChecks(new Set(Array.isArray(playerFate) ? playerFate : (Array.isArray(sessionState.fateUsedChecks) ? sessionState.fateUsedChecks : []))); + setRollResults(Array.isArray(playerResults) ? playerResults.slice(0, 5) : (Array.isArray(sessionState.rollResults) ? sessionState.rollResults.slice(0, 5) : [])); + setDeclarationText(sessionState.declarationText || ''); + setWpTarget(String(sessionState.wpTarget || '60')); + if (sessionState.miniRollerState && typeof sessionState.miniRollerState === 'object') { + setMiniRollerState(prev => ({ ...prev, [activeSceneIndex]: sessionState.miniRollerState })); + } + }, [activeMission?.id, currentSceneIndex, activeSceneIndex, currentScene, authedPlayer]); // sync outcome notes when a different mission loads (keyed on id, not scenes array) useEffect(() => { @@ -724,6 +733,45 @@ export default function MissionTab({ authedPlayer }) { await loadMissions({ silent: true }); } + async function patchActiveSceneSessionState(updates, sceneIndex = activeSceneIndex, scene = currentScene) { + if (!scene) return; + const sessionState = { + ...(scene.sessionState || {}), + ...updates, + updatedAt: new Date().toISOString(), + }; + await patchSceneAt(sceneIndex, { sessionState }); + } + + function playerSessionStatePatch({ + rolled = rolledChecks, + fate = fateUsedChecks, + results = rollResults, + declaration = declarationText, + wp = wpTarget, + extra = {}, + } = {}) { + const sessionState = currentScene?.sessionState || {}; + const playerKey = authedPlayer || 'unknown'; + return { + ...extra, + rolledChecksByPlayer: { + ...(sessionState.rolledChecksByPlayer || {}), + [playerKey]: [...rolled], + }, + fateUsedChecksByPlayer: { + ...(sessionState.fateUsedChecksByPlayer || {}), + [playerKey]: [...fate], + }, + rollResultsByPlayer: { + ...(sessionState.rollResultsByPlayer || {}), + [playerKey]: results, + }, + wpTarget: wp, + declarationText: declaration, + }; + } + async function updateCombatState(updates) { if (!activeMission?.scenes?.length) return; const index = activeMission.current_scene; @@ -885,8 +933,10 @@ export default function MissionTab({ authedPlayer }) { rolledBy: authedPlayer, ...result, }; - setRolledChecks(prev => new Set([...prev, check.name])); - setRollResults(prev => [entry, ...prev].slice(0, 5)); + const nextRolledChecks = new Set([...rolledChecks, check.name]); + const nextRollResults = [entry, ...rollResults].slice(0, 5); + setRolledChecks(nextRolledChecks); + setRollResults(nextRollResults); await recordMissionRoll('check', check.name, { sceneTitle: titleForScene(currentScene, activeSceneIndex), check, @@ -894,12 +944,16 @@ export default function MissionTab({ authedPlayer }) { roll, ...result, }); + await patchActiveSceneSessionState(playerSessionStatePatch({ + rolled: nextRolledChecks, + results: nextRollResults, + })); } function miniState(sceneIndex, scene) { const checks = listForScene(scene, 'checks').length ? listForScene(scene, 'checks') : defaultChecksForScene(scene); const enemies = sceneEnemyObjects(scene); - const current = miniRollerState[sceneIndex] || {}; + const current = miniRollerState[sceneIndex] || scene?.sessionState?.miniRollerState || {}; const defaultPlayer = isGM ? (playerList[0] || '') : authedPlayer; return { player: current.player || defaultPlayer, @@ -1020,7 +1074,14 @@ export default function MissionTab({ authedPlayer }) { }, }, }; - await patchSceneAt(sceneIndex, { rollLocks }); + await patchSceneAt(sceneIndex, { + rollLocks, + sessionState: { + ...(scene.sessionState || {}), + miniRollerState: state, + updatedAt: new Date().toISOString(), + }, + }); await recordMissionRollForScene(feedSceneIndex, state.player, 'check', check.name, { sceneTitle: titleForScene(scene, sceneIndex), mini: true, @@ -1134,24 +1195,42 @@ export default function MissionTab({ authedPlayer }) { rolledBy: authedPlayer, ...result, }; - setRolledChecks(prev => new Set([...prev, '__fear__'])); - setRollResults(prev => [entry, ...prev].slice(0, 5)); + const nextRolledChecks = new Set([...rolledChecks, '__fear__']); + const nextRollResults = [entry, ...rollResults].slice(0, 5); + setRolledChecks(nextRolledChecks); + setRollResults(nextRollResults); await recordMissionRoll('fear', 'Fear Test', { target, roll, ...result }); + await patchActiveSceneSessionState(playerSessionStatePatch({ + rolled: nextRolledChecks, + results: nextRollResults, + })); } // #8 — decision declare async function rollDecision() { const text = declarationText.trim(); if (!text) return; - setRolledChecks(prev => new Set([...prev, playPrompt?.check?.name || '__decision__'])); + const nextRolledChecks = new Set([...rolledChecks, playPrompt?.check?.name || '__decision__']); + setRolledChecks(nextRolledChecks); await recordMissionRoll('decision', playPrompt?.title || 'Decision', { declaration: text }); setDeclarationText(''); + await patchActiveSceneSessionState(playerSessionStatePatch({ + rolled: nextRolledChecks, + declaration: '', + })); } // #9 — fate point re-roll - function spendFate(checkName) { - setFateUsedChecks(prev => new Set([...prev, checkName])); - setRolledChecks(prev => { const next = new Set(prev); next.delete(checkName); return next; }); + async function spendFate(checkName) { + const nextFateUsedChecks = new Set([...fateUsedChecks, checkName]); + const nextRolledChecks = new Set(rolledChecks); + nextRolledChecks.delete(checkName); + setFateUsedChecks(nextFateUsedChecks); + setRolledChecks(nextRolledChecks); + await patchActiveSceneSessionState(playerSessionStatePatch({ + rolled: nextRolledChecks, + fate: nextFateUsedChecks, + })); } function openSceneInRoller() {