Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="ko">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>모비노기 미터기 - 글기 데미지 분리</title>
- <style>
- body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background-color: #1a1a1a;
- color: #ffffff;
- margin: 0;
- padding: 20px;
- user-select: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- }
- .container {
- max-width: 800px;
- margin: 0 auto;
- padding: 0 20px;
- }
- .header {
- text-align: center;
- margin-bottom: 30px;
- padding: 20px;
- background: linear-gradient(135deg, #2c3e50, #34495e);
- border-radius: 10px;
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
- }
- .header h1 {
- margin: 0;
- font-size: 2.5em;
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
- }
- .battle-info {
- display: flex;
- justify-content: space-around;
- margin-top: 15px;
- font-size: 1.1em;
- }
- .player-rankings {
- background-color: #2c2c2c;
- border-radius: 10px;
- padding: 20px 30px;
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
- }
- .ranking-header {
- display: grid;
- grid-template-columns: 60px 1fr 240px; /* Damage column width adjusted */
- gap: 15px;
- padding: 15px 20px;
- background-color: #34495e;
- border-radius: 8px;
- margin-bottom: 20px;
- font-weight: bold;
- font-size: 0.9em;
- color: #ecf0f1;
- }
- .ranking-header > div:last-child {
- text-align: right;
- }
- .player-bar {
- margin-bottom: 12px;
- position: relative;
- background-color: #4a4a4a;
- border-radius: 8px;
- overflow: visible;
- height: 50px;
- transition: all 0.3s ease;
- cursor: pointer;
- }
- .player-bar-content {
- display: grid;
- grid-template-columns: 60px 1fr 240px; /* Damage column width adjusted */
- gap: 15px;
- align-items: center;
- position: relative;
- z-index: 2;
- height: 100%;
- padding: 0 7px;
- }
- .player-bar:hover {
- transform: translateX(5px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
- }
- .rank-section {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 10px;
- }
- .rank-number {
- background-color: #34495e;
- color: #ecf0f1;
- border-radius: 50%;
- width: 35px;
- height: 35px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.1em;
- font-weight: bold;
- }
- .rank-1 { background: linear-gradient(135deg, #f1c40f, #f39c12); color: #2c3e50; }
- .rank-2 { background: linear-gradient(135deg, #95a5a6, #bdc3c7); color: #2c3e50; }
- .rank-3 { background: linear-gradient(135deg, #e67e22, #d35400); color: white; }
- /* --- NEW: Stacked Bar Styles --- */
- .player-bar-fill-wrapper {
- position: absolute;
- top: 0;
- left: 0;
- height: 100%;
- display: flex; /* Use flexbox for stacking */
- transition: width 0.5s ease;
- border-radius: 8px;
- z-index: 1;
- overflow: hidden; /* Hide overflow from child elements */
- }
- .damage-segment {
- height: 100%;
- transition: width 0.5s ease;
- }
- .other-targets-damage {
- /* Make "other" damage slightly transparent or darker */
- filter: brightness(0.7);
- }
- /* --- End of New Styles --- */
- .player-name {
- font-weight: bold;
- font-size: 1em;
- text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
- color: #ffffff;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- padding-left: 15px;
- display: flex;
- align-items: center;
- }
- .stat-value {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- font-weight: bold;
- font-size: 0.95em;
- text-align: right;
- padding: 0 5px;
- gap: 4px; /* Space between damage numbers and DPS */
- }
- .damage-value .plus-sign {
- color: #bdc3c7;
- font-weight: normal;
- }
- .damage-value .total-dps {
- color: #f39c12; /* Highlight DPS */
- font-weight: normal;
- margin-left: 5px;
- }
- .connection-status {
- position: fixed;
- top: 20px;
- right: 20px;
- padding: 10px 15px;
- border-radius: 5px;
- font-weight: bold;
- z-index: 1000;
- }
- .connected {
- background-color: #27ae60;
- color: white;
- }
- .disconnected {
- background-color: #e74c3c;
- color: white;
- }
- .no-data {
- text-align: center;
- color: #7f8c8d;
- font-size: 1.2em;
- margin: 50px 0;
- }
- /* -- Skill Breakdown (mostly unchanged) -- */
- .skill-breakdown {
- background-color: #3a3a3a;
- border-radius: 8px;
- margin-top: 10px;
- padding: 15px;
- border-left: 4px solid #3498db;
- display: none;
- }
- .skill-breakdown.show {
- display: block;
- }
- .skill-breakdown.show.animate {
- animation: slideDown 0.3s ease-out;
- }
- @keyframes slideDown {
- from { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; }
- to { opacity: 1; max-height: 500px; padding-top: 15px; padding-bottom: 15px; }
- }
- .skill-breakdown-header {
- font-size: 1.1em;
- font-weight: bold;
- margin-bottom: 15px;
- color: #3498db;
- }
- .skill-item {
- margin-bottom: 8px;
- position: relative;
- background-color: #4a4a4a;
- border-radius: 4px;
- overflow: visible;
- height: 30px;
- display: flex;
- align-items: center;
- }
- .skill-item-fill {
- height: 100%;
- background: linear-gradient(90deg, #e74c3c, #c0392b);
- transition: width 0.3s ease;
- border-radius: 4px;
- position: relative;
- }
- .skill-item-info {
- position: absolute;
- left: 10px; top: 50%;
- transform: translateY(-50%);
- font-size: 0.85em; font-weight: bold;
- text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
- z-index: 2; color: #ffffff;
- }
- .skill-item-stats {
- position: absolute;
- right: 10px; top: 50%;
- transform: translateY(-50%);
- font-size: 0.85em; font-weight: bold;
- text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
- color: #ffffff; z-index: 3;
- background: rgba(0, 0, 0, 0.3);
- padding: 2px 6px;
- border-radius: 3px;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>👥 모비노기 미터기 - 플레이어 랭킹</h1>
- <div class="battle-info">
- <span>전투 시간: <span id="battle-time">0초</span></span>
- <span>총 데미지: <span id="total-damage">0</span></span>
- <span>RDPS: <span id="raid-dps">0</span></span>
- <span>참여 플레이어: <span id="player-count">0</span></span>
- </div>
- </div>
- <div class="connection-status disconnected" id="connection-status">
- 연결 끊김
- </div>
- <div class="player-rankings" id="player-rankings">
- </div>
- </div>
- <script>
- let ws = null;
- let battleData = {};
- let startTime = 0;
- let lastUpdateTime = 0;
- let totalBattleDamage = 0;
- let expandedPlayers = new Set();
- const classMapping = {
- 'SwordMaster': { name: '검술', color: 'linear-gradient(90deg, #8B4513, #A0522D)' },
- 'Arbalist': { name: '석궁사수', color: 'linear-gradient(90deg, #228B22, #32CD32)' },
- 'FireMage': { name: '화법', color: 'linear-gradient(90deg, #FF4500, #FF6347)' },
- 'IceMage': { name: '얼탱', color: 'linear-gradient(90deg, #4169E1, #87CEEB)' },
- 'Fighter': { name: '격투가', color: 'linear-gradient(90deg, #DC143C, #B22222)' },
- 'LongBowMan': { name: '장궁병', color: 'linear-gradient(90deg, #2E8B57, #3CB371)' },
- 'Healer': { name: '힐러', color: 'linear-gradient(90deg, #FFD700, #FFA500)' },
- 'ExpertWarrior': { name: '전사', color: 'linear-gradient(90deg, #696969, #808080)' },
- 'GreatSwordWarrior': { name: '대검전사', color: 'linear-gradient(90deg, #4B0082, #6A5ACD)' },
- 'HighThief': { name: '도적', color: 'linear-gradient(90deg, #2F4F4F, #708090)' },
- 'DualBlades': { name: '듀블', color: 'linear-gradient(90deg, #8B008B, #DA70D6)' },
- 'HighArcher': { name: '궁수', color: 'linear-gradient(90deg, #006400, #228B22)' },
- 'HighMage': { name: '마법사', color: 'linear-gradient(90deg, #4B0082, #9400D3)' },
- 'Priest': { name: '사제', color: 'linear-gradient(90deg, #F0F8FF, #E6E6FA)' },
- 'Bard': { name: '음유시인', color: 'linear-gradient(90deg, #FF69B4, #FFB6C1)' },
- 'Monk': { name: '수도사', color: 'linear-gradient(90deg, #CD853F, #D2691E)' },
- 'Dancer': { name: '댄서', color: 'linear-gradient(90deg, #FF1493, #FF69B4)' },
- 'BattleMusician': { name: '악사', color: 'linear-gradient(90deg, #9932CC, #BA55D3)' }
- };
- function detectPlayerClass(userData) {
- if (!userData.skills) return null;
- for (const skillName of Object.keys(userData.skills)) {
- for (const [classCode, classInfo] of Object.entries(classMapping)) {
- if (skillName.includes(classCode)) {
- return classCode;
- }
- }
- }
- return null;
- }
- function connectWebSocket() {
- try {
- ws = new WebSocket("ws://localhost:8000");
- ws.onopen = function(evt) {
- console.log("웹소켓 연결됨");
- updateConnectionStatus(true);
- };
- ws.onclose = function(evt) {
- console.log("웹소켓 연결 해제됨");
- updateConnectionStatus(false);
- setTimeout(connectWebSocket, 3000);
- };
- ws.onmessage = function(evt) {
- processDamageData(evt.data);
- };
- ws.onerror = function(evt) {
- console.error("웹소켓 오류:", evt);
- updateConnectionStatus(false);
- };
- } catch (error) {
- console.error("웹소켓 연결 실패:", error);
- updateConnectionStatus(false);
- setTimeout(connectWebSocket, 3000);
- }
- }
- function updateConnectionStatus(connected) {
- const statusElement = document.getElementById('connection-status');
- if (connected) {
- statusElement.textContent = '연결됨';
- statusElement.className = 'connection-status connected';
- } else {
- statusElement.textContent = '연결 끊김';
- statusElement.className = 'connection-status disconnected';
- }
- }
- function processDamageData(data) {
- const parts = data.split('|');
- if (parts.length !== 7) return;
- const timestamp = parseInt(parts[0]);
- const userId = parts[1];
- const target = parts[2];
- const skill = parts[3];
- const damage = parseInt(parts[4]);
- const critFlag = parseInt(parts[5]);
- const addHitFlag = parseInt(parts[6]);
- if (lastUpdateTime === 0 || timestamp - lastUpdateTime > 10000) {
- startNewBattle(timestamp);
- }
- lastUpdateTime = timestamp;
- // Initialize user data if needed
- if (!battleData[userId]) {
- battleData[userId] = {
- totalDamage: 0,
- damageByTarget: {}, // MODIFIED: Track damage per target
- hitCount: 0,
- critCount: 0,
- addHitCount: 0,
- skills: {}
- };
- }
- // Initialize target-specific damage if needed
- if (!battleData[userId].damageByTarget[target]) {
- battleData[userId].damageByTarget[target] = 0;
- }
- // Initialize skill data if needed
- if (!battleData[userId].skills[skill]) {
- battleData[userId].skills[skill] = {
- damage: 0,
- hits: 0,
- crits: 0,
- addHits: 0
- };
- }
- // Update data
- battleData[userId].totalDamage += damage;
- battleData[userId].damageByTarget[target] += damage; // MODIFIED: Add damage to specific target
- battleData[userId].skills[skill].damage += damage; // Keep total skill damage for breakdown
- if (!addHitFlag) {
- battleData[userId].hitCount += 1;
- battleData[userId].skills[skill].hits += 1;
- }
- if (critFlag) {
- battleData[userId].critCount += 1;
- battleData[userId].skills[skill].crits += 1;
- }
- if (addHitFlag) {
- battleData[userId].addHitCount += 1;
- battleData[userId].skills[skill].addHits += 1;
- }
- totalBattleDamage += damage;
- }
- function startNewBattle(timestamp) {
- console.log("새로운 전투 시작");
- battleData = {};
- startTime = timestamp;
- totalBattleDamage = 0;
- expandedPlayers.clear();
- updateDisplay();
- }
- function updateDisplay() {
- updateBattleInfo();
- updatePlayerRankings();
- }
- function updateBattleInfo() {
- const battleTime = lastUpdateTime > 0 ? Math.floor((lastUpdateTime - startTime) / 1000) : 0;
- const playerCount = Object.values(battleData).filter(ud => detectPlayerClass(ud)).length;
- let raidDps = 0;
- if (battleTime > 0) {
- raidDps = Math.floor(totalBattleDamage / battleTime);
- }
- document.getElementById('battle-time').textContent = `${battleTime}초`;
- document.getElementById('total-damage').textContent = totalBattleDamage.toLocaleString();
- document.getElementById('raid-dps').textContent = raidDps.toLocaleString();
- document.getElementById('player-count').textContent = playerCount;
- }
- function updatePlayerRankings() {
- const container = document.getElementById('player-rankings');
- if (Object.keys(battleData).length === 0) {
- container.innerHTML = `<div class="ranking-header"><div>순위</div><div>플레이어</div><div>데미지 (DPS)</div></div><div class="no-data">데미지 데이터를 기다리는 중...</div>`;
- return;
- }
- const sortedPlayers = Object.entries(battleData).sort(([,a], [,b]) => b.totalDamage - a.totalDamage);
- const maxDamage = sortedPlayers.length > 0 ? sortedPlayers[0][1].totalDamage : 1;
- const battleTime = lastUpdateTime > 0 ? Math.max(1, (lastUpdateTime - startTime) / 1000) : 1;
- let html = `<div class="ranking-header"><div>순위</div><div>플레이어</div><div>데미지 (DPS)</div></div>`;
- let displayedPlayerIndex = 0;
- sortedPlayers.forEach(([userId, userData]) => {
- const detectedClass = detectPlayerClass(userData);
- if (!detectedClass || !classMapping[detectedClass]) {
- return;
- }
- // --- NEW: Calculate damage split ---
- let mainTargetDamage = 0;
- let otherTargetsDamage = 0;
- const targetDamages = Object.values(userData.damageByTarget);
- if (targetDamages.length > 0) {
- mainTargetDamage = Math.max(...targetDamages);
- }
- otherTargetsDamage = userData.totalDamage - mainTargetDamage;
- const mainDamagePercentOfTotal = userData.totalDamage > 0 ? (mainTargetDamage / userData.totalDamage) * 100 : 0;
- const otherDamagePercentOfTotal = 100 - mainDamagePercentOfTotal;
- // --- End of new calculation ---
- const playerDps = Math.floor(userData.totalDamage / battleTime);
- const barWidth = (userData.totalDamage / maxDamage) * 100;
- const barColor = classMapping[detectedClass].color;
- const className = classMapping[detectedClass].name;
- const rankClass = displayedPlayerIndex < 3 ? `rank-${displayedPlayerIndex + 1}` : '';
- const displayName = `${className} ${userId.substring(0, 8)}`;
- html += `
- <div class="player-bar" onclick="toggleSkillBreakdown('${userId}')">
- <div class="player-bar-fill-wrapper" style="width: ${barWidth}%;">
- <div class="damage-segment main-target-damage" style="width: ${mainDamagePercentOfTotal}%; background: ${barColor};"></div>
- <div class="damage-segment other-targets-damage" style="width: ${otherDamagePercentOfTotal}%; background: ${barColor};"></div>
- </div>
- <div class="player-bar-content">
- <div class="rank-section">
- <div class="rank-number ${rankClass}">${displayedPlayerIndex + 1}</div>
- </div>
- <div class="player-name">
- ${displayName}
- </div>
- <div class="stat-value damage-value">
- ${mainTargetDamage.toLocaleString()}
- <span class="plus-sign">+</span>
- ${otherTargetsDamage.toLocaleString()}
- <span class="total-dps">(${playerDps.toLocaleString()})</span>
- </div>
- </div>
- </div>
- <div class="skill-breakdown ${expandedPlayers.has(userId) ? 'show' : ''}" id="skills-${userId}">
- ${generateSkillBreakdown(userId, userData)}
- </div>
- `;
- displayedPlayerIndex++;
- });
- if (displayedPlayerIndex === 0) {
- html += `<div class="no-data">감지된 플레이어 데이터가 없습니다.</div>`;
- }
- container.innerHTML = html;
- }
- function generateSkillBreakdown(userId, userData) {
- // This breakdown currently shows total skill damage across all targets.
- // A more advanced version might track skills per target.
- if (!userData.skills || Object.keys(userData.skills).length === 0) {
- return '<div style="color: #7f8c8d; text-align: center; padding: 20px;">스킬 데이터가 없습니다.</div>';
- }
- const sortedSkills = Object.entries(userData.skills).sort(([,a], [,b]) => b.damage - a.damage);
- const maxSkillDamage = sortedSkills.length > 0 ? sortedSkills[0][1].damage : 1;
- let skillHtml = `<div class="skill-breakdown-header">유저 ${userId.substring(0, 8)} - 스킬 상세 (전체 타겟 합산)</div>`;
- sortedSkills.forEach(([skillName, skillData]) => {
- const skillPercentage = userData.totalDamage > 0 ? ((skillData.damage / userData.totalDamage) * 100).toFixed(1) : 0;
- const skillTotalHits = skillData.hits + skillData.addHits;
- const skillCritRate = skillTotalHits > 0 ? ((skillData.crits / skillTotalHits) * 100).toFixed(1) : 0;
- const barWidth = (skillData.damage / maxSkillDamage) * 100;
- skillHtml += `
- <div class="skill-item">
- <div class="skill-item-fill" style="width: ${barWidth}%">
- <div class="skill-item-info">${skillName}</div>
- </div>
- <div class="skill-item-stats">
- 타수 ${skillData.hits} | 크리 ${skillCritRate}% | 추가타율 ${skillData.hits > 0 ? ((skillData.addHits / skillData.hits) * 100).toFixed(1) : 0}% | ${skillData.damage.toLocaleString()} (${skillPercentage}%)
- </div>
- </div>
- `;
- });
- return skillHtml;
- }
- function toggleSkillBreakdown(userId) {
- const skillBreakdown = document.getElementById(`skills-${userId}`);
- if (!skillBreakdown) return;
- if (expandedPlayers.has(userId)) {
- expandedPlayers.delete(userId);
- skillBreakdown.classList.remove('show');
- } else {
- // Collapse others first for single-expansion mode
- document.querySelectorAll('.skill-breakdown.show').forEach(el => el.classList.remove('show'));
- expandedPlayers.clear();
- expandedPlayers.add(userId);
- // Update content before showing to ensure it's fresh
- const userData = battleData[userId];
- if (userData) {
- skillBreakdown.innerHTML = generateSkillBreakdown(userId, userData);
- }
- skillBreakdown.classList.add('show', 'animate');
- setTimeout(() => skillBreakdown.classList.remove('animate'), 300);
- }
- }
- // Initialize
- connectWebSocket();
- setInterval(updateDisplay, 1000);
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement