// timeline.js export class Timeline { constructor(world) { this.world = world; this.events = []; this.cursorIndex = 0; this.currentTime = -1; } /** * Parses the custom syntax and builds the reversible event array. */ compile(script) { this.events = []; let currentTime = 0; let subTick = 0; // We use a shadow map to track what the world *will* look like // at any point during parsing, so we can capture accurate undo states. const shadowWorld = new Map(); const lines = script.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')); for (const line of lines) { if (line.startsWith('@')) { currentTime = parseFloat(line.substring(1)); subTick = 0; continue; } const tokens = line.split(' '); const action = tokens[0]; const x = parseFloat(tokens[1]); const y = parseFloat(tokens[2]); const z = parseFloat(tokens[3]); const key = `${x},${y},${z}`; const propsArray = tokens.slice(action === 'place' ? 5 : 4); const state = {}; for (const prop of propsArray) { const [k, v] = prop.split('='); state[k] = v; } // Capture the current shadow state for the undo action const prevState = shadowWorld.has(key) ? JSON.parse(JSON.stringify(shadowWorld.get(key))) : null; if (action === 'place') { const id = tokens[4]; const blockData = { id, state }; // Update shadow world shadowWorld.set(key, blockData); this.events.push({ time: currentTime, subTick: subTick++, forward: (world) => world.set(id, x, y, z, { ...state }), backward: (world) => { if (prevState) world.set(prevState.id, x, y, z, { ...prevState.state }); else world.del(x, y, z); } }); } else if (action === 'update') { // Update shadow world if (shadowWorld.has(key)) { const block = shadowWorld.get(key); Object.assign(block.state, state); } this.events.push({ time: currentTime, subTick: subTick++, forward: (world) => { const block = world.get(x, y, z); if (block) world.set(block.id, x, y, z, { ...block.state, ...state }); }, backward: (world) => { if (prevState) world.set(prevState.id, x, y, z, { ...prevState.state }); } }); } else if (action === 'del') { shadowWorld.delete(key); this.events.push({ time: currentTime, subTick: subTick++, forward: (world) => world.del(x, y, z), backward: (world) => { if (prevState) world.set(prevState.id, x, y, z, { ...prevState.state }); } }); } } } /** * Scrubs the timeline to an exact tick, resolving all forward and backward * events deterministically based on their sub-tick ordering. */ seek(targetTime) { // Scrub Forward while (this.cursorIndex < this.events.length && this.events[this.cursorIndex].time <= targetTime) { this.events[this.cursorIndex].forward(this.world); this.cursorIndex++; } // Scrub Backward // Notice the strictly reverse evaluation order while (this.cursorIndex > 0 && this.events[this.cursorIndex - 1].time > targetTime) { this.cursorIndex--; this.events[this.cursorIndex].backward(this.world); } this.currentTime = targetTime; } }