export class Block { constructor(id, pos, state) { this.id = id; this.pos = pos; this.state = state; } } export class World { constructor(script = '') { this.blocks = new Map(); this.listeners = new Set(); // Timeline State this.events = []; this.labels = new Map(); this.currentIndex = 0; this.currentTime = -Infinity; this.initialState = new Map(); this.initialIndex = 0; this.eventCount = 0; if (script) { this.#compile(script); } } subscribe(callback) { this.listeners.add(callback); return () => this.listeners.delete(callback); } notify() { for (const listener of this.listeners) listener(); } set(id, x, y, z, state = {}) { const key = `${x},${y},${z}`; const isAir = id === 'air' || id === 'minecraft:air'; if (isAir) { if (this.blocks.delete(key)) { this.notify(); } } else { this.blocks.set(key, new Block(id, [x, y, z], state)); this.notify(); } } get(x, y, z) { return this.blocks.get(`${x},${y},${z}`); } #apply(data, x, y, z) { const key = `${x},${y},${z}`; if (data) { this.blocks.set(key, new Block(data.id, [x, y, z], {...data.state})); } else { this.blocks.delete(key); } } reset() { this.blocks.clear(); for (const [key, block] of this.initialState.entries()) { this.blocks.set(key, new Block(block.id, [...block.pos], {...block.state})); } this.currentIndex = this.initialIndex; if (this.initialIndex > 0 && this.events.length > 0) { this.currentTime = this.events[this.initialIndex - 1].time; } else { this.currentTime = -Infinity; } this.notify(); } #compile(script) { let currentTime = -1; let capturedInitialState = false; const shadowWorld = new Map(); const captureInitial = () => { if (capturedInitialState) return; for (const [k, v] of shadowWorld.entries()) { this.initialState.set(k, {id: v.id, pos: [...v.pos], state: {...v.state}}); } this.initialIndex = this.events.length; capturedInitialState = true; }; const lines = script.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')); let currentBatch = []; for (let line of lines) { let mergeNext = false; // Check for the continuation operator if (line.endsWith('&')) { mergeNext = true; line = line.slice(0, -1).trim(); } const tokens = line.split(/\s+/); const cmd = tokens[0]; if (cmd === 't') { const newTime = parseFloat(tokens[1]); if (newTime >= 0 && !capturedInitialState) captureInitial(); // Force flush any pending batch if time suddenly changes if (currentBatch.length > 0) { this.events.push({time: currentTime, actions: currentBatch}); currentBatch = []; } currentTime = newTime; continue; } if (cmd === 'l') { this.labels.set(tokens[1], currentTime); continue; } if (cmd === 'p') { const x = parseFloat(tokens[1]); const y = parseFloat(tokens[2]); const z = parseFloat(tokens[3]); const key = `${x},${y},${z}`; let id = null; const newState = {}; for (let i = 4; i < tokens.length; i++) { const token = tokens[i]; if (token.includes('=')) { const [k, v] = token.split('='); newState[k] = v; } else { id = token; } } const prevBlock = shadowWorld.get(key); const prev = prevBlock ? {id: prevBlock.id, state: {...prevBlock.state}} : null; const isAir = id === 'air' || id === 'minecraft:air'; if (isAir) { shadowWorld.delete(key); } else if (id) { shadowWorld.set(key, {id, pos: [x, y, z], state: newState}); } else if (prevBlock) { Object.assign(prevBlock.state, newState); } else { console.warn(`Timeline warning: Attempted to update state of non-existent block at ${key}`); continue; } const nextBlock = shadowWorld.get(key); const next = nextBlock ? {id: nextBlock.id, state: {...nextBlock.state}} : null; currentBatch.push({x, y, z, prev, next}); // Commit the batch to the timeline if it's not waiting for a continuation if (!mergeNext) { this.events.push({time: currentTime, actions: currentBatch}); currentBatch = []; } } } if (currentBatch.length > 0) { this.events.push({time: currentTime, actions: currentBatch}); } if (!capturedInitialState) captureInitial(); this.eventCount = this.events.length - this.initialIndex; this.reset(); } seek(target) { let targetTime = target; if (typeof target === 'string') { targetTime = this.labels.get(target); if (targetTime === undefined) throw new Error(`Label not found: ${target}`); } while (this.currentIndex < this.events.length && this.events[this.currentIndex].time <= targetTime) { const batch = this.events[this.currentIndex]; for (const action of batch.actions) { this.#apply(action.next, action.x, action.y, action.z); } this.currentIndex++; } while (this.currentIndex > 0 && this.events[this.currentIndex - 1].time > targetTime) { this.currentIndex--; const batch = this.events[this.currentIndex]; for (let i = batch.actions.length - 1; i >= 0; i--) { const action = batch.actions[i]; this.#apply(action.prev, action.x, action.y, action.z); } } this.currentTime = targetTime; this.notify(); } seekIndex(targetIndex) { targetIndex = targetIndex + this.initialIndex; while (this.currentIndex < this.events.length && this.currentIndex < targetIndex) { const batch = this.events[this.currentIndex]; for (const action of batch.actions) { this.#apply(action.next, action.x, action.y, action.z); } this.currentIndex++; } while (this.currentIndex > 0 && this.currentIndex > targetIndex) { this.currentIndex--; const batch = this.events[this.currentIndex]; for (let i = batch.actions.length - 1; i >= 0; i--) { const action = batch.actions[i]; this.#apply(action.prev, action.x, action.y, action.z); } } if (this.currentIndex < this.events.length) { this.currentTime = this.events[this.currentIndex].time; } else if (this.events.length > 0) { this.currentTime = this.events[this.events.length - 1].time; } this.notify(); } }