457 lines
17 KiB
JavaScript
457 lines
17 KiB
JavaScript
/**
|
|
* minigames/flappy2p.js
|
|
* 🐦🐦 Flappy-Duell — 2-Spieler "Best of 2-5", basierend auf flappy.js
|
|
* 🔵 P1 = ↑ Pfeil · 🔴 P2 = W. Beide fliegen durch dieselben Röhren.
|
|
* Stirbt einer (Wand/Röhre/Boden), gibt es einen Punkt für den anderen → neue Runde.
|
|
* Spielfiguren kollidieren NICHT miteinander.
|
|
*/
|
|
window.MG_flappy2p = (function () {
|
|
|
|
const ID = 'flappy2p';
|
|
const EMOJI = '🐦🐦';
|
|
const NAME = 'Flappy-Duell';
|
|
const DESC = 'Best of 2-5 · 🔵 ↑ Pfeil oben gegen 🔴 W. Wer abstürzt, gibt einen Punkt ab.';
|
|
const CONTROLS = '↑ / W';
|
|
const MULTI = 1;
|
|
const REQUIRES = '2p';
|
|
|
|
const lerp = (a, b, t) => a + (b - a) * t;
|
|
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
|
const hx = (h, i) => parseInt(h.replace('#', '').substr(i, 2), 16);
|
|
function shade(hex, amt) {
|
|
const r = clamp(hx(hex, 0) + amt, 0, 255), g = clamp(hx(hex, 2) + amt, 0, 255), b = clamp(hx(hex, 4) + amt, 0, 255);
|
|
return '#' + [r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
const P1_COL = '#3b82f6', P1_LIGHT = '#93c5fd';
|
|
const P2_COL = '#ef4444', P2_LIGHT = '#fca5a5';
|
|
|
|
function run(wrap, W, H, cfg, onDone) {
|
|
const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H);
|
|
|
|
// ── Einstellungen (vom mgSettings durchgereicht) ──
|
|
const BEST_OF = clamp(parseInt(cfg.bestOf) || 3, 2, 5); // 2..5
|
|
const TARGET = Math.ceil(BEST_OF / 2); // Runden bis Sieg
|
|
const BASE_SPEED = (cfg && cfg.speed) || 2.7;
|
|
const SPEED_INC = (cfg && cfg.rampSpeed === false) ? 0 : 0.07;
|
|
const BASE_GAP = (cfg && cfg.gap) || 168;
|
|
const GAP_DEC = (cfg && cfg.shrinkGap === false) ? 0 : 1.6;
|
|
const MIN_GAP = Math.max(90, BASE_GAP - 36);
|
|
const minTop = 36;
|
|
|
|
// ── Physik (pro festem 1/60-Schritt) ──
|
|
const STEP = 1000 / 60;
|
|
const GRAV = 0.42, FLAP = -7.2, PW = 46;
|
|
const GROUND_H = Math.min(54, Math.round(H * 0.13));
|
|
const PLAY_H = H - GROUND_H;
|
|
const BIRD_X = Math.round(W * 0.28);
|
|
const BR = 16;
|
|
|
|
// ── Theme-Hintergrund ──
|
|
let accent = (cfg && cfg.theme && cfg.theme.primary) || '#5ec5b8';
|
|
if (!/^#[0-9a-f]{6}$/i.test(accent)) accent = '#5ec5b8';
|
|
const accentLight = shade(accent, 70);
|
|
const accentDeep = shade(accent, -120);
|
|
const fig1Emoji = (cfg && cfg.figure) || '🐦';
|
|
const fig2Emoji = (cfg && cfg.figure2) || '🐤';
|
|
|
|
// ── Match-State ──
|
|
const scores = [0, 0];
|
|
let roundIdx = 0;
|
|
let state = 'intro'; // intro | playing | roundEnd | duelEnd
|
|
let stateTimer = 0;
|
|
let roundResult = null;
|
|
let startBtnRect = null;
|
|
let stopped = false, raf = null;
|
|
let lastTime = 0, acc = 0;
|
|
|
|
// Pre-rendered Glow-Sprites pro Spieler (einmalig)
|
|
const SPR = 72, dpr = window.devicePixelRatio || 1;
|
|
function makeSprite(emoji, glow) {
|
|
const s = document.createElement('canvas');
|
|
s.width = SPR * dpr; s.height = SPR * dpr;
|
|
const sx = s.getContext('2d'); sx.scale(dpr, dpr);
|
|
sx.textAlign = 'center'; sx.textBaseline = 'middle';
|
|
sx.font = `${Math.round(SPR * 0.55)}px serif`;
|
|
sx.shadowColor = glow; sx.shadowBlur = 18; sx.fillText(emoji, SPR/2, SPR/2);
|
|
sx.shadowBlur = 10; sx.fillText(emoji, SPR/2, SPR/2);
|
|
return s;
|
|
}
|
|
const sprP1 = makeSprite(fig1Emoji, P1_LIGHT);
|
|
const sprP2 = makeSprite(fig2Emoji, P2_LIGHT);
|
|
|
|
// Sky einmal vorrendern
|
|
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
|
|
skyGrad.addColorStop(0, '#080810');
|
|
skyGrad.addColorStop(0.6, shade(accentDeep, -40));
|
|
skyGrad.addColorStop(1, accentDeep);
|
|
|
|
// Deterministische Sterne im Hintergrund
|
|
const stars = [];
|
|
for (let i = 0; i < 34; i++)
|
|
stars.push({ x: Math.random()*W, y: Math.random()*PLAY_H, r: Math.random()<0.3?2:1, a: 0.3+Math.random()*0.5, p: Math.random()*6.28 });
|
|
|
|
// ── Runden-State ──
|
|
let birds, pipes, score, speed, worldScroll, pWorldScroll, simTime, shake, flash, puffs;
|
|
|
|
function curGAP(sc) { return Math.max(MIN_GAP, BASE_GAP - sc * GAP_DEC); }
|
|
function rndGapY(sc) {
|
|
const g = curGAP(sc), maxTop = PLAY_H - g - 40;
|
|
return minTop + Math.random() * Math.max(10, maxTop - minTop);
|
|
}
|
|
function spawnPipe() { pipes.push({ x: W + PW, px: W + PW, gap: rndGapY(score), g: curGAP(score), passed: false }); }
|
|
|
|
function makeBird(yFrac) {
|
|
const y0 = PLAY_H * yFrac;
|
|
return { y: y0, py: y0, vy: 0, angle: 0, alive: true, flapAnim: 0, trail: [] };
|
|
}
|
|
|
|
function newRound() {
|
|
birds = [ makeBird(0.4), makeBird(0.6) ];
|
|
pipes = [];
|
|
spawnPipe();
|
|
score = 0;
|
|
speed = BASE_SPEED;
|
|
worldScroll = 0; pWorldScroll = 0;
|
|
simTime = 0; shake = 0; flash = 0;
|
|
puffs = [];
|
|
acc = 0;
|
|
lastTime = 0;
|
|
state = 'playing';
|
|
stateTimer = 0;
|
|
roundResult = null;
|
|
}
|
|
|
|
function endRound(winnerIdx) {
|
|
if (winnerIdx === 0 || winnerIdx === 1) scores[winnerIdx]++;
|
|
roundResult = winnerIdx;
|
|
shake = 14; flash = 0.5;
|
|
state = 'roundEnd';
|
|
stateTimer = 0;
|
|
}
|
|
|
|
function advanceRound() {
|
|
roundIdx++;
|
|
if (scores[0] >= TARGET || scores[1] >= TARGET || roundIdx >= BEST_OF) {
|
|
state = 'duelEnd'; stateTimer = 0;
|
|
} else {
|
|
newRound();
|
|
}
|
|
}
|
|
|
|
function finalWinner() {
|
|
if (scores[0] > scores[1]) return 0;
|
|
if (scores[1] > scores[0]) return 1;
|
|
return -1;
|
|
}
|
|
|
|
function finishUp() {
|
|
if (stopped) return;
|
|
stopped = true;
|
|
cancelAnimationFrame(raf);
|
|
document.removeEventListener('keydown', onKey);
|
|
canvas.removeEventListener('mousedown', onCanvasDown);
|
|
canvas.removeEventListener('touchstart', onCanvasTouch);
|
|
onDone(finalWinner());
|
|
}
|
|
|
|
// ── Eingabe ──
|
|
function flap(i) {
|
|
if (state !== 'playing') return;
|
|
const b = birds[i]; if (!b || !b.alive) return;
|
|
b.vy = FLAP; b.flapAnim = 1;
|
|
for (let k = 0; k < 3; k++)
|
|
puffs.push({ x: BIRD_X - BR + (Math.random()-0.5)*6, y: b.y + (Math.random()-0.5)*14, r: 2, a: 0.6, col: i===0 ? P1_LIGHT : P2_LIGHT });
|
|
}
|
|
|
|
function onKey(e) {
|
|
if (state === 'intro') return; // Start nur per Klick
|
|
if (state !== 'playing') return;
|
|
const k = e.key;
|
|
if (k === 'ArrowUp') { flap(0); e.preventDefault(); }
|
|
else if (k === 'w' || k === 'W') { flap(1); e.preventDefault(); }
|
|
}
|
|
|
|
function canvasCoords(clientX, clientY) {
|
|
const r = canvas.getBoundingClientRect();
|
|
const d = window.devicePixelRatio || 1;
|
|
const x = (clientX - r.left) * (canvas.width / r.width / d);
|
|
const y = (clientY - r.top) * (canvas.height / r.height / d);
|
|
return { x, y };
|
|
}
|
|
function pointInBtn(x, y) {
|
|
return startBtnRect && x >= startBtnRect.x && x <= startBtnRect.x + startBtnRect.w
|
|
&& y >= startBtnRect.y && y <= startBtnRect.y + startBtnRect.h;
|
|
}
|
|
function onCanvasDown(e) {
|
|
const p = canvasCoords(e.clientX, e.clientY);
|
|
if (state === 'intro') {
|
|
if (pointInBtn(p.x, p.y)) newRound();
|
|
return;
|
|
}
|
|
if (state === 'playing') {
|
|
// Tap-Steuerung als Bonus (für Touch/Tablets): linke Hälfte = P2, rechte Hälfte = P1
|
|
flap(p.x < W / 2 ? 1 : 0);
|
|
}
|
|
}
|
|
function onCanvasTouch(e) {
|
|
const t = e.changedTouches && e.changedTouches[0];
|
|
if (!t) return;
|
|
const p = canvasCoords(t.clientX, t.clientY);
|
|
if (state === 'intro') {
|
|
if (pointInBtn(p.x, p.y)) { e.preventDefault(); newRound(); }
|
|
return;
|
|
}
|
|
if (state === 'playing') {
|
|
e.preventDefault();
|
|
flap(p.x < W / 2 ? 1 : 0);
|
|
}
|
|
}
|
|
document.addEventListener('keydown', onKey);
|
|
canvas.addEventListener('mousedown', onCanvasDown);
|
|
canvas.addEventListener('touchstart', onCanvasTouch, { passive: false });
|
|
|
|
function hitPipe(b, p) {
|
|
const r = BR - 4;
|
|
if (BIRD_X + r > p.x && BIRD_X - r < p.x + PW)
|
|
return b.y - r < p.gap || b.y + r > p.gap + p.g;
|
|
return false;
|
|
}
|
|
|
|
// ── Physik-Schritt (1/60) ──
|
|
function step() {
|
|
pWorldScroll = worldScroll;
|
|
simTime += STEP;
|
|
if (shake > 0) shake = Math.max(0, shake - 0.7);
|
|
if (flash > 0) flash = Math.max(0, flash - 0.04);
|
|
|
|
// Birds bewegen
|
|
for (const b of birds) {
|
|
if (!b.alive) continue;
|
|
b.py = b.y;
|
|
b.vy += GRAV;
|
|
b.y += b.vy;
|
|
b.angle = lerp(b.angle, clamp(b.vy * 0.06, -0.5, 1.4), 0.18);
|
|
b.flapAnim = Math.max(0, b.flapAnim - 0.08);
|
|
b.trail.unshift(b.y); if (b.trail.length > 8) b.trail.pop();
|
|
}
|
|
// Puff-Partikel
|
|
puffs.forEach(p => { p.r += 1.1; p.a -= 0.06; p.x -= speed; });
|
|
puffs = puffs.filter(p => p.a > 0);
|
|
|
|
// Röhren scrollen
|
|
worldScroll += speed;
|
|
pipes.forEach(p => { p.px = p.x; p.x -= speed; });
|
|
if (!pipes.length || pipes[pipes.length - 1].x < W - 210) spawnPipe();
|
|
pipes = pipes.filter(p => p.x > -PW - 10);
|
|
pipes.forEach(p => {
|
|
if (!p.passed && p.x + PW < BIRD_X) {
|
|
p.passed = true; score++;
|
|
speed = BASE_SPEED + score * SPEED_INC;
|
|
}
|
|
});
|
|
|
|
// Kollisionen pro Vogel (Vögel untereinander NICHT)
|
|
for (const b of birds) {
|
|
if (!b.alive) continue;
|
|
if (b.y + (BR - 4) > PLAY_H) { b.y = PLAY_H - (BR - 4); b.alive = false; continue; }
|
|
if (b.y - (BR - 4) < 0) { b.y = BR - 4; b.alive = false; continue; }
|
|
for (const p of pipes) { if (hitPipe(b, p)) { b.alive = false; break; } }
|
|
}
|
|
|
|
// Round-Ende?
|
|
if (!birds[0].alive || !birds[1].alive) {
|
|
let winner;
|
|
if (!birds[0].alive && !birds[1].alive) winner = -1;
|
|
else if (!birds[0].alive) winner = 1;
|
|
else winner = 0;
|
|
endRound(winner);
|
|
}
|
|
}
|
|
|
|
// ── Render ──
|
|
function drawPipe(x, p) {
|
|
const topH = p.gap, botY = p.gap + p.g, glow = `${accent}28`;
|
|
const body = (yy, hh) => {
|
|
if (hh <= 0) return;
|
|
ctx.fillStyle = glow; ctx.fillRect(x - 6, yy, PW + 12, hh);
|
|
MGAPI.roundRect(ctx, x, yy, PW, hh, 6, 'rgba(10,12,24,0.80)', null);
|
|
ctx.fillStyle = accent; ctx.fillRect(x + 3, yy, 3, hh); ctx.fillRect(x + PW - 6, yy, 3, hh);
|
|
ctx.fillStyle = accentLight; ctx.fillRect(x + PW / 2 - 1, yy, 2, hh);
|
|
};
|
|
const cap = yy => {
|
|
ctx.fillStyle = glow; ctx.fillRect(x - 9, yy - 2, PW + 18, 20);
|
|
MGAPI.roundRect(ctx, x - 5, yy, PW + 10, 16, 5, accent, null);
|
|
ctx.fillStyle = accentLight; ctx.fillRect(x - 2, yy + 3, PW + 4, 3);
|
|
};
|
|
body(0, topH - 18); body(botY + 18, PLAY_H - botY - 18);
|
|
cap(topH - 18); cap(botY + 2);
|
|
}
|
|
|
|
function drawFloor(scroll) {
|
|
const gy = PLAY_H;
|
|
ctx.fillStyle = `${accent}33`; ctx.fillRect(0, gy - 3, W, 6);
|
|
ctx.fillStyle = accentLight; ctx.fillRect(0, gy - 1, W, 2);
|
|
ctx.fillStyle = '#070710'; ctx.fillRect(0, gy, W, GROUND_H);
|
|
ctx.fillStyle = `${accent}2e`;
|
|
const tile = 34, off = -(((scroll % tile) + tile) % tile);
|
|
for (let x = off; x < W; x += tile) ctx.fillRect(x, gy + 6, 16, 2);
|
|
}
|
|
|
|
function drawStartButton() {
|
|
const bw = Math.min(320, W * 0.62), bh = 56;
|
|
const bx = (W - bw) / 2, by = H / 2 + 10;
|
|
startBtnRect = { x: bx, y: by, w: bw, h: bh };
|
|
ctx.fillStyle = 'rgba(0,0,0,0.45)';
|
|
MGAPI.roundRect(ctx, bx, by + 4, bw, bh, 14, 'rgba(0,0,0,0.45)', null);
|
|
const grd = ctx.createLinearGradient(0, by, 0, by + bh);
|
|
grd.addColorStop(0, '#7c3aed'); grd.addColorStop(1, '#5b21b6');
|
|
MGAPI.roundRect(ctx, bx, by, bw, bh, 14, grd, null);
|
|
MGAPI.roundRect(ctx, bx + 4, by + 3, bw - 8, bh / 2 - 4, 12, 'rgba(255,255,255,0.18)', null);
|
|
MGAPI.text(ctx, '▶ Duell starten', bx + bw / 2, by + bh / 2, {
|
|
size: 22, color: '#fff', weight: 'bold', family: "'Fredoka One',cursive"
|
|
});
|
|
}
|
|
|
|
function drawOverlayText(title, sub, color) {
|
|
ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, 0, W, H);
|
|
MGAPI.text(ctx, title, W/2, H/2 - 22, {
|
|
size: 28, weight:'bold', color: color || '#fff',
|
|
family: "'Fredoka One',cursive", shadow:'rgba(0,0,0,0.7)', shadowBlur: 12
|
|
});
|
|
if (sub) MGAPI.text(ctx, sub, W/2, H/2 + 18, { size: 15, color:'rgba(255,255,255,0.88)' });
|
|
}
|
|
|
|
function render(alpha) {
|
|
// Shake
|
|
let sx = 0, sy = 0;
|
|
if (shake > 0.2) { sx = (Math.random()-0.5)*shake; sy = (Math.random()-0.5)*shake; }
|
|
ctx.save(); ctx.translate(sx, sy);
|
|
|
|
// Himmel
|
|
ctx.fillStyle = skyGrad; ctx.fillRect(-12, -12, W + 24, H + 24);
|
|
// Sterne (twinkern via simTime)
|
|
stars.forEach(s => {
|
|
ctx.globalAlpha = s.a * (0.5 + 0.5*Math.sin((simTime||0)*0.004 + s.p));
|
|
ctx.fillStyle = '#cdd6ff';
|
|
ctx.fillRect(s.x, s.y, s.r, s.r);
|
|
});
|
|
ctx.globalAlpha = 1;
|
|
|
|
// Pipes + Boden
|
|
if (state !== 'intro') {
|
|
pipes.forEach(p => drawPipe(lerp(p.px, p.x, alpha), p));
|
|
drawFloor(lerp(pWorldScroll || 0, worldScroll || 0, alpha));
|
|
|
|
// Puffs
|
|
puffs.forEach(p => {
|
|
ctx.globalAlpha = p.a; ctx.fillStyle = p.col || '#fff';
|
|
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.fill();
|
|
});
|
|
ctx.globalAlpha = 1;
|
|
|
|
// Birds (mit Komet-Schweif pro Spieler)
|
|
const drawBird = (b, col, sprite, dimmed) => {
|
|
const headY = lerp(b.py, b.y, alpha);
|
|
// Schweif
|
|
for (let i = b.trail.length - 1; i >= 0; i--) {
|
|
const t = 1 - i / b.trail.length;
|
|
ctx.globalAlpha = 0.28 * t;
|
|
ctx.fillStyle = col;
|
|
ctx.beginPath(); ctx.arc(BIRD_X - i*7, b.trail[i], (BR * 0.55) * t + 1.5, 0, Math.PI*2); ctx.fill();
|
|
}
|
|
ctx.globalAlpha = dimmed ? 0.45 : 1;
|
|
ctx.save(); ctx.translate(BIRD_X, headY); ctx.rotate(b.angle);
|
|
ctx.drawImage(sprite, -SPR/2, -SPR/2, SPR, SPR);
|
|
ctx.restore();
|
|
ctx.globalAlpha = 1;
|
|
};
|
|
if (birds) {
|
|
drawBird(birds[0], P1_LIGHT, sprP1, !birds[0].alive);
|
|
drawBird(birds[1], P2_LIGHT, sprP2, !birds[1].alive);
|
|
}
|
|
} else {
|
|
// Im Intro: Boden andeuten (statisch)
|
|
drawFloor(0);
|
|
}
|
|
|
|
ctx.restore();
|
|
|
|
// HUD oben (Stil von 1P-Flappy: kompakt)
|
|
MGAPI.text(ctx, `🔵 ${scores[0]}`, 8, 14, { align:'left', size:13, color:P1_COL });
|
|
const roundLabel = state === 'duelEnd' ? 'Best of ' + BEST_OF
|
|
: `Runde ${Math.min(roundIdx+1, BEST_OF)} / ${BEST_OF}`;
|
|
MGAPI.text(ctx, roundLabel, W/2, 14, { align:'center', size:12, color:'rgba(255,255,255,0.85)' });
|
|
MGAPI.text(ctx, `${scores[1]} 🔴`, W-8, 14, { align:'right', size:13, color:P2_COL });
|
|
|
|
// Flash beim Crash
|
|
if (flash > 0) { ctx.fillStyle = `rgba(255,255,255,${flash})`; ctx.fillRect(0, 0, W, H); }
|
|
|
|
// Overlays
|
|
if (state === 'intro') {
|
|
MGAPI.text(ctx, `Flappy-Duell · Best of ${BEST_OF}`, W/2, H/2 - 56, {
|
|
size: 26, weight:'bold', color:'#fff', family:"'Fredoka One',cursive",
|
|
shadow:'rgba(0,0,0,0.5)', shadowBlur: 10
|
|
});
|
|
MGAPI.text(ctx, '🔵 ↑ Pfeil oben · 🔴 W',
|
|
W/2, H/2 - 24, { size: 14, color:'rgba(255,255,255,0.88)' });
|
|
drawStartButton();
|
|
} else if (state === 'roundEnd') {
|
|
let t, c;
|
|
if (roundResult === 0) { t = '🔵 Runde für Spieler 1!'; c = P1_LIGHT; }
|
|
else if (roundResult === 1) { t = '🔴 Runde für Spieler 2!'; c = P2_LIGHT; }
|
|
else { t = '⚖️ Beide abgestürzt!'; c = '#fde68a'; }
|
|
drawOverlayText(t, `Stand: 🔵 ${scores[0]} : ${scores[1]} 🔴`, c);
|
|
} else if (state === 'duelEnd') {
|
|
const w = finalWinner();
|
|
let t, c;
|
|
if (w === 0) { t = '🏆 Spieler 1 gewinnt!'; c = P1_LIGHT; }
|
|
else if (w === 1) { t = '🏆 Spieler 2 gewinnt!'; c = P2_LIGHT; }
|
|
else { t = '🤝 Unentschieden!'; c = '#fde68a'; }
|
|
drawOverlayText(t, `Endstand: 🔵 ${scores[0]} : ${scores[1]} 🔴`, c);
|
|
}
|
|
}
|
|
|
|
function loop(ts) {
|
|
if (stopped) return;
|
|
raf = requestAnimationFrame(loop);
|
|
|
|
if (state === 'playing') {
|
|
if (!lastTime) lastTime = ts;
|
|
acc += Math.min(ts - lastTime, STEP * 5);
|
|
lastTime = ts;
|
|
while (acc >= STEP) { step(); acc -= STEP; }
|
|
render(acc / STEP);
|
|
return;
|
|
}
|
|
|
|
// Sonstige States: stateTimer mit dt zählen, render normal
|
|
if (!lastTime) lastTime = ts;
|
|
const dt = Math.min(50, ts - lastTime);
|
|
lastTime = ts;
|
|
stateTimer += dt;
|
|
if (state === 'roundEnd' && stateTimer >= 1800) advanceRound();
|
|
else if (state === 'duelEnd' && stateTimer >= 2200) finishUp();
|
|
render(0);
|
|
}
|
|
|
|
raf = requestAnimationFrame(loop);
|
|
|
|
return {
|
|
stop() {
|
|
if (stopped) return;
|
|
stopped = true;
|
|
cancelAnimationFrame(raf);
|
|
document.removeEventListener('keydown', onKey);
|
|
canvas.removeEventListener('mousedown', onCanvasDown);
|
|
canvas.removeEventListener('touchstart', onCanvasTouch);
|
|
},
|
|
};
|
|
}
|
|
|
|
function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, idx => MGAPI.onResult(idx)); }
|
|
function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, idx => MGAPI.onResult(idx)); }
|
|
|
|
return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, requires: REQUIRES, launch, preview };
|
|
|
|
})();
|