Cursed Clash
SELECT YOUR SORCERER
Loading...
DOMAIN
EXPANSION
MALEVOLENT SHRINE
DRIVING RECORD
RECORD SET
Tokyo Expressway
01:45.32
Driving Grade
SPECIAL GRADE
function switchBGM(type) {
const bgm = document.getElementById('music-bgm');
const boss = document.getElementById('music-boss');
if(bgm) bgm.pause();
if(boss) boss.pause();
const target = type === 'boss' ? boss : bgm;
if(target && target.src) {
target.currentTime = 0;
target.play().catch(()=>{});
}
}
function playSound(type) {
const customSound = document.getElementById(`sfx-${type}`);
if (customSound && customSound.src && customSound.src !== window.location.href) {
customSound.currentTime = 0;
customSound.play().catch(()=>{});
return;
}
// Procedural Fallbacks
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'hit') {
osc.type = 'square';
osc.frequency.setValueAtTime(150, now);
osc.frequency.exponentialRampToValueAtTime(40, now + 0.1);
gain.gain.setValueAtTime(0.5, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
} else if (type === 'block') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(200, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.2);
gain.gain.setValueAtTime(0.3, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
} else if (type === 'energy') {
osc.type = 'sine';
osc.frequency.setValueAtTime(300, now);
osc.frequency.linearRampToValueAtTime(800, now + 0.5);
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(0.3, now + 0.5);
osc.start(now);
osc.stop(now + 0.5);
} else if (type === 'ultimate') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(50, now);
osc.frequency.linearRampToValueAtTime(20, now + 1.5);
gain.gain.setValueAtTime(0.8, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 1.5);
// Add noise
const bufferSize = audioCtx.sampleRate * 1.5;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const noiseFilter = audioCtx.createBiquadFilter();
noiseFilter.type = 'lowpass';
noiseFilter.frequency.setValueAtTime(1000, now);
noiseFilter.frequency.linearRampToValueAtTime(100, now + 1.5);
const noiseGain = audioCtx.createGain();
noiseGain.gain.setValueAtTime(0.5, now);
noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 1.5);
noise.connect(noiseFilter);
noiseFilter.connect(noiseGain);
noiseGain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 1.5);
noise.start(now);
}
}
// ==========================================
// 5. CORE LOGIC
// ==========================================
function getImageUrl(id) {
const configUrl = getConfig('images', id);
if (configUrl) return configUrl;
// Fallbacks
if (id.startsWith('bg_')) return DEFAULT_BGS[id] || DEFAULT_BGS.bg_tokyo;
const charId = id.replace('char_', '');
return DEFAULT_AVATARS[charId] || DEFAULT_AVATARS.gojo;
}
function showScreen(screenId) {
['screen-select', 'screen-battle', 'cinematic-overlay', 'screen-result'].forEach(id => {
if(els[id]) els[id].classList.add('hidden-screen');
});
if(els[screenId]) els[screenId].classList.remove('hidden-screen');
}
// --- Selection Screen ---
let selectedCharIndex = 0;
const charKeys = Object.keys(CHARACTERS);
function initSelectScreen() {
window.appState.gameState = 'select';
showScreen('screen-select');
els['char-carousel'].innerHTML = '';
charKeys.forEach((key, index) => {
const char = CHARACTERS[key];
const div = document.createElement('div');
div.className = `char-card absolute inset-0 border-4 rounded-xl overflow-hidden cursor-pointer ${index === 0 ? 'active z-20' : 'z-10'}`;
div.style.backgroundImage = `url('${getImageUrl(char.imgId)}')`;
div.style.backgroundSize = 'cover';
div.style.backgroundPosition = 'center';
// Stack them visually
if (index !== 0) div.style.transform = `translateX(${(index) * 110}%) scale(0.8)`;
div.addEventListener('pointerdown', () => selectCharacter(index));
els['char-carousel'].appendChild(div);
});
updateSelectUI();
}
function selectCharacter(index) {
playSound('block');
selectedCharIndex = index;
const cards = els['char-carousel'].children;
for(let i=0; i index ? 110 : -110;
cards[i].style.transform = `translateX(${offset}%) scale(0.8)`;
}
}
updateSelectUI();
}
function updateSelectUI() {
const char = CHARACTERS[charKeys[selectedCharIndex]];
els['select-char-name'].textContent = char.name;
els['select-char-name'].style.color = char.color;
document.documentElement.style.setProperty('--cursed-energy', char.color);
}
// --- Battle Logic ---
let battleLoopId;
function startLadder() {
window.appState.ladderStage = 0;
const charDef = CHARACTERS[charKeys[selectedCharIndex]];
window.appState.player.id = charDef.id;
// Re-apply max HP from config in case it changed
const maxHp = window.appState.config.playerMaxHp;
window.appState.player.maxHp = maxHp;
window.appState.player.hp = maxHp;
window.appState.player.energy = 0;
els['sprite-player'].style.backgroundImage = `url('${getImageUrl(charDef.imgId)}')`;
els['player-name'].textContent = charDef.name.toUpperCase();
els['player-name'].style.color = charDef.color;
startStage();
}
function startStage() {
const stage = window.appState.ladderStage;
if (stage >= ENEMIES.length) {
showResult(true);
return;
}
const enemyDef = ENEMIES[stage];
window.appState.enemy.id = enemyDef.id;
window.appState.enemy.maxHp = enemyDef.hp;
window.appState.enemy.hp = enemyDef.hp;
window.appState.enemy.state = 'idle';
els['sprite-enemy'].style.backgroundImage = `url('${getImageUrl(enemyDef.imgId)}')`;
els['enemy-name'].textContent = enemyDef.name.toUpperCase();
els['hud-ladder'].textContent = `STAGE ${stage + 1}`;
els['battle-bg'].style.backgroundImage = `url('${getImageUrl(enemyDef.bg)}')`;
if (stage === ENEMIES.length - 1) switchBGM('boss');
else switchBGM('battle');
updateBars();
showScreen('screen-battle');
window.appState.gameState = 'battle';
els['combat-status'].textContent = "FIGHT!";
gsap.fromTo(els['combat-status'], {scale: 2, opacity: 0}, {scale: 1, opacity: 1, duration: 0.5, yoyo: true, repeat: 1, onComplete: () => els['combat-status'].textContent = ""});
scheduleEnemyAttack();
}
function updateBars() {
const p = window.appState.player;
const e = window.appState.enemy;
els['player-hp-fill'].style.transform = `scaleX(${Math.max(0, p.hp / p.maxHp)})`;
els['enemy-hp-fill'].style.transform = `scaleX(${Math.max(0, e.hp / e.maxHp)})`;
const energyPercent = Math.min(100, p.energy);
els['player-energy-fill'].style.width = `${energyPercent}%`;
if (energyPercent >= 100) {
els['btn-mic'].classList.remove('opacity-50', 'pointer-events-none');
els['btn-mic'].classList.add('ready');
} else {
els['btn-mic'].classList.add('opacity-50', 'pointer-events-none');
els['btn-mic'].classList.remove('ready');
}
}
function spawnFloatingText(text, containerId, color = 'white', scale = 1) {
const container = document.getElementById(containerId);
if(!container) return;
const el = document.createElement('div');
el.className = 'damage-text';
el.textContent = text;
el.style.color = color;
el.style.left = `${20 + Math.random() * 40}%`;
el.style.top = `${30 + Math.random() * 40}%`;
container.appendChild(el);
gsap.to(el, {
y: -100,
opacity: 0,
scale: scale,
duration: 0.8,
ease: "power2.out",
onComplete: () => el.remove()
});
}
// --- Player Actions ---
function handlePlayerAttack() {
if (window.appState.gameState !== 'battle') return;
// Animation
const sprite = els['sprite-player'];
sprite.classList.remove('attacking-player');
void sprite.offsetWidth; // trigger reflow
sprite.classList.add('attacking-player');
// Damage
const dmg = window.appState.config.baseDamage + Math.floor(Math.random() * 5);
window.appState.enemy.hp -= dmg;
// Energy
window.appState.player.energy += window.appState.config.energyPerHit;
playSound('hit');
triggerHitEffect('enemy', dmg);
checkWinLoss();
}
function setBlocking(isBlocking) {
if (window.appState.gameState !== 'battle') return;
window.appState.player.isBlocking = isBlocking;
if(isBlocking) {
els['sprite-player'].style.filter = 'brightness(0.7) sepia(1) hue-rotate(200deg)';
els['btn-block'].style.opacity = '0.8';
} else {
els['sprite-player'].style.filter = '';
els['btn-block'].style.opacity = '0.3';
}
}
// --- Enemy AI ---
function scheduleEnemyAttack() {
if (window.appState.gameState !== 'battle') return;
const enemyDef = ENEMIES[window.appState.ladderStage];
const delay = enemyDef.attackRate * (0.8 + Math.random() * 0.4); // Add variance
battleLoopId = setTimeout(() => {
if (window.appState.gameState !== 'battle') return;
// Show intent
els['enemy-intent'].style.opacity = 1;
playSound('energy');
setTimeout(() => {
if (window.appState.gameState !== 'battle') return;
els['enemy-intent'].style.opacity = 0;
executeEnemyAttack(enemyDef.damage);
scheduleEnemyAttack();
}, 600); // Reaction time window
}, delay);
}
function executeEnemyAttack(baseDamage) {
const sprite = els['sprite-enemy'];
sprite.classList.remove('attacking-enemy');
void sprite.offsetWidth;
sprite.classList.add('attacking-enemy');
let dmg = baseDamage;
let color = '#ef4444';
let txt = `-${dmg}`;
if (window.appState.player.isBlocking) {
dmg = Math.floor(dmg * 0.2); // 80% reduction
playSound('block');
color = '#9ca3af';
txt = 'BLOCKED';
} else {
playSound('hit');
}
window.appState.player.hp -= dmg;
triggerHitEffect('player', txt, color);
checkWinLoss();
}
function triggerHitEffect(target, text, color = 'white') {
const spriteId = target === 'enemy' ? 'sprite-enemy' : 'sprite-player';
const zoneId = target === 'enemy' ? 'zone-enemy' : 'zone-player';
const sprite = els[spriteId];
sprite.classList.remove('hit');
void sprite.offsetWidth;
sprite.classList.add('hit');
document.getElementById('app-container').classList.remove('shake');
void document.getElementById('app-container').offsetWidth;
document.getElementById('app-container').classList.add('shake');
spawnFloatingText(text, zoneId, color);
updateBars();
}
function checkWinLoss() {
if (window.appState.enemy.hp <= 0) {
window.appState.gameState = 'transition';
clearTimeout(battleLoopId);
window.appState.enemy.hp = 0;
updateBars();
els['combat-status'].textContent = "PURIFIED";
els['combat-status'].style.color = '#10b981';
// Explode enemy
gsap.to(els['sprite-enemy'], {
scale: 1.5, opacity: 0, filter: 'brightness(5)', duration: 0.5,
onComplete: () => {
window.appState.ladderStage++;
setTimeout(startStage, 1000);
}
});
} else if (window.appState.player.hp <= 0) {
window.appState.gameState = 'transition';
clearTimeout(battleLoopId);
window.appState.player.hp = 0;
updateBars();
els['combat-status'].textContent = "DEFEATED";
els['combat-status'].style.color = '#ef4444';
gsap.to(els['sprite-player'], {
y: 100, opacity: 0, rotation: -20, duration: 0.5,
onComplete: () => setTimeout(() => showResult(false), 1000)
});
}
}
// ==========================================
// 6. VOICE / ULTIMATE SYSTEM
// ==========================================
async function startRecording() {
if(window.appState.player.energy < 100) return;
if(window.appState.recording) return;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4';
window.appState.mediaRecorder = new MediaRecorder(stream, { mimeType });
window.appState.audioChunks = [];
window.appState.mediaRecorder.ondataavailable = e => {
if (e.data.size > 0) window.appState.audioChunks.push(e.data);
};
window.appState.mediaRecorder.onstop = processAudio;
window.appState.mediaRecorder.start();
window.appState.recording = true;
els['btn-mic'].classList.add('recording');
els['btn-mic'].classList.remove('ready');
els['mic-status'].style.opacity = '1';
// Slow down time/enemy attacks while recording
clearTimeout(battleLoopId);
} catch (err) {
console.error('Mic access denied', err);
// Fallback: If mic fails, just execute ultimate immediately
executeUltimate();
}
}
function stopRecording() {
if(!window.appState.recording || !window.appState.mediaRecorder) return;
window.appState.mediaRecorder.stop();
window.appState.mediaRecorder.stream.getTracks().forEach(t => t.stop());
window.appState.recording = false;
els['btn-mic'].classList.remove('recording');
els['mic-status'].style.opacity = '0';
}
async function processAudio() {
els['mic-status'].textContent = 'PROCESSING...';
els['mic-status'].style.opacity = '1';
const blob = new Blob(window.appState.audioChunks, { type: 'audio/webm' });
const reader = new FileReader();
reader.onloadend = async () => {
const base64Audio = reader.result;
const charDef = CHARACTERS[window.appState.player.id];
try {
// Send to parent API
const taskId = crypto.randomUUID();
window.parent.postMessage({
origin: 'sekai_gaming_iframe_api',
type: 'gen_transcript',
taskId: taskId,
data: { audio: base64Audio, initialPrompt: charDef.ultimateWords.join(' ') }
}, '*');
// Wait for response via event listener setup in init()
const handleResponse = (e) => {
const msg = e.data;
if (msg?.origin === 'sekai_gaming_iframe_api' && msg?.type === 'receive_gen_transcript' && msg.taskId === taskId) {
window.removeEventListener('message', handleResponse);
els['mic-status'].style.opacity = '0';
els['mic-status'].textContent = 'LISTENING...';
if (msg.error) {
console.error('Transcription error', msg.error);
executeUltimate(); // Fallback on error so game doesn't break
return;
}
const text = msg.data.text.toLowerCase();
console.log("Transcribed:", text);
// Fuzzy match
const matched = charDef.ultimateWords.some(word => text.includes(word));
if (matched) {
executeUltimate();
} else {
// Whiffed
spawnFloatingText("FAILED", 'zone-player', '#ef4444');
window.appState.player.energy = 0;
updateBars();
scheduleEnemyAttack(); // Resume enemy
}
}
};
window.addEventListener('message', handleResponse);
} catch (e) {
console.error("PostMessage failed", e);
executeUltimate(); // Fallback
}
};
reader.readAsDataURL(blob);
}
function executeUltimate() {
window.appState.gameState = 'cinematic';
const charDef = CHARACTERS[window.appState.player.id];
window.appState.player.energy = 0;
updateBars();
playSound('ultimate');
// Setup Cinematic Screen
els['cinematic-text'].innerHTML = charDef.ultName.replace(': ', '
');
els['cinematic-text'].style.color = charDef.color;
els['cinematic-bg'].style.backgroundImage = `url('${getImageUrl(charDef.imgId)}')`;
showScreen('cinematic-overlay');
// GSAP Animation Sequence
const tl = gsap.timeline();
tl.fromTo(els['cinematic-text'],
{ scale: 0.5, opacity: 0, filter: 'blur(10px)' },
{ scale: 1, opacity: 1, filter: 'blur(0px)', duration: 0.5, ease: 'back.out(1.7)' }
)
.to(els['cinematic-bg'], { scale: 1, duration: 2, ease: 'power1.out' }, '<')
.to('#cinematic-overlay', { backgroundColor: charDef.color, duration: 0.1, yoyo: true, repeat: 3 }, '+=0.5')
.to('#cinematic-overlay', { opacity: 0, duration: 0.3 }, '+=0.2')
.call(() => {
showScreen('screen-battle');
// Apply massive damage
const dmg = 9999;
window.appState.enemy.hp -= dmg;
triggerHitEffect('enemy', "ANNIHILATED", charDef.color);
checkWinLoss();
});
}
// ==========================================
// 7. RESULT / SHARE SYSTEM
// ==========================================
function showResult(isWin) {
window.appState.gameState = 'result';
showScreen('screen-result');
const charDef = CHARACTERS[window.appState.player.id];
els['result-avatar'].style.backgroundImage = `url('${getImageUrl(charDef.imgId)}')`;
els['result-avatar'].style.borderColor = charDef.color;
if (isWin) {
playSound('win');
els['result-title'].textContent = "VICTORY";
els['result-title'].style.color = '#22c55e';
els['result-desc'].textContent = "You defeated";
els['result-enemy'].textContent = ENEMIES[ENEMIES.length-1].name.toUpperCase();
els['result-rank'].textContent = "SPECIAL GRADE";
} else {
els['result-title'].textContent = "DEFEATED";
els['result-title'].style.color = '#ef4444';
els['result-desc'].textContent = "Felled by";
els['result-enemy'].textContent = ENEMIES[window.appState.ladderStage].name.toUpperCase();
els['result-rank'].textContent = `STAGE ${window.appState.ladderStage + 1}`;
}
// Report App Result (Fire-and-forget)
const score = isWin ? 1000 + (window.appState.player.hp) : window.appState.ladderStage * 100;
try {
window.parent.postMessage({
origin: 'sekai_gaming_iframe_api',
type: 'save_app_result',
taskId: crypto.randomUUID(),
data: {
score: score,
public: true,
result: { win: isWin, character: charDef.name, stage: window.appState.ladderStage }
}
}, '*');
} catch(e){}
}
async function handleShare() {
const shareBtn = els['btn-share'];
if (shareBtn.dataset.loading === 'true') return;
shareBtn.dataset.loading = 'true';
shareBtn.disabled = true;
shareBtn.innerHTML = ' GENERATING...';
lucide.createIcons();
try {
const element = document.getElementById('share-zone');
const jpgImg = await window.snapdom.toJpg(element, {
quality: 0.8,
width: element.offsetWidth * 2,
height: element.offsetHeight * 2,
});
window.parent.postMessage({
origin: 'sekai_gaming_iframe_api',
type: 'invoke_share',
taskId: crypto.randomUUID(),
data: {
title: `I reached ${els['result-rank'].textContent} in Cursed Clash!`,
coverImageBase64: jpgImg.src
}
}, '*');
} catch (e) {
console.error('Share failed:', e);
} finally {
shareBtn.dataset.loading = 'false';
shareBtn.disabled = false;
shareBtn.innerHTML = ' SHARE RESULT';
lucide.createIcons();
}
}
// ==========================================
// 8. INITIALIZATION & EVENTS
// ==========================================
function bindEvents() {
// UI Buttons
els['btn-start'].addEventListener('click', startLadder);
els['btn-restart'].addEventListener('click', initSelectScreen);
els['btn-share'].addEventListener('click', handleShare);
// Combat Controls
const attackZone = els['btn-attack'];
const blockZone = els['btn-block'];
const micBtn = els['btn-mic'];
// Touch events for mobile
attackZone.addEventListener('pointerdown', handlePlayerAttack);
blockZone.addEventListener('pointerdown', () => setBlocking(true));
blockZone.addEventListener('pointerup', () => setBlocking(false));
blockZone.addEventListener('pointerleave', () => setBlocking(false));
micBtn.addEventListener('pointerdown', (e) => {
e.preventDefault();
startRecording();
});
micBtn.addEventListener('pointerup', (e) => {
e.preventDefault();
stopRecording();
});
micBtn.addEventListener('pointerleave', (e) => {
e.preventDefault();
stopRecording();
});
}
function initApp() {
applyAllEditableValues();
initBGM();
cacheDOM();
bindEvents();
lucide.createIcons();
initSelectScreen();
}
document.addEventListener('DOMContentLoaded', initApp);
// ==========================================
// 9. PREVIEW EDITING API
// ==========================================
(function setupPreviewEditingAPI() {
window.addEventListener('message', (event) => {
const msg = event.data;
if (msg?.origin !== 'sekai_gaming_iframe_api') return;
switch (msg.type) {
case 'get_editable_metadata':
window.parent.postMessage({
origin: 'sekai_gaming_iframe_api',
type: 'receive_editable_metadata',
taskId: msg.taskId,
data: window.sekaiEditable || {}
}, '*');
break;
case 'apply_change':
const success = applySingleChange(msg.data.id, msg.data.value, msg.data.name, msg.data.description);
window.parent.postMessage({
origin: 'sekai_gaming_iframe_api',
type: 'receive_apply_change',
taskId: msg.taskId,
data: { success }
}, '*');
// Refresh UI if needed
if(window.appState.gameState === 'select') initSelectScreen();
break;
case 'get_current_html':
window.parent.postMessage({
origin: 'sekai_gaming_iframe_api',
type: 'receive_current_html',
taskId: msg.taskId,
data: { html: document.documentElement.outerHTML }
}, '*');
break;
case 'reset_changes':
location.reload();
break;
}
});
function applySingleChange(id, value, name, description) {
const categories = ['tune', 'images', 'videos', 'music', 'sfx', 'colors', 'text', 'prompts', 'voices'];
for (const cat of categories) {
const item = window.sekaiEditable[cat]?.find(i => i.id === id);
if (!item) continue;
item.value = value;
if (name) item.name = name;
if (description) item.description = description;
if (item.cssVar) {
document.documentElement.style.setProperty(item.cssVar, value);
} else if (item.path) {
setNestedValue(window, item.path, value);
} else if (item.selector) {
const el = document.querySelector(item.selector);
if (!el) return false;
if (el.tagName === 'AUDIO') {
const wasPlaying = !el.paused;
el.src = value;
el.load();
if (wasPlaying) el.play().catch(() => {});
} else if (el.tagName === 'VIDEO') {
el.src = value;
el.load();
} else if (item.property === 'src') {
el.src = value;
} else if (item.property === 'backgroundImage') {
el.style.backgroundImage = `url('${value}')`;
} else if (item.property === 'placeholder') {
el.placeholder = value;
} else {
el.textContent = value;
}
}
return true;
}
return false;
}
})();