| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/arni-solutions.de/web/slots/js/engine/ |
Upload File : |
/* Reel Royale – GameEngine: zentraler Spielablauf-Orchestrator. */
import { $, money, postJSON, wait, applyTheme } from '../util.js';
export class GameEngine {
constructor({ boot, reels, anim, audio, particles, freeSpins, bonus, stats, gamble, session }) {
this.boot = boot;
this.reels = reels;
this.anim = anim;
this.audio = audio;
this.particles = particles;
this.fs = freeSpins;
this.bonus = bonus;
this.stats = stats;
this.gamble = gamble;
this.session = session;
this.balance = boot.user.balance;
this.freeSpins = boot.user.free_spins;
this.game = null;
this.busy = false;
this.lines = 10;
this.levelIdx = 0; // Index in levelOptions
this.auto = { active: false, count: 0, total: 0 };
this.turbo = false;
this.reels.onReelStop = () => this.audio.play('reelStop');
this.reels.onAnticipate = () => this.audio.play('anticipate');
}
get coinValue() { return this.boot.coinValue; }
get level() { return this.boot.levelOptions[this.levelIdx]; }
get betPerLine() { return this.level * this.coinValue; }
get totalBet() { return this.betPerLine * this.lines; }
loadGame(game) {
this.game = game;
this.lines = game.max_lines;
this.levelIdx = 0;
this.reels.build(game);
applyTheme(game.theme);
this.particles.setMode(game.theme.particle, game.theme.accent);
this.audio.startMusic(game.theme.music);
this.stats.use(game.id);
// Banner
const banner = $('featureBanner');
banner.textContent = '★ ' + (game.feature.name || 'Freispiele');
banner.hidden = false;
$('gameTitle').textContent = game.title;
$('gameSubtitle').textContent = game.subtitle;
$('statRtp').textContent = 'RTP ' + Math.round((game.rtp || 0.96) * 100) + '%';
$('statVol').textContent = game.volatility || '';
this.updateReadouts();
this.refreshHud();
// Falls offene Freispiele aus dieser Session zu diesem Spiel gehören → Zustand wiederherstellen
if (this.freeSpins > 0 && this.boot.user.fs_game === game.id) {
const total = this.boot.user.fs_total || this.freeSpins;
const played = Math.max(0, total - this.freeSpins);
this.fs.active = true; this.fs.total = total; this.fs.played = played;
document.body.classList.add('rr-freespin');
this._updateFsLabel(played, total, true);
$('spinLabel').textContent = 'FREE';
}
}
adjustLines(d) { if (this.busy) return; this.lines = Math.max(1, Math.min(this.game.max_lines, this.lines + d)); this.updateReadouts(); this.audio.play('click'); }
adjustBet(d) { if (this.busy) return; this.levelIdx = Math.max(0, Math.min(this.boot.levelOptions.length - 1, this.levelIdx + d)); this.updateReadouts(); this.audio.play('click'); }
maxBet() { if (this.busy) return; this.levelIdx = this.boot.levelOptions.length - 1; this.lines = this.game.max_lines; this.updateReadouts(); this.audio.play('click'); }
updateReadouts() {
$('lineValue').textContent = this.lines;
$('betValue').textContent = money(this.betPerLine);
$('totalBetValue').textContent = money(this.totalBet);
}
refreshHud() {
const b = $('hudBalance'); b.textContent = money(this.balance); this._bump(b);
if (this.session) this.session.render(this.balance);
const chip = $('hudFsChip');
if (this.fs.active && this.fs.total > 0) {
chip.hidden = false; $('hudFreeSpins').textContent = `${Math.min(this.fs.played, this.fs.total)} / ${this.fs.total}`;
} else if (this.freeSpins > 0) {
chip.hidden = false; $('hudFreeSpins').textContent = this.freeSpins;
} else chip.hidden = true;
}
_bump(el) { el.classList.remove('bump'); void el.offsetWidth; el.classList.add('bump'); }
setSpinning(on) {
const btn = $('spinBtn');
btn.classList.toggle('is-spinning', on);
btn.disabled = on;
let label = on ? '' : (this.freeSpins > 0 ? 'FREE' : 'SPIN');
// Auto-Dreh: aktuellen Spin als „x/y" anzeigen (Lade-Kreis läuft dahinter)
if (on && this.auto.active && this.auto.total) {
label = (this.auto.total - this.auto.count + 1) + '/' + this.auto.total;
}
$('spinLabel').textContent = label;
}
startAuto(count) {
this.auto = { active: true, count, total: count };
$('autoBtn').textContent = 'STOP';
$('spinBtn').classList.add('is-auto');
if (!this.busy && this.freeSpins === 0) this.spin();
}
stopAuto() {
this.auto = { active: false, count: 0, total: 0 };
$('autoBtn').textContent = 'AUTO';
$('spinBtn').classList.remove('is-auto');
}
async spin() {
if (this.busy || !this.game) return;
const isFreeBefore = this.freeSpins > 0;
if (!isFreeBefore && this.balance < this.totalBet) {
this.toast('Zu wenig Coins', 'Bitte Einsatz senken oder Tagesbonus holen.', 'error');
this.stopAuto();
return;
}
this.busy = true;
this.setSpinning(true);
this.anim.clear();
$('winPop').hidden = true;
this.audio.play('reelStart');
$('winSummary').textContent = isFreeBefore ? 'Freispiel läuft…' : 'Walzen drehen…';
let data;
try {
data = await postJSON('api/spin.php', {
game: this.game.id, lines: this.lines, level: this.level, csrf: this.boot.csrf,
});
} catch (err) {
this.busy = false; this.setSpinning(false); this.stopAuto();
this.toast('Fehler', err.message, 'error');
$('winSummary').textContent = err.message;
return;
}
const fastSpin = this.turbo || this.auto.active || isFreeBefore;
if (data.respin && data.respin.first_grid && data.respin.held) {
// Hold & Respin: erst Auslöse-Dreh (3 Freispielsymbole), dann Scatter-Walzen halten
// und die übrigen einmal neu drehen (Chance auf mehr Freispielsymbole → mehr Freispiele).
await this.reels.spin(data.respin.first_grid, { fast: fastSpin });
this.audio.play('anticipate');
this.toast('🎰 Respin', 'Freispiel-Symbole gehalten', 'gold');
await wait(650);
await this.reels.spin(data.grid, { fast: fastSpin, hold: data.respin.held });
} else {
await this.reels.spin(data.grid, { fast: fastSpin });
}
// Zustand übernehmen
this.balance = data.balance;
const fsBefore = this.freeSpins;
this.freeSpins = data.free_spins;
this.stats.record(data);
// Spezial-Highlights
const meta = data.meta || {};
this.anim.markSpecial(meta.expanded, 'is-special');
this.anim.markSpecial(meta.transformed, 'is-special');
this.anim.markSpecial(meta.walk, 'is-special');
if (meta.collected && meta.collected.length) this.anim.showCollected(meta.collected);
// Vollbild-Bonus des Glückssymbols („extra viel Punkte")
if (meta.fullscreen) {
this.audio.play('bigwin');
this.particles.coinShower(90);
this.particles.confetti(90);
if (this.particles.fireworks) this.particles.fireworks(6);
this.toast('🍀 VOLLBILD · Glückssymbol', `+${money(meta.fullscreen_win || 0)} ${this.boot.currency}`, 'gold');
}
// Multiplikator / Sammler-Anzeige
this._updateFeatureBars(data);
// Gewinnlinien
if (data.wins && data.wins.length) this.anim.highlightWins(data.wins);
// Tanzende Symbole: Scatter, Bonus und Gewinn-Symbole pulsieren
this.anim.danceSpecial(this.game);
this.refreshHud();
$('lastWin').textContent = money(data.total_win);
// Gewinn-Feedback
const tiers = this.boot.winTiers;
const tier = this.anim.tier(data.total_win, data.total_bet || (this.betPerLine * this.lines) || 1, tiers);
if (data.total_win > 0) {
this.audio.play('win');
this.particles.burst(window.innerWidth / 2, window.innerHeight / 2, this.game.theme.particle === 'fire' ? 'fire' : 'gold', 16);
this.anim.popWin(data.total_win);
$('winSummary').textContent = `Gewinn: +${money(data.total_win)} ${this.boot.currency}`;
if (tier && data.wins) await wait(700);
} else {
$('winSummary').textContent = isFreeBefore ? 'Kein Gewinn in diesem Freispiel.' : 'Kein Treffer. Nächster Spin?';
}
// Bonusspiel (Rad)
let stopForBonus = false;
if (meta.bonus) {
stopForBonus = true;
await this.bonus.play(meta.bonus, data.total_bet);
this.refreshHud();
}
// Big/Mega/Epic Overlay
let stopForBig = false;
if (tier && tier.key !== 'small') {
stopForBig = ['big', 'mega', 'epic'].includes(tier.key);
if (stopForBig) {
await this.anim.bigWin(tier.label, data.total_win, tier.key);
} else {
this.toast(tier.label, `+${money(data.total_win)} ${this.boot.currency}`, 'gold');
}
}
// Verdopplungsspiel (Gamble) – nur Basisspiel-Gewinne, nicht beim Freispiel-Auslöser/Bonus
if (this.gamble && data.gamble_available && !isFreeBefore && !data.fs_entered && !(meta && meta.bonus) && data.total_win > 0) {
const autoGamble = this.auto.active;
const bal = await this.gamble.offer(data.total_win, autoGamble);
if (bal != null && !Number.isNaN(bal)) {
this.balance = bal;
this.refreshHud();
}
}
// Freispiel-Status (servergesteuert)
const fsTotal = data.fs_total || 0;
const fsPlayed = data.fs_played || 0;
let enteredFree = false;
if (data.fs_entered && !this.fs.active) {
enteredFree = true;
this.stopAuto();
await this.fs.enter(this.freeSpins, data.special, this.game, fsTotal);
} else if (data.is_free_spin && data.free_spins_won > 0 && this.fs.active) {
await this.fs.retrigger(data.free_spins_won, fsTotal);
}
this.fs.setProgress(fsPlayed, fsTotal, data.fs_active);
this._updateFsLabel(fsPlayed, fsTotal, data.fs_active);
if (data.fs_ended) {
await this.fs.end(data.fs_session_win);
this._hideFeatureBars();
}
this.busy = false;
this.setSpinning(false);
// Auto-Stopp nur bei Freispiel-Auslösung (Big Win & Bonus laufen automatisch weiter)
if (enteredFree && !this.fs.active) this.stopAuto();
// Fortsetzung
if (this.freeSpins > 0) {
await wait(750);
this.spin();
} else if (this.auto.active) {
this.auto.count--;
if (this.auto.count <= 0) this.stopAuto();
else { await wait(420); if (this.auto.active) this.spin(); }
}
}
_updateFeatureBars(data) {
const bar = $('collectBar');
const mult = data.spin_mult || 1;
const feat = this.game.feature.type;
const label = bar.querySelector('span');
if (this.fs.active && feat === 'collector') {
bar.hidden = false;
const isLion = this.game.id === 'lion_pride';
const count = data.fs_collected || 0;
const cur = (data.meta && data.meta.collect_win) || 0;
if (label) label.textContent = isLion ? '🦁 Löwinnen erobert' : 'Sammler';
$('collectTotal').textContent = `${count}` + (cur > 0 ? ` · +${money(cur)}` : '');
const pill = $('multPill');
if (mult > 1) { pill.hidden = false; pill.textContent = '×' + mult; } else pill.hidden = true;
} else if (this.fs.active && mult > 1) {
bar.hidden = false;
if (label) label.textContent = 'Bonus';
$('collectTotal').textContent = '×' + mult + ' Multiplikator';
$('multPill').hidden = true;
} else {
bar.hidden = true;
}
}
_hideFeatureBars() { $('collectBar').hidden = true; $('multPill').hidden = true; }
_updateFsLabel(played, total, active) {
const chip = $('hudFsChip');
const banner = $('featureBanner');
if (active && total > 0) {
chip.hidden = false;
$('hudFreeSpins').textContent = `${Math.min(played, total)} / ${total}`;
if (this.game) banner.textContent = `★ ${this.game.feature.name} · Freispiel ${Math.min(played, total)}/${total}`;
} else {
chip.hidden = true;
if (this.game) banner.textContent = '★ ' + (this.game.feature.name || 'Freispiele');
}
}
toast(title, msg, kind = 'win') {
const wrap = $('toasts');
const t = document.createElement('div');
t.className = 'rr-toast ' + kind;
t.innerHTML = `<b>${title}</b><span>${msg}</span>`;
wrap.appendChild(t);
requestAnimationFrame(() => t.classList.add('show'));
setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 350); }, this.boot.toastMs || 3200);
}
}