UNPKG

kenzo-graphics-library-v2

Version:

Lightweight 2D/3D JavaScript engine using HTML5 canvas, math-based and fast.

1,015 lines (869 loc) 29.1 kB
// kgl.js - Kenzo Graphics Library v2 (Custom Math-Based 3D Engine) // Lightweight 3D and 2D engine for Web, Not meant for heavy use. export class Kgl { constructor(canvas) { if (!KGLconfig.checkConflicts('3D')) return; this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.width = canvas.width; this.height = canvas.height; this.mouse = { x: 0, y: 0, down: false }; this.keys = {}; this.angleY = 0; this.angleX = 0; this.light = { x: 1, y: 1, z: -1 }; window.addEventListener('keydown', e => this.keys[e.key] = true); window.addEventListener('keyup', e => this.keys[e.key] = false); canvas.addEventListener('mousemove', e => { if (this.mouse.down) { this.angleY += (e.movementX || 0) * 0.01; this.angleX += (e.movementY || 0) * 0.01; } this.mouse.x = e.offsetX; this.mouse.y = e.offsetY; }); canvas.addEventListener('mousedown', () => this.mouse.down = true); canvas.addEventListener('mouseup', () => this.mouse.down = false); } clear(color = '#000') { this.ctx.fillStyle = color; this.ctx.fillRect(0, 0, this.width, this.height); } rotate(v, ax, ay) { let cosY = Math.cos(ay), sinY = Math.sin(ay); let cosX = Math.cos(ax), sinX = Math.sin(ax); // Rotate around Y axis let xz = { x: v.x * cosY - v.z * sinY, z: v.x * sinY + v.z * cosY }; // Rotate around X axis let yz = { y: v.y * cosX - xz.z * sinX, z: v.y * sinX + xz.z * cosX }; return { x: xz.x, y: yz.y, z: yz.z }; } project(v) { const fov = 300; const scale = fov / (fov + v.z); return { x: this.width / 2 + v.x * scale, y: this.height / 2 - v.y * scale }; } drawSolid(vertices, faces, color = '#33f', outline = true) { const rotated = vertices.map(v => this.rotate(v, this.angleX, this.angleY)); const projected = rotated.map(v => this.project(v)); // Prepare faces with Z-depth const allFaces = faces.map(face => { const v0 = rotated[face[0]], v1 = rotated[face[1]], v2 = rotated[face[2]]; // Normal const ux = v1.x - v0.x, uy = v1.y - v0.y, uz = v1.z - v0.z; const vx = v2.x - v0.x, vy = v2.y - v0.y, vz = v2.z - v0.z; const normal = { x: uy * vz - uz * vy, y: uz * vx - ux * vz, z: ux * vy - uy * vx }; // Lighting const brightness = this.getLighting(normal); // Face center z (for painter's sort) const depth = (v0.z + v1.z + v2.z) / 3; return { points: face.map(i => projected[i]), brightness, depth }; }); // Painter's algorithm (sort back to front) allFaces.sort((a, b) => b.depth - a.depth); // Render allFaces.forEach(({ points, brightness }) => { this.ctx.beginPath(); this.ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) { this.ctx.lineTo(points[i].x, points[i].y); } this.ctx.closePath(); this.ctx.fillStyle = this.shade(color, brightness); this.ctx.fill(); if (outline) { this.ctx.strokeStyle = '#0005'; this.ctx.stroke(); } }); } drawTextured(vertices, faces, uvs, textureImg) { const rotated = vertices.map(v => this.rotate(v, this.angleX, this.angleY)); const projected = rotated.map(v => this.project(v)); const texCanvas = document.createElement('canvas'); texCanvas.width = textureImg.width; texCanvas.height = textureImg.height; const texCtx = texCanvas.getContext('2d'); texCtx.drawImage(textureImg, 0, 0); try { const texData = texCtx.getImageData(0, 0, texCanvas.width, texCanvas.height).data; // proceed with rendering } catch (err) { console.warn("KGL warning: Canvas tainted. Texture sampling disabled for cross-origin image."); return; // or fall back to untextured rendering } const putPixel = (x, y, r, g, b) => { this.ctx.fillStyle = `rgb(${r},${g},${b})`; this.ctx.fillRect(x, y, 1, 1); }; const sampleTex = (u, v) => { const tx = Math.floor(u * textureImg.width) % textureImg.width; const ty = Math.floor(v * textureImg.height) % textureImg.height; const idx = (ty * textureImg.width + tx) * 4; return [ texData[idx], texData[idx + 1], texData[idx + 2] ]; }; faces.forEach((face, fIndex) => { const [i0, i1, i2] = face; const [v0, v1, v2] = [projected[i0], projected[i1], projected[i2]]; const [uv0, uv1, uv2] = uvs[fIndex]; // Bounding box const minX = Math.max(0, Math.floor(Math.min(v0.x, v1.x, v2.x))); const maxX = Math.min(this.width, Math.ceil(Math.max(v0.x, v1.x, v2.x))); const minY = Math.max(0, Math.floor(Math.min(v0.y, v1.y, v2.y))); const maxY = Math.min(this.height, Math.ceil(Math.max(v0.y, v1.y, v2.y))); const edgeFunc = (a, b, c) => (c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x); const area = edgeFunc(v0, v1, v2); for (let y = minY; y < maxY; y++) { for (let x = minX; x < maxX; x++) { const p = { x, y }; const w0 = edgeFunc(v1, v2, p); const w1 = edgeFunc(v2, v0, p); const w2 = edgeFunc(v0, v1, p); if (w0 >= 0 && w1 >= 0 && w2 >= 0) { const alpha = w0 / area; const beta = w1 / area; const gamma = w2 / area; const u = uv0[0] * alpha + uv1[0] * beta + uv2[0] * gamma; const v = uv0[1] * alpha + uv1[1] * beta + uv2[1] * gamma; const [r, g, b] = sampleTex(u, v); putPixel(x, y, r, g, b); } } } }); } getLighting(normal) { const len = Math.hypot(normal.x, normal.y, normal.z); const nx = normal.x / len; const ny = normal.y / len; const nz = normal.z / len; const l = this.light; const ln = Math.hypot(l.x, l.y, l.z); const lx = l.x / ln, ly = l.y / ln, lz = l.z / ln; return Math.max(0.2, lx * nx + ly * ny + lz * nz); } shade(hex, brightness) { const num = parseInt(hex.slice(1), 16); const r = ((num >> 16) & 255) * brightness; const g = ((num >> 8) & 255) * brightness; const b = (num & 255) * brightness; return `rgb(${r|0},${g|0},${b|0})`; } updateRotation(speedX = 0.01, speedY = 0.01) { this.angleX += speedX; this.angleY += speedY; } } export class KglUI { constructor(kgl) { this.kgl = kgl; this.ctx = kgl.ctx; this.elements = []; this.hovered = null; this.kgl.canvas.addEventListener('click', (e) => { const x = e.offsetX, y = e.offsetY; for (const el of this.elements) { if (el.type === 'button' && x >= el.x && x <= el.x + el.width && y >= el.y && y <= el.y + el.height) { el.onClick?.(); } } }); } addButton(x, y, width, height, text, onClick, options = {}) { this.elements.push({ type: 'button', x, y, width, height, text, onClick, options }); } addText(x, y, text, options = {}) { this.elements.push({ type: 'text', x, y, text, options }); } addImage(x, y, img, width, height) { this.elements.push({ type: 'image', x, y, img, width, height }); } addModal(x, y, width, height, contentFn, options = {}) { this.elements.push({ type: 'modal', x, y, width, height, contentFn, options }); } addBackground(color) { this.elements.push({ type: 'background', color }); } render() { for (const el of this.elements) { switch (el.type) { case 'background': this.ctx.fillStyle = el.color; this.ctx.fillRect(0, 0, this.kgl.width, this.kgl.height); break; case 'text': this.ctx.fillStyle = el.options.color || '#fff'; this.ctx.font = el.options.font || '16px sans-serif'; this.ctx.fillText(el.text, el.x, el.y); break; case 'button': this.ctx.fillStyle = el.options.bg || '#444'; this.ctx.fillRect(el.x, el.y, el.width, el.height); this.ctx.strokeStyle = el.options.border || '#fff'; this.ctx.strokeRect(el.x, el.y, el.width, el.height); this.ctx.fillStyle = el.options.color || '#fff'; this.ctx.font = el.options.font || '14px sans-serif'; this.ctx.fillText(el.text, el.x + 10, el.y + el.height / 2 + 5); break; case 'image': this.ctx.drawImage(el.img, el.x, el.y, el.width, el.height); break; case 'modal': this.ctx.fillStyle = el.options.bg || 'rgba(0,0,0,0.7)'; this.ctx.fillRect(el.x, el.y, el.width, el.height); this.ctx.strokeStyle = el.options.border || '#fff'; this.ctx.strokeRect(el.x, el.y, el.width, el.height); el.contentFn?.(this.ctx, el); break; } } } clearUI() { this.elements = []; } } export class Kgl2D { constructor(canvas) { if (!KGLconfig.checkConflicts('2D')) return; this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.width = canvas.width; this.height = canvas.height; this.mouse = { x: 0, y: 0, down: false }; this.keys = {}; window.addEventListener('keydown', e => this.keys[e.key] = true); window.addEventListener('keyup', e => this.keys[e.key] = false); canvas.addEventListener('mousemove', e => { this.mouse.x = e.offsetX; this.mouse.y = e.offsetY; }); canvas.addEventListener('mousedown', () => this.mouse.down = true); canvas.addEventListener('mouseup', () => this.mouse.down = false); } clear(color = '#000') { this.ctx.fillStyle = color; this.ctx.fillRect(0, 0, this.width, this.height); } setFillColor(color) { this.ctx.fillStyle = color; } setStrokeColor(color) { this.ctx.strokeStyle = color; } setFont(font = '16px sans-serif') { this.ctx.font = font; } drawRect(x, y, w, h, fill = true) { if (fill) this.ctx.fillRect(x, y, w, h); else this.ctx.strokeRect(x, y, w, h); } drawCircle(x, y, r, fill = true) { this.ctx.beginPath(); this.ctx.arc(x, y, r, 0, Math.PI * 2); this.ctx.closePath(); if (fill) this.ctx.fill(); else this.ctx.stroke(); } drawLine(x1, y1, x2, y2) { this.ctx.beginPath(); this.ctx.moveTo(x1, y1); this.ctx.lineTo(x2, y2); this.ctx.stroke(); } drawText(text, x, y, color = '#fff') { this.ctx.fillStyle = color; this.ctx.fillText(text, x, y); } drawImage(img, x, y, w, h) { this.ctx.drawImage(img, x, y, w, h); } drawPolygon(points, fill = true) { if (points.length < 2) return; this.ctx.beginPath(); this.ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) { this.ctx.lineTo(points[i].x, points[i].y); } this.ctx.closePath(); if (fill) this.ctx.fill(); else this.ctx.stroke(); } drawGrid(cellSize = 20, color = '#222') { this.ctx.strokeStyle = color; for (let x = 0; x <= this.width; x += cellSize) { this.ctx.beginPath(); this.ctx.moveTo(x, 0); this.ctx.lineTo(x, this.height); this.ctx.stroke(); } for (let y = 0; y <= this.height; y += cellSize) { this.ctx.beginPath(); this.ctx.moveTo(0, y); this.ctx.lineTo(this.width, y); this.ctx.stroke(); } } setGlobalAlpha(alpha) { this.ctx.globalAlpha = alpha; } resetAlpha() { this.ctx.globalAlpha = 1.0; } isKeyDown(key) { return this.keys[key] === true; } isMouseInside(x, y, w, h) { return this.mouse.x >= x && this.mouse.x <= x + w && this.mouse.y >= y && this.mouse.y <= y + h; } } // KGL HTSTB - HTML Display STUB (Not a stub actually) export class KglHTSTB { constructor(canvas) { if (!KGLconfig.checkConflicts('HTSTB')) return; this.canvas = canvas; this.elements = []; this.container = document.createElement('div'); this.container.style.position = 'relative'; this.container.style.width = canvas.width + 'px'; this.container.style.height = canvas.height + 'px'; this.container.style.pointerEvents = 'none'; // Allow canvas to still handle events canvas.parentNode.insertBefore(this.container, canvas); this.container.appendChild(canvas); } createBox(x, y, width, height, html, options = {}) { const el = document.createElement('div'); el.innerHTML = html; el.style.position = 'absolute'; el.style.left = x + 'px'; el.style.top = y + 'px'; el.style.width = width + 'px'; el.style.height = height + 'px'; el.style.color = options.color || '#fff'; el.style.background = options.background || '#222'; el.style.border = options.border || '1px solid #555'; el.style.font = options.font || '14px sans-serif'; el.style.padding = options.padding || '5px'; el.style.overflow = options.overflow || 'auto'; el.style.pointerEvents = 'auto'; // Allow interaction this.container.appendChild(el); this.elements.push(el); return el; } clear() { for (const el of this.elements) { el.remove(); } this.elements = []; } resize(width, height) { this.container.style.width = width + 'px'; this.container.style.height = height + 'px'; } } export class KGLconfig { static UseFaceCulling = false; static DisableHTSTB = false; static Disable2D = false; static Disable3D = false; static File2D = false; static File3D = false; static FileUI = false; static IsInDev = false; static AppName = "Untitled App"; static Version = "0.0.1"; static set(options = {}) { for (let key in options) { if (key in KGLconfig) { KGLconfig[key] = options[key]; } } } static info() { return { AppName: KGLconfig.AppName, Version: KGLconfig.Version, Mode: KGLconfig.IsInDev ? "Development" : "Production" }; } static isEnabled(feature) { return KGLconfig[feature] === true; } static checkConflicts(component) { // Throw error if face culling used in 2D if (KGLconfig.UseFaceCulling && component === '2D') { throw new Error("KGL error : 500 (Face culling is not for 2D)"); } // Prevent component if globally disabled if (component === '2D' && KGLconfig.Disable2D) return false; if (component === '3D' && KGLconfig.Disable3D) return false; if (component === 'HTSTB' && KGLconfig.DisableHTSTB) return false; return true; } } export class KglUtilities { static WriteErrorsInCanvas = false; static Opt = false; static initSoundSystem() { if (!window.AudioContext && !window.webkitAudioContext) { console.warn("KGLUtilities: Web Audio API not supported."); return; } KglUtilities.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } static playSound(url) { if (!KglUtilities.audioCtx) { KglUtilities.initSoundSystem(); } fetch(url) .then(res => res.arrayBuffer()) .then(data => KglUtilities.audioCtx.decodeAudioData(data)) .then(buffer => { const source = KglUtilities.audioCtx.createBufferSource(); source.buffer = buffer; source.connect(KglUtilities.audioCtx.destination); source.start(0); }) .catch(err => { KglUtilities._handleError("Audio Error: " + err.message); }); } static optimize3D(kglInstance) { if (!KglUtilities.Opt || !kglInstance || !kglInstance.ctx) return; // Basic optimization: reduce anti-aliasing artifacts with pixel snapping kglInstance.ctx.imageSmoothingEnabled = false; // Pre-render lighting vectors or any matrix caching logic (dummy example) kglInstance.light = { x: (kglInstance.light.x / 2).toFixed(3), y: (kglInstance.light.y / 2).toFixed(3), z: (kglInstance.light.z / 2).toFixed(3) }; } static writeErrorToCanvas(canvas, message) { if (!KglUtilities.WriteErrorsInCanvas || !canvas) return; const ctx = canvas.getContext('2d'); ctx.font = '12px Tahoma'; ctx.fillStyle = '#f55'; ctx.fillText(message, 5, 15); } static _handleError(message) { console.error("KGL Error:", message); if (KglUtilities.WriteErrorsInCanvas && document.querySelector('canvas')) { KglUtilities.writeErrorToCanvas(document.querySelector('canvas'), message); } } } export class KGLvoxel { constructor(kgl, chunkSize = 16) { this.kgl = kgl; this.chunkSize = chunkSize; this.voxelSize = 20; this.chunks = new Map(); // Map<string, Voxel[]> } _getChunkKey(x, y, z) { const cx = Math.floor(x / this.chunkSize); const cy = Math.floor(y / this.chunkSize); const cz = Math.floor(z / this.chunkSize); return `${cx},${cy},${cz}`; } addVoxel(x, y, z, color = '#6cf') { const key = this._getChunkKey(x, y, z); if (!this.chunks.has(key)) this.chunks.set(key, []); this.chunks.get(key).push({ x, y, z, color }); } removeVoxel(x, y, z) { const key = this._getChunkKey(x, y, z); if (!this.chunks.has(key)) return; const list = this.chunks.get(key); this.chunks.set(key, list.filter(v => v.x !== x || v.y !== y || v.z !== z)); } getVisibleChunkKeys(center, viewDistance = 2) { const keys = []; const baseX = Math.floor(center.x / this.chunkSize); const baseY = Math.floor(center.y / this.chunkSize); const baseZ = Math.floor(center.z / this.chunkSize); for (let dx = -viewDistance; dx <= viewDistance; dx++) { for (let dy = -viewDistance; dy <= viewDistance; dy++) { for (let dz = -viewDistance; dz <= viewDistance; dz++) { const key = `${baseX + dx},${baseY + dy},${baseZ + dz}`; if (this.chunks.has(key)) { keys.push(key); } } } } return keys; } drawVisibleChunks(playerPos = { x: 0, y: 0, z: 0 }) { const visibleKeys = this.getVisibleChunkKeys(playerPos); for (const key of visibleKeys) { const voxels = this.chunks.get(key); for (const voxel of voxels) { this._drawVoxelCube(voxel); } } } _drawVoxelCube({ x, y, z, color }) { const s = this.voxelSize / 2; const v = [ { x: x - s, y: y - s, z: z - s }, { x: x + s, y: y - s, z: z - s }, { x: x + s, y: y + s, z: z - s }, { x: x - s, y: y + s, z: z - s }, { x: x - s, y: y - s, z: z + s }, { x: x + s, y: y - s, z: z + s }, { x: x + s, y: y + s, z: z + s }, { x: x - s, y: y + s, z: z + s }, ]; const f = [ [0, 1, 2, 3], [4, 5, 6, 7], [0, 1, 5, 4], [3, 2, 6, 7], [1, 2, 6, 5], [0, 3, 7, 4] ]; this.kgl.drawSolid(v, f, color); } clearChunks() { this.chunks.clear(); } voxelCount() { let total = 0; for (const list of this.chunks.values()) { total += list.length; } return total; } } export class KGLentity { constructor(kgl) { this.kgl = kgl; this.entities = []; } addEntity({ position = { x: 0, y: 0, z: 0 }, color = '#f55', model = null, behavior = null }) { const entity = { id: crypto.randomUUID(), pos: position, color, model, // { vertices: [...], faces: [...] } behavior, // function(entity, deltaTime) }; this.entities.push(entity); return entity.id; } removeEntityById(id) { this.entities = this.entities.filter(e => e.id !== id); } update(deltaTime = 1 / 60) { for (const entity of this.entities) { if (typeof entity.behavior === 'function') { entity.behavior(entity, deltaTime); } } } drawEntities() { for (const entity of this.entities) { if (entity.model) { const offsetVerts = entity.model.vertices.map(v => ({ x: v.x + entity.pos.x, y: v.y + entity.pos.y, z: v.z + entity.pos.z })); this.kgl.drawSolid(offsetVerts, entity.model.faces, entity.color); } else { // fallback: draw small cube const s = 10; const v = [ { x: entity.pos.x - s, y: entity.pos.y - s, z: entity.pos.z - s }, { x: entity.pos.x + s, y: entity.pos.y - s, z: entity.pos.z - s }, { x: entity.pos.x + s, y: entity.pos.y + s, z: entity.pos.z - s }, { x: entity.pos.x - s, y: entity.pos.y + s, z: entity.pos.z - s }, { x: entity.pos.x - s, y: entity.pos.y - s, z: entity.pos.z + s }, { x: entity.pos.x + s, y: entity.pos.y - s, z: entity.pos.z + s }, { x: entity.pos.x + s, y: entity.pos.y + s, z: entity.pos.z + s }, { x: entity.pos.x - s, y: entity.pos.y + s, z: entity.pos.z + s }, ]; const f = [ [0, 1, 2, 3], [4, 5, 6, 7], [0, 1, 5, 4], [3, 2, 6, 7], [1, 2, 6, 5], [0, 3, 7, 4] ]; this.kgl.drawSolid(v, f, entity.color); } } } getEntityById(id) { return this.entities.find(e => e.id === id); } clear() { this.entities = []; } count() { return this.entities.length; } } export class KGLscene { constructor(kgl) { this.kgl = kgl; this.scenes = new Map(); // Map<string, {onLoad, onUpdate, onDraw, onUnload}> this.currentScene = null; this.sceneData = {}; // Shared between scenes if needed } createScene(name, { onLoad = () => {}, onUpdate = () => {}, onDraw = () => {}, onUnload = () => {} } = {}) { this.scenes.set(name, { onLoad, onUpdate, onDraw, onUnload }); } switchScene(name) { if (!this.scenes.has(name)) { console.warn(`KGLscene: Scene "${name}" does not exist.`); return; } if (this.currentScene && this.scenes.get(this.currentScene).onUnload) { this.scenes.get(this.currentScene).onUnload(this.sceneData); } this.currentScene = name; this.scenes.get(name).onLoad?.(this.sceneData); } update(deltaTime = 1 / 60) { if (!this.currentScene) return; const scene = this.scenes.get(this.currentScene); scene.onUpdate?.(deltaTime, this.sceneData); } render() { if (!this.currentScene) return; const scene = this.scenes.get(this.currentScene); scene.onDraw?.(this.kgl.ctx, this.sceneData); } deleteScene(name) { if (this.currentScene === name) this.currentScene = null; this.scenes.delete(name); } clearScenes() { this.scenes.clear(); this.currentScene = null; } } export class KGLnoise { constructor(seed = 42) { this.seed = seed; this.perm = new Uint8Array(512); this._initPerm(); } _initPerm() { const p = new Uint8Array(256); for (let i = 0; i < 256; i++) p[i] = i; let rand = this.seed; for (let i = 255; i > 0; i--) { rand = (rand * 1664525 + 1013904223) >>> 0; const j = rand % (i + 1); [p[i], p[j]] = [p[j], p[i]]; } for (let i = 0; i < 512; i++) { this.perm[i] = p[i & 255]; } } fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } lerp(t, a, b) { return a + t * (b - a); } grad(hash, x, y) { const h = hash & 3; return ((h & 1) ? -x : x) + ((h & 2) ? -y : y); } perlin(x, y = 0) { const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255; x -= Math.floor(x); y -= Math.floor(y); const u = this.fade(x); const v = this.fade(y); const aa = this.perm[X + this.perm[Y]]; const ab = this.perm[X + this.perm[Y + 1]]; const ba = this.perm[X + 1 + this.perm[Y]]; const bb = this.perm[X + 1 + this.perm[Y + 1]]; const gradAA = this.grad(aa, x, y); const gradBA = this.grad(ba, x - 1, y); const gradAB = this.grad(ab, x, y - 1); const gradBB = this.grad(bb, x - 1, y - 1); const lerpX1 = this.lerp(u, gradAA, gradBA); const lerpX2 = this.lerp(u, gradAB, gradBB); return (this.lerp(v, lerpX1, lerpX2) + 1) / 2; } normal(x, y = 0) { // Simple pseudo-random noise const n = Math.sin(x * 12.9898 + y * 78.233 + this.seed) * 43758.5453; return (n - Math.floor(n)); } kenzo(x, y = 0) { // Mixed: blend Perlin and Normal const p = this.perlin(x, y); const n = this.normal(x, y); return (p * 0.6 + n * 0.4); } } export class KGLtcp { constructor(serverUrl) { this.serverUrl = serverUrl; this.socket = null; this.isConnected = false; this.onMessageHandlers = []; this.onConnect = () => {}; this.onDisconnect = () => {}; } connect() { this.socket = new WebSocket(this.serverUrl); this.socket.addEventListener('open', () => { this.isConnected = true; console.log("KGLtcp: Connected to", this.serverUrl); this.onConnect(); }); this.socket.addEventListener('message', (event) => { try { const data = JSON.parse(event.data); for (const handler of this.onMessageHandlers) { handler(data); } } catch (err) { console.warn("KGLtcp: Invalid JSON received", err); } }); this.socket.addEventListener('close', () => { this.isConnected = false; console.warn("KGLtcp: Disconnected from server"); this.onDisconnect(); }); this.socket.addEventListener('error', (err) => { console.error("KGLtcp: Error", err); }); } send(data) { if (this.isConnected && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(data)); } else { console.warn("KGLtcp: Not connected, send failed"); } } onMessage(callback) { this.onMessageHandlers.push(callback); } close() { if (this.socket) { this.socket.close(); } } } // KGML - KGL Modification Loader // Allows runtime injection of modules, extensions, or patches into the KGL ecosystem export class KGML { constructor() { this.mods = new Map(); this.loadQueue = []; this.loaded = false; } registerMod(name, mod) { if (this.mods.has(name)) { console.warn(`KGML: Mod '${name}' already registered.`); return false; } if (typeof mod.init !== 'function') { console.error(`KGML: Mod '${name}' must have an init() function.`); return false; } this.mods.set(name, mod); if (this.loaded) { mod.init(); } else { this.loadQueue.push(mod); } return true; } unregisterMod(name) { if (this.mods.has(name)) { const mod = this.mods.get(name); if (typeof mod.destroy === 'function') mod.destroy(); this.mods.delete(name); return true; } return false; } initAll() { this.loaded = true; for (const mod of this.mods.values()) { mod.init(); } while (this.loadQueue.length > 0) { const mod = this.loadQueue.shift(); try { mod.init(); } catch (e) { console.error(`KGML: Failed to load mod:`, e); } } } injectTo(target, injectFn) { try { injectFn(target); } catch (e) { console.warn("KGML: Injection failed", e); } } listMods() { return Array.from(this.mods.keys()); } getMod(name) { return this.mods.get(name); } } export class KGLconsole { static ANSI = { reset: '\x1b[0m', black: '\x1b[30m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m', }; static print = { color(colorName, text) { const code = KGLconsole.ANSI[colorName.toLowerCase()]; if (!code) { console.warn(`KGLconsole: Unknown color '${colorName}'`); console.log(text); return; } console.log(`${code}%s${KGLconsole.ANSI.reset}`, text); }, info(text) { console.log(`${KGLconsole.ANSI.cyan}%s${KGLconsole.ANSI.reset}`, text); }, warn(text) { console.warn(`${KGLconsole.ANSI.yellow}%s${KGLconsole.ANSI.reset}`, text); }, error(text) { console.error(`${KGLconsole.ANSI.red}%s${KGLconsole.ANSI.reset}`, text); }, success(text) { console.log(`${KGLconsole.ANSI.green}%s${KGLconsole.ANSI.reset}`, text); } }; }