luneraria-control-center/public/assets/js/main.js
Martin Stickelbroeck 0df07eefd1
Some checks are pending
CI/CD Cloud Deployment / test-and-build (push) Waiting to run
feat: Champions of Luneraria - Portal & LiveOps Schaltzentrale
- Triple-A Werbeseite mit Gameplay-Videos, 3-Lane Radar und Einheiten-Showcase
- LiveOps-Schaltzentrale mit CCU-Tracking, Download-Statistiken und Match-Balance
- Reale Engine-Benchmarks (C# vs Godot, U2-U5.5 FPS, GPU-Skinning Entlastung)
- Zero-Dependency Node.js Backend mit REST-APIs und Server-Sent Events (SSE)
- Cloud-Ready mit Dockerfile, docker-compose.yml und GitHub Actions CI/CD
2026-10-11 20:07:51 +02:00

458 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* CHAMPIONS OF LUNERARIA - Studio Grade Interactions
* Cursor Ambient Tracking, Battle Flow Stage Machine, Champion Roster System,
* 3D Card Gyroscope/Tilt, and Accessible Lightbox Modal.
*/
document.addEventListener('DOMContentLoaded', () => {
initCursorTracker();
initHeaderScroll();
initAudioSynthesizer();
initHeroParticles();
initBattleFlowSteps();
initChampionRoster();
initArmyFilters();
init3DCardTilts();
initCinemaTheater();
initStudioLightbox();
initPlatformMatrix();
initScrollTop();
initScrollAnimations();
});
/* --------------------------------------------------------------------------
1. Global Cursor Ambient Glow Tracker
-------------------------------------------------------------------------- */
function initCursorTracker() {
window.addEventListener('mousemove', (e) => {
document.documentElement.style.setProperty('--mouse-x', `${e.clientX}px`);
document.documentElement.style.setProperty('--mouse-y', `${e.clientY}px`);
}, { passive: true });
}
/* --------------------------------------------------------------------------
2. Sticky Header Glass & Scroll State
-------------------------------------------------------------------------- */
function initHeaderScroll() {
const header = document.querySelector('.site-header');
if (!header) return;
window.addEventListener('scroll', () => {
header.classList.toggle('scrolled', window.scrollY > 40);
}, { passive: true });
}
/* --------------------------------------------------------------------------
3. Ambient Web Audio Synthesizer Trigger
-------------------------------------------------------------------------- */
function initAudioSynthesizer() {
const btn = document.getElementById('btnAudioSynth');
if (!btn || !window.luneAudio) return;
btn.addEventListener('click', () => {
const isPlaying = window.luneAudio.toggle();
btn.classList.toggle('is-playing', isPlaying);
btn.setAttribute('aria-pressed', isPlaying ? 'true' : 'false');
const label = btn.querySelector('.audio-label');
if (label) {
label.textContent = isPlaying ? 'Atmosphäre: An' : 'Atmosphäre: Aus';
}
});
}
/* --------------------------------------------------------------------------
4. Hero Floating Ambient Sparks Canvas
-------------------------------------------------------------------------- */
function initHeroParticles() {
const canvas = document.getElementById('heroParticleCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let width, height;
let particles = [];
function resize() {
width = canvas.width = canvas.offsetWidth;
height = canvas.height = canvas.offsetHeight;
}
window.addEventListener('resize', resize, { passive: true });
resize();
class Spark {
constructor() {
this.reset();
}
reset() {
this.x = Math.random() * width;
this.y = height + Math.random() * 20;
this.size = Math.random() * 2.2 + 0.6;
this.speedY = Math.random() * 0.6 + 0.25;
this.speedX = (Math.random() - 0.5) * 0.35;
this.alpha = Math.random() * 0.5 + 0.15;
this.color = Math.random() > 0.45 ? '56, 232, 216' : '245, 158, 11';
}
update() {
this.y -= this.speedY;
this.x += this.speedX;
if (this.y < -10) this.reset();
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${this.color}, ${this.alpha})`;
ctx.fill();
}
}
const count = Math.min(width < 768 ? 24 : 48, 55);
for (let i = 0; i < count; i++) {
const p = new Spark();
p.y = Math.random() * height;
particles.push(p);
}
function frame() {
ctx.clearRect(0, 0, width, height);
particles.forEach(p => {
p.update();
p.draw();
});
requestAnimationFrame(frame);
}
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
frame();
}
}
/* --------------------------------------------------------------------------
5. Interactive Battle Flow (Rundenzyklus Timeline)
-------------------------------------------------------------------------- */
const FLOW_DATA = {
1: {
title: 'Phase 1: Die Geheime Aufstellung',
desc: 'Wähle Truppen im HUD, investiere in Einkommen und positioniere deine Einheiten auf den Aufmarschfeldern deiner Lane. Bis zum Signal der Schlacht bleibt deine Aufstellung für die Feinde unsichtbar.',
img: 'assets/img/planung-hud.jpg'
},
2: {
title: 'Phase 2: Der Simultane Aufmarsch',
desc: 'Das Signal ertönt. Beide Heere setzen sich simultan in Bewegung. Formationen formieren sich automatisch auf Basis von Bewegungsgeschwindigkeit, Reichweite und Formationslinien.',
img: 'assets/img/aufmarsch.jpg'
},
3: {
title: 'Phase 3: Der Deterministische Clash (20 Hz)',
desc: 'Schwerter, Pfeile und Magmasäulen kollidieren. Der autoritative Regelkern berechnet jede Interaktion auf der Millisekunde genau. Rüstungsklassen (Leicht, Schwer, Äther) entscheiden über Durchschlagskraft.',
img: 'assets/img/held-kampf.jpg'
},
4: {
title: 'Phase 4: Die Bossgrotte „Mondschlund“',
desc: 'Wähle den riskanten Vorstoß: Sende Truppen zur Grotte, bezwinge die Schlundkrone und entsende den Superboss (6.800 HP) auf eine gegnerische Lane zur massiven Belagerung.',
img: 'assets/img/boss-lane.jpg'
},
5: {
title: 'Phase 5: Der Fall des Mondherz-Ancients',
desc: 'Türme stürzen in Trümmer. Erst wenn die Grenzwachten und Bastionen gefallen sind, öffnet sich der Zugang zum gegnerischen Kristallkern. Sein Einsturz krönt das Siegerteam.',
img: 'assets/img/ancient-fall.jpg'
}
};
function initBattleFlowSteps() {
const buttons = document.querySelectorAll('.flow-step-btn');
const titleEl = document.getElementById('flowStageTitle');
const descEl = document.getElementById('flowStageDesc');
const imgEl = document.getElementById('flowStageImg');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
buttons.forEach(b => b.classList.remove('is-active'));
btn.classList.add('is-active');
const step = btn.dataset.step;
const data = FLOW_DATA[step];
if (!data) return;
if (titleEl) titleEl.textContent = data.title;
if (descEl) descEl.textContent = data.desc;
if (imgEl) {
imgEl.style.opacity = '0.3';
setTimeout(() => {
imgEl.src = data.img;
imgEl.style.opacity = '1';
}, 150);
}
});
});
}
/* --------------------------------------------------------------------------
6. Champion Roster Showcase (Riot / Dota Style)
-------------------------------------------------------------------------- */
const CHAMPION_PROFILES = {
sylvara: {
badge: 'Elfen · Assassine / Flankenspringerin',
name: 'Sylvara, die Mondklinge',
quote: '„Zwei Klingen, ein Sprung – und die hintere Reihe verstummt vor dem ersten Zauber.“',
image: 'assets/img/held-kampf.jpg',
abilities: [
{ key: 'Passiv', name: 'Schattensprung', desc: 'Springt beim ersten Angriff der Runde direkt in die gegnerische Backline auf verwundbare Fernkämpfer.' },
{ key: 'Skill 1', name: 'Mondsichel-Wurf', desc: 'Schleudert rotierende Mondklingen durch Gegnerketten, die auf dem Rückweg magischen Schnittschaden verursachen.' },
{ key: 'Ultimate', name: 'Mondsicheltanz', desc: 'Wirbelt mit blendendem Mondlicht durch bis zu 5 Ziele. Löst bedingungsgesteuert aus, sobald ein Fernkämpfer isoliert ist.' }
]
},
brunhild: {
badge: 'Menschen · Frontkämpferin / Sprungangriff',
name: 'Brunhild, die Walküre',
quote: '„Mein Speer verfehlt nie sein Zeichen. Der Himmel stürzt über den Feinden ein.“',
image: 'assets/img/fig-funkenlanze.jpg',
abilities: [
{ key: 'Passiv', name: 'Duellstolz', desc: 'Erhöht ihren ausgeteilten Schaden gegen feindliche Helden und Bosse um 35 %.' },
{ key: 'Skill 1', name: 'Speer der Vergeltung', desc: 'Markiert das Ziel mit einer Leuchtgaleere und senkt gegnerische Rüstung um 40 %.' },
{ key: 'Ultimate', name: 'Walkürensturz', desc: 'Katapultiert sich in die Lüfte und schlägt mit Flächenbeben und 2-Sekunden-Betäubung auf das markierte Ziel ein.' }
]
},
funkenlanze: {
badge: 'Glutpakt · Elite-Heldin / Hitzestrahl',
name: 'Funkenlanze von der Sonnenesse',
quote: '„Glühende Schlacke schmiedet das Heer – kein Turm widersteht unserer Hitze.“',
image: 'assets/img/aufmarsch.jpg',
abilities: [
{ key: 'Passiv', name: 'Hitzeschild', desc: 'Absorbiert Puls- und Ätherschaden und lädt bei Treffern ihre Lanze mit glühendem Magma auf.' },
{ key: 'Skill 1', name: 'Lanzenstoß', desc: 'Durchdringt schwere Panzerung mit linearem Hitzestrahl über 12 Meter Kampfreichweite.' },
{ key: 'Ultimate', name: 'Eruptionszündung', desc: 'Verwandelt die Lanze in eine lodernde Magmasäule, die Belagerungsschaden an Gebäuden verdoppelt.' }
]
}
};
function initChampionRoster() {
const tabs = document.querySelectorAll('.roster-tab-btn');
const badgeEl = document.getElementById('rosterBadge');
const nameEl = document.getElementById('rosterName');
const quoteEl = document.getElementById('rosterQuote');
const imgEl = document.getElementById('rosterImg');
const stackEl = document.getElementById('rosterAbilityStack');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('is-active'));
tab.classList.add('is-active');
const heroKey = tab.dataset.champion;
const data = CHAMPION_PROFILES[heroKey];
if (!data) return;
if (badgeEl) badgeEl.textContent = data.badge;
if (nameEl) nameEl.textContent = data.name;
if (quoteEl) quoteEl.textContent = data.quote;
if (imgEl) imgEl.src = data.image;
if (stackEl) {
stackEl.innerHTML = data.abilities.map(ab => `
<div class="hero-ability-card">
<div class="ability-card-head">
<span class="ability-title">${ab.name}</span>
<span class="ability-key-pill">${ab.key}</span>
</div>
<div class="ability-description-text">${ab.desc}</div>
</div>
`).join('');
}
});
});
}
/* --------------------------------------------------------------------------
7. 3D Card Gyroscope/Tilt
-------------------------------------------------------------------------- */
function init3DCardTilts() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const cards = document.querySelectorAll('.unit-inspected-card, .roster-portrait-frame');
cards.forEach(card => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateX = ((y - centerY) / centerY) * -6;
const rotateY = ((x - centerX) / centerX) * 6;
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateY(-4px)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
});
});
}
/* --------------------------------------------------------------------------
8. Glutpakt Army Filter
-------------------------------------------------------------------------- */
function initArmyFilters() {
const buttons = document.querySelectorAll('.army-filter-btn');
const cards = document.querySelectorAll('.unit-inspected-card');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
buttons.forEach(b => b.classList.remove('is-active'));
btn.classList.add('is-active');
const filter = btn.dataset.filter;
cards.forEach(card => {
if (filter === 'all' || card.dataset.category === filter) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
});
});
}
/* --------------------------------------------------------------------------
9. Cinema Video Theater
-------------------------------------------------------------------------- */
function initCinemaTheater() {
const video = document.getElementById('cinemaVideoPlayer');
const buttons = document.querySelectorAll('.scene-btn');
const title = document.getElementById('cinemaTitleText');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
buttons.forEach(b => b.classList.remove('is-active'));
btn.classList.add('is-active');
const src = btn.dataset.src;
const sceneTitle = btn.dataset.title;
if (video && src) {
video.src = src;
video.play().catch(() => {});
}
if (title && sceneTitle) {
title.textContent = sceneTitle;
}
});
});
}
/* --------------------------------------------------------------------------
10. Studio Lightbox Modal
-------------------------------------------------------------------------- */
function initStudioLightbox() {
const modal = document.getElementById('studioLightboxModal');
const imgEl = document.getElementById('lightboxActiveImg');
const closeBtn = document.getElementById('lightboxCloseTrigger');
const items = document.querySelectorAll('.mosaic-shot-card');
if (!modal || !imgEl) return;
items.forEach(card => {
card.addEventListener('click', () => {
const img = card.querySelector('img');
if (img) imgEl.src = img.src;
modal.classList.add('is-open');
document.body.style.overflow = 'hidden';
if (closeBtn) closeBtn.focus();
});
});
function close() {
modal.classList.remove('is-open');
document.body.style.overflow = '';
}
if (closeBtn) closeBtn.addEventListener('click', close);
modal.addEventListener('click', (e) => {
if (e.target === modal) close();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.classList.contains('is-open')) close();
});
}
/* --------------------------------------------------------------------------
11. Platform Specification Matrix
-------------------------------------------------------------------------- */
const PLATFORMS = {
mac: {
os: 'macOS 14 Sonoma (empfohlen: macOS 15+)',
gpu: 'Apple Silicon (M1 / M2 / M3 / M4 / M5) oder Metal-GPU',
ram: '8 GB RAM (empfohlen: 16 GB)',
storage: '350 MB freier Speicherplatz',
pkg: 'ChampionsOfLuneraria-macOS-universal.zip'
},
win: {
os: 'Windows 11 / Windows 10 (64-Bit, Version 21H2+)',
gpu: 'DirectX 12 / Vulkan fähige GPU (GTX 1060 oder besser)',
ram: '8 GB RAM (empfohlen: 16 GB)',
storage: '320 MB freier Speicherplatz',
pkg: 'ChampionsOfLuneraria-Windows-x64.zip'
},
linux: {
os: 'Fedora 44, Ubuntu 24.04 LTS (glibc 2.38+, 64-Bit)',
gpu: 'Vulkan 1.3 / OpenGL 4.5 kompatible Treiber (Mesa 24+)',
ram: '8 GB RAM (empfohlen: 16 GB)',
storage: '310 MB freier Speicherplatz',
pkg: 'ChampionsOfLuneraria-Linux-x64.zip'
}
};
function initPlatformMatrix() {
const buttons = document.querySelectorAll('.platform-select-btn');
const osEl = document.getElementById('specOS');
const gpuEl = document.getElementById('specGPU');
const ramEl = document.getElementById('specRAM');
const storageEl = document.getElementById('specStorage');
const pkgEl = document.getElementById('specPackageName');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
buttons.forEach(b => b.classList.remove('is-active'));
btn.classList.add('is-active');
const data = PLATFORMS[btn.dataset.platform];
if (!data) return;
if (osEl) osEl.textContent = data.os;
if (gpuEl) gpuEl.textContent = data.gpu;
if (ramEl) ramEl.textContent = data.ram;
if (storageEl) storageEl.textContent = data.storage;
if (pkgEl) pkgEl.textContent = data.pkg;
});
});
}
/* --------------------------------------------------------------------------
12. Scroll Top & Modern Guidance Fallbacks
-------------------------------------------------------------------------- */
function initScrollTop() {
const btn = document.getElementById('btnScrollTop');
if (btn) {
btn.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
}
function initScrollAnimations() {
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
}
});
}, {
threshold: 0.12,
rootMargin: '0px 0px -40px 0px'
});
document.querySelectorAll('.animate-on-scroll').forEach(el => observer.observe(el));
}
}