Advertisement
bblkk

MDM

Jun 6th, 2025 (edited)
1,514
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 23.43 KB | Source Code | 0 0
  1. <!DOCTYPE html>
  2. <html lang="ko">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>모비노기 미터기 - 글기 데미지 분리</title>
  7.     <style>
  8.         body {
  9.             font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  10.             background-color: #1a1a1a;
  11.             color: #ffffff;
  12.             margin: 0;
  13.             padding: 20px;
  14.             user-select: none;
  15.             -webkit-user-select: none;
  16.             -moz-user-select: none;
  17.             -ms-user-select: none;
  18.         }
  19.  
  20.         .container {
  21.             max-width: 800px;
  22.             margin: 0 auto;
  23.             padding: 0 20px;
  24.         }
  25.  
  26.         .header {
  27.             text-align: center;
  28.             margin-bottom: 30px;
  29.             padding: 20px;
  30.             background: linear-gradient(135deg, #2c3e50, #34495e);
  31.             border-radius: 10px;
  32.             box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
  33.         }
  34.  
  35.         .header h1 {
  36.             margin: 0;
  37.             font-size: 2.5em;
  38.             text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
  39.         }
  40.  
  41.         .battle-info {
  42.             display: flex;
  43.             justify-content: space-around;
  44.             margin-top: 15px;
  45.             font-size: 1.1em;
  46.         }
  47.  
  48.         .player-rankings {
  49.             background-color: #2c2c2c;
  50.             border-radius: 10px;
  51.             padding: 20px 30px;
  52.             box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
  53.         }
  54.  
  55.         .ranking-header {
  56.             display: grid;
  57.             grid-template-columns: 60px 1fr 240px; /* Damage column width adjusted */
  58.             gap: 15px;
  59.             padding: 15px 20px;
  60.             background-color: #34495e;
  61.             border-radius: 8px;
  62.             margin-bottom: 20px;
  63.             font-weight: bold;
  64.             font-size: 0.9em;
  65.             color: #ecf0f1;
  66.         }
  67.  
  68.         .ranking-header > div:last-child {
  69.             text-align: right;
  70.         }
  71.  
  72.         .player-bar {
  73.             margin-bottom: 12px;
  74.             position: relative;
  75.             background-color: #4a4a4a;
  76.             border-radius: 8px;
  77.             overflow: visible;
  78.             height: 50px;
  79.             transition: all 0.3s ease;
  80.             cursor: pointer;
  81.         }
  82.  
  83.         .player-bar-content {
  84.             display: grid;
  85.             grid-template-columns: 60px 1fr 240px; /* Damage column width adjusted */
  86.             gap: 15px;
  87.             align-items: center;
  88.             position: relative;
  89.             z-index: 2;
  90.             height: 100%;
  91.             padding: 0 7px;
  92.         }
  93.  
  94.         .player-bar:hover {
  95.             transform: translateX(5px);
  96.             box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
  97.         }
  98.  
  99.         .rank-section {
  100.             display: flex;
  101.             align-items: center;
  102.             justify-content: center;
  103.             padding: 0 10px;
  104.         }
  105.  
  106.         .rank-number {
  107.             background-color: #34495e;
  108.             color: #ecf0f1;
  109.             border-radius: 50%;
  110.             width: 35px;
  111.             height: 35px;
  112.             display: flex;
  113.             align-items: center;
  114.             justify-content: center;
  115.             font-size: 1.1em;
  116.             font-weight: bold;
  117.         }
  118.  
  119.         .rank-1 { background: linear-gradient(135deg, #f1c40f, #f39c12); color: #2c3e50; }
  120.         .rank-2 { background: linear-gradient(135deg, #95a5a6, #bdc3c7); color: #2c3e50; }
  121.         .rank-3 { background: linear-gradient(135deg, #e67e22, #d35400); color: white; }
  122.  
  123.         /* --- NEW: Stacked Bar Styles --- */
  124.         .player-bar-fill-wrapper {
  125.             position: absolute;
  126.             top: 0;
  127.             left: 0;
  128.             height: 100%;
  129.             display: flex; /* Use flexbox for stacking */
  130.             transition: width 0.5s ease;
  131.             border-radius: 8px;
  132.             z-index: 1;
  133.             overflow: hidden; /* Hide overflow from child elements */
  134.         }
  135.  
  136.         .damage-segment {
  137.             height: 100%;
  138.             transition: width 0.5s ease;
  139.         }
  140.  
  141.         .other-targets-damage {
  142.             /* Make "other" damage slightly transparent or darker */
  143.             filter: brightness(0.7);
  144.         }
  145.         /* --- End of New Styles --- */
  146.  
  147.         .player-name {
  148.             font-weight: bold;
  149.             font-size: 1em;
  150.             text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
  151.             color: #ffffff;
  152.             white-space: nowrap;
  153.             overflow: hidden;
  154.             text-overflow: ellipsis;
  155.             padding-left: 15px;
  156.             display: flex;
  157.             align-items: center;
  158.         }
  159.  
  160.         .stat-value {
  161.             display: flex;
  162.             align-items: center;
  163.             justify-content: flex-end;
  164.             font-weight: bold;
  165.             font-size: 0.95em;
  166.             text-align: right;
  167.             padding: 0 5px;
  168.             gap: 4px; /* Space between damage numbers and DPS */
  169.         }
  170.  
  171.         .damage-value .plus-sign {
  172.             color: #bdc3c7;
  173.             font-weight: normal;
  174.         }
  175.        
  176.         .damage-value .total-dps {
  177.             color: #f39c12; /* Highlight DPS */
  178.             font-weight: normal;
  179.             margin-left: 5px;
  180.         }
  181.  
  182.         .connection-status {
  183.             position: fixed;
  184.             top: 20px;
  185.             right: 20px;
  186.             padding: 10px 15px;
  187.             border-radius: 5px;
  188.             font-weight: bold;
  189.             z-index: 1000;
  190.         }
  191.  
  192.         .connected {
  193.             background-color: #27ae60;
  194.             color: white;
  195.         }
  196.  
  197.         .disconnected {
  198.             background-color: #e74c3c;
  199.             color: white;
  200.         }
  201.  
  202.         .no-data {
  203.             text-align: center;
  204.             color: #7f8c8d;
  205.             font-size: 1.2em;
  206.             margin: 50px 0;
  207.         }
  208.        
  209.         /* -- Skill Breakdown (mostly unchanged) -- */
  210.         .skill-breakdown {
  211.             background-color: #3a3a3a;
  212.             border-radius: 8px;
  213.             margin-top: 10px;
  214.             padding: 15px;
  215.             border-left: 4px solid #3498db;
  216.             display: none;
  217.         }
  218.  
  219.         .skill-breakdown.show {
  220.             display: block;
  221.         }
  222.  
  223.         .skill-breakdown.show.animate {
  224.             animation: slideDown 0.3s ease-out;
  225.         }
  226.  
  227.         @keyframes slideDown {
  228.             from { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; }
  229.             to { opacity: 1; max-height: 500px; padding-top: 15px; padding-bottom: 15px; }
  230.         }
  231.  
  232.         .skill-breakdown-header {
  233.             font-size: 1.1em;
  234.             font-weight: bold;
  235.             margin-bottom: 15px;
  236.             color: #3498db;
  237.         }
  238.  
  239.         .skill-item {
  240.             margin-bottom: 8px;
  241.             position: relative;
  242.             background-color: #4a4a4a;
  243.             border-radius: 4px;
  244.             overflow: visible;
  245.             height: 30px;
  246.             display: flex;
  247.             align-items: center;
  248.         }
  249.  
  250.         .skill-item-fill {
  251.             height: 100%;
  252.             background: linear-gradient(90deg, #e74c3c, #c0392b);
  253.             transition: width 0.3s ease;
  254.             border-radius: 4px;
  255.             position: relative;
  256.         }
  257.  
  258.         .skill-item-info {
  259.             position: absolute;
  260.             left: 10px; top: 50%;
  261.             transform: translateY(-50%);
  262.             font-size: 0.85em; font-weight: bold;
  263.             text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
  264.             z-index: 2; color: #ffffff;
  265.         }
  266.  
  267.         .skill-item-stats {
  268.             position: absolute;
  269.             right: 10px; top: 50%;
  270.             transform: translateY(-50%);
  271.             font-size: 0.85em; font-weight: bold;
  272.             text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
  273.             color: #ffffff; z-index: 3;
  274.             background: rgba(0, 0, 0, 0.3);
  275.             padding: 2px 6px;
  276.             border-radius: 3px;
  277.         }
  278.     </style>
  279. </head>
  280. <body>
  281.     <div class="container">
  282.         <div class="header">
  283.             <h1>👥 모비노기 미터기 - 플레이어 랭킹</h1>
  284.             <div class="battle-info">
  285.                 <span>전투 시간: <span id="battle-time">0초</span></span>
  286.                 <span>총 데미지: <span id="total-damage">0</span></span>
  287.                 <span>RDPS: <span id="raid-dps">0</span></span>
  288.                 <span>참여 플레이어: <span id="player-count">0</span></span>
  289.             </div>
  290.         </div>
  291.  
  292.         <div class="connection-status disconnected" id="connection-status">
  293.             연결 끊김
  294.         </div>
  295.  
  296.         <div class="player-rankings" id="player-rankings">
  297.             </div>
  298.     </div>
  299.  
  300.     <script>
  301.         let ws = null;
  302.         let battleData = {};
  303.         let startTime = 0;
  304.         let lastUpdateTime = 0;
  305.         let totalBattleDamage = 0;
  306.         let expandedPlayers = new Set();
  307.  
  308.         const classMapping = {
  309.             'SwordMaster': { name: '검술', color: 'linear-gradient(90deg, #8B4513, #A0522D)' },
  310.             'Arbalist': { name: '석궁사수', color: 'linear-gradient(90deg, #228B22, #32CD32)' },
  311.             'FireMage': { name: '화법', color: 'linear-gradient(90deg, #FF4500, #FF6347)' },
  312.             'IceMage': { name: '얼탱', color: 'linear-gradient(90deg, #4169E1, #87CEEB)' },
  313.             'Fighter': { name: '격투가', color: 'linear-gradient(90deg, #DC143C, #B22222)' },
  314.             'LongBowMan': { name: '장궁병', color: 'linear-gradient(90deg, #2E8B57, #3CB371)' },
  315.             'Healer': { name: '힐러', color: 'linear-gradient(90deg, #FFD700, #FFA500)' },
  316.             'ExpertWarrior': { name: '전사', color: 'linear-gradient(90deg, #696969, #808080)' },
  317.             'GreatSwordWarrior': { name: '대검전사', color: 'linear-gradient(90deg, #4B0082, #6A5ACD)' },
  318.             'HighThief': { name: '도적', color: 'linear-gradient(90deg, #2F4F4F, #708090)' },
  319.             'DualBlades': { name: '듀블', color: 'linear-gradient(90deg, #8B008B, #DA70D6)' },
  320.             'HighArcher': { name: '궁수', color: 'linear-gradient(90deg, #006400, #228B22)' },
  321.             'HighMage': { name: '마법사', color: 'linear-gradient(90deg, #4B0082, #9400D3)' },
  322.             'Priest': { name: '사제', color: 'linear-gradient(90deg, #F0F8FF, #E6E6FA)' },
  323.             'Bard': { name: '음유시인', color: 'linear-gradient(90deg, #FF69B4, #FFB6C1)' },
  324.             'Monk': { name: '수도사', color: 'linear-gradient(90deg, #CD853F, #D2691E)' },
  325.             'Dancer': { name: '댄서', color: 'linear-gradient(90deg, #FF1493, #FF69B4)' },
  326.             'BattleMusician': { name: '악사', color: 'linear-gradient(90deg, #9932CC, #BA55D3)' }
  327.         };
  328.  
  329.         function detectPlayerClass(userData) {
  330.             if (!userData.skills) return null;
  331.  
  332.             for (const skillName of Object.keys(userData.skills)) {
  333.                 for (const [classCode, classInfo] of Object.entries(classMapping)) {
  334.                     if (skillName.includes(classCode)) {
  335.                         return classCode;
  336.                     }
  337.                 }
  338.             }
  339.             return null;
  340.         }
  341.  
  342.         function connectWebSocket() {
  343.             try {
  344.                 ws = new WebSocket("ws://localhost:8000");
  345.  
  346.                 ws.onopen = function(evt) {
  347.                     console.log("웹소켓 연결됨");
  348.                     updateConnectionStatus(true);
  349.                 };
  350.  
  351.                 ws.onclose = function(evt) {
  352.                     console.log("웹소켓 연결 해제됨");
  353.                     updateConnectionStatus(false);
  354.                     setTimeout(connectWebSocket, 3000);
  355.                 };
  356.  
  357.                 ws.onmessage = function(evt) {
  358.                     processDamageData(evt.data);
  359.                 };
  360.  
  361.                 ws.onerror = function(evt) {
  362.                     console.error("웹소켓 오류:", evt);
  363.                     updateConnectionStatus(false);
  364.                 };
  365.             } catch (error) {
  366.                 console.error("웹소켓 연결 실패:", error);
  367.                 updateConnectionStatus(false);
  368.                 setTimeout(connectWebSocket, 3000);
  369.             }
  370.         }
  371.  
  372.         function updateConnectionStatus(connected) {
  373.             const statusElement = document.getElementById('connection-status');
  374.             if (connected) {
  375.                 statusElement.textContent = '연결됨';
  376.                 statusElement.className = 'connection-status connected';
  377.             } else {
  378.                 statusElement.textContent = '연결 끊김';
  379.                 statusElement.className = 'connection-status disconnected';
  380.             }
  381.         }
  382.  
  383.         function processDamageData(data) {
  384.             const parts = data.split('|');
  385.             if (parts.length !== 7) return;
  386.  
  387.             const timestamp = parseInt(parts[0]);
  388.             const userId = parts[1];
  389.             const target = parts[2];
  390.             const skill = parts[3];
  391.             const damage = parseInt(parts[4]);
  392.             const critFlag = parseInt(parts[5]);
  393.             const addHitFlag = parseInt(parts[6]);
  394.  
  395.             if (lastUpdateTime === 0 || timestamp - lastUpdateTime > 10000) {
  396.                 startNewBattle(timestamp);
  397.             }
  398.  
  399.             lastUpdateTime = timestamp;
  400.  
  401.             // Initialize user data if needed
  402.             if (!battleData[userId]) {
  403.                 battleData[userId] = {
  404.                     totalDamage: 0,
  405.                     damageByTarget: {}, // MODIFIED: Track damage per target
  406.                     hitCount: 0,
  407.                     critCount: 0,
  408.                     addHitCount: 0,
  409.                     skills: {}
  410.                 };
  411.             }
  412.  
  413.             // Initialize target-specific damage if needed
  414.             if (!battleData[userId].damageByTarget[target]) {
  415.                 battleData[userId].damageByTarget[target] = 0;
  416.             }
  417.  
  418.             // Initialize skill data if needed
  419.             if (!battleData[userId].skills[skill]) {
  420.                 battleData[userId].skills[skill] = {
  421.                     damage: 0,
  422.                     hits: 0,
  423.                     crits: 0,
  424.                     addHits: 0
  425.                 };
  426.             }
  427.  
  428.             // Update data
  429.             battleData[userId].totalDamage += damage;
  430.             battleData[userId].damageByTarget[target] += damage; // MODIFIED: Add damage to specific target
  431.             battleData[userId].skills[skill].damage += damage; // Keep total skill damage for breakdown
  432.  
  433.             if (!addHitFlag) {
  434.                 battleData[userId].hitCount += 1;
  435.                 battleData[userId].skills[skill].hits += 1;
  436.             }
  437.  
  438.             if (critFlag) {
  439.                 battleData[userId].critCount += 1;
  440.                 battleData[userId].skills[skill].crits += 1;
  441.             }
  442.  
  443.             if (addHitFlag) {
  444.                 battleData[userId].addHitCount += 1;
  445.                 battleData[userId].skills[skill].addHits += 1;
  446.             }
  447.  
  448.             totalBattleDamage += damage;
  449.         }
  450.  
  451.         function startNewBattle(timestamp) {
  452.             console.log("새로운 전투 시작");
  453.             battleData = {};
  454.             startTime = timestamp;
  455.             totalBattleDamage = 0;
  456.             expandedPlayers.clear();
  457.             updateDisplay();
  458.         }
  459.  
  460.         function updateDisplay() {
  461.             updateBattleInfo();
  462.             updatePlayerRankings();
  463.         }
  464.  
  465.         function updateBattleInfo() {
  466.             const battleTime = lastUpdateTime > 0 ? Math.floor((lastUpdateTime - startTime) / 1000) : 0;
  467.             const playerCount = Object.values(battleData).filter(ud => detectPlayerClass(ud)).length;
  468.  
  469.             let raidDps = 0;
  470.             if (battleTime > 0) {
  471.                 raidDps = Math.floor(totalBattleDamage / battleTime);
  472.             }
  473.  
  474.             document.getElementById('battle-time').textContent = `${battleTime}초`;
  475.             document.getElementById('total-damage').textContent = totalBattleDamage.toLocaleString();
  476.             document.getElementById('raid-dps').textContent = raidDps.toLocaleString();
  477.             document.getElementById('player-count').textContent = playerCount;
  478.         }
  479.  
  480.         function updatePlayerRankings() {
  481.             const container = document.getElementById('player-rankings');
  482.  
  483.             if (Object.keys(battleData).length === 0) {
  484.                 container.innerHTML = `<div class="ranking-header"><div>순위</div><div>플레이어</div><div>데미지 (DPS)</div></div><div class="no-data">데미지 데이터를 기다리는 중...</div>`;
  485.                 return;
  486.             }
  487.            
  488.             const sortedPlayers = Object.entries(battleData).sort(([,a], [,b]) => b.totalDamage - a.totalDamage);
  489.  
  490.             const maxDamage = sortedPlayers.length > 0 ? sortedPlayers[0][1].totalDamage : 1;
  491.             const battleTime = lastUpdateTime > 0 ? Math.max(1, (lastUpdateTime - startTime) / 1000) : 1;
  492.  
  493.             let html = `<div class="ranking-header"><div>순위</div><div>플레이어</div><div>데미지 (DPS)</div></div>`;
  494.  
  495.             let displayedPlayerIndex = 0;
  496.             sortedPlayers.forEach(([userId, userData]) => {
  497.                 const detectedClass = detectPlayerClass(userData);
  498.                 if (!detectedClass || !classMapping[detectedClass]) {
  499.                     return;
  500.                 }
  501.  
  502.                 // --- NEW: Calculate damage split ---
  503.                 let mainTargetDamage = 0;
  504.                 let otherTargetsDamage = 0;
  505.  
  506.                 const targetDamages = Object.values(userData.damageByTarget);
  507.                 if (targetDamages.length > 0) {
  508.                     mainTargetDamage = Math.max(...targetDamages);
  509.                 }
  510.                 otherTargetsDamage = userData.totalDamage - mainTargetDamage;
  511.                
  512.                 const mainDamagePercentOfTotal = userData.totalDamage > 0 ? (mainTargetDamage / userData.totalDamage) * 100 : 0;
  513.                 const otherDamagePercentOfTotal = 100 - mainDamagePercentOfTotal;
  514.                 // --- End of new calculation ---
  515.  
  516.                 const playerDps = Math.floor(userData.totalDamage / battleTime);
  517.                 const barWidth = (userData.totalDamage / maxDamage) * 100;
  518.                 const barColor = classMapping[detectedClass].color;
  519.                 const className = classMapping[detectedClass].name;
  520.                 const rankClass = displayedPlayerIndex < 3 ? `rank-${displayedPlayerIndex + 1}` : '';
  521.                const displayName = `${className} ${userId.substring(0, 8)}`;
  522.  
  523.                html += `
  524.                    <div class="player-bar" onclick="toggleSkillBreakdown('${userId}')">
  525.                         <div class="player-bar-fill-wrapper" style="width: ${barWidth}%;">
  526.                             <div class="damage-segment main-target-damage" style="width: ${mainDamagePercentOfTotal}%; background: ${barColor};"></div>
  527.                             <div class="damage-segment other-targets-damage" style="width: ${otherDamagePercentOfTotal}%; background: ${barColor};"></div>
  528.                         </div>
  529.                         <div class="player-bar-content">
  530.                             <div class="rank-section">
  531.                                 <div class="rank-number ${rankClass}">${displayedPlayerIndex + 1}</div>
  532.                             </div>
  533.                             <div class="player-name">
  534.                                 ${displayName}
  535.                             </div>
  536.                             <div class="stat-value damage-value">
  537.                                 ${mainTargetDamage.toLocaleString()}
  538.                                 <span class="plus-sign">+</span>
  539.                                 ${otherTargetsDamage.toLocaleString()}
  540.                                 <span class="total-dps">(${playerDps.toLocaleString()})</span>
  541.                             </div>
  542.                         </div>
  543.                     </div>
  544.                     <div class="skill-breakdown ${expandedPlayers.has(userId) ? 'show' : ''}" id="skills-${userId}">
  545.                         ${generateSkillBreakdown(userId, userData)}
  546.                     </div>
  547.                 `;
  548.  
  549.                 displayedPlayerIndex++;
  550.             });
  551.            
  552.             if (displayedPlayerIndex === 0) {
  553.                 html += `<div class="no-data">감지된 플레이어 데이터가 없습니다.</div>`;
  554.             }
  555.  
  556.             container.innerHTML = html;
  557.         }
  558.  
  559.         function generateSkillBreakdown(userId, userData) {
  560.             // This breakdown currently shows total skill damage across all targets.
  561.             // A more advanced version might track skills per target.
  562.             if (!userData.skills || Object.keys(userData.skills).length === 0) {
  563.                 return '<div style="color: #7f8c8d; text-align: center; padding: 20px;">스킬 데이터가 없습니다.</div>';
  564.             }
  565.  
  566.             const sortedSkills = Object.entries(userData.skills).sort(([,a], [,b]) => b.damage - a.damage);
  567.             const maxSkillDamage = sortedSkills.length > 0 ? sortedSkills[0][1].damage : 1;
  568.  
  569.             let skillHtml = `<div class="skill-breakdown-header">유저 ${userId.substring(0, 8)} - 스킬 상세 (전체 타겟 합산)</div>`;
  570.             sortedSkills.forEach(([skillName, skillData]) => {
  571.                 const skillPercentage = userData.totalDamage > 0 ? ((skillData.damage / userData.totalDamage) * 100).toFixed(1) : 0;
  572.                 const skillTotalHits = skillData.hits + skillData.addHits;
  573.                 const skillCritRate = skillTotalHits > 0 ? ((skillData.crits / skillTotalHits) * 100).toFixed(1) : 0;
  574.                 const barWidth = (skillData.damage / maxSkillDamage) * 100;
  575.                
  576.                 skillHtml += `
  577.                     <div class="skill-item">
  578.                         <div class="skill-item-fill" style="width: ${barWidth}%">
  579.                             <div class="skill-item-info">${skillName}</div>
  580.                         </div>
  581.                         <div class="skill-item-stats">
  582.                             타수 ${skillData.hits} | 크리 ${skillCritRate}% | 추가타율 ${skillData.hits > 0 ? ((skillData.addHits / skillData.hits) * 100).toFixed(1) : 0}% | ${skillData.damage.toLocaleString()} (${skillPercentage}%)
  583.                         </div>
  584.                     </div>
  585.                 `;
  586.             });
  587.  
  588.             return skillHtml;
  589.         }
  590.  
  591.         function toggleSkillBreakdown(userId) {
  592.             const skillBreakdown = document.getElementById(`skills-${userId}`);
  593.             if (!skillBreakdown) return;
  594.  
  595.             if (expandedPlayers.has(userId)) {
  596.                 expandedPlayers.delete(userId);
  597.                 skillBreakdown.classList.remove('show');
  598.             } else {
  599.                 // Collapse others first for single-expansion mode
  600.                 document.querySelectorAll('.skill-breakdown.show').forEach(el => el.classList.remove('show'));
  601.                 expandedPlayers.clear();
  602.                
  603.                 expandedPlayers.add(userId);
  604.                 // Update content before showing to ensure it's fresh
  605.                 const userData = battleData[userId];
  606.                 if (userData) {
  607.                     skillBreakdown.innerHTML = generateSkillBreakdown(userId, userData);
  608.                 }
  609.                 skillBreakdown.classList.add('show', 'animate');
  610.                 setTimeout(() => skillBreakdown.classList.remove('animate'), 300);
  611.             }
  612.         }
  613.  
  614.         // Initialize
  615.         connectWebSocket();
  616.         setInterval(updateDisplay, 1000);
  617.     </script>
  618. </body>
  619. </html>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement