Spaces:
Running
Running
File size: 7,354 Bytes
020d2dc 94ca139 6742ef0 020d2dc 71eae78 020d2dc 6742ef0 020d2dc 71eae78 020d2dc 71eae78 020d2dc 94ca139 7d93bf0 020d2dc 94ca139 7d93bf0 020d2dc 71eae78 020d2dc 71eae78 020d2dc 71eae78 94ca139 835b445 db17af1 94e856b 14ad8f2 94e856b d16591c 152c254 94ca139 71eae78 7d93bf0 71eae78 020d2dc 71eae78 020d2dc ec98146 7d93bf0 835b445 45cc183 6f7f7b5 ec98146 020d2dc 71eae78 020d2dc 71eae78 020d2dc 6742ef0 54c8b9e 6742ef0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | import { createLoop } from './loop.js';
import { createInput } from './input.js';
import { createAudio } from './audio.js';
import { createChecks } from './checks.js';
import { createScene, W, H } from './scene.js';
import { createLevelScene } from './scene_level.js';
import { createRenderer } from './render.js';
import { loadManifest, loadClips } from './assets.js';
import { overrides, TUNING, DEPTH } from './tuning.js';
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d', { alpha: false });
// Internal resolution is a fixed 1920x1080. The canvas is scaled to fit the
// window and letterboxed; the backing store never changes size, so nothing in
// the demo has to know how big the window is.
function fit() {
const scale = Math.min(window.innerWidth / W, window.innerHeight / H);
canvas.style.width = `${Math.round(W * scale)}px`;
canvas.style.height = `${Math.round(H * scale)}px`;
}
window.addEventListener('resize', fit);
fit();
function message(text) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = '#8a8a86';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#000';
ctx.font = '28px ui-monospace, Menlo, Consolas, monospace';
ctx.fillText(text, 60, 80);
}
message('loading…');
const params = new URLSearchParams(location.search);
// ?manifest=… swaps the whole asset set without touching a line of code. This
// is how the G6 swap test runs: manifest.swaptest.json points drained_walk at
// the real reference drawing, at a different size, aspect ratio and pivot.
const manifestUrl = params.get('manifest') || 'manifest.json';
// The level is the default. The fifteen-second demo stays reachable at
// ?mode=demo, because it is what is deployed and being played, and it should
// not go dark while the level is half-built.
const mode = params.get('mode') === 'demo' ? 'demo' : 'level';
let clips;
let report;
let totalMs;
let sounds;
let letters;
try {
const manifest = await loadManifest(manifestUrl);
({ clips, report, totalMs } = await loadClips(manifest));
sounds = manifest.sounds ?? null;
letters = manifest.letters ?? null;
} catch (err) {
message(`asset load failed: ${err.message}`);
throw err;
}
// Silhouette generation runs once per frame of animation at load. The spec
// budgets ~200ms for it; log the real number so a regression is obvious.
console.log(`silhouettes generated in ${totalMs}ms`);
console.table(report);
if (Object.keys(overrides).length) console.log('tuning overridden from the URL:', overrides);
// ?speed=3 makes the Drained cover ground three times faster. Demo only, for
// playtesting — it changes nothing at the default of 1.
const speedScale = Math.min(8, Math.max(0.25, Number(params.get('speed')) || 1));
// A browser keeps the audio clock suspended until the player has touched
// something, and drops anything played before that WITHOUT an error. So the
// first key or tap wakes it, and M mutes.
const audio = createAudio();
audio.useRecording('footsteps', sounds?.footsteps);
audio.useRecording('drainedWalk', sounds?.drainedWalk);
audio.useRecording('thunder', sounds?.thunder);
// One voice per kind of drained. Each falls back to its generated version if
// the file has not decoded yet, which it will not have until a first gesture.
for (const k of ['drainedSwing', 'drainedHaul', 'drainedBear', 'drainedRush', 'postThrust']) {
audio.useRecording(k, sounds?.[k]);
}
for (const k of ['keyTake', 'torchFire', 'wake', 'gasp', 'villageBed']) {
audio.useRecording(k, sounds?.[k]);
}
for (const ev of ['keydown', 'pointerdown', 'touchstart']) {
window.addEventListener(ev, () => audio.resume(), { passive: true });
}
window.addEventListener('keydown', (e) => {
if (e.code === 'KeyM' && !e.repeat) audio.setEnabled(!audio.enabled);
});
const scene = mode === 'demo'
? createScene(clips, { speedScale })
: await createLevelScene(clips, { audio, letters });
const renderer = createRenderer(ctx);
const drawScene = mode === 'demo' ? renderer.drawDemo : renderer.drawLevel;
const input = createInput(canvas, { onReset: () => scene.reset() });
const loop = createLoop({
step: 1 / 60,
update: (dt) => {
scene.update(dt, input.state);
// TAKE on the touch pad only exists when there is something to take. On a
// keyboard this is the "[E] take" line in the readout; a phone has no
// readout worth reading, so the button itself has to say it.
if (mode === 'level') {
const st = scene.state;
// TAKE doubles as MARK. The spec put marking on "the empty wheel slot",
// which stopped existing when MAP took it — and it should not have its
// own button anyway: TAKE is already the contextual one, and both acts
// are "put this where it goes". So the button says which it is.
//
// EVERY OFFER THE SCENE MAKES, and the scene is the only thing that
// decides. This used to re-run the ground scan itself, which was two
// places that had to agree about what is within reach.
input.setTakeAvailable?.(st.canMark || st.canTakeKey ||
st.canPlaceKey || st.canTake);
}
},
render: (alpha) => {
const t0 = performance.now();
drawScene(scene, alpha, loop.stats);
// Smoothed so the readout is legible rather than jittering every frame.
loop.stats.drawMs += (performance.now() - t0 - loop.stats.drawMs) * 0.1;
},
});
loop.stats.drawMs = 0;
loop.stats.pass1Ms = 0;
loop.stats.pass2Ms = 0;
loop.stats.bgDrawn = 0;
loop.start();
// Exposed for verification from the console and by the browser tooling.
// Not used by the demo itself.
// --- the depth experiment ---------------------------------------------------
// X cycles: nothing, each cue on its own, then all of them. Judging these by
// eye needs fast A/B, and typing five URLs is not fast.
const DEPTH_STEPS = [
{ name: 'none' },
{ name: 'occlusion 3x', OCC: 3 },
{ name: 'particles 400 soft', PART: 400 },
{ name: 'particles 400 hard', PART: 400, PART_HARD: 1 },
{ name: 'blur 4', BLUR: 4 },
{ name: 'grain 0.05', GRAIN: 0.05 },
{ name: 'shafts', SHAFT: 1 },
{ name: 'ALL', OCC: 3, PART: 400, BLUR: 4, GRAIN: 0.05, SHAFT: 1 },
];
let depthStep = 0;
function applyDepth(step) {
Object.assign(DEPTH, { OCC: 1, PART: 0, PART_HARD: 0, BLUR: 0, BLUR_SKY: 0, GRAIN: 0, SHAFT: 0 });
Object.assign(DEPTH, step);
delete DEPTH.name;
scene.state.background?.setBlur(Math.round(DEPTH.BLUR), Math.round(DEPTH.BLUR_SKY));
DEPTH.label = step.name;
}
// Whatever the URL asked for is step 0, so the cycle always returns to it.
DEPTH_STEPS[0] = { name: 'url', OCC: DEPTH.OCC, PART: DEPTH.PART,
PART_HARD: DEPTH.PART_HARD, BLUR: DEPTH.BLUR, BLUR_SKY: DEPTH.BLUR_SKY,
GRAIN: DEPTH.GRAIN, SHAFT: DEPTH.SHAFT };
DEPTH.label = 'url';
window.addEventListener('keydown', (e) => {
if (e.code !== 'KeyX' || e.repeat) return;
depthStep = (depthStep + 1) % DEPTH_STEPS.length;
applyDepth(DEPTH_STEPS[depthStep]);
});
// The standing tests. Nothing runs them automatically; they are there so a
// check is the same check every time instead of a script pasted into a console.
const checks = mode === 'level'
? createChecks({ renderer, scene, canvas, clips, audio })
: null;
window.__demo = { mode, loop, scene, input, renderer, canvas, clips, report, totalMs, audio, checks,
depth: { DEPTH, steps: DEPTH_STEPS, apply: applyDepth } };
|