您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Dark Souls style location popup with customizable settings
// ==UserScript== // @name Internet Roadtrip: Dark Souls Location Popup // @namespace http://tampermonkey.net/ // @version 1.2.1 // @author joawatte19 + TotallyNotSamm // @description Dark Souls style location popup with customizable settings // @match https://neal.fun/internet-roadtrip/ // @license MIT // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @require https://cdn.jsdelivr.net/npm/[email protected] // @icon // ==/UserScript== (async () => { if (!IRF?.isInternetRoadtrip) return; const container = await IRF.vdom.container; // Settings with defaults const settings = { volume: 0.8, triggerType: 'neighborhood', // 'neighborhood', 'state', 'country' popupDuration: 3000, colors: { neighborhood: '#e3e2e0', state: '#ffd700', country: '#ff6b6b' }, styles: { neighborhood: { fontSize: '4rem', textShadow: '0 0 8px rgba(0,0,0,0.6), 0 0 2px #ccc', borderBottom: '4px solid rgba(227,226,224,0.5)' }, state: { fontSize: '5rem', textShadow: '0 0 12px rgba(255,215,0,0.8), 0 0 4px #ffd700', borderBottom: '6px solid rgba(255,215,0,0.7)', fontWeight: 'bold' }, country: { fontSize: '6rem', textShadow: '0 0 16px rgba(255,107,107,0.9), 0 0 6px #ff6b6b', borderBottom: '8px solid rgba(255,107,107,0.8)', fontWeight: 'bold', letterSpacing: '0.1em' } } }; // Load saved settings const loadSettings = async () => { settings.volume = await GM_getValue('ds_volume', 0.8); settings.triggerType = await GM_getValue('ds_triggerType', 'neighborhood'); settings.popupDuration = await GM_getValue('ds_popupDuration', 3000); settings.colors.neighborhood = await GM_getValue('ds_color_neighborhood', '#e3e2e0'); settings.colors.state = await GM_getValue('ds_color_state', '#ffd700'); settings.colors.country = await GM_getValue('ds_color_country', '#ff6b6b'); }; await loadSettings(); let lastLoc = {}; let locationHistory = new Map(); // For polling/debouncing // Create IRF panel tab async function createSettingsTab() { const tab = await IRF.ui.panel.createTabFor(GM.info, { tabName: "Dark Souls" }); // Create volume control const volumeDisplay = document.createElement('span'); volumeDisplay.textContent = `${Math.round(settings.volume * 100)}%`; const volumeLabel = document.createElement('label'); volumeLabel.style.cssText = 'display: block; margin-bottom: 5px; color: #ccc;'; volumeLabel.textContent = 'Volume: '; volumeLabel.appendChild(volumeDisplay); const volumeSlider = document.createElement('input'); volumeSlider.type = 'range'; volumeSlider.min = '0'; volumeSlider.max = '100'; volumeSlider.value = settings.volume * 100; volumeSlider.className = IRF.ui.panel.styles.slider; volumeSlider.addEventListener('input', async () => { settings.volume = volumeSlider.value / 100; volumeDisplay.textContent = `${volumeSlider.value}%`; await GM_setValue('ds_volume', settings.volume); }); // Create trigger type select const triggerLabel = document.createElement('label'); triggerLabel.style.cssText = 'display: block; margin-bottom: 5px; color: #ccc;'; triggerLabel.textContent = 'Trigger Type:'; const triggerSelect = document.createElement('select'); triggerSelect.className = IRF.ui.panel.styles.select; triggerSelect.innerHTML = ` <option value="neighborhood" ${settings.triggerType === 'neighborhood' ? 'selected' : ''}>Neighborhood Change</option> <option value="state" ${settings.triggerType === 'state' ? 'selected' : ''}>State Change</option> <option value="country" ${settings.triggerType === 'country' ? 'selected' : ''}>Country Change</option> `; triggerSelect.addEventListener('change', async () => { settings.triggerType = triggerSelect.value; await GM_setValue('ds_triggerType', settings.triggerType); }); // Create duration control const durationDisplay = document.createElement('span'); durationDisplay.textContent = `${settings.popupDuration / 1000}s`; const durationLabel = document.createElement('label'); durationLabel.style.cssText = 'display: block; margin-bottom: 5px; color: #ccc;'; durationLabel.textContent = 'Duration: '; durationLabel.appendChild(durationDisplay); const durationSlider = document.createElement('input'); durationSlider.type = 'range'; durationSlider.min = '1000'; durationSlider.max = '8000'; durationSlider.step = '500'; durationSlider.value = settings.popupDuration; durationSlider.className = IRF.ui.panel.styles.slider; durationSlider.addEventListener('input', async () => { settings.popupDuration = parseInt(durationSlider.value); durationDisplay.textContent = `${settings.popupDuration / 1000}s`; await GM_setValue('ds_popupDuration', settings.popupDuration); }); // Create color controls const colorLabel = document.createElement('label'); colorLabel.style.cssText = 'display: block; margin-bottom: 8px; color: #ccc;'; colorLabel.textContent = 'Colors:'; const colorGrid = document.createElement('div'); colorGrid.style.cssText = 'display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: 15px;'; ['neighborhood', 'state', 'country'].forEach(type => { const colorDiv = document.createElement('div'); const colorSubLabel = document.createElement('label'); colorSubLabel.style.cssText = 'font-size: 12px; color: #aaa;'; colorSubLabel.textContent = type.charAt(0).toUpperCase() + type.slice(1) + ':'; const colorInput = document.createElement('input'); colorInput.type = 'color'; colorInput.value = settings.colors[type]; colorInput.style.cssText = 'width: 100%; height: 30px; border: none; border-radius: 4px;'; colorInput.addEventListener('change', async () => { settings.colors[type] = colorInput.value; await GM_setValue(`ds_color_${type}`, settings.colors[type]); }); colorDiv.appendChild(colorSubLabel); colorDiv.appendChild(colorInput); colorGrid.appendChild(colorDiv); }); // Create test button const testButton = document.createElement('button'); testButton.textContent = 'Test Popup'; testButton.style.cssText = 'width: 100%; padding: 8px; margin-top: 15px;'; testButton.addEventListener('click', () => { const loc = container.state.currentLocation; // Determine what to show based on trigger type let locationText = 'Unknown Location'; let popupType = 'neighborhood'; if (loc) { switch (settings.triggerType) { case 'neighborhood': locationText = loc.neighborhood || 'Unknown Neighborhood'; popupType = 'neighborhood'; break; case 'state': locationText = loc.state || 'Unknown State'; popupType = 'state'; break; case 'country': locationText = loc.country || 'Unknown Country'; popupType = 'country'; break; } } showPopup(locationText, popupType); }); // Add all elements to tab const createSection = (label, input) => { const section = document.createElement('div'); section.style.marginBottom = '15px'; section.appendChild(label); section.appendChild(input); return section; }; tab.container.appendChild(createSection(volumeLabel, volumeSlider)); tab.container.appendChild(createSection(triggerLabel, triggerSelect)); tab.container.appendChild(createSection(durationLabel, durationSlider)); const colorSection = document.createElement('div'); colorSection.style.marginBottom = '15px'; colorSection.appendChild(colorLabel); colorSection.appendChild(colorGrid); tab.container.appendChild(colorSection); tab.container.appendChild(testButton); } // Location tracking with debouncing function shouldShowPopup(field, value) { const now = Date.now(); const key = `${field}:${value}`; const lastShown = locationHistory.get(key); console.log(`[DS Debounce] Checking ${key}, last shown: ${lastShown ? new Date(lastShown).toLocaleTimeString() : 'never'}`); // Don't show if same location was shown in last 10 minutes if (lastShown && (now - lastShown) < 600000) { const timeLeft = Math.round((600000 - (now - lastShown)) / 1000); console.log(`[DS Debounce] Blocked - ${timeLeft}s remaining`); return false; } locationHistory.set(key, now); console.log(`[DS Debounce] Allowed - showing popup`); // Clean up old entries (older than 15 minutes) for (const [historyKey, timestamp] of locationHistory.entries()) { if (now - timestamp > 900000) { locationHistory.delete(historyKey); } } return true; } function getFieldType(field) { if (field === 'neighborhood') return 'neighborhood'; if (field === 'county') return 'neighborhood'; if (field === 'state') return 'state'; if (field === 'country') return 'country'; return 'neighborhood'; } const originalUpdateData = container.methods.updateData; container.state.updateData = new Proxy(originalUpdateData, { apply: (target, thisArg, args) => { const result = Reflect.apply(target, thisArg, args); const loc = container.state.currentLocation; // Only check fields based on trigger type setting let fields = []; switch (settings.triggerType) { case 'neighborhood': fields = ["neighborhood", "county"]; break; case 'state': fields = ["state"]; break; case 'country': fields = ["country"]; break; } let changedField = null; for (const field of fields) { if (loc?.[field] && loc[field] !== lastLoc[field]) { changedField = field; break; } } const changedValue = changedField ? loc[changedField] : null; lastLoc = { ...loc }; if (changedValue && shouldShowPopup(changedField, changedValue)) { const fieldType = getFieldType(changedField); showPopup(changedValue, fieldType); } return result; } }); function showPopup(text, type = 'neighborhood') { const existing = document.getElementById("ds-location-popup"); if (existing) existing.remove(); const overlay = document.createElement("div"); overlay.id = "ds-overlay"; Object.assign(overlay.style, { position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh", backgroundColor: "#000", opacity: "0", zIndex: 9998, pointerEvents: "none", transition: "opacity 1.2s ease-in-out", }); document.body.appendChild(overlay); requestAnimationFrame(() => { overlay.style.opacity = "0.4"; }); const popup = document.createElement("div"); popup.id = "ds-location-popup"; popup.textContent = text; // Base styles Object.assign(popup.style, { position: "fixed", top: "40%", left: "50%", transform: "translate(-50%, -50%) scale(0.95)", color: settings.colors[type], fontFamily: "adobe-garamond-pro, Georgia, serif", whiteSpace: "nowrap", maxWidth: "90vw", userSelect: "none", zIndex: 9999, opacity: "0", textAlign: "center", lineHeight: "1.2", transition: "opacity 1.2s ease-in-out, transform 1.2s ease-in-out, top 1.2s ease-in-out, left 1.2s ease-in-out, font-size 1.2s ease-in-out", pointerEvents: "none", padding: "1rem", paddingBottom: "0.05rem", ...settings.styles[type] }); document.body.appendChild(popup); // Play sound if (settings.volume > 0) { const audio = new Audio("https://media.vocaroo.com/mp3/1f7euqkLrXJs"); audio.volume = settings.volume; audio.play().catch(() => {}); } // Fade in requestAnimationFrame(() => { popup.style.opacity = "1"; popup.style.transform = "translate(-50%, -50%) scale(1)"; }); // Fade out after popup duration setTimeout(() => { popup.style.opacity = "0"; popup.style.transform = "translate(-50%, -50%) scale(1.05)"; overlay.style.opacity = "0"; setTimeout(() => { popup.remove(); overlay.remove(); }, 1500); }, settings.popupDuration); } // Initialize settings tab setTimeout(createSettingsTab, 0); // Manual trigger function. showDSLocationPopup(); 'neighborhood' 'state' 'country' const manualTriggers = { showDSLocationPopup: (testType = null) => { const loc = container.state.currentLocation; if (!loc) { console.warn("No location data available."); return; } const fields = ["neighborhood", "state", "country"]; for (const field of fields) { if (loc[field]) { const fieldType = testType || getFieldType(field); showPopup(loc[field], fieldType); break; } } }, }; // Expose functions to global scope Object.assign(window, manualTriggers); // Also expose via unsafeWindow if (typeof unsafeWindow !== 'undefined') { Object.assign(unsafeWindow, manualTriggers); } })();