Some checks are pending
CI/CD Cloud Deployment / test-and-build (push) Waiting to run
- 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
458 lines
17 KiB
JavaScript
458 lines
17 KiB
JavaScript
/**
|
||
* 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));
|
||
}
|
||
}
|