Skip to content

Instantly share code, notes, and snippets.

@xMarch
Last active August 2, 2025 20:20
Show Gist options
  • Select an option

  • Save xMarch/d7b7db3e4d8cfb6e94e922c20d8c8d9f to your computer and use it in GitHub Desktop.

Select an option

Save xMarch/d7b7db3e4d8cfb6e94e922c20d8c8d9f to your computer and use it in GitHub Desktop.
Artale Market Weapon AP Calculator
// ==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