您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
던담 딜표에 커스텀패널을 추가함
// ==UserScript== // @name 던담 커스텀 패널 // @namespace http://tampermonkey.net/ // @version 14.1.3 // 다중 입력 기능 추가 // @description 던담 딜표에 커스텀패널을 추가함 // @author ww99w & Gemini // @match https://dundam.xyz/character* // @grant GM_addStyle // @license MIT // ==/UserScript== (function() { 'use strict'; console.log('[던담 시뮬] 스크립트 시작 (v14.1.3)'); // 전역 스킬 정보 캐시 let dundamSkillInfoCache = null; // --- 스타일 정의 (v14.1.2와 거의 동일, input type text 너비 조정) --- GM_addStyle(` .cc-part .sk.tr:has(.skcount) { cursor: pointer; transition: all 0.2s ease-in-out; } .sk.tr.sim-skill-modified { border: 1.5px solid #4CAF50 !important; box-shadow: 0 0 10px rgba(76, 175, 80, 0.3); } .sim-control-panel-row { width: 100%; box-sizing: border-box; padding-left: 54px; padding-right: 16px; margin-top: -4px; margin-bottom: 12px; } .sim-control-panel-container { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 12px 16px; box-sizing: border-box; background-color: #2c2e34; border-radius: 6px; border: 1px solid rgba(0, 0, 0, 0.4); box-shadow: 0 2px 6px rgba(0,0,0,0.35); } .sim-input-group { display: flex; align-items: center; gap: 6px; margin-right: 15px; } .sim-input-group .sim-label { color: #c0c0c0; font-size: 14px; font-weight: 500; } .sim-input-group input[type="number"], .sim-input-group input[type="text"] { /* type="text" 추가 */ width: 100px; /* 여러 값 입력을 위해 너비 증가 */ height: 32px; border: 1px solid #181818; background-color: #202225; color: #e0e0e0; border-radius: 4px; text-align: center; font-size: 14px; padding: 0 5px; } .sim-control-panel-container button { height: 32px; padding: 0 14px; border: 1px solid #505050; background-image: linear-gradient(to bottom, #525252, #454545); color: #e0e0e0; cursor: pointer; border-radius: 4px; font-size: 13px; font-weight: 500; transition: background-image 0.2s ease; } .sim-control-panel-container button:hover { background-image: linear-gradient(to bottom, #5e5e5e, #505050); } .sim-control-panel-container button.sim-apply-all-btn { margin-left: auto; } .sim-control-panel-container button.sim-reset-btn { border-color: #704040; background-image: linear-gradient(to bottom, #704040, #603030); } .sim-control-panel-container button.sim-reset-btn:hover { background-image: linear-gradient(to bottom, #804a4a, #704040); } .sim-input-group input[type="text"]::-webkit-outer-spin-button, .sim-input-group input[type="text"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .sim-input-group input[type="text"] { -moz-appearance: textfield; } .sim-skill-sub-info-container { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; width: auto !important; height: 100% !important; } .sim-skill-sub-info-wrapper { display: flex; justify-content: center; align-items: center; gap: 8px; font-size: 11px; margin-top: 2px; line-height: 1.2; width: 100%; } .sim-cast-change-indicator, .sim-cooldown-indicator { white-space: nowrap; } .sim-cooldown-indicator { color: #999; } .sim-cast-change-indicator { font-weight: bold; } .sim-cast-change-indicator.positive { color: #77b67a; } .sim-cast-change-indicator.negative { color: #d96c6c; } .sim-skill-damage-change-indicator { font-weight: bold; margin-left: 6px; font-size: 12px; white-space: nowrap; } .sim-skill-damage-change-indicator.positive { color: #81c784; } .sim-skill-damage-change-indicator.negative { color: #e57373; } .sim-total-damage-change-indicator { font-size: 1.1rem; font-weight: bold; margin-left: 10px; } .sim-total-damage-change-indicator.positive { color: #81c784; } .sim-total-damage-change-indicator.negative { color: #e57373; } `); const parseDamageString = (text) => parseInt(String(text).replace(/,/g, ''), 10); const formatDamageNumber = (num) => Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); function getBaseCooldownData(forceRefresh = false) { if (!forceRefresh && dundamSkillInfoCache && Object.keys(dundamSkillInfoCache).length > 0) { return dundamSkillInfoCache; } const skillInfoTab = document.querySelector('.tab__content[name="스킬정보"]'); if (!skillInfoTab) { console.warn('[던담 시뮬] getBaseCooldownData: "스킬정보" 탭 없음.'); return null; } const skillRows = skillInfoTab.querySelectorAll('.skinfo .sk.tr'); if (skillRows.length === 0) { console.warn('[던담 시뮬] getBaseCooldownData: "스킬정보" 탭 내 스킬 행 없음.'); return null; } const parsedData = {}; skillRows.forEach(row => { const nameEl = row.querySelector('.skn span'); const cooldownEl = row.querySelector('.skc ul li.skt span.val'); if (nameEl && cooldownEl) { const name = nameEl.textContent.trim(); const cooldown = parseFloat(cooldownEl.textContent); if (name && !isNaN(cooldown)) parsedData[name] = { baseCooldown: cooldown }; } }); if (Object.keys(parsedData).length > 0) { dundamSkillInfoCache = parsedData; return dundamSkillInfoCache; } return null; } function toggleModifiedIndicator(skillRow) { const damageEl = skillRow.querySelector('.skl .val'), countEl = skillRow.querySelector('.skc-count-p > span'); if (!damageEl || !countEl || !skillRow.dataset.originalDamage || !skillRow.dataset.originalReportedCasts) return; const currentDamage = parseDamageString(damageEl.textContent), currentCount = parseInt(countEl.textContent.replace('회', ''), 10); if (currentDamage !== parseInt(skillRow.dataset.originalDamage, 10) || currentCount !== parseInt(skillRow.dataset.originalReportedCasts, 10)) { skillRow.classList.add('sim-skill-modified'); } else { skillRow.classList.remove('sim-skill-modified'); } } function updateAllDisplayMetrics(currentDealTable) { const skillRows = currentDealTable.querySelectorAll('.cc-part .sk.tr'); const mainTotalDamageDisplayEl = currentDealTable.querySelector('.hd-part .dval'); if (!mainTotalDamageDisplayEl) return; let newTotalDamage = 0; skillRows.forEach(row => { if (row.querySelector('.skcount')) { const damageEl = row.querySelector('.skl .val'); if (damageEl) newTotalDamage += parseDamageString(damageEl.textContent); } }); mainTotalDamageDisplayEl.textContent = formatDamageNumber(newTotalDamage); const totalSumRow = Array.from(skillRows).find(row => row.querySelector('.skn span')?.textContent === '총 합'); if (totalSumRow) { const totalSumDamageEl = totalSumRow.querySelector('.skl .val'); if (totalSumDamageEl) totalSumDamageEl.textContent = formatDamageNumber(newTotalDamage); const originalTotalDamage = parseFloat(currentDealTable.dataset.originalTotalDamage); const totalChangeIndicatorEl = totalSumRow.querySelector('.sim-total-damage-change-indicator'); if (totalChangeIndicatorEl && originalTotalDamage > 0) { const totalChange = (newTotalDamage / originalTotalDamage - 1) * 100; if (Math.abs(totalChange) > 0.001) { totalChangeIndicatorEl.textContent = `(${totalChange >= 0 ? '+' : ''}${totalChange.toFixed(2)}%)`; totalChangeIndicatorEl.className = `sim-total-damage-change-indicator ${totalChange >= 0 ? 'positive' : 'negative'}`; } else { totalChangeIndicatorEl.textContent = ''; } } } skillRows.forEach(row => { if (row.querySelector('.skcount')) { const damageEl = row.querySelector('.skl .val'), percentEl = row.querySelector('.ser-percent.share'), skillDamageChangeEl = row.querySelector('.sim-skill-damage-change-indicator'); if (!damageEl || !percentEl || !skillDamageChangeEl) return; const currentSkillDamage = parseDamageString(damageEl.textContent), newPercent = newTotalDamage > 0 ? (currentSkillDamage / newTotalDamage * 100) : 0; percentEl.textContent = `${newPercent.toFixed(2)}%`; const originalSkillDamage = parseFloat(row.dataset.originalDamage); if (originalSkillDamage > 0) { const damageChangePercent = (currentSkillDamage / originalSkillDamage - 1) * 100; if (Math.abs(damageChangePercent) > 0.001) { skillDamageChangeEl.textContent = `(${damageChangePercent >= 0 ? '+' : ''}${damageChangePercent.toFixed(1)}%)`; skillDamageChangeEl.className = `sim-skill-damage-change-indicator ${damageChangePercent >= 0 ? 'positive' : 'negative'}`; } else { skillDamageChangeEl.textContent = ''; } } else { skillDamageChangeEl.textContent = ''; } } }); } /** * 여러 퍼센트 값을 파싱하여 누적 배율을 계산하는 헬퍼 함수 */ function parseCumulativeMultiplier(inputText, isReduction = false) { let totalMultiplier = 1.0; if (inputText && inputText.trim() !== "") { const values = inputText.trim().split(/\s+/); values.forEach(valStr => { const percent = parseFloat(valStr); if (!isNaN(percent)) { totalMultiplier *= (1 + (isReduction ? -percent : percent) / 100); } }); } return totalMultiplier; } function createControlPanelElement(skillRow) { const currentSkillName = skillRow.querySelector('.skn span')?.textContent.trim(); let allSkillInfo = getBaseCooldownData(true); let baseCDForThisSkill = skillRow.dataset.originalCooldown || "0"; if (allSkillInfo && allSkillInfo[currentSkillName] && allSkillInfo[currentSkillName].baseCooldown > 0) { const newBaseCD = allSkillInfo[currentSkillName].baseCooldown.toString(); if (skillRow.dataset.originalCooldown !== newBaseCD || !skillRow.dataset.originalCooldown) { skillRow.dataset.originalCooldown = newBaseCD; baseCDForThisSkill = newBaseCD; const subInfoContainer = skillRow.querySelector('.sim-skill-sub-info-wrapper'); if (subInfoContainer) { const cdDisplayEl = subInfoContainer.querySelector('.sim-cooldown-indicator'); if (cdDisplayEl) cdDisplayEl.textContent = `[쿨: ${parseFloat(baseCDForThisSkill).toFixed(1)}초]`; } } } else if (currentSkillName && allSkillInfo && !allSkillInfo[currentSkillName]){ skillRow.dataset.originalCooldown = "0"; baseCDForThisSkill = "0"; } const panelContainer = document.createElement('div'); panelContainer.className = 'sim-control-panel-container'; panelContainer.addEventListener('click', e => e.stopPropagation()); const createInputGroup = (labelText, inputPlaceholder, datasetKey, isMultiValue = false) => { const group = document.createElement('div'); group.className = 'sim-input-group'; const label = document.createElement('span'); label.className = 'sim-label'; label.textContent = labelText; const input = document.createElement('input'); input.type = isMultiValue ? 'text' : 'number'; // 딜, 쿨감은 text로 변경 input.placeholder = inputPlaceholder; if (skillRow.dataset[datasetKey]) { input.value = skillRow.dataset[datasetKey]; } group.append(label, input); return { group, input }; }; const dmgElements = createInputGroup('스증(%)', '예: 20 10 -5', 'simDmgPercentMulti', true); const countElements = createInputGroup('횟수+', '0', 'simAdditionalCasts'); // 횟수는 단일 값 유지 const cdElements = createInputGroup('쿨감(%)', '예: 10 5', 'simCooldownReductionMulti', true); if (!baseCDForThisSkill || baseCDForThisSkill === "0") { cdElements.input.disabled = true; cdElements.input.title = "스킬정보 탭에서 쿨타임 정보를 찾을 수 없거나 쿨타임이 0초입니다."; } else { cdElements.input.disabled = false; cdElements.input.title = "";} const applyAllBtn = document.createElement('button'); applyAllBtn.className = 'sim-apply-all-btn'; applyAllBtn.textContent = '모두 적용'; const resetBtn = document.createElement('button'); resetBtn.className = 'sim-reset-btn'; resetBtn.textContent = '초기화'; applyAllBtn.onclick = () => { const parentDealTable = skillRow.closest('.deal'); const originalDamage = parseInt(skillRow.dataset.originalDamage, 10); const originalReportedCasts = parseInt(skillRow.dataset.originalReportedCasts, 10); const currentOriginalCooldown = parseFloat(skillRow.dataset.originalCooldown || "0"); skillRow.dataset.simDmgPercentMulti = dmgElements.input.value; // 문자열 그대로 저장 skillRow.dataset.simAdditionalCasts = countElements.input.value; skillRow.dataset.simCooldownReductionMulti = cdElements.input.value; // 문자열 그대로 저장 const cumulativeDmgMultiplier = parseCumulativeMultiplier(dmgElements.input.value, false); const cumulativeCdMultiplier = parseCumulativeMultiplier(cdElements.input.value, true); const additionalCasts = parseInt(countElements.input.value, 10) || 0; let simulatedCasts = originalReportedCasts; let currentCooldown = currentOriginalCooldown; if (currentOriginalCooldown > 0) { // 0으로 나누기 방지 currentCooldown = currentOriginalCooldown * cumulativeCdMultiplier; currentCooldown = Math.max(0.1, currentCooldown); simulatedCasts = 1 + Math.floor(40 / currentCooldown); } simulatedCasts += additionalCasts; simulatedCasts = Math.max(0, simulatedCasts); const damagePerOriginalCast = originalReportedCasts > 0 ? originalDamage / originalReportedCasts : 0; const newTotalSkillDamage = damagePerOriginalCast * simulatedCasts * cumulativeDmgMultiplier; skillRow.querySelector('.skc-count-p > span').textContent = `${simulatedCasts}회`; skillRow.querySelector('.skl .val').textContent = formatDamageNumber(newTotalSkillDamage); const subInfoContainer = skillRow.querySelector('.sim-skill-sub-info-wrapper'); if (subInfoContainer) { const castChangeEl = subInfoContainer.querySelector('.sim-cast-change-indicator'); const castDiff = simulatedCasts - originalReportedCasts; if (castChangeEl) { if (castDiff !== 0) { castChangeEl.textContent = `(${castDiff > 0 ? '+' : ''}${castDiff}회)`; castChangeEl.className = `sim-cast-change-indicator ${castDiff > 0 ? 'positive' : 'negative'}`; } else { castChangeEl.textContent = ''; } } const cdDisplayEl = subInfoContainer.querySelector('.sim-cooldown-indicator'); if (cdDisplayEl && currentOriginalCooldown > 0) { const cdDiff = currentCooldown - currentOriginalCooldown; cdDisplayEl.textContent = `[쿨: ${currentCooldown.toFixed(1)}초 ${cdDiff !== 0 && Math.abs(cdDiff) > 0.01 ? (cdDiff < 0 ? '▼' : '▲') + Math.abs(cdDiff).toFixed(1) : ''}]`; } else if (cdDisplayEl) { cdDisplayEl.textContent = '';} } updateAllDisplayMetrics(parentDealTable); toggleModifiedIndicator(skillRow); }; resetBtn.onclick = () => { const parentDealTable = skillRow.closest('.deal'); skillRow.querySelector('.skl .val').textContent = formatDamageNumber(skillRow.dataset.originalDamage); skillRow.querySelector('.skc-count-p > span').textContent = `${skillRow.dataset.originalReportedCasts}회`; dmgElements.input.value = ''; countElements.input.value = ''; cdElements.input.value = ''; delete skillRow.dataset.simDmgPercentMulti; delete skillRow.dataset.simAdditionalCasts; delete skillRow.dataset.simCooldownReductionMulti; const subInfoContainer = skillRow.querySelector('.sim-skill-sub-info-wrapper'); if (subInfoContainer) { const castChangeEl = subInfoContainer.querySelector('.sim-cast-change-indicator'); if (castChangeEl) castChangeEl.textContent = ''; const cdDisplayEl = subInfoContainer.querySelector('.sim-cooldown-indicator'); if (cdDisplayEl && skillRow.dataset.originalCooldown && skillRow.dataset.originalCooldown !== "0") { cdDisplayEl.textContent = `[쿨: ${parseFloat(skillRow.dataset.originalCooldown).toFixed(1)}초]`; } else if (cdDisplayEl) { cdDisplayEl.textContent = ''; } } updateAllDisplayMetrics(parentDealTable); toggleModifiedIndicator(skillRow); }; panelContainer.append(dmgElements.group, countElements.group, cdElements.group, applyAllBtn, resetBtn); const controlPanelRow = document.createElement('div'); controlPanelRow.className = 'sim-control-panel-row'; controlPanelRow.appendChild(panelContainer); return controlPanelRow; } function setupSkillRowInteractions(skillRow, allSkillBaseInfo) { if (!skillRow.querySelector('.skcount') || skillRow.dataset.simLogicAssigned === 'true') return; skillRow.dataset.simLogicAssigned = 'true'; const skillNameElement = skillRow.querySelector('.skn span'), skillName = skillNameElement ? skillNameElement.textContent.trim() : null; if (!skillName) { console.warn("[던담 시뮬] setupSkillRowInteractions: 스킬명 없음", skillRow); return; } const damageEl = skillRow.querySelector('.skl .val'), countDisplayContainer = skillRow.querySelector('.skc-count-p'), shareEl = skillRow.querySelector('.ser-percent.share'); if (damageEl && countDisplayContainer && shareEl) { skillRow.dataset.originalDamage = parseDamageString(damageEl.textContent); const countDisplaySpan = countDisplayContainer.querySelector('span'); if (countDisplaySpan) skillRow.dataset.originalReportedCasts = parseInt(countDisplaySpan.textContent.replace('회', ''), 10); const countParentContainer = countDisplayContainer.parentElement; let subInfoWrapper = countParentContainer.querySelector('.sim-skill-sub-info-wrapper'); if (!subInfoWrapper) { subInfoWrapper = document.createElement('div'); subInfoWrapper.className = 'sim-skill-sub-info-wrapper'; countDisplayContainer.insertAdjacentElement('afterend', subInfoWrapper); } let castChangeEl = subInfoWrapper.querySelector('.sim-cast-change-indicator'); if (!castChangeEl) { castChangeEl = document.createElement('span'); castChangeEl.className = 'sim-cast-change-indicator'; subInfoWrapper.appendChild(castChangeEl); } let cdDisplayEl = subInfoWrapper.querySelector('.sim-cooldown-indicator'); if (!cdDisplayEl) { cdDisplayEl = document.createElement('span'); cdDisplayEl.className = 'sim-cooldown-indicator'; subInfoWrapper.appendChild(cdDisplayEl); } if (allSkillBaseInfo && allSkillBaseInfo[skillName] && allSkillBaseInfo[skillName].baseCooldown > 0) { const baseCD = allSkillBaseInfo[skillName].baseCooldown; skillRow.dataset.originalCooldown = baseCD; cdDisplayEl.textContent = `[쿨: ${baseCD.toFixed(1)}초]`; } else { skillRow.dataset.originalCooldown = "0"; cdDisplayEl.textContent = ''; } } if (shareEl && (!shareEl.nextElementSibling || !shareEl.nextElementSibling.classList.contains('sim-skill-damage-change-indicator'))) { const skillDamageChangeEl = document.createElement('span'); skillDamageChangeEl.className = 'sim-skill-damage-change-indicator'; shareEl.insertAdjacentElement('afterend', skillDamageChangeEl); } skillRow.addEventListener('click', () => { const nextRow = skillRow.nextElementSibling; if (nextRow && nextRow.classList.contains('sim-control-panel-row')) { nextRow.remove(); } else { const panel = createControlPanelElement(skillRow); skillRow.insertAdjacentElement('afterend', panel); } }); } function initializeTableFeatures(dealTable) { if (!dealTable.querySelector('.cc-part .sk.tr .skn span')) { return; } if (dealTable.dataset.simSetupDone === 'true' && dundamSkillInfoCache && Object.keys(dundamSkillInfoCache).length > 0) { return; } const mainTotalDamageDisplayEl = dealTable.querySelector('.hd-part .dval'); if (mainTotalDamageDisplayEl && !dealTable.dataset.originalTotalDamage) { dealTable.dataset.originalTotalDamage = parseDamageString(mainTotalDamageDisplayEl.textContent); } const totalSumRow = Array.from(dealTable.querySelectorAll('.cc-part .sk.tr')).find(row => row.querySelector('.skn span')?.textContent === '총 합'); if (totalSumRow) { const totalSumDamageValueEl = totalSumRow.querySelector('.skl .val'); if (totalSumDamageValueEl && (!totalSumDamageValueEl.nextElementSibling || !totalSumDamageValueEl.nextElementSibling.classList.contains('sim-total-damage-change-indicator'))) { const totalChangeIndicatorEl = document.createElement('span'); totalChangeIndicatorEl.className = 'sim-total-damage-change-indicator'; totalSumDamageValueEl.insertAdjacentElement('afterend', totalChangeIndicatorEl); } } const allSkillBaseInfo = getBaseCooldownData(); dealTable.querySelectorAll('.cc-part .sk.tr').forEach(row => { setupSkillRowInteractions(row, allSkillBaseInfo); }); if (allSkillBaseInfo && Object.keys(allSkillBaseInfo).length > 0) { dealTable.dataset.simSetupDone = 'true'; updateAllDisplayMetrics(dealTable); } } const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { let newDealContentReady = false; for (const addedNode of mutation.addedNodes) { if (addedNode.nodeType === Node.ELEMENT_NODE) { if ((addedNode.matches('.deal') || addedNode.querySelector('.deal')) && addedNode.querySelector('.cc-part .sk.tr .skn span')) { newDealContentReady = true; break; } if (addedNode.matches('.sk.tr') && addedNode.querySelector('.skn span') && addedNode.closest('.deal .cc-part')) { newDealContentReady = true; break; } } } if (newDealContentReady) { document.querySelectorAll('.deal').forEach(initializeTableFeatures); break; } } } }); console.log('[던담 시뮬] 초기 테이블 설정 시도'); document.querySelectorAll('.deal').forEach(initializeTableFeatures); observer.observe(document.body, { childList: true, subtree: true }); console.log('[던담 시뮬] 스크립트 초기화 및 관찰 시작'); })();