Last active
August 2, 2025 20:20
-
-
Save xMarch/d7b7db3e4d8cfb6e94e922c20d8c8d9f to your computer and use it in GitHub Desktop.
Artale Market Weapon AP Calculator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Artale Market Weapon AP Calculator | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.1.1 | |
| // @description Calculates and displays '總魔攻' and '有效AP' on the Artale Market transaction page, with a themed filter panel. | |
| // @author Gemini | |
| // @homepage https://gist.github.com/xMarch/d7b7db3e4d8cfb6e94e922c20d8c8d9f | |
| // @match https://artale-market.org/transaction* | |
| // @grant GM_addStyle | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // --- Configuration --- | |
| const luckLookupTable = { | |
| "木製短杖": 0, "竹杖": 14, "高級木製短杖": 15, "金屬短杖": 20, "冰精短杖": 25, "鋰礦短杖": 30, | |
| "法師短杖": 35, "妖精短杖": 40, "水晶短杖": 40, "路燈": 40, "大魔法師短杖": 50, "羽扇": 48, | |
| "聖賢短杖": 0, "愛心手杖": 0, "火焰之杖": 60, "日本扇": 63, "天使之翼": 70, "鳳凰短杖": 80, | |
| "聖權杖": 90, "鬼頭杖": 100, "聖龍短杖": 110, "木製長杖": 13, "祖母綠長杖": 18, "藍寶石長杖": 18, | |
| "古樹長杖": 23, "法師長杖": 28, "毒菇": 10, "風輪長杖": 33, "楓葉之杖": 0, "翡赤之杖": 38, | |
| "護法之杖": 43, "楓葉法杖": 0, "夜行權杖": 0, "亞克長杖": 48, "黃金環魔杖": 40, "白龍之杖": 58, | |
| "楓葉智慧長杖": 0, "魔翼之杖": 68, "死靈法杖": 78, "魔靈之魂": 88, "椎茸": 92, "香菇": 92, | |
| "怒濤之杖": 98, "藍色雨傘": 0, "綠色雨傘": 10, "紫色雨傘": 0, "黃色雨傘": 0, "褐色雨傘": 5, | |
| "紅色雨傘": 0, "黑色雨傘": 6, "真‧楓葉巨傘": 0 | |
| }; | |
| // --- Styling --- | |
| GM_addStyle(` | |
| /* Core stat styles */ | |
| .custom-stat-container { display: flex; justify-content: space-between; align-items: center; } | |
| .info-icon { display: inline-block; width: 16px; height: 16px; margin-left: 5px; cursor: help; color: #6b7280; } | |
| .custom-tooltip { visibility: hidden; width: max-content; background-color: #1f2937; color: #fff; text-align: center; border-radius: 6px; padding: 5px 10px; position: absolute; z-index: 10; bottom: 125%; left: 50%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s; } | |
| .custom-tooltip::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #1f2937 transparent transparent transparent; } | |
| .info-icon:hover .custom-tooltip { visibility: visible; opacity: 1; } | |
| .total-magic-attack-value { font-weight: bold; color: #3b82f6; } | |
| .effective-ap-value { font-weight: bold; transition: color 0.3s ease; } | |
| .dark .custom-tooltip { background-color: #374151; } | |
| .dark .info-icon { color: #d1d5db; } | |
| /* Filter Panel Styles - Themed */ | |
| #filter-panel-container { | |
| position: fixed; right: 0; top: 150px; z-index: 9999; display: flex; align-items: flex-start; | |
| transform: translateX(0); transition: transform 0.3s ease-in-out; | |
| } | |
| #filter-panel-container.collapsed { transform: translateX(220px); } | |
| #filter-panel { | |
| background: var(--color-base-200); color: var(--foreground); | |
| border: 1px solid #e5e7eb; border-radius: 8px 0 0 8px; | |
| box-shadow: -2px 2px 10px rgba(0,0,0,0.1); display: flex; | |
| } | |
| .dark #filter-panel { border-color: #374151; } | |
| #panel-content { padding: 16px; width: 220px; } | |
| #panel-content h3 { margin-top: 0; margin-bottom: 16px; font-size: 1.1rem; } | |
| #panel-toggle-btn { | |
| color: var(--foreground); border: none; background: transparent; | |
| padding: 12px 6px; cursor: pointer; align-self: stretch; | |
| } | |
| #panel-toggle-btn svg { transition: transform 0.3s ease; } | |
| .filter-option { margin-bottom: 12px; } | |
| .filter-option label { margin-left: 4px; } | |
| .filter-controls { margin-top: 8px; padding-left: 8px; border-left: 2px solid #e5e7eb; } | |
| .dark .filter-controls { border-color: #374151; } | |
| .filter-controls label { display: block; font-size: 0.9rem; } | |
| .filter-controls input[type="number"] { width: 100%; padding: 4px 8px; border-radius: 4px; border: 1px solid #d1d5db; background: var(--background); color: var(--foreground); } | |
| .dark .filter-controls input[type="number"] { border-color: #4b5563; } | |
| .blur-type-selector, .scroll-filter-wrapper { display: flex; flex-direction: column; gap: 8px; } | |
| .radio-label-wrapper { display: flex; align-items: center; flex-wrap: wrap; } | |
| .blur-type-selector .radio-label-wrapper { gap: 4px; } | |
| .blur-type-selector .radio-label-wrapper input[type="number"] { flex-grow: 1; } | |
| .scroll-filter-wrapper .radio-label-wrapper label, | |
| .scroll-filter-wrapper .radio-label-wrapper input[type="number"] { | |
| flex-grow: 1; | |
| } | |
| .filter-separator { border: none; border-top: 1px solid #e5e7eb; margin: 12px 0; } | |
| .dark .filter-separator { border-color: #374151; } | |
| /* Row Blur Style */ | |
| tr.blurred-row { filter: blur(2px); opacity: 0.4; transition: all 0.3s ease; pointer-events: none; } | |
| `); | |
| function createFilterPanel() { | |
| if (document.getElementById('filter-panel-container')) return; | |
| const panelContainer = document.createElement('div'); | |
| panelContainer.id = 'filter-panel-container'; | |
| panelContainer.classList.add('collapsed'); | |
| panelContainer.innerHTML = ` | |
| <div id="filter-panel"> | |
| <button id="panel-toggle-btn" title="Toggle Filter Panel"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16" style="transform: rotate(0deg);"><path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/></svg> | |
| </button> | |
| <div id="panel-content"> | |
| <h3>魔攻過濾器</h3> | |
| <div class="filter-option"> | |
| <div class="radio-label-wrapper"> | |
| <input type="radio" id="mode-highlight" name="filter-mode" value="highlight" checked> | |
| <label for="mode-highlight">AP基準比對</label> | |
| </div> | |
| <div id="highlight-controls" class="filter-controls"> | |
| <label for="baseline-ap">基準AP:</label> | |
| <input type="number" id="baseline-ap" value="30"> | |
| </div> | |
| </div> | |
| <div class="filter-option"> | |
| <div class="radio-label-wrapper"> | |
| <input type="radio" id="mode-blur" name="filter-mode" value="blur"> | |
| <label for="mode-blur">過濾低於AP</label> | |
| </div> | |
| <div id="blur-controls" class="filter-controls" style="display:none;"> | |
| <div class="blur-type-selector"> | |
| <div class="radio-label-wrapper"> | |
| <input type="radio" id="blur-type-total" name="blur-type" value="total" checked> | |
| <label for="blur-type-total">總魔攻:</label> | |
| <input type="number" id="min-ap-total" value="0"> | |
| </div> | |
| <div class="radio-label-wrapper"> | |
| <input type="radio" id="blur-type-effective" name="blur-type" value="effective"> | |
| <label for="blur-type-effective">有效AP:</label> | |
| <input type="number" id="min-ap-effective" value="30"> | |
| </div> | |
| </div> | |
| <hr class="filter-separator"> | |
| <div class="scroll-filter-wrapper"> | |
| <div class="radio-label-wrapper"> | |
| <input type="checkbox" id="blur-scroll-toggle"> | |
| <label for="blur-scroll-toggle">卷軸剩餘數</label> | |
| <input type="number" id="min-scroll-count" value="7"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(panelContainer); | |
| const container = document.getElementById('filter-panel-container'); | |
| const toggleBtn = document.getElementById('panel-toggle-btn'); | |
| const modeRadios = document.querySelectorAll('input[name="filter-mode"]'); | |
| const highlightControls = document.getElementById('highlight-controls'); | |
| const blurControls = document.getElementById('blur-controls'); | |
| toggleBtn.addEventListener('click', () => { | |
| container.classList.toggle('collapsed'); | |
| toggleBtn.querySelector('svg').style.transform = container.classList.contains('collapsed') ? 'rotate(0deg)' : 'rotate(180deg)'; | |
| }); | |
| modeRadios.forEach(radio => { | |
| radio.addEventListener('change', () => { | |
| highlightControls.style.display = radio.value === 'highlight' ? 'block' : 'none'; | |
| blurControls.style.display = radio.value === 'blur' ? 'block' : 'none'; | |
| applyFilters(); | |
| }); | |
| }); | |
| ['baseline-ap', 'min-ap-total', 'min-ap-effective', 'blur-type-total', 'blur-type-effective', 'blur-scroll-toggle', 'min-scroll-count'].forEach(id => { | |
| document.getElementById(id).addEventListener('input', applyFilters); | |
| }); | |
| } | |
| function applyFilters() { | |
| const mode = document.querySelector('input[name="filter-mode"]:checked').value; | |
| const baselineAp = parseInt(document.getElementById('baseline-ap').value, 10) || 0; | |
| const rows = document.querySelectorAll('tr[data-processed="true"]'); | |
| rows.forEach(row => { | |
| row.classList.remove('blurred-row'); | |
| const apValueSpan = row.querySelector('.effective-ap-value'); | |
| if (apValueSpan) apValueSpan.style.color = ''; | |
| if (mode === 'blur') { | |
| let shouldBlur = false; | |
| // Check AP filter | |
| const blurType = document.querySelector('input[name="blur-type"]:checked').value; | |
| if (blurType === 'total') { | |
| const minAp = parseInt(document.getElementById('min-ap-total').value, 10) || 0; | |
| const totalAp = parseInt(row.dataset.totalMagicAttack, 10); | |
| if (row.hasAttribute('data-total-magic-attack') && totalAp < minAp) shouldBlur = true; | |
| } else { // effective | |
| const minAp = parseInt(document.getElementById('min-ap-effective').value, 10) || 0; | |
| if (!row.hasAttribute('data-effective-ap') && minAp > 0) { | |
| shouldBlur = true; | |
| } else if (row.hasAttribute('data-effective-ap')) { | |
| const effectiveAp = parseInt(row.dataset.effectiveAp, 10); | |
| if (effectiveAp < minAp) shouldBlur = true; | |
| } | |
| } | |
| // Check scroll filter if not already blurred | |
| const scrollFilterEnabled = document.getElementById('blur-scroll-toggle').checked; | |
| if (scrollFilterEnabled && !shouldBlur) { | |
| const minScrollCount = parseInt(document.getElementById('min-scroll-count').value, 10) || 0; | |
| const scrollsRemaining = parseInt(row.dataset.scrollsRemaining, 10); | |
| if (scrollsRemaining < minScrollCount) shouldBlur = true; | |
| } | |
| if (shouldBlur) row.classList.add('blurred-row'); | |
| } else if (mode === 'highlight' && row.hasAttribute('data-effective-ap')) { | |
| const effectiveAp = parseInt(row.dataset.effectiveAp, 10); | |
| if (apValueSpan) apValueSpan.style.color = getColorForDelta(effectiveAp - baselineAp); | |
| } | |
| }); | |
| } | |
| function getColorForDelta(delta) { | |
| const clampedDelta = Math.max(-20, Math.min(20, delta)); | |
| const hue = 60 + (clampedDelta / 20) * 60; | |
| return `hsl(${hue}, 90%, 45%)`; | |
| } | |
| function createInfoIcon(magicAttack, intelligence) { | |
| const infoIcon = document.createElement('div'); | |
| infoIcon.className = 'info-icon'; | |
| infoIcon.style.position = 'relative'; | |
| infoIcon.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" /></svg> | |
| <div class="custom-tooltip">原始數值<br>魔攻: ${magicAttack}, 智力: ${intelligence}</div> | |
| `; | |
| return infoIcon; | |
| } | |
| function processTableRows() { | |
| const tableBody = document.querySelector('table.table-zebra tbody'); | |
| if (!tableBody) return; | |
| tableBody.querySelectorAll('tr').forEach(row => { | |
| if (row.dataset.processed) return; | |
| const cells = row.querySelectorAll('td'); | |
| if (cells.length < 6) return; | |
| const itemName = cells[2].textContent.trim(); | |
| let magicAttack = null, intelligence = null, scrollsRemaining = null; | |
| let magicAttackDiv = null, intelligenceDiv = null; | |
| cells[5].querySelectorAll('.grid > div').forEach(div => { | |
| const statNameSpan = div.querySelector('span.font-medium'); | |
| if (statNameSpan) { | |
| const statName = statNameSpan.textContent.trim(); | |
| const statValue = parseInt(div.querySelector('span:last-child').textContent.trim(), 10); | |
| if (statName === '魔攻:') { magicAttack = statValue; magicAttackDiv = div; } | |
| else if (statName === '智力:') { intelligence = statValue; intelligenceDiv = div; } | |
| else if (statName === '卷軸剩餘數:') { scrollsRemaining = statValue; } | |
| } | |
| }); | |
| row.dataset.scrollsRemaining = scrollsRemaining !== null ? scrollsRemaining : 0; | |
| if (magicAttack !== null) { | |
| const finalIntelligence = intelligence || 0; | |
| const totalMagicAttack = magicAttack + finalIntelligence; | |
| row.dataset.totalMagicAttack = totalMagicAttack; | |
| const combinedDisplay = document.createElement('div'); | |
| combinedDisplay.innerHTML = `<div class="custom-stat-container"><span class="font-medium">總魔攻:</span><span class="total-magic-attack-value">${totalMagicAttack}</span></div>`; | |
| if (luckLookupTable.hasOwnProperty(itemName)) { | |
| const effectiveAP = totalMagicAttack - luckLookupTable[itemName]; | |
| row.dataset.effectiveAp = effectiveAP; | |
| combinedDisplay.innerHTML += `<div class="custom-stat-container"><span class="font-medium">有效AP:</span><span class="effective-ap-value">${effectiveAP}</span></div>`; | |
| } | |
| combinedDisplay.appendChild(createInfoIcon(magicAttack, finalIntelligence)); | |
| magicAttackDiv?.parentNode?.replaceChild(combinedDisplay, magicAttackDiv); | |
| intelligenceDiv?.parentNode?.removeChild(intelligenceDiv); | |
| } | |
| row.dataset.processed = 'true'; | |
| }); | |
| applyFilters(); | |
| } | |
| // --- Main Execution --- | |
| window.addEventListener('load', () => { | |
| createFilterPanel(); | |
| processTableRows(); | |
| const observer = new MutationObserver(() => { | |
| clearTimeout(window.artaleMarketDebounce); | |
| window.artaleMarketDebounce = setTimeout(processTableRows, 100); | |
| }); | |
| const targetNode = document.querySelector('body'); | |
| if (targetNode) observer.observe(targetNode, { childList: true, subtree: true }); | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment