UNPKG

@hiddentao/clockwork-engine

Version:

A TypeScript/PIXI.js game engine for deterministic, replayable games with built-in rendering

399 lines (398 loc) 12.1 kB
/** * Memory Rendering Layer * * Headless rendering implementation that tracks state without actual rendering. * Used for testing, replay validation, and server-side game logic. */ import { asNodeId, asSpritesheetId, asTextureId } from "../types"; import { EventCallbackManager } from "../utils/EventCallbackManager"; import { calculateBoundsWithAnchor } from "../utils/boundsCalculation"; import { screenToWorld, worldToScreen } from "../utils/coordinateTransforms"; import { withNode } from "../utils/nodeHelpers"; export class MemoryRenderingLayer { constructor() { this.nextNodeId = 1; this.nextTextureId = 1; this.nextSpritesheetId = 1; this.nodes = new Map(); this.textures = new Map(); this.spritesheets = new Map(); this.tickCallbackManager = new EventCallbackManager(); this.tickerSpeed = 1; this.canvasSize = { width: 800, height: 600 }; } // Node lifecycle createNode() { const id = asNodeId(this.nextNodeId++); this.nodes.set(id, { id, parent: null, children: [], position: { x: 0, y: 0 }, rotation: 0, scale: { x: 1, y: 1 }, anchor: { x: 0, y: 0 }, alpha: 1, visible: true, zIndex: 0, size: { width: 0, height: 0 }, graphics: [], }); return id; } destroyNode(id) { this.nodes.delete(id); } // Hierarchy addChild(parent, child) { const parentNode = this.nodes.get(parent); const childNode = this.nodes.get(child); if (parentNode && childNode) { parentNode.children.push(child); childNode.parent = parent; } } removeChild(parent, child) { const parentNode = this.nodes.get(parent); const childNode = this.nodes.get(child); if (parentNode && childNode) { const index = parentNode.children.indexOf(child); if (index !== -1) { parentNode.children.splice(index, 1); } childNode.parent = null; } } // Transform setPosition(id, x, y) { withNode(this.nodes, id, (node) => { node.position = { x, y }; }); } setRotation(id, radians) { withNode(this.nodes, id, (node) => { node.rotation = radians; }); } setScale(id, scaleX, scaleY) { withNode(this.nodes, id, (node) => { node.scale = { x: scaleX, y: scaleY }; }); } setAnchor(id, anchorX, anchorY) { withNode(this.nodes, id, (node) => { node.anchor = { x: anchorX, y: anchorY }; }); } setAlpha(id, alpha) { withNode(this.nodes, id, (node) => { node.alpha = alpha; }); } setVisible(id, visible) { withNode(this.nodes, id, (node) => { node.visible = visible; }); } setZIndex(id, z) { withNode(this.nodes, id, (node) => { node.zIndex = z; }); } // Size setSize(id, width, height) { withNode(this.nodes, id, (node) => { node.size = { width, height }; }); } getSize(id) { return withNode(this.nodes, id, (node) => node.size, { width: 0, height: 0, }); } // Visual effects setTint(id, color) { withNode(this.nodes, id, (node) => { node.tint = color; }); } setBlendMode(id, mode) { withNode(this.nodes, id, (node) => { node.blendMode = mode; }); } setTextureFiltering(id, filtering) { withNode(this.nodes, id, (node) => { node.textureFiltering = filtering; }); } // Bounds query getBounds(id) { return withNode(this.nodes, id, (node) => calculateBoundsWithAnchor(node.position, node.size, node.anchor), { x: 0, y: 0, width: 0, height: 0 }); } // Visual content setSprite(id, textureId) { const node = this.nodes.get(id); if (node) { node.spriteTexture = textureId; } } setAnimatedSprite(id, textureIds, ticksPerFrame) { const node = this.nodes.get(id); if (node) { node.animationData = { textures: textureIds, ticksPerFrame, loop: false, playing: false, }; } } playAnimation(id, loop) { const node = this.nodes.get(id); if (node && node.animationData) { node.animationData.loop = loop; node.animationData.playing = true; } } stopAnimation(id) { const node = this.nodes.get(id); if (node && node.animationData) { node.animationData.playing = false; } } setAnimationCompleteCallback(_id, _callback) { // Memory layer doesn't play animations, so this is a no-op } // Primitives drawRectangle(id, x, y, w, h, fill, stroke, strokeWidth) { const node = this.nodes.get(id); if (node) { node.graphics.push({ type: "rectangle", data: { x, y, w, h, fill, stroke, strokeWidth }, }); } } drawCircle(id, x, y, radius, fill, stroke, strokeWidth) { const node = this.nodes.get(id); if (node) { node.graphics.push({ type: "circle", data: { x, y, radius, fill, stroke, strokeWidth }, }); } } drawPolygon(id, points, fill, stroke, strokeWidth) { const node = this.nodes.get(id); if (node) { node.graphics.push({ type: "polygon", data: { points, fill, stroke, strokeWidth }, }); } } drawRoundRect(id, x, y, w, h, radius, fill, stroke, strokeWidth) { const node = this.nodes.get(id); if (node) { node.graphics.push({ type: "roundRect", data: { x, y, w, h, radius, fill, stroke, strokeWidth }, }); } } drawLine(id, x1, y1, x2, y2, color, width) { const node = this.nodes.get(id); if (node) { node.graphics.push({ type: "line", data: { x1, y1, x2, y2, color, width }, }); } } drawPolyline(id, points, color, width) { const node = this.nodes.get(id); if (node) { node.graphics.push({ type: "polyline", data: { points, color, width }, }); } } clearGraphics(id) { const node = this.nodes.get(id); if (node) { node.graphics = []; } } // Textures async loadTexture(url) { const id = asTextureId(this.nextTextureId++); this.textures.set(id, url); return id; } async loadSpritesheet(imageUrl, jsonData) { const id = asSpritesheetId(this.nextSpritesheetId++); const frames = new Map(); // Parse spritesheet frames if (jsonData?.frames) { for (const frameName of Object.keys(jsonData.frames)) { const textureId = asTextureId(this.nextTextureId++); frames.set(frameName, textureId); this.textures.set(textureId, `${imageUrl}#${frameName}`); } } this.spritesheets.set(id, { url: imageUrl, data: jsonData, frames, }); return id; } getTexture(spritesheet, frameName) { const sheet = this.spritesheets.get(spritesheet); return sheet?.frames.get(frameName) ?? null; } // Viewport setViewport(id, options) { const node = this.nodes.get(id); if (node) { node.viewport = { screenWidth: options.screenWidth, screenHeight: options.screenHeight, worldWidth: options.worldWidth, worldHeight: options.worldHeight, position: { x: 0, y: 0 }, zoom: 1, options, }; } } getViewportPosition(id) { return withNode(this.nodes, id, (node) => node.viewport?.position ?? { x: 0, y: 0 }, { x: 0, y: 0 }); } setViewportPosition(id, x, y) { withNode(this.nodes, id, (node) => { if (node.viewport) { node.viewport.position = { x, y }; } }); } setViewportZoom(id, zoom) { withNode(this.nodes, id, (node) => { if (node.viewport) { node.viewport.zoom = zoom; } }); } getViewportZoom(id) { return withNode(this.nodes, id, (node) => node.viewport?.zoom ?? 1, 1); } worldToScreen(id, x, y) { return withNode(this.nodes, id, (node) => node.viewport ? worldToScreen(x, y, node.viewport.position, node.viewport.zoom) : { x, y }, { x, y }); } screenToWorld(id, x, y) { return withNode(this.nodes, id, (node) => node.viewport ? screenToWorld(x, y, node.viewport.position, node.viewport.zoom) : { x, y }, { x, y }); } // Game loop onTick(callback) { this.tickCallbackManager.register(callback); } setTickerSpeed(speed) { this.tickerSpeed = speed; } getFPS() { return 60; } // Manual rendering render() { // No-op for headless rendering } // Canvas resize resize(width, height) { this.canvasSize = { width, height }; } // Cleanup destroy() { this.nodes.clear(); this.textures.clear(); this.spritesheets.clear(); this.tickCallbackManager.clear(); } // Test helpers (not part of RenderingLayer interface) hasNode(id) { return this.nodes.has(id); } getChildren(id) { return withNode(this.nodes, id, (node) => [...node.children], []); } getPosition(id) { return withNode(this.nodes, id, (node) => ({ ...node.position }), { x: 0, y: 0, }); } getRotation(id) { return withNode(this.nodes, id, (node) => node.rotation, 0); } getScale(id) { return withNode(this.nodes, id, (node) => ({ ...node.scale }), { x: 1, y: 1, }); } getAnchor(id) { return withNode(this.nodes, id, (node) => ({ ...node.anchor }), { x: 0, y: 0, }); } getAlpha(id) { return withNode(this.nodes, id, (node) => node.alpha, 1); } getVisible(id) { return withNode(this.nodes, id, (node) => node.visible, true); } getZIndex(id) { return withNode(this.nodes, id, (node) => node.zIndex, 0); } getTint(id) { return withNode(this.nodes, id, (node) => node.tint, undefined); } getBlendMode(id) { return withNode(this.nodes, id, (node) => node.blendMode, undefined); } getTextureFiltering(id) { return withNode(this.nodes, id, (node) => node.textureFiltering, undefined); } getSpriteTexture(id) { return withNode(this.nodes, id, (node) => node.spriteTexture, undefined); } getAnimationData(id) { return withNode(this.nodes, id, (node) => node.animationData, undefined); } isAnimationPlaying(id) { return withNode(this.nodes, id, (node) => node.animationData?.playing ?? false, false); } getGraphics(id) { return withNode(this.nodes, id, (node) => [...node.graphics], []); } getViewportData(id) { return withNode(this.nodes, id, (node) => node.viewport, undefined); } getTickerSpeed() { return this.tickerSpeed; } getCanvasSize() { return { ...this.canvasSize }; } // Manual tick for testing tick(deltaTicks) { this.tickCallbackManager.trigger(deltaTicks); } }