723 lines
33 KiB
JavaScript
723 lines
33 KiB
JavaScript
/**
|
|
* js/world.js — Gemeinsame Welt-Engine für Editor (Vorschau/Generator) und Spieler.
|
|
*
|
|
* API:
|
|
* World.generate(themeId, seed, fieldCount) → world (normalized, deterministisch)
|
|
* World.render(ctx, world, state, t) → zeichnet ein Frame
|
|
* World.hitTestPad(world, x, y, W, H) → fieldIndex | -1
|
|
* World.padCenter(world, i, W, H) → {x,y}
|
|
* World.resolveTheme(id) → mapped theme id (Legacy)
|
|
* World.randomSeed() → uint32
|
|
* World.hashSeed(str) → uint32
|
|
*
|
|
* state = { W, H, pos, visited, fields, storyItems, figEmoji, figX, figY,
|
|
* gameName?, devName?, hover? (fieldIndex), locked? }
|
|
*/
|
|
window.World = (function () {
|
|
|
|
// ── Seedable PRNG (mulberry32) ─────────────────────────────
|
|
function mulberry32(seed) {
|
|
let s = (seed >>> 0) || 1;
|
|
return function () {
|
|
s = (s + 0x6D2B79F5) | 0;
|
|
let t = s;
|
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
};
|
|
}
|
|
|
|
function hashSeed(str) {
|
|
let h = 2166136261;
|
|
const s = String(str || '');
|
|
for (let i = 0; i < s.length; i++) {
|
|
h ^= s.charCodeAt(i);
|
|
h = Math.imul(h, 16777619);
|
|
}
|
|
return (h >>> 0) || 1;
|
|
}
|
|
|
|
function randomSeed() { return (Math.floor(Math.random() * 0x100000000)) >>> 0; }
|
|
|
|
// ── Theme registry (4 Welten) ───────────────────────────────
|
|
const THEMES = {
|
|
underwater: {
|
|
id: 'underwater', name: 'Unterwasser', emoji: '🌊',
|
|
primary: '#22d3ee', accent: '#0ea5e9',
|
|
bgGrad: ['#052236', '#083b56', '#0e567a'],
|
|
pad: { fill: '#0891b2', edge: '#155e75', glow: 'rgba(34,211,238,0.5)' },
|
|
},
|
|
space: {
|
|
id: 'space', name: 'Weltall', emoji: '🌌',
|
|
primary: '#a78bfa', accent: '#7c3aed',
|
|
bgGrad: ['#050314', '#0f0a2e', '#1e1b4b'],
|
|
pad: { fill: '#7c3aed', edge: '#4c1d95', glow: 'rgba(167,139,250,0.55)' },
|
|
},
|
|
haunted: {
|
|
id: 'haunted', name: 'Geisterhaus', emoji: '👻',
|
|
primary: '#86efac', accent: '#22d3ee',
|
|
bgGrad: ['#0a0a0e', '#16162a', '#1a1a2e'],
|
|
pad: { fill: '#475569', edge: '#1e293b', glow: 'rgba(134,239,172,0.45)' },
|
|
},
|
|
fantasy: {
|
|
id: 'fantasy', name: 'Fantasy', emoji: '🧙',
|
|
primary: '#f0abfc', accent: '#c084fc',
|
|
bgGrad: ['#1a0a2e', '#3b0764', '#581c87'],
|
|
pad: { fill: '#a855f7', edge: '#581c87', glow: 'rgba(240,171,252,0.55)' },
|
|
},
|
|
};
|
|
|
|
// Mapping für entfernte Welten (Migration alter Saves/URLs)
|
|
const LEGACY_MAP = {
|
|
ocean: 'underwater', jungle: 'fantasy', volcano: 'space', snow: 'space',
|
|
city: 'space', candy: 'fantasy', desert: 'fantasy', school: 'fantasy', future: 'space',
|
|
};
|
|
|
|
function resolveTheme(id) { return THEMES[id] ? id : (LEGACY_MAP[id] || 'space'); }
|
|
|
|
// ── Pad-Platzierung (Snake-Grid + Jitter, normalisierte Koords) ─────
|
|
function placePads(rng, fieldCount) {
|
|
const aspect = 16 / 9;
|
|
const cols = Math.max(2, Math.min(fieldCount, Math.round(Math.sqrt(fieldCount * aspect))));
|
|
const rows = Math.ceil(fieldCount / cols);
|
|
const marginX = 0.07, marginY = 0.13; // Platz oben für Header
|
|
const usableW = 1 - 2 * marginX;
|
|
const usableH = 1 - marginY - 0.10;
|
|
const cellW = usableW / cols;
|
|
const cellH = usableH / rows;
|
|
|
|
const pads = [];
|
|
for (let i = 0; i < fieldCount; i++) {
|
|
const r = Math.floor(i / cols);
|
|
const c = r % 2 === 0 ? i % cols : (cols - 1) - (i % cols);
|
|
const cx = marginX + (c + 0.5) * cellW;
|
|
const cy = marginY + (r + 0.5) * cellH;
|
|
const jx = (rng() - 0.5) * cellW * 0.55;
|
|
const jy = (rng() - 0.5) * cellH * 0.45;
|
|
const ux = Math.max(marginX, Math.min(1 - marginX, cx + jx));
|
|
const uy = Math.max(marginY, Math.min(1 - 0.06, cy + jy));
|
|
pads.push({ ux, uy });
|
|
}
|
|
return pads;
|
|
}
|
|
|
|
// Catmull-Rom-Spline durch Pads (normalized space)
|
|
function spline(pads, samplesPerSeg) {
|
|
const out = [];
|
|
const n = pads.length;
|
|
if (n < 2) return pads.slice();
|
|
for (let i = 0; i < n - 1; i++) {
|
|
const p0 = pads[Math.max(0, i - 1)];
|
|
const p1 = pads[i];
|
|
const p2 = pads[i + 1];
|
|
const p3 = pads[Math.min(n - 1, i + 2)];
|
|
for (let s = 0; s < samplesPerSeg; s++) {
|
|
const t = s / samplesPerSeg, t2 = t * t, t3 = t2 * t;
|
|
const ux = 0.5 * ((2 * p1.ux) + (-p0.ux + p2.ux) * t + (2 * p0.ux - 5 * p1.ux + 4 * p2.ux - p3.ux) * t2 + (-p0.ux + 3 * p1.ux - 3 * p2.ux + p3.ux) * t3);
|
|
const uy = 0.5 * ((2 * p1.uy) + (-p0.uy + p2.uy) * t + (2 * p0.uy - 5 * p1.uy + 4 * p2.uy - p3.uy) * t2 + (-p0.uy + 3 * p1.uy - 3 * p2.uy + p3.uy) * t3);
|
|
out.push({ ux, uy });
|
|
}
|
|
}
|
|
out.push({ ux: pads[n - 1].ux, uy: pads[n - 1].uy });
|
|
return out;
|
|
}
|
|
|
|
// ── Theme-spezifische Dekoration deterministisch generieren ────
|
|
function makeDecor(rng, themeId, fieldCount) {
|
|
const d = {};
|
|
if (themeId === 'underwater') {
|
|
d.bubbles = []; for (let i = 0; i < 30; i++) d.bubbles.push({ ux: rng(), uy: rng(), r: 2 + rng() * 5, sp: 0.04 + rng() * 0.10, ph: rng() * 6.28 });
|
|
d.grass = []; for (let i = 0; i < 14; i++) d.grass.push({ ux: rng(), h: 0.10 + rng() * 0.22, sw: 1.5 + rng() * 2.5, ph: rng() * 6.28 });
|
|
d.rocks = []; for (let i = 0; i < 8; i++) d.rocks.push({ ux: rng(), w: 0.04 + rng() * 0.08, h: 0.05 + rng() * 0.08, col: rng() });
|
|
d.rays = []; for (let i = 0; i < 5; i++) d.rays.push({ x: rng(), w: 60 + rng() * 80, sk: rng(), ph: rng() * 6.28 });
|
|
}
|
|
else if (themeId === 'space') {
|
|
d.stars = []; for (let i = 0; i < 90; i++) d.stars.push({ ux: rng(), uy: rng(), r: 0.5 + rng() * 1.8, sp: 0.5 + rng() * 1.8, ph: rng() * 6.28 });
|
|
d.planets = []; for (let i = 0; i < 3; i++) d.planets.push({ ux: 0.1 + rng() * 0.8, uy: rng() * 0.5, r: 0.03 + rng() * 0.045, hue: rng() * 360 });
|
|
d.nebula = []; for (let i = 0; i < 2; i++) d.nebula.push({ ux: rng(), uy: rng() * 0.6, r: 0.18 + rng() * 0.12, hue: 250 + rng() * 80 });
|
|
}
|
|
else if (themeId === 'haunted') {
|
|
d.trees = []; for (let i = 0; i < 7; i++) d.trees.push({ ux: rng(), h: 0.30 + rng() * 0.30, lean: (rng() - 0.5) * 0.4 });
|
|
d.graves = []; for (let i = 0; i < 8; i++) d.graves.push({ ux: rng(), w: 0.03 + rng() * 0.04, h: 0.04 + rng() * 0.05 });
|
|
d.wisps = []; for (let i = 0; i < 14; i++) d.wisps.push({ ux: rng(), uy: rng(), r: 4 + rng() * 5, sp: 0.02 + rng() * 0.05, ph: rng() * 6.28 });
|
|
d.moonX = 0.78 + rng() * 0.12;
|
|
}
|
|
else if (themeId === 'fantasy') {
|
|
d.trees = []; for (let i = 0; i < 9; i++) d.trees.push({ ux: rng(), h: 0.25 + rng() * 0.30 });
|
|
d.flies = []; for (let i = 0; i < 40; i++) d.flies.push({ ux: rng(), uy: rng(), r: 1.5 + rng() * 2, sp: 0.6 + rng() * 1.2, ph: rng() * 6.28 });
|
|
d.runes = []; for (let i = 0; i < 5; i++) d.runes.push({ ux: 0.05 + rng() * 0.9, uy: 0.1 + rng() * 0.5, r: 0.018 + rng() * 0.020, ph: rng() * 6.28 });
|
|
}
|
|
return d;
|
|
}
|
|
|
|
// ── generate ────────────────────────────────────────────────
|
|
function generate(themeIdRaw, seed, fieldCount) {
|
|
const themeId = resolveTheme(themeIdRaw);
|
|
const s = ((seed >>> 0) || 1) >>> 0;
|
|
const rng = mulberry32(s);
|
|
const pads = placePads(rng, fieldCount);
|
|
const path = spline(pads, 22);
|
|
const decor = makeDecor(rng, themeId, fieldCount);
|
|
return { theme: themeId, seed: s, fieldCount, pads, path, decor };
|
|
}
|
|
|
|
// ── Hilfen ──────────────────────────────────────────────────
|
|
function padR(W, H) { return Math.min(W, H) * 0.052; }
|
|
function padCenter(world, i, W, H) { const p = world.pads[i]; return { x: p.ux * W, y: p.uy * H }; }
|
|
|
|
function hitTestPad(world, x, y, W, H) {
|
|
const r = padR(W, H) * 1.25; // etwas größer für Touch
|
|
for (let i = 0; i < world.pads.length; i++) {
|
|
const p = world.pads[i], px = p.ux * W, py = p.uy * H;
|
|
if ((x - px) * (x - px) + (y - py) * (y - py) < r * r) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// ── Background-Dekoration zeichnen ──────────────────────────
|
|
function drawBgDecor(ctx, world, theme, state, t) {
|
|
const W = state.W, H = state.H, tt = t * 0.001;
|
|
|
|
if (theme.id === 'underwater') {
|
|
// Caustic-Lichtstrahlen
|
|
ctx.save(); ctx.globalCompositeOperation = 'lighter';
|
|
world.decor.rays.forEach(ry => {
|
|
const x = ((ry.x + Math.sin(tt * 0.2 + ry.ph) * 0.03) % 1 + 1) % 1 * W;
|
|
const g = ctx.createLinearGradient(x, 0, x + ry.w * 0.6, H);
|
|
g.addColorStop(0, 'rgba(125,211,252,0.10)');
|
|
g.addColorStop(1, 'rgba(125,211,252,0)');
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x + ry.w, 0); ctx.lineTo(x + ry.w * 1.2 + 60, H); ctx.lineTo(x + 40, H); ctx.closePath(); ctx.fill();
|
|
});
|
|
ctx.restore();
|
|
// Felsen
|
|
world.decor.rocks.forEach(r => {
|
|
const x = r.ux * W, w = r.w * W, h = r.h * H;
|
|
ctx.fillStyle = r.col > 0.5 ? '#0e7490' : '#155e75';
|
|
ctx.beginPath(); ctx.ellipse(x, H - h * 0.3, w, h * 0.7, 0, Math.PI, 2 * Math.PI); ctx.fill();
|
|
});
|
|
// Seetang
|
|
world.decor.grass.forEach(gr => {
|
|
const x = gr.ux * W, sway = Math.sin(tt * 1.4 + gr.ph) * 14;
|
|
ctx.strokeStyle = 'rgba(20,184,166,0.65)'; ctx.lineWidth = gr.sw; ctx.lineCap = 'round';
|
|
ctx.beginPath(); ctx.moveTo(x, H);
|
|
ctx.quadraticCurveTo(x + sway * 0.3, H - gr.h * H * 0.5, x + sway, H - gr.h * H);
|
|
ctx.stroke();
|
|
});
|
|
// Blasen
|
|
world.decor.bubbles.forEach(b => {
|
|
const x = b.ux * W + Math.sin(tt + b.ph) * 12;
|
|
const y = ((b.uy - tt * b.sp) % 1 + 1) % 1 * H;
|
|
ctx.globalAlpha = 0.35 + 0.35 * Math.sin(tt * 2 + b.ph);
|
|
ctx.fillStyle = 'rgba(186,230,253,0.6)';
|
|
ctx.beginPath(); ctx.arc(x, y, b.r, 0, Math.PI * 2); ctx.fill();
|
|
ctx.strokeStyle = 'rgba(255,255,255,0.4)'; ctx.lineWidth = 0.5; ctx.stroke();
|
|
});
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
else if (theme.id === 'space') {
|
|
// Nebel
|
|
world.decor.nebula.forEach(n => {
|
|
const x = n.ux * W, y = n.uy * H, r = n.r * Math.min(W, H);
|
|
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
|
g.addColorStop(0, `hsla(${n.hue},80%,60%,0.20)`);
|
|
g.addColorStop(1, `hsla(${n.hue},80%,40%,0)`);
|
|
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
|
});
|
|
// Sterne
|
|
world.decor.stars.forEach(s => {
|
|
const x = s.ux * W, y = s.uy * H;
|
|
ctx.globalAlpha = 0.4 + 0.5 * Math.sin(tt * s.sp + s.ph);
|
|
ctx.fillStyle = '#cdd6ff';
|
|
ctx.fillRect(x, y, s.r, s.r);
|
|
});
|
|
ctx.globalAlpha = 1;
|
|
// Planeten
|
|
world.decor.planets.forEach(p => {
|
|
const x = p.ux * W, y = p.uy * H, r = p.r * Math.min(W, H);
|
|
const g = ctx.createRadialGradient(x - r * 0.3, y - r * 0.3, 0, x, y, r);
|
|
g.addColorStop(0, `hsla(${p.hue},70%,65%,0.55)`);
|
|
g.addColorStop(1, `hsla(${p.hue},70%,30%,0.05)`);
|
|
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
|
});
|
|
}
|
|
else if (theme.id === 'haunted') {
|
|
// Mond
|
|
const mx = world.decor.moonX * W, my = H * 0.16, mr = Math.min(W, H) * 0.06;
|
|
ctx.fillStyle = 'rgba(220,230,200,0.18)';
|
|
ctx.beginPath(); ctx.arc(mx, my, mr * 2.2, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = 'rgba(220,230,200,0.92)';
|
|
ctx.beginPath(); ctx.arc(mx, my, mr, 0, Math.PI * 2); ctx.fill();
|
|
// Bäume
|
|
ctx.fillStyle = 'rgba(5,5,12,0.85)';
|
|
world.decor.trees.forEach(tr => {
|
|
const x = tr.ux * W, h = tr.h * H;
|
|
ctx.beginPath(); ctx.moveTo(x - 8, H);
|
|
ctx.quadraticCurveTo(x + tr.lean * 30, H - h * 0.55, x + tr.lean * 50, H - h);
|
|
ctx.lineTo(x + tr.lean * 50 + 4, H - h * 0.97);
|
|
ctx.lineTo(x + 8, H); ctx.closePath(); ctx.fill();
|
|
});
|
|
// Grabsteine
|
|
world.decor.graves.forEach(g => {
|
|
const x = g.ux * W, w = g.w * W, h = g.h * H;
|
|
ctx.fillStyle = 'rgba(60,60,75,0.85)'; ctx.fillRect(x - w / 2, H - h, w, h);
|
|
ctx.beginPath(); ctx.arc(x, H - h, w / 2, Math.PI, 2 * Math.PI); ctx.fill();
|
|
});
|
|
// Wisps
|
|
world.decor.wisps.forEach(b => {
|
|
const x = b.ux * W + Math.sin(tt + b.ph) * 30;
|
|
const y = ((b.uy - tt * b.sp) % 1 + 1) % 1 * H;
|
|
const a = 0.3 + 0.35 * Math.sin(tt * 2 + b.ph);
|
|
const g2 = ctx.createRadialGradient(x, y, 0, x, y, b.r * 3.5);
|
|
g2.addColorStop(0, `rgba(134,239,172,${a * 0.7})`);
|
|
g2.addColorStop(1, 'rgba(134,239,172,0)');
|
|
ctx.fillStyle = g2;
|
|
ctx.beginPath(); ctx.arc(x, y, b.r * 3.5, 0, Math.PI * 2); ctx.fill();
|
|
});
|
|
}
|
|
else if (theme.id === 'fantasy') {
|
|
// Doppelmond
|
|
ctx.fillStyle = 'rgba(240,171,252,0.85)';
|
|
ctx.beginPath(); ctx.arc(W * 0.78, H * 0.14, Math.min(W, H) * 0.045, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = 'rgba(240,171,252,0.6)';
|
|
ctx.beginPath(); ctx.arc(W * 0.87, H * 0.22, Math.min(W, H) * 0.025, 0, Math.PI * 2); ctx.fill();
|
|
// Wald
|
|
ctx.fillStyle = 'rgba(15,5,30,0.85)';
|
|
world.decor.trees.forEach(tr => {
|
|
const x = tr.ux * W, h = tr.h * H, w = h * 0.45;
|
|
ctx.beginPath(); ctx.moveTo(x - w / 2, H); ctx.lineTo(x, H - h); ctx.lineTo(x + w / 2, H); ctx.closePath(); ctx.fill();
|
|
});
|
|
// Glühwürmchen
|
|
world.decor.flies.forEach(b => {
|
|
const x = b.ux * W + Math.cos(tt * b.sp + b.ph) * 20;
|
|
const y = b.uy * H + Math.sin(tt * b.sp * 1.2 + b.ph) * 12;
|
|
const a = 0.4 + 0.55 * Math.sin(tt * 3 + b.ph);
|
|
const g = ctx.createRadialGradient(x, y, 0, x, y, b.r * 4);
|
|
g.addColorStop(0, `rgba(253,224,71,${a})`);
|
|
g.addColorStop(1, 'rgba(253,224,71,0)');
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath(); ctx.arc(x, y, b.r * 4, 0, Math.PI * 2); ctx.fill();
|
|
});
|
|
// Runen
|
|
world.decor.runes.forEach(r => {
|
|
const x = r.ux * W, y = r.uy * H + Math.sin(tt + r.ph) * 8;
|
|
const rr = r.r * Math.min(W, H);
|
|
ctx.save(); ctx.translate(x, y); ctx.rotate(tt * 0.4 + r.ph);
|
|
ctx.strokeStyle = 'rgba(240,171,252,0.65)'; ctx.lineWidth = 1.5;
|
|
ctx.strokeRect(-rr, -rr, rr * 2, rr * 2);
|
|
ctx.beginPath(); ctx.moveTo(-rr * 0.5, 0); ctx.lineTo(rr * 0.5, 0); ctx.moveTo(0, -rr * 0.5); ctx.lineTo(0, rr * 0.5); ctx.stroke();
|
|
ctx.restore();
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Pfad zeichnen ───────────────────────────────────────────
|
|
function drawPath(ctx, world, theme, state, t) {
|
|
if (world.path.length < 2) return;
|
|
const W = state.W, H = state.H;
|
|
// Outer glow
|
|
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
|
ctx.strokeStyle = theme.pad.glow;
|
|
ctx.lineWidth = 16;
|
|
ctx.beginPath();
|
|
ctx.moveTo(world.path[0].ux * W, world.path[0].uy * H);
|
|
for (let i = 1; i < world.path.length; i++) ctx.lineTo(world.path[i].ux * W, world.path[i].uy * H);
|
|
ctx.stroke();
|
|
// Inner line
|
|
ctx.strokeStyle = theme.primary; ctx.lineWidth = 5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(world.path[0].ux * W, world.path[0].uy * H);
|
|
for (let i = 1; i < world.path.length; i++) ctx.lineTo(world.path[i].ux * W, world.path[i].uy * H);
|
|
ctx.stroke();
|
|
// Animierte fließende Lichtpunkte
|
|
const tt = t * 0.0007;
|
|
for (let d = 0; d < 8; d++) {
|
|
const frac = ((tt + d / 8) % 1 + 1) % 1;
|
|
const idx = Math.min(world.path.length - 1, Math.floor(frac * (world.path.length - 1)));
|
|
const p = world.path[idx];
|
|
ctx.fillStyle = '#fff'; ctx.globalAlpha = 0.55;
|
|
ctx.beginPath(); ctx.arc(p.ux * W, p.uy * H, 2.6, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
// ── Pads zeichnen ───────────────────────────────────────────
|
|
function drawPads(ctx, world, theme, state, t) {
|
|
const W = state.W, H = state.H, R = padR(W, H), tt = t * 0.001;
|
|
const MG_ICONS = { snake: '🐍', flappy: '🐦', memory: '🃏', quiz: '❓', reaction: '⚡', basketball: '🏀', catch: '🍎', maze: '🌀', simon: '🔴', puzzle: '🧩', spotdiff: '🔍', typing: '⌨️' };
|
|
|
|
for (let i = 0; i < world.pads.length; i++) {
|
|
const p = world.pads[i], x = p.ux * W, y = p.uy * H;
|
|
const isStart = i === 0, isEnd = i === world.pads.length - 1;
|
|
const isActive = state.pos === i;
|
|
const isVisited = state.visited && state.visited.has(i);
|
|
const isHover = state.hover === i;
|
|
const gameId = state.fields ? state.fields[i] : null;
|
|
|
|
ctx.save();
|
|
|
|
// Glow ring
|
|
if (isActive) {
|
|
const pulse = 0.6 + 0.4 * Math.sin(tt * 4);
|
|
const g = ctx.createRadialGradient(x, y, R * 0.9, x, y, R * 1.9);
|
|
g.addColorStop(0, theme.pad.glow); g.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath(); ctx.arc(x, y, R * 1.9 * pulse, 0, Math.PI * 2); ctx.fill();
|
|
} else if (isHover) {
|
|
const g = ctx.createRadialGradient(x, y, R * 0.9, x, y, R * 1.6);
|
|
g.addColorStop(0, 'rgba(255,255,255,0.25)'); g.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath(); ctx.arc(x, y, R * 1.6, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
|
|
// Pad fill
|
|
const grd = ctx.createRadialGradient(x - R * 0.35, y - R * 0.35, 0, x, y, R);
|
|
if (isStart) { grd.addColorStop(0, '#6ee7b7'); grd.addColorStop(1, '#065f46'); }
|
|
else if (isEnd) { grd.addColorStop(0, '#fde68a'); grd.addColorStop(1, '#78350f'); }
|
|
else if (gameId) { grd.addColorStop(0, theme.primary); grd.addColorStop(1, theme.pad.edge); }
|
|
else { grd.addColorStop(0, theme.pad.fill); grd.addColorStop(1, theme.pad.edge); }
|
|
ctx.fillStyle = grd;
|
|
|
|
if (theme.id === 'haunted' && !isStart && !isEnd) {
|
|
// Grabstein-Form
|
|
ctx.beginPath();
|
|
ctx.moveTo(x - R * 0.85, y + R * 0.95);
|
|
ctx.lineTo(x - R * 0.85, y - R * 0.35);
|
|
ctx.quadraticCurveTo(x - R * 0.85, y - R * 1.0, x, y - R * 1.0);
|
|
ctx.quadraticCurveTo(x + R * 0.85, y - R * 1.0, x + R * 0.85, y - R * 0.35);
|
|
ctx.lineTo(x + R * 0.85, y + R * 0.95);
|
|
ctx.closePath(); ctx.fill();
|
|
} else {
|
|
ctx.beginPath(); ctx.arc(x, y, R, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
|
|
// Rim
|
|
ctx.strokeStyle = isActive ? '#fff' : 'rgba(255,255,255,0.35)';
|
|
ctx.lineWidth = isActive ? 2.5 : 1.2;
|
|
ctx.beginPath(); ctx.arc(x, y, R, 0, Math.PI * 2); ctx.stroke();
|
|
|
|
// Glas-Highlight
|
|
ctx.beginPath();
|
|
ctx.arc(x - R * 0.32, y - R * 0.32, R * 0.45, Math.PI * 1.05, Math.PI * 1.9);
|
|
ctx.lineWidth = 2.5; ctx.strokeStyle = 'rgba(255,255,255,0.35)';
|
|
ctx.stroke();
|
|
|
|
ctx.restore();
|
|
|
|
// Icon + Nummer
|
|
const icon = isStart ? '▶' : isEnd ? '🏁' : gameId ? (MG_ICONS[gameId] || '🎮') : '';
|
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
if (icon) {
|
|
ctx.font = `bold ${Math.round(R * 0.9)}px ${gameId ? 'serif' : 'Nunito,sans-serif'}`;
|
|
ctx.fillStyle = '#fff';
|
|
ctx.fillText(icon, x, y);
|
|
} else if (!isStart && !isEnd) {
|
|
ctx.font = `bold ${Math.round(R * 0.7)}px Nunito,sans-serif`;
|
|
ctx.fillStyle = 'rgba(255,255,255,0.85)';
|
|
ctx.fillText(String(i), x, y);
|
|
}
|
|
|
|
// Visited check
|
|
if (isVisited && !isActive && i > 0) {
|
|
ctx.font = `bold ${Math.round(R * 0.5)}px Nunito,sans-serif`;
|
|
ctx.fillStyle = '#86efac';
|
|
ctx.fillText('✓', x + R * 0.78, y - R * 0.78);
|
|
}
|
|
// Story-Badge
|
|
const hasStory = state.storyItems && state.storyItems.some(s => s.fieldIndex === i);
|
|
if (hasStory) {
|
|
ctx.font = `${Math.round(R * 0.5)}px serif`;
|
|
ctx.fillText('📖', x - R * 0.8, y - R * 0.8);
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawFigure(ctx, world, theme, state, t) {
|
|
const W = state.W, H = state.H, R = padR(W, H);
|
|
// Neue Multi-Figuren-API: state.figures [{emoji,x,y,active,dimmed}]
|
|
// Backwards-Compat: alte Felder state.figEmoji/figX/figY → in 1-Element-Array umwandeln
|
|
let figs = state.figures;
|
|
if (!figs || !figs.length) {
|
|
if (state.figEmoji == null || state.figX == null) return;
|
|
figs = [{ emoji: state.figEmoji, x: state.figX, y: state.figY, active: true, dimmed: false }];
|
|
}
|
|
// Same-Pad-Offset: wenn zwei Figuren ~auf gleicher Pixel-Position sind, leicht versetzen
|
|
const fsBase = R * 1.5;
|
|
const pts = figs.map(f => ({ ...f, dx: 0, dy: 0 }));
|
|
if (pts.length >= 2) {
|
|
for (let i = 0; i < pts.length; i++) {
|
|
for (let j = i+1; j < pts.length; j++) {
|
|
const dx = pts[j].x - pts[i].x, dy = pts[j].y - pts[i].y;
|
|
if (dx*dx + dy*dy < (R*0.6)*(R*0.6)) {
|
|
pts[i].dx -= R * 0.55; pts[j].dx += R * 0.55;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Inaktive zuerst zeichnen (damit aktiver oben liegt)
|
|
pts.sort((a, b) => (a.active === b.active ? 0 : (a.active ? 1 : -1)));
|
|
pts.forEach(p => {
|
|
const fx = p.x + p.dx, fy = p.y + p.dy;
|
|
const active = !!p.active && !p.dimmed;
|
|
const fs = active ? fsBase : fsBase * 0.88;
|
|
const bounce = active ? Math.sin(t * 0.005) * 3 : 0;
|
|
// Aura
|
|
const g = ctx.createRadialGradient(fx, fy, 0, fx, fy, fs * 1.1);
|
|
g.addColorStop(0, p.dimmed ? 'rgba(100,100,100,0.25)' : theme.pad.glow); g.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath(); ctx.arc(fx, fy, fs * 1.1, 0, Math.PI * 2); ctx.fill();
|
|
// Emoji
|
|
ctx.font = `${Math.round(fs)}px serif`;
|
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
if (active) { ctx.shadowColor = theme.primary; ctx.shadowBlur = 15; }
|
|
ctx.globalAlpha = p.dimmed ? 0.45 : 1;
|
|
ctx.fillText(p.emoji || '🎮', fx, fy - R * 0.05 + bounce);
|
|
ctx.shadowBlur = 0; ctx.globalAlpha = 1;
|
|
});
|
|
}
|
|
|
|
function drawHeader(ctx, state) {
|
|
if (!state.gameName) return;
|
|
const W = state.W, H = state.H;
|
|
ctx.textAlign = 'center';
|
|
ctx.font = `bold ${Math.round(Math.min(W, H) * 0.038)}px 'Fredoka One',cursive`;
|
|
ctx.fillStyle = 'rgba(255,255,255,0.95)';
|
|
ctx.shadowColor = 'rgba(0,0,0,0.8)'; ctx.shadowBlur = 14;
|
|
ctx.fillText(String(state.gameName).toUpperCase(), W / 2, H * 0.055);
|
|
if (state.devName) {
|
|
ctx.font = `${Math.round(Math.min(W, H) * 0.018)}px Nunito,sans-serif`;
|
|
ctx.fillStyle = 'rgba(255,255,255,0.65)';
|
|
ctx.fillText('von ' + state.devName, W / 2, H * 0.085);
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
// ── Vordergrund: passierende Bewohner (Schwärme, Wal, Komet, Drache, Fledermaus, Blitz) ──
|
|
function drawFish(ctx, x, y, s, color) {
|
|
ctx.save(); ctx.translate(x, y); ctx.fillStyle = color;
|
|
ctx.beginPath(); ctx.ellipse(0, 0, s, s * 0.55, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.beginPath(); ctx.moveTo(-s * 0.7, 0); ctx.lineTo(-s * 1.5, -s * 0.6); ctx.lineTo(-s * 1.5, s * 0.6); ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(s * 0.4, -s * 0.1, s * 0.16, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(s * 0.42, -s * 0.1, s * 0.08, 0, Math.PI * 2); ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
function drawFishLayer(ctx, tt, W, H, color, count, yFrac, speed, size, bob) {
|
|
const cycle = W + 240;
|
|
for (let i = 0; i < count; i++) {
|
|
const x = ((tt * speed * 60 + i * (cycle / count)) % cycle) - 120;
|
|
const y = yFrac * H + Math.sin(tt * 1.4 + i * 0.7) * bob;
|
|
drawFish(ctx, x, y, size, color);
|
|
}
|
|
}
|
|
function drawWhale(ctx, W, H, c) {
|
|
const x = -260 + c * (W + 520);
|
|
const y = H * 0.42 + Math.sin(c * Math.PI * 2) * 22;
|
|
ctx.save(); ctx.translate(x, y); ctx.fillStyle = 'rgba(20,40,68,0.55)';
|
|
ctx.beginPath(); ctx.ellipse(0, 0, 110, 36, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.beginPath(); ctx.moveTo(-100, 0); ctx.lineTo(-140, -30); ctx.lineTo(-140, 30); ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.beginPath(); ctx.arc(55, -6, 4, 0, Math.PI * 2); ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
function drawShootingStar(ctx, W, H, c, idx) {
|
|
// Position bewegt sich von oben-rechts nach unten-links → Kopf führt, Schweif trailt
|
|
const sx = W * (1.15 - c * 1.4);
|
|
const sy = H * (0.08 + idx * 0.08 + c * 0.55);
|
|
const len = 70 + 50 * Math.sin(c * Math.PI);
|
|
ctx.save(); ctx.translate(sx, sy); ctx.rotate(-0.45 - idx * 0.12);
|
|
// Kopf (hell) am Ursprung = aktuelle Position, Schweif erstreckt sich in +x = entgegen Flugrichtung
|
|
const g = ctx.createLinearGradient(0, 0, len, 0);
|
|
g.addColorStop(0, 'rgba(255,255,255,0.95)');
|
|
g.addColorStop(1, 'rgba(255,255,255,0)');
|
|
ctx.strokeStyle = g; ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(len, 0); ctx.stroke();
|
|
ctx.fillStyle = '#fff'; ctx.shadowColor = '#fff'; ctx.shadowBlur = 8;
|
|
ctx.beginPath(); ctx.arc(0, 0, 3, 0, Math.PI * 2); ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawUFO(ctx, W, H, c, theme) {
|
|
// Sanft auf-und-ab fliegend, von links nach rechts
|
|
const x = -60 + c * (W + 120);
|
|
const y = H * 0.28 + Math.sin(c * Math.PI * 5) * 18;
|
|
ctx.save(); ctx.translate(x, y);
|
|
// Beam unter dem UFO
|
|
const beamA = 0.10 + 0.10 * Math.sin(c * Math.PI * 12);
|
|
ctx.fillStyle = `rgba(160,255,255,${beamA})`;
|
|
ctx.beginPath(); ctx.moveTo(-6, 5); ctx.lineTo(-20, 38); ctx.lineTo(20, 38); ctx.lineTo(6, 5); ctx.closePath(); ctx.fill();
|
|
// Untertasse
|
|
const dish = ctx.createLinearGradient(0, -2, 0, 8);
|
|
dish.addColorStop(0, '#666c80'); dish.addColorStop(1, '#1f2433');
|
|
ctx.fillStyle = dish;
|
|
ctx.beginPath(); ctx.ellipse(0, 2, 26, 8, 0, 0, Math.PI * 2); ctx.fill();
|
|
// Glas-Dome
|
|
const dome = ctx.createRadialGradient(-3, -7, 1, 0, -3, 14);
|
|
dome.addColorStop(0, 'rgba(180,235,255,0.95)'); dome.addColorStop(1, 'rgba(80,150,200,0.7)');
|
|
ctx.fillStyle = dome;
|
|
ctx.beginPath(); ctx.ellipse(0, -3, 13, 9, 0, Math.PI, 2 * Math.PI); ctx.fill();
|
|
// Highlight im Dome
|
|
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
|
ctx.beginPath(); ctx.ellipse(-4, -6, 3, 2, 0, 0, Math.PI * 2); ctx.fill();
|
|
// Blinkende Lichter unten
|
|
for (let i = -2; i <= 2; i++) {
|
|
const on = Math.sin(c * Math.PI * 22 + i * 1.2) > 0;
|
|
ctx.fillStyle = on ? '#ffd166' : 'rgba(255,209,102,0.35)';
|
|
ctx.shadowColor = '#ffd166'; ctx.shadowBlur = on ? 6 : 0;
|
|
ctx.beginPath(); ctx.arc(i * 7, 8, 1.8, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
ctx.shadowBlur = 0;
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawGhost(ctx, W, H, c, tt) {
|
|
// Schwebt in Wellenform durchs Bild
|
|
const x = -50 + c * (W + 100);
|
|
const y = H * 0.32 + Math.sin(c * Math.PI * 3) * 30;
|
|
ctx.save(); ctx.translate(x, y);
|
|
ctx.shadowColor = 'rgba(220,230,245,0.7)'; ctx.shadowBlur = 22;
|
|
ctx.fillStyle = 'rgba(230,235,245,0.88)';
|
|
// Körper: runde Oberseite, geschwungene Unterkante
|
|
ctx.beginPath();
|
|
ctx.arc(0, -8, 22, Math.PI, 2 * Math.PI);
|
|
ctx.lineTo(22, 16);
|
|
for (let i = 4; i >= -4; i--) {
|
|
const wx = (i / 4) * 22;
|
|
const wy = 16 + Math.sin(tt * 4 + i * 0.7 + c * 6) * 5;
|
|
ctx.lineTo(wx, wy);
|
|
}
|
|
ctx.lineTo(-22, 16);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
// Augen
|
|
ctx.fillStyle = '#0c0c14';
|
|
ctx.beginPath(); ctx.ellipse(-7, -8, 3, 4, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.beginPath(); ctx.ellipse(7, -8, 3, 4, 0, 0, Math.PI * 2); ctx.fill();
|
|
// Mund (Oval)
|
|
ctx.beginPath(); ctx.ellipse(0, 2, 3.5, 5, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
function drawComet(ctx, W, H, c, theme) {
|
|
const x = W * (1.1 - c * 1.3), y = H * (0.12 + c * 0.5);
|
|
ctx.save(); ctx.translate(x, y);
|
|
const g = ctx.createLinearGradient(0, 0, 70, 0);
|
|
g.addColorStop(0, theme.primary); g.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath(); ctx.moveTo(0, -7); ctx.lineTo(70, 0); ctx.lineTo(0, 7); ctx.closePath(); ctx.fill();
|
|
ctx.shadowColor = theme.primary; ctx.shadowBlur = 14;
|
|
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI * 2); ctx.fill();
|
|
ctx.shadowBlur = 0; ctx.restore();
|
|
}
|
|
function drawBat(ctx, W, H, c, idx, tt) {
|
|
const x = c * (W + 120) - 60;
|
|
const y = H * (0.18 + idx * 0.08) + Math.sin(tt * 5 + idx) * 14;
|
|
const flap = Math.sin(tt * 14) * 0.4 + 0.7;
|
|
ctx.save(); ctx.translate(x, y); ctx.fillStyle = 'rgba(10,10,18,0.92)';
|
|
ctx.beginPath(); ctx.ellipse(0, 0, 5, 4, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(-12, -9 * flap, -20, -2); ctx.quadraticCurveTo(-10, 4, 0, 2); ctx.fill();
|
|
ctx.beginPath(); ctx.moveTo(0, 0); ctx.quadraticCurveTo(12, -9 * flap, 20, -2); ctx.quadraticCurveTo(10, 4, 0, 2); ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
function drawMist(ctx, W, H, tt) {
|
|
for (let i = 0; i < 3; i++) {
|
|
const x = ((tt * (10 + i * 6) + i * 240) % (W + 480)) - 240;
|
|
const y = H - 50 - i * 10;
|
|
const g = ctx.createRadialGradient(x, y, 0, x, y, 220);
|
|
g.addColorStop(0, 'rgba(200,200,225,0.15)'); g.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, 220, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
}
|
|
function drawLeaves(ctx, W, H, tt) {
|
|
for (let i = 0; i < 7; i++) {
|
|
const x = ((tt * 22 + i * 240) % (W + 200)) - 100;
|
|
const y = ((tt * 14 + i * 130) % (H + 80)) - 40;
|
|
const rot = tt * 1.4 + i;
|
|
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
|
|
ctx.fillStyle = `hsla(${280 + i * 8},65%,55%,0.55)`;
|
|
ctx.beginPath(); ctx.ellipse(0, 0, 7, 3.5, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
}
|
|
function drawDragon(ctx, W, H, c, theme) {
|
|
const x = -140 + c * (W + 320);
|
|
const y = H * 0.16 + Math.sin(c * Math.PI * 3) * 30;
|
|
ctx.save(); ctx.translate(x, y);
|
|
ctx.shadowColor = theme.primary; ctx.shadowBlur = 18;
|
|
ctx.fillStyle = 'rgba(30,10,40,0.92)';
|
|
// S-Body
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, 0); ctx.quadraticCurveTo(-30, -16, -60, 0);
|
|
ctx.quadraticCurveTo(-90, 16, -120, 4); ctx.lineTo(-120, 12);
|
|
ctx.quadraticCurveTo(-90, 24, -60, 14); ctx.quadraticCurveTo(-30, 10, 0, 14);
|
|
ctx.closePath(); ctx.fill();
|
|
// Kopf
|
|
ctx.beginPath(); ctx.ellipse(10, 8, 13, 9, 0, 0, Math.PI * 2); ctx.fill();
|
|
// Flügel
|
|
const wing = Math.sin(c * Math.PI * 16) * 14;
|
|
ctx.beginPath(); ctx.moveTo(-22, 6); ctx.quadraticCurveTo(-42, -22 - wing, -60, -10); ctx.lineTo(-30, 4); ctx.fill();
|
|
ctx.beginPath(); ctx.moveTo(-50, 6); ctx.quadraticCurveTo(-72, -28 - wing, -86, -8); ctx.lineTo(-58, 4); ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
ctx.fillStyle = theme.primary;
|
|
ctx.beginPath(); ctx.arc(17, 6, 2.4, 0, Math.PI * 2); ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawForeground(ctx, world, theme, state, t) {
|
|
const W = state.W, H = state.H, tt = t * 0.001;
|
|
if (theme.id === 'underwater') {
|
|
drawFishLayer(ctx, tt, W, H, 'rgba(252,211,77,0.85)', 6, 0.22, 0.5, 11, 7);
|
|
drawFishLayer(ctx, tt, W, H, 'rgba(125,211,252,0.7)', 5, 0.42, 0.7, 9, 5);
|
|
drawFishLayer(ctx, tt, W, H, 'rgba(74,222,128,0.7)', 7, 0.62, 0.4, 8, 6);
|
|
const wc = (tt % 22) / 14; if (wc <= 1) drawWhale(ctx, W, H, wc);
|
|
}
|
|
else if (theme.id === 'space') {
|
|
for (let i = 0; i < 3; i++) {
|
|
const c = ((tt + i * 4.3) % 11) / 3.5;
|
|
if (c <= 1) drawShootingStar(ctx, W, H, c, i);
|
|
}
|
|
const cc = (tt % 28) / 18; if (cc <= 1) drawComet(ctx, W, H, cc, theme);
|
|
// UFO sporadisch (alle ~36 s, fliegt ~14 s)
|
|
const uc = (tt % 36) / 14; if (uc <= 1) drawUFO(ctx, W, H, uc, theme);
|
|
}
|
|
else if (theme.id === 'haunted') {
|
|
// Blitz-Flicker: kurz, alle ~9 s
|
|
const flickerPhase = (tt * 0.5) % 9;
|
|
if (flickerPhase < 0.15) {
|
|
ctx.fillStyle = `rgba(200,210,230,${(0.15 - flickerPhase) * 1.8})`;
|
|
ctx.fillRect(0, 0, W, H);
|
|
}
|
|
drawMist(ctx, W, H, tt);
|
|
for (let i = 0; i < 2; i++) {
|
|
const c = ((tt + i * 6.5) % 12) / 7.5;
|
|
if (c <= 1) drawBat(ctx, W, H, c, i, tt);
|
|
}
|
|
// Gespenst sporadisch (alle ~28 s, schwebt ~16 s durchs Bild)
|
|
const gc = (tt % 28) / 16; if (gc <= 1) drawGhost(ctx, W, H, gc, tt);
|
|
}
|
|
else if (theme.id === 'fantasy') {
|
|
drawLeaves(ctx, W, H, tt);
|
|
// Drache sporadisch (alle ~38 s, fliegt ~17 s) — wirkt seltener/zufälliger
|
|
const dc = (tt % 38) / 17; if (dc <= 1) drawDragon(ctx, W, H, dc, theme);
|
|
}
|
|
}
|
|
|
|
// ── render() ────────────────────────────────────────────────
|
|
function render(ctx, world, state, t) {
|
|
const W = state.W, H = state.H;
|
|
const theme = THEMES[world.theme];
|
|
// Background gradient
|
|
const bg = ctx.createLinearGradient(0, 0, 0, H);
|
|
bg.addColorStop(0, theme.bgGrad[0]); bg.addColorStop(0.55, theme.bgGrad[1]); bg.addColorStop(1, theme.bgGrad[2]);
|
|
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
|
|
// Atmosphäre
|
|
drawBgDecor(ctx, world, theme, state, t);
|
|
// Pfad + Pads + Vordergrund-Bewohner + Figur + Header
|
|
drawPath(ctx, world, theme, state, t);
|
|
drawPads(ctx, world, theme, state, t);
|
|
drawForeground(ctx, world, theme, state, t);
|
|
drawFigure(ctx, world, theme, state, t);
|
|
drawHeader(ctx, state);
|
|
}
|
|
|
|
return { THEMES, generate, render, hitTestPad, padCenter, resolveTheme, randomSeed, hashSeed };
|
|
|
|
})();
|