// 보스 레이드 웹 제어판 - WebSocket 클라이언트 const WS_PORT = {{WS_PORT}}; let ws = null; let reconnectTimer = null; let currentState = {}; let lastHP = -1; let lastPhase = -1; const RAID_STATE_KR = { 'Idle': '대기', 'Appear': '등장', 'Battle': '전투 중', 'Defeat': '처치', 'Result': '결과', }; // ============ WebSocket ============ function connect() { const host = window.location.hostname || 'localhost'; ws = new WebSocket(`ws://${host}:${WS_PORT}/bossraid`); ws.onopen = () => { setConnectionStatus(true); clearTimeout(reconnectTimer); send('get_state'); addLog('서버 연결됨', 'system'); }; ws.onmessage = (e) => { try { const msg = JSON.parse(e.data); if (msg.type === 'state_update') { handleStateUpdate(msg.data); } } catch (err) { console.error('파싱 오류:', err); } }; ws.onclose = () => { setConnectionStatus(false); scheduleReconnect(); }; ws.onerror = () => { ws.close(); }; } function scheduleReconnect() { clearTimeout(reconnectTimer); reconnectTimer = setTimeout(connect, 2000); } function send(action, params) { if (!ws || ws.readyState !== WebSocket.OPEN) return; const msg = Object.assign({ action }, params || {}); ws.send(JSON.stringify(msg)); } // ============ 상태 업데이트 ============ function handleStateUpdate(data) { const prev = currentState; currentState = data; // 보스 이름 setText('boss-name', data.bossName || '-'); // 레이드 상태 const stateEl = document.getElementById('raid-state'); stateEl.textContent = RAID_STATE_KR[data.raidState] || data.raidState || '대기'; stateEl.className = 'raid-state ' + (data.raidState || '').toLowerCase(); // HP 바 const ratio = data.hpRatio || 0; const fill = document.getElementById('hp-bar-fill'); fill.style.width = (ratio * 100) + '%'; fill.style.backgroundColor = getHPColor(ratio); setText('hp-text', `${data.currentHP} / ${data.maxHP} (${Math.round(ratio * 100)}%)`); // 페이즈 / 상태 setText('phase-name', data.phaseName || '-'); setText('boss-state', data.bossState || '-'); // 일시정지 버튼 const pauseBtn = document.getElementById('pause-btn'); if (data.isPaused) { pauseBtn.textContent = '일시정지 해제'; pauseBtn.classList.add('btn-paused'); pauseBtn.classList.remove('btn-yellow'); } else { pauseBtn.textContent = '일시정지'; pauseBtn.classList.remove('btn-paused'); pauseBtn.classList.add('btn-yellow'); } // HP 잠금 슬라이더 동기화 if (data.hpLockRatio !== undefined) { document.getElementById('hp-lock-slider').value = Math.round(data.hpLockRatio * 100); document.getElementById('hp-lock-value').textContent = Math.round(data.hpLockRatio * 100) + '%'; } // 볼륨 슬라이더 동기화 if (data.bgmVolume !== undefined) { document.getElementById('bgm-volume').value = Math.round(data.bgmVolume * 100); document.getElementById('bgm-volume-value').textContent = Math.round(data.bgmVolume * 100) + '%'; } if (data.sfxVolume !== undefined) { document.getElementById('sfx-volume').value = Math.round(data.sfxVolume * 100); document.getElementById('sfx-volume-value').textContent = Math.round(data.sfxVolume * 100) + '%'; } // 유저 HP const pRatio = data.playerHPRatio || 0; const pFill = document.getElementById('player-hp-fill'); if (pFill) { pFill.style.width = (pRatio * 100) + '%'; pFill.style.backgroundColor = pRatio > 0.3 ? '#2979ff' : '#ff1744'; } setText('player-hp-text', `${data.playerHP || 0} / ${data.playerMaxHP || 0} (${Math.round(pRatio * 100)}%)`); const pState = document.getElementById('player-state'); if (pState) { if (data.playerDead) { pState.textContent = '사망'; pState.style.background = '#ff1744'; } else if (data.playerGodMode) { pState.textContent = '무적'; pState.style.background = '#ffd600'; pState.style.color = '#1a1a2e'; } else { pState.textContent = '생존'; pState.style.background = '#00c853'; pState.style.color = '#fff'; } } // 무적 버튼 const godBtn = document.getElementById('god-mode-btn'); if (godBtn) { if (data.playerGodMode) { godBtn.textContent = '무적 해제'; godBtn.classList.add('btn-paused'); } else { godBtn.textContent = '무적 모드'; godBtn.classList.remove('btn-paused'); } } // 유저 사망 로그 if (data.playerDead && prev.playerDead !== true) { addLog('유저 사망! GAME OVER', 'crit'); } // 데미지 로그 if (lastHP >= 0 && data.currentHP < lastHP && data.raidState === 'Battle') { const dmg = lastHP - data.currentHP; if (dmg > (data.maxHP * 0.03)) { addLog(`크리티컬! -${dmg} 데미지 (HP: ${data.currentHP})`, 'crit'); } else { addLog(`-${dmg} 데미지 (HP: ${data.currentHP})`, 'normal'); } } // 페이즈 전환 로그 if (lastPhase >= 0 && data.phase !== lastPhase) { addLog(`페이즈 전환 → ${data.phaseName}`, 'phase'); } // 사망 로그 if (data.isDead && prev.isDead !== true) { addLog('보스 처치 완료!', 'system'); } // 레이드 상태 변경 로그 if (prev.raidState && data.raidState !== prev.raidState) { const from = RAID_STATE_KR[prev.raidState] || prev.raidState; const to = RAID_STATE_KR[data.raidState] || data.raidState; addLog(`레이드: ${from} → ${to}`, 'system'); } lastHP = data.currentHP; lastPhase = data.phase; } // ============ UI 액션 ============ function manualHit(isCritical) { const dmg = parseInt(document.getElementById('manual-damage').value) || 50; send('manual_hit', { damage: dmg, critical: isCritical }); } function setHP() { const val = parseInt(document.getElementById('hp-slider').value) / 100; send('set_hp', { ratio: val }); } function hpSliderChanged() { const val = document.getElementById('hp-slider').value; document.getElementById('hp-slider-value').textContent = val + '%'; } function toggleGodMode() { const isGod = currentState.playerGodMode || false; send('toggle_god_mode', { value: !isGod }); } function setCooldown() { const val = parseFloat(document.getElementById('cooldown').value) || 0.3; send('set_cooldown', { value: val }); addLog(`쿨타임 설정: ${val}초`, 'system'); } function setDamageCap() { const val = parseInt(document.getElementById('damage-cap').value) || 0; send('set_damage_cap', { value: val }); addLog(`데미지 캡: ${val === 0 ? '해제' : val}`, 'system'); } function setHPLock() { const val = parseInt(document.getElementById('hp-lock-slider').value) / 100; send('set_hp_lock', { ratio: val }); addLog(`HP 잠금: ${Math.round(val * 100)}%`, 'system'); } function hpLockChanged() { const val = document.getElementById('hp-lock-slider').value; document.getElementById('hp-lock-value').textContent = val + '%'; } function bgmVolumeChanged() { const val = parseInt(document.getElementById('bgm-volume').value); document.getElementById('bgm-volume-value').textContent = val + '%'; send('set_bgm_volume', { value: val / 100 }); } function sfxVolumeChanged() { const val = parseInt(document.getElementById('sfx-volume').value); document.getElementById('sfx-volume-value').textContent = val + '%'; send('set_sfx_volume', { value: val / 100 }); } function confirmKill() { if (confirm('보스를 강제 처치하시겠습니까?')) { send('force_kill'); addLog('강제 처치 실행', 'system'); } } // ============ 헬퍼 ============ function setText(id, text) { const el = document.getElementById(id); if (el) el.textContent = text; } function setConnectionStatus(connected) { const bar = document.getElementById('connection-bar'); const status = document.getElementById('connection-status'); if (connected) { bar.className = 'connected'; status.textContent = '연결됨'; status.style.color = '#00c853'; } else { bar.className = 'disconnected'; status.textContent = '연결 끊김 — 재연결 중...'; status.style.color = '#ff1744'; } } function getHPColor(ratio) { if (ratio > 0.5) return '#00c853'; if (ratio > 0.2) return '#ffd600'; return '#ff1744'; } function addLog(text, type) { const log = document.getElementById('hit-log'); const entry = document.createElement('div'); entry.className = 'log-entry-' + type; const time = new Date().toLocaleTimeString('ko-KR', { hour12: false }); entry.textContent = `[${time}] ${text}`; log.prepend(entry); while (log.children.length > 100) { log.removeChild(log.lastChild); } } // ============ 초기화 ============ setConnectionStatus(false); connect();