Persist mission scene session state
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user