您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
The definitive toolkit. Features ad-hoc group spawning, intelligent deselection, conflict-free hotkeys, and a powerful group manager.
// ==UserScript== // @name Infinite Craft - Multiple word spawns // @version 1.0.2 // @namespace http://tampermonkey.net/ // @description The definitive toolkit. Features ad-hoc group spawning, intelligent deselection, conflict-free hotkeys, and a powerful group manager. // @author Google Gemini AI & ChessScholar // @match https://neal.fun/infinite-craft/ // @icon https://neal.fun/favicons/infinite-craft.png // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @run-at document-idle // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Configuration & Storage Keys --- const SPAWN_COUNT_KEY = 'pt_spawnCount_v1'; const SAVED_GROUPS_KEY = 'pt_elementGroups_v1'; const DEFAULT_SPAWN_COUNT = 1; // --- Global State Variables --- let multiSelectItems = new Map(); let currentlyEditingGroup = { name: null, items: new Map() }; let allDiscoveredItems = []; // --- Storage Management & Autosaving --- const getSetting = async (key, defaultValue) => await GM_getValue(key, defaultValue); const setSetting = async (key, value) => await GM_setValue(key, value); async function autosaveCurrentGroup() { if (!currentlyEditingGroup || !currentlyEditingGroup.name) return; const groups = await getSetting(SAVED_GROUPS_KEY, {}); groups[currentlyEditingGroup.name] = Array.from(currentlyEditingGroup.items.values()); await setSetting(SAVED_GROUPS_KEY, groups); await updateGroupDropdown(); } // --- Core Initialization --- function initializeAllFeatures() { addCustomStyles(); createPowerToolsPanel(); initializeQuickSpawn(); initializeMultiSelectAndAdHocSpawning(); initializeDeselection(); initializeGroupManagerControls(); } // --- Feature 1: Quick Spawn (Shift + Middle-click) --- function initializeQuickSpawn() { window.addEventListener('mousedown', async (event) => { if (event.shiftKey && event.button === 1 && !isElementInPanel(event.target)) { const clickedItem = event.target.closest('.item'); if (clickedItem) { event.preventDefault(); event.stopImmediatePropagation(); const spawnCount = await getSetting(SPAWN_COUNT_KEY, DEFAULT_SPAWN_COUNT); spawnItemsInCenter([getItemData(clickedItem)], spawnCount); } } }, true); } // --- Feature 2: On-the-fly Group Creation & Spawning --- function initializeMultiSelectAndAdHocSpawning() { const sidebar = document.getElementById('sidebar'); sidebar.addEventListener('mousedown', async (event) => { const itemElement = event.target.closest('.item'); if (!itemElement || isElementInPanel(itemElement)) return; // Case 1: Spawn the entire selected group by clicking any member. if (multiSelectItems.size > 0 && multiSelectItems.has(getItemData(itemElement).text) && !event.ctrlKey) { event.preventDefault(); event.stopImmediatePropagation(); const spawnCount = await getSetting(SPAWN_COUNT_KEY, DEFAULT_SPAWN_COUNT); const itemsToSpawn = Array.from(multiSelectItems.values()).map(item => item.data); spawnItemsInCenter(itemsToSpawn, spawnCount); return; } // Case 2: Toggle selection with pure Ctrl+Click. if (event.ctrlKey && !event.shiftKey && !event.altKey) { event.preventDefault(); event.stopImmediatePropagation(); toggleItemInMultiSelect(itemElement); } }, true); } function initializeDeselection() { window.addEventListener('mousedown', (event) => { if (event.ctrlKey || multiSelectItems.size === 0 || isElementInPanel(event.target)) { return; } const clickedItem = event.target.closest('.item'); if (clickedItem && multiSelectItems.has(getItemData(clickedItem).text)) { return; } clearMultiSelect(); }, true); } function toggleItemInMultiSelect(element) { const itemData = getItemData(element); if (multiSelectItems.has(itemData.text)) { multiSelectItems.get(itemData.text).element.classList.remove('pt-multi-selected-item'); multiSelectItems.delete(itemData.text); } else { element.classList.add('pt-multi-selected-item'); multiSelectItems.set(itemData.text, { data: itemData, element }); } updateCreateButtonState(); } function updateCreateButtonState() { const btn = document.getElementById('pt-create-group-btn'); if (btn) btn.disabled = (multiSelectItems.size === 0); } function clearMultiSelect() { multiSelectItems.forEach(item => item.element.classList.remove('pt-multi-selected-item')); multiSelectItems.clear(); updateCreateButtonState(); } async function createNewGroupFromSelection() { if (multiSelectItems.size === 0) return; const name = prompt("Enter the name for your new group:"); if (!name || !name.trim()) return; const groups = await getSetting(SAVED_GROUPS_KEY, {}); if (groups[name.trim()] && !confirm(`A group named "${name.trim()}" already exists. Overwrite it?`)) return; groups[name.trim()] = Array.from(multiSelectItems.values()).map(item => item.data); await setSetting(SAVED_GROUPS_KEY, groups); await updateGroupDropdown(); clearMultiSelect(); } // --- Feature 3: The Group Manager & Spawner --- function initializeGroupManagerControls() { document.getElementById('pt-manage-groups-btn').addEventListener('click', openGroupManagerModal); document.getElementById('pt-create-group-btn').addEventListener('click', createNewGroupFromSelection); const dropdown = document.getElementById('pt-saved-groups-dropdown'); const spawnBtn = document.getElementById('pt-spawn-group-btn'); dropdown.addEventListener('change', () => spawnBtn.disabled = !dropdown.value); spawnBtn.addEventListener('click', async () => { const groupName = dropdown.value; if (!groupName) return; const spawnCount = await getSetting(SPAWN_COUNT_KEY, DEFAULT_SPAWN_COUNT); const groups = await getSetting(SAVED_GROUPS_KEY, {}); spawnItemsInCenter(groups[groupName], spawnCount); }); updateGroupDropdown(); } // --- The Group Manager Modal --- async function openGroupManagerModal() { clearMultiSelect(); const containerVue = document.querySelector(".container").__vue__; allDiscoveredItems = containerVue?.items ?? []; const overlay = document.createElement('div'); overlay.id = 'pt-modal-overlay'; overlay.innerHTML = ` <div id="pt-modal-box"> <div class="pt-modal-header"> <h3>Group Manager</h3> <button id="pt-modal-close-btn" title="Close">X</button> </div> <div class="pt-modal-instructions"> Click an item in Discoveries to add. Click an item in the group to remove. Changes save automatically. </div> <div class="pt-modal-content"> <div class="pt-modal-panel"> <h4>Your Groups</h4> <div class="pt-modal-controls"> <button id="pt-modal-new-group-btn">Create New Group</button> </div> <ul id="pt-modal-group-list" class="pt-item-list"></ul> </div> <div class="pt-modal-panel"> <h4 id="pt-modal-group-title">Select or Create a Group</h4> <ul id="pt-modal-group-contents" class="pt-item-list"></ul> </div> <div class="pt-modal-panel"> <h4>Add Items from Discoveries</h4> <input type="text" id="pt-modal-search-input" placeholder="Search all items..."> <ul id="pt-modal-all-items-list" class="pt-item-list"></ul> </div> </div> <div class="pt-modal-footer"> <button id="pt-modal-delete-btn" class="pt-danger-btn" disabled>Delete Group</button> </div> </div>`; document.body.appendChild(overlay); const closeModal = () => document.body.removeChild(overlay); document.getElementById('pt-modal-close-btn').addEventListener('click', closeModal); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); document.getElementById('pt-modal-new-group-btn').addEventListener('click', async () => { const name = prompt("Enter the name for your new group:"); if (name && name.trim()) { const groups = await getSetting(SAVED_GROUPS_KEY, {}); if (groups[name.trim()]) { alert("A group with this name already exists."); return; } groups[name.trim()] = []; await setSetting(SAVED_GROUPS_KEY, groups); await renderGroupListInModal(); resetEditState({ name: name.trim(), items: new Map() }); } }); document.getElementById('pt-modal-delete-btn').addEventListener('click', async () => { if (!currentlyEditingGroup.name || !confirm(`Are you sure you want to delete "${currentlyEditingGroup.name}"?`)) return; const groups = await getSetting(SAVED_GROUPS_KEY, {}); delete groups[currentlyEditingGroup.name]; await setSetting(SAVED_GROUPS_KEY, groups); resetEditState({ name: null, items: new Map() }); await renderGroupListInModal(); await updateGroupDropdown(); }); const searchInput = document.getElementById('pt-modal-search-input'); searchInput.addEventListener('input', () => { const filter = searchInput.value.toLowerCase(); document.querySelectorAll('#pt-modal-all-items-list li').forEach(item => { item.style.display = item.dataset.text.toLowerCase().includes(filter) ? 'flex' : 'none'; }); }); renderAllItemsInModal(); await renderGroupListInModal(); resetEditState({ name: null, items: new Map() }); } function resetEditState(newState) { currentlyEditingGroup = newState; const title = document.getElementById('pt-modal-group-title'); const deleteBtn = document.getElementById('pt-modal-delete-btn'); document.querySelectorAll('#pt-modal-group-list li').forEach(li => { li.classList.toggle('pt-active', li.dataset.groupName === currentlyEditingGroup.name); }); if (currentlyEditingGroup.name) { title.textContent = `Editing: ${currentlyEditingGroup.name}`; deleteBtn.disabled = false; } else { title.textContent = 'Select or Create a Group'; deleteBtn.disabled = true; } renderGroupContentsInModal(); } async function renderGroupListInModal() { const list = document.getElementById('pt-modal-group-list'); const groups = await getSetting(SAVED_GROUPS_KEY, {}); list.innerHTML = ''; Object.keys(groups).sort().forEach(name => { const li = document.createElement('li'); li.innerHTML = `<span>${name}</span>`; li.dataset.groupName = name; li.addEventListener('click', () => { const itemsMap = new Map(); (groups[name] || []).forEach(item => itemsMap.set(item.text, item)); resetEditState({ name, items: itemsMap }); }); list.appendChild(li); }); } function renderAllItemsInModal() { const list = document.getElementById('pt-modal-all-items-list'); list.innerHTML = ''; allDiscoveredItems.forEach(item => { const li = document.createElement('li'); li.dataset.text = item.text; li.innerHTML = `<span>${item.emoji} ${item.text}</span>`; li.addEventListener('click', () => { if (!currentlyEditingGroup.name) { alert("Please select or create a group first."); return; } if (!currentlyEditingGroup.items.has(item.text)) { currentlyEditingGroup.items.set(item.text, item); renderGroupContentsInModal(); autosaveCurrentGroup(); } }); list.appendChild(li); }); } function renderGroupContentsInModal() { const list = document.getElementById('pt-modal-group-contents'); list.innerHTML = ''; if (currentlyEditingGroup.items.size === 0) { list.innerHTML = `<li class="pt-empty-list">No items in this group.</li>`; } else { Array.from(currentlyEditingGroup.items.values()).sort((a,b) => a.text.localeCompare(b.text)).forEach(item => { const li = document.createElement('li'); li.dataset.text = item.text; li.innerHTML = `<span>${item.emoji} ${item.text}</span>`; li.addEventListener('click', () => { currentlyEditingGroup.items.delete(item.text); renderGroupContentsInModal(); autosaveCurrentGroup(); }); list.appendChild(li); }); } } async function updateGroupDropdown() { const dropdown = document.getElementById('pt-saved-groups-dropdown'); const spawnBtn = document.getElementById('pt-spawn-group-btn'); const groups = await getSetting(SAVED_GROUPS_KEY, {}); const groupNames = Object.keys(groups).sort(); const currentVal = dropdown.value; dropdown.innerHTML = '<option value="">-- Select a Group --</option>'; groupNames.forEach(name => { const option = document.createElement('option'); option.value = name; option.textContent = name; dropdown.appendChild(option); }); dropdown.value = groupNames.includes(currentVal) ? currentVal : ""; spawnBtn.disabled = !dropdown.value; } // --- UI Creation --- function createPowerToolsPanel() { const container = document.createElement('div'); container.id = 'pt-container'; container.innerHTML = ` <div class="pt-spawner-controls"> <select id="pt-saved-groups-dropdown"></select> <button id="pt-spawn-group-btn" disabled>Spawn</button> <input type="number" id="pt-spawn-count-input" min="1" title="Spawn Count" value="${DEFAULT_SPAWN_COUNT}"> </div> <hr class="pt-divider"> <div class="pt-controls-container"> <button id="pt-manage-groups-btn">Manage Groups</button> <button id="pt-create-group-btn" disabled>Create</button> </div> <div class="pt-instructions">Ctrl+Click to select. Ctrl+Shift+Click to multi-spawn. </div> `; document.getElementById('sidebar').appendChild(container); getSetting(SPAWN_COUNT_KEY, DEFAULT_SPAWN_COUNT).then(count => { const input = document.getElementById('pt-spawn-count-input'); input.value = count; input.addEventListener('change', () => setSetting(SPAWN_COUNT_KEY, parseInt(input.value, 10) || DEFAULT_SPAWN_COUNT)); }); } function addCustomStyles() { GM_addStyle(` /* Main panel styling */ #pt-container { padding: 10px; border-top: 1px solid var(--border-color); background: var(--sidebar-bg); } .pt-header { font-size: 1.2em; text-align: center; font-weight: bold; margin-bottom: 10px; } .pt-divider { border: none; border-top: 1px solid var(--border-color); opacity: 0.5; margin: 10px 0; } .pt-spawner-controls { display: flex; gap: 8px; margin-bottom: 10px; } .pt-controls-container { display: flex; gap: 8px; } .pt-controls-container button, .pt-spawner-controls button { flex-shrink: 0; } .pt-controls-container button { flex-grow: 1; } #pt-saved-groups-dropdown { flex-grow: 1; } #pt-spawn-count-input { width: 60px; text-align: center; flex-shrink: 0; } #pt-spawn-group-btn:disabled, #pt-create-group-btn:disabled { opacity: 0.5; cursor: not-allowed; } .pt-instructions { font-size: 0.8em; text-align: center; opacity: 0.6; margin-top: 8px; } .item.pt-multi-selected-item { background: linear-gradient(180deg, #6ea8ff, #428dff 80%) !important; color: white !important; border-color: #297bff !important; } /* --- Custom Modal Theme --- */ #pt-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.75); display: flex; align-items: center; justify-content: center; z-index: 10000; font-family: sans-serif; } #pt-modal-box { background: #2c2c2c; color: #e0e0e0; border: 1px solid #555; width: 95vw; max-width: 800px; height: 80vh; border-radius: 8px; display: flex; flex-direction: column; box-shadow: 0 10px 30px #000; } .pt-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 0 20px; border-bottom: 1px solid #444; flex-shrink: 0; } .pt-modal-instructions { text-align: center; padding: 8px 20px; font-size: 0.9em; background: #333; border-bottom: 1px solid #444; flex-shrink: 0; } #pt-modal-close-btn { background: none; border: none; font-size: 1.5em; cursor: pointer; color: #e0e0e0; opacity: 0.7; } #pt-modal-close-btn:hover { opacity: 1; } .pt-modal-content { display: flex; padding: 10px; gap: 10px; flex-grow: 1; overflow: hidden; } .pt-modal-panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #333; padding: 10px; border-radius: 5px; } .pt-modal-panel h4 { text-align: center; margin: 0 0 10px 0; flex-shrink: 0; } .pt-modal-panel input[type="text"] { margin-bottom: 10px; flex-shrink: 0; background: #444; color: #e0e0e0; border: 1px solid #666; padding: 8px; border-radius: 4px; } .pt-modal-controls { flex-shrink: 0; margin-bottom: 10px; } .pt-item-list { list-style: none; padding: 0; margin: 0; overflow-y: auto; } .pt-item-list li { display: flex; justify-content: space-between; align-items: center; padding: 6px; border-radius: 3px; cursor: pointer; margin-bottom: 4px; background: #444; } .pt-item-list li:hover { background-color: #555; } .pt-item-list li.pt-empty-list { justify-content: center; opacity: 0.6; cursor: default; background: none !important; } #pt-modal-group-list li.pt-active { background-color: #4a90e2; color: white; font-weight: bold; } .pt-modal-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 10px 20px; border-top: 1px solid #444; flex-shrink: 0; background: #333; } #pt-modal-box button { background-color: #555; color: #fff; border: 1px solid #666; padding: 8px 12px; border-radius: 4px; cursor: pointer; } #pt-modal-box button:hover { background-color: #666; } #pt-modal-box button:disabled { background-color: #444; color: #888; cursor: not-allowed; } .pt-danger-btn { background-color: #c94040 !important; } .pt-danger-btn:hover { background-color: #e06363 !important; } `); } // --- Spawning & Utility --- function spawnItemsInCenter(items, count = 1) { if (!items || items.length === 0) return; const createInstance = unsafeWindow.IC.createInstance; const sidebarWidth = document.getElementById('sidebar')?.offsetWidth ?? 300; const canvasCenterX = sidebarWidth + (window.innerWidth - sidebarWidth) / 2; const canvasCenterY = window.innerHeight / 2; items.forEach(itemData => { for (let i = 0; i < count; i++) { createInstance({ ...itemData, x: canvasCenterX + (Math.random()*250 - 125), y: canvasCenterY + (Math.random()*250 - 125), animate: true }); } }); } const getItemData = (element) => ({ text: element.getAttribute('data-item-text'), emoji: element.getAttribute('data-item-emoji'), id: element.getAttribute('data-item-id'), discovery: element.hasAttribute('data-item-discovery') }); const isElementInPanel = (element) => element.closest('#pt-container, #pt-modal-overlay'); // --- Script Entry Point --- function waitForGame() { const interval = setInterval(() => { if (document.querySelector(".container")?.__vue__?.items) { clearInterval(interval); initializeAllFeatures(); } }, 200); } waitForGame(); })();