Add opt-in phone gameplay recording and JSON export

This commit is contained in:
wuwi committed 2026-09-07 10:19:38 +02:00
1 parent ede1f48152
commit fe64242090
8 files changed
+394 -12

No files matched your search

+49
View File
@@ -15,6 +15,7 @@ import { cv } from './dom.js';
import { haptic } from './haptics.js';
import * as stats from './stats.js';
import * as profile from './profile.js';
import * as recorder from './recorder.js';
import './input.js';
let last = performance.now();
@@ -89,6 +90,7 @@ let pickedTour = remembered('tour') || TOURS[0].id;
// menus share, so its offset outlives the stage that earned it unless it is reset here — the one
// place every stage change goes through.
const go = stage => {
if (stage === 'stats') refreshRecorder();
if (stage === 'setup') syncSetup();
if (stage === 'train') buildTrain();
intro.dataset.stage = stage;
@@ -792,6 +794,53 @@ function start(modeId = pickedMode) {
const openStats = () => {
stats.drawStats('all', SPRV); go('stats');
};
// A phone can record without a console. Opt in from Stats or ?record=1; export is local/share
// sheet only. Keep this off the normal splash and give active recordings one small live marker.
const recordingMeta = () => ({
build: document.querySelector('script[src*="main.js"]').getAttribute('src'),
viewport: [innerWidth, innerHeight], pixelRatio: devicePixelRatio,
touch: matchMedia('(pointer: coarse)').matches,
});
function refreshRecorder() {
const r = recorder.recordingInfo();
document.getElementById('recStatus').textContent = r.warning || (r.full
? 'Recording full. Export it, then start a new recording.'
: (r.active ? 'Recording' : r.exists ? 'Paused' : 'Off') + ' · ' + r.points + ' points saved on this phone.');
document.getElementById('recToggle').textContent = r.active ? 'STOP RECORDING' : r.exists ? 'RESUME RECORDING' : 'RECORD PLAYTEST';
document.getElementById('recToggle').disabled = r.full;
document.getElementById('recExport').disabled = !r.events;
document.getElementById('recNew').hidden = !r.exists || r.active;
const lamp = document.getElementById('recLamp');
lamp.hidden = !r.active && !r.full;
lamp.textContent = r.full ? 'REC FULL' : 'REC';
}
recorder.onRecorderChange(refreshRecorder);
document.getElementById('recToggle').addEventListener('click', () => {
const r = recorder.recordingInfo();
if (!r.exists) recorder.beginRecording(recordingMeta());
else recorder.setRecording(!r.active, recordingMeta());
});
document.getElementById('recNew').addEventListener('click', () => recorder.beginRecording(recordingMeta()));
document.getElementById('recExport').addEventListener('click', async () => {
try {
const result = await recorder.exportRecording();
if (result === 'download') document.getElementById('recStatus').textContent = 'Download requested. Your recording is also kept on this phone.';
} catch {
document.getElementById('recStatus').textContent = 'Export could not open. Your recording is kept; please try again.';
}
});
if (new URLSearchParams(location.search).get('record') === '1') {
if (recorder.recordingInfo().exists) recorder.setRecording(true, recordingMeta());
else recorder.beginRecording(recordingMeta());
}
refreshRecorder();
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
recorder.recordEvent('run_end', { outcome: 'backgrounded' }); recorder.saveRecording();
}
});
addEventListener('pagehide', () => recorder.saveRecording());
// entering a question screen rebuilds it, so the LAST tag always names the run actually played —
// and the FIRST visit ever detours through the identity screen (the leagues): the club wants a
// name before it deals a match. ENTER THE CLUB lands on the mode screen, so the detour costs the