// ==UserScript== // @name Infinite Craft Helper // @namespace http://tampermonkey.net/ // @version 1.0.4-feature/auto-play-game-interface // @description Control panel overlay for Infinite Craft with GameInterface foundation // @author You // @match https://neal.fun/infinite-craft/* // @match https://neal.fun/infinite-craft // @updateURL https://raw.githubusercontent.com/bytecodeguru/infinite-crafter/feature/auto-play-game-interface/infinite-craft-helper.user.js // @downloadURL https://raw.githubusercontent.com/bytecodeguru/infinite-crafter/feature/auto-play-game-interface/infinite-craft-helper.user.js // @supportURL https://github.com/bytecodeguru/infinite-crafter/issues // @homepageURL https://github.com/bytecodeguru/infinite-crafter // @grant none // ==/UserScript== (function () { 'use strict'; // === auto-play/action-simulator.js === /** * ActionSimulator module * Provides utilities for simulating mouse interactions with human-like timing. */ const DEFAULT_DELAY_RANGE = { min: 35, max: 85 }; const ACTION_SIM_DEFAULT_LOGGER = { log: (...args) => console.log('[ActionSimulator]', ...args), warn: (...args) => console.warn('[ActionSimulator]', ...args), error: (...args) => console.error('[ActionSimulator]', ...args) }; function defaultDocument() { if (typeof window !== 'undefined' && window.document) { return window.document; } return null; } function defaultDelayProvider(min, max) { if (typeof min !== 'number') { min = DEFAULT_DELAY_RANGE.min; } if (typeof max !== 'number') { max = DEFAULT_DELAY_RANGE.max; } if (max < min) { max = min; } const range = max - min; if (range <= 0) { return min; } return min + Math.random() * range; } class ActionSimulator { constructor({ logger = ACTION_SIM_DEFAULT_LOGGER, document = defaultDocument(), delayProvider = defaultDelayProvider } = {}) { this.logger = logger; this.document = document; this.delayProvider = delayProvider; this.defaultDelayRange = { ...DEFAULT_DELAY_RANGE }; this.pointerPosition = { x: 0, y: 0 }; } setLogger(logger) { this.logger = logger; } setDelayRange(range = {}) { if (typeof range.min === 'number') { this.defaultDelayRange.min = Math.max(0, range.min); } if (typeof range.max === 'number') { this.defaultDelayRange.max = Math.max(this.defaultDelayRange.min, range.max); } } async clickElement(element, options = {}) { if (!element) { this.log('warn', 'clickElement called without a target element'); return false; } const coords = this.resolveCoordinates({ target: element, options }); if (!options.immediate) { await this.waitForDelay(options.beforeMove); } await this.movePointer({ target: element, coords, skipDelay: true }); await this.waitForDelay(options.beforeMouseDown); await this.dispatchMouseEvent({ target: element, type: 'mousedown', coords, extra: { buttons: 1, button: 0 } }); await this.waitForDelay(options.beforeMouseUp); await this.dispatchMouseEvent({ target: element, type: 'mouseup', coords, extra: { buttons: 0, button: 0 } }); await this.waitForDelay(options.beforeClick); await this.dispatchMouseEvent({ target: element, type: 'click', coords, extra: { buttons: 0, button: 0, detail: 1 } }); this.log('log', 'clickElement completed', { x: coords.x, y: coords.y, name: element?.dataset?.item || element?.textContent?.trim() }); return true; } async moveMouseTo(x, y, options = {}) { const coords = { x, y }; await this.movePointer({ coords, skipDelay: !!options.immediate, extra: options.extra, target: options.target }); return coords; } async movePointer({ target, coords, skipDelay = false, extra = {} }) { const resolved = coords || this.resolveCoordinates({ target }); if (!skipDelay) { await this.waitForDelay(); } await this.dispatchMouseEvent({ target, type: 'mousemove', coords: resolved, extra: { movementX: resolved.x - this.pointerPosition.x, movementY: resolved.y - this.pointerPosition.y, ...extra } }); this.pointerPosition = { x: resolved.x, y: resolved.y }; return resolved; } async performSequence(actions = []) { for (const action of actions) { switch (action.type) { case 'move': await this.movePointer({ target: action.target, coords: action.coords ? { x: action.coords.x, y: action.coords.y } : null, skipDelay: !!action.immediate, extra: action.extra }); break; case 'down': await this.waitForDelay(action.before); await this.dispatchMouseEvent({ target: action.target, type: 'mousedown', coords: this.resolveCoordinates({ target: action.target, options: action }), extra: { buttons: action.buttons ?? 1, button: action.button ?? 0 } }); break; case 'up': await this.waitForDelay(action.before); await this.dispatchMouseEvent({ target: action.target, type: 'mouseup', coords: this.resolveCoordinates({ target: action.target, options: action }), extra: { buttons: action.buttons ?? 0, button: action.button ?? 0 } }); break; case 'click': await this.clickElement(action.target, action.options || {}); break; case 'wait': await this.sleep(action.duration ?? 0); break; default: this.log('warn', 'Unknown action type', action); break; } } } async waitForDelay(options = {}) { const { min, max } = this.getDelayRange(options); const ms = Math.max(0, Math.round(this.delayProvider(min, max))); if (ms > 0) { await this.sleep(ms); } return ms; } sleep(ms) { if (!ms) { return Promise.resolve(); } return new Promise(resolve => setTimeout(resolve, ms)); } resolveCoordinates({ target, options = {} }) { if (options && typeof options.x === 'number' && typeof options.y === 'number') { return { x: options.x, y: options.y }; } if (target && typeof target.getBoundingClientRect === 'function') { const rect = target.getBoundingClientRect(); const x = rect.left + (rect.width / 2); const y = rect.top + (rect.height / 2); return { x, y, rect }; } return { ...this.pointerPosition }; } getDelayRange(options = {}) { const delay = options.delay || {}; const min = Math.max(0, typeof delay.min === 'number' ? delay.min : (typeof options.minDelay === 'number' ? options.minDelay : this.defaultDelayRange.min)); const max = Math.max(min, typeof delay.max === 'number' ? delay.max : (typeof options.maxDelay === 'number' ? options.maxDelay : this.defaultDelayRange.max)); return { min, max }; } dispatchTarget(target) { if (target && typeof target.dispatchEvent === 'function') { return target; } const doc = this.document || defaultDocument(); if (!doc) { return null; } if (doc.body && typeof doc.body.dispatchEvent === 'function') { return doc.body; } return doc; } async dispatchMouseEvent({ target, type, coords, extra = {} }) { const eventTarget = this.dispatchTarget(target); if (!eventTarget) { this.log('warn', `dispatchMouseEvent skipped: no target available for ${type}`); return false; } const init = this.buildMouseEventInit(coords, extra); const event = this.createMouseEvent(type, init); this.log('log', `Dispatching ${type}`, init); return eventTarget.dispatchEvent(event); } buildMouseEventInit(coords = {}, extra = {}) { const x = Math.round(coords?.x ?? this.pointerPosition.x ?? 0); const y = Math.round(coords?.y ?? this.pointerPosition.y ?? 0); return { bubbles: true, cancelable: true, composed: true, clientX: x, clientY: y, pageX: x, pageY: y, screenX: x, screenY: y, ...extra }; } createMouseEvent(type, init) { const doc = this.document || defaultDocument(); if (typeof MouseEvent === 'function') { return new MouseEvent(type, init); } if (doc && typeof doc.createEvent === 'function') { const event = doc.createEvent('MouseEvents'); event.initMouseEvent( type, init.bubbles, init.cancelable, init.view || doc.defaultView || doc.parentWindow, 0, init.screenX || init.clientX, init.screenY || init.clientY, init.clientX, init.clientY, init.ctrlKey || false, init.altKey || false, init.shiftKey || false, init.metaKey || false, init.button || 0, init.relatedTarget || null ); return event; } return { type, ...init }; } log(level, message, details) { const logger = this.logger || ACTION_SIM_DEFAULT_LOGGER; const method = typeof logger[level] === 'function' ? logger[level] : logger.log; if (typeof method === 'function') { if (details !== undefined) { method.call(logger, message, details); } else { method.call(logger, message); } } } } function createActionSimulator(options = {}) { return new ActionSimulator(options); } { ActionSimulator }; // === auto-play/sidebar-helpers.js === /** * Sidebar helper utilities for GameInterface * Provides DOM selectors, element mapping, and validation helpers. */ const SELECTORS = { sidebar: ['#sidebar', '.sidebar', '.sidebar-container', '.game-elements-sidebar', '[data-testid="sidebar"]'], sidebarItems: ['.item', '.element', '.sidebar-item', '.inventory-item', '[data-element]'], playArea: [ '#instances', '#instances-top', '#select-box', '#particles', '.container.infinite-craft', '[data-container][class*="infinite-craft"]', '#board', '.board', '.play-area', '.game-board', '[data-testid="board"]' ], nameNodes: ['.label', '.name', '.element-name', '.title', '[data-element-name]'] }; const DRAG_CLASS_BLOCKLIST = ['disabled', 'item-disabled']; function normalizeName(raw) { if (!raw) { return ''; } return raw .replace(/[\u{1F300}-\u{1F5FF}\u{1F600}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu, ' ') .replace(/\s+/g, ' ') .trim(); } function selectFirst(root, selectors) { if (!root) { return null; } for (const selector of selectors) { const element = root.querySelector(selector); if (element) { return element; } } return null; } function collectAll(root, selectors) { if (!root) { return []; } const elements = new Set(); selectors.forEach(selector => { root.querySelectorAll(selector).forEach(element => elements.add(element)); }); return Array.from(elements); } function toPlainDataset(element) { if (!element || !element.dataset) { return {}; } return Object.assign({}, element.dataset); } function isElementVisible(element) { if (!element || typeof element.getBoundingClientRect !== 'function') { return false; } const rect = element.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } function getElementBounds(element) { if (!element || typeof element.getBoundingClientRect !== 'function') { return null; } const rect = element.getBoundingClientRect(); return { top: rect.top, right: rect.right, bottom: rect.bottom, left: rect.left, width: rect.width, height: rect.height }; } function getElementName(element) { if (!element) { return ''; } const dataset = element.dataset || {}; const datasetName = dataset.element || dataset.name || dataset.itemText || dataset.itemName || dataset.item; if (datasetName) { return normalizeName(datasetName); } const attributeName = element.getAttribute?.('data-element-name') || element.getAttribute?.('data-item-text') || element.getAttribute?.('data-item'); if (attributeName) { return normalizeName(attributeName); } const nameNode = selectFirst(element, SELECTORS.nameNodes); if (nameNode && nameNode.textContent) { return normalizeName(nameNode.textContent); } if (element.textContent) { return normalizeName(element.textContent); } return ''; } function getElementEmoji(element) { if (!element) { return ''; } const dataset = element.dataset || {}; if (dataset.itemEmoji) { return dataset.itemEmoji; } const emojiNode = element.querySelector?.('.item-emoji'); if (emojiNode && emojiNode.textContent) { return emojiNode.textContent.trim(); } return ''; } function hasBlockedClass(element) { return Boolean(element && DRAG_CLASS_BLOCKLIST.some(className => element.classList?.contains(className))); } function isPotentiallyDraggable(element) { if (!element) { return false; } if (typeof element.matches === 'function' && element.matches('[data-item]')) { return true; } const dataset = element.dataset || {}; if (dataset.item !== undefined || dataset.draggable === 'true') { return true; } if (element.getAttribute?.('draggable') === 'true') { return true; } return false; } function createElementInfo(element, index) { const dataset = toPlainDataset(element); const name = getElementName(element); const bounds = getElementBounds(element); const isVisible = isElementVisible(element); const isDraggable = isPotentiallyDraggable(element) && !hasBlockedClass(element); const identifier = dataset.itemId || dataset.id || element.id || null; return { element, index, id: identifier, name, normalizedName: normalizeName(name), emoji: getElementEmoji(element), dataset, bounds, isVisible, isDraggable }; } function toElementInfo(target) { if (!target) { return null; } if (target.element instanceof HTMLElement) { return target; } if (target instanceof HTMLElement) { return createElementInfo(target, -1); } return null; } function validateSidebarElement(info) { if (!info) { return { isValid: false, issues: ['Element info missing'], info: null }; } const issues = []; if (!info.name) { issues.push('Missing element name'); } if (!info.isVisible) { issues.push('Element not visible'); } if (!info.isDraggable) { issues.push('Element not draggable'); } if (!info.bounds || typeof info.bounds.width !== 'number') { issues.push('Missing bounds'); } return { isValid: issues.length === 0, issues, info }; } function findDuplicatesByName(elements) { const counts = elements.reduce((acc, info) => { const key = (info.normalizedName || info.name || '').toLowerCase(); if (!key) { return acc; } acc[key] = (acc[key] || 0) + 1; return acc; }, {}); return Object.entries(counts) .filter(([, count]) => count > 1) .map(([name]) => name); } // === auto-play/game-interface.js === /** * GameInterface module * Provides DOM access helpers for the Infinite Craft game UI. */ const DEFAULT_LOGGER = { log: (...args) => console.log('[GameInterface]', ...args), warn: (...args) => console.warn('[GameInterface]', ...args), error: (...args) => console.error('[GameInterface]', ...args) }; function callLogger(logger, level, message, ...args) { if (!logger || typeof logger[level] !== 'function') { return; } logger[level](message, ...args); } function makeTestResult(name, passed, successDetails, failureDetails) { return { name, passed, details: passed ? successDetails : failureDetails }; } function defaultDocument() { if (typeof window !== 'undefined' && window.document) { return window.document; } return null; } class GameInterface { constructor({ logger = DEFAULT_LOGGER, doc = defaultDocument() } = {}) { this.logger = logger; this.document = doc; } } function getSidebarContainerMethod() { if (!this.document) { callLogger(this.logger, 'warn', 'Document not available, sidebar lookup skipped'); return null; } const container = selectFirst(this.document, SELECTORS.sidebar); if (!container) { callLogger(this.logger, 'warn', 'Sidebar container not found', { selectorsTried: SELECTORS.sidebar }); } return container; } function getSidebarElementsMethod(options = {}) { const container = this.getSidebarContainer(); if (!container) { return []; } const { includeHidden = false, limit = null } = options; const infos = collectAll(container, SELECTORS.sidebarItems) .map((element, index) => createElementInfo(element, index)) .filter(info => includeHidden || info.isVisible); if (!infos.length) { callLogger(this.logger, 'warn', 'No sidebar elements detected', { selectorsTried: SELECTORS.sidebarItems, includeHidden }); } if (typeof limit === 'number' && limit > 0) { return infos.slice(0, limit); } return infos; } function getDraggableElementsMethod(options = {}) { return this.getSidebarElements(options).filter(info => info.isDraggable); } function getElementCountMethod(options = {}) { return this.getSidebarElements(options).length; } function findElementByNameMethod(name, options = {}) { if (!name) { return null; } const normalizedTarget = normalizeName(name).toLowerCase(); if (!normalizedTarget) { return null; } const includeHidden = options.includeHidden !== undefined ? options.includeHidden : true; const elements = this.getSidebarElements({ ...options, includeHidden }); return elements.find(info => matchesName(info, normalizedTarget)) || null; } function matchesName(info, normalizedTarget) { if (!info || !normalizedTarget) { return false; } const candidates = []; if (info.normalizedName) { candidates.push(info.normalizedName); } if (info.name) { candidates.push(normalizeName(info.name)); } const datasetValues = info.dataset ? Object.values(info.dataset) : []; datasetValues.forEach(value => { const normalized = normalizeName(value); if (normalized) { candidates.push(normalized); } }); const textContent = info.element?.textContent; if (textContent) { const normalized = normalizeName(textContent); if (normalized) { candidates.push(normalized); } } return candidates .map(value => value.toLowerCase()) .some(value => value === normalizedTarget); } function findElementsByPredicateMethod(predicate, options = {}) { if (typeof predicate !== 'function') { return []; } return this.getSidebarElements(options).filter(info => { try { return Boolean(predicate(info)); } catch (error) { callLogger(this.logger, 'warn', 'Predicate threw while evaluating element', { error, info }); return false; } }); } function findElementByPredicateMethod(predicate, options = {}) { return this.findElementsByPredicate(predicate, options)[0] || null; } function getAvailableElementNamesMethod(options = {}) { return this.getSidebarElements(options).map(info => info.name).filter(Boolean); } function isElementDraggableMethod(target) { const info = toElementInfo(target); if (!info) { return false; } return validateSidebarElement(info).isValid; } function validateSidebarElementMethod(target) { return validateSidebarElement(toElementInfo(target)); } function getSidebarSnapshotMethod(options = {}) { const includeHidden = options.includeHidden !== undefined ? options.includeHidden : true; const elements = this.getSidebarElements({ ...options, includeHidden }); const validations = elements.map(info => validateSidebarElement(info)); const valid = validations.filter(result => result && result.isValid).map(result => result.info); const invalid = validations.filter(result => result && !result.isValid); return { timestamp: Date.now(), elements, valid, invalid, total: elements.length, validCount: valid.length, invalidCount: invalid.length, includeHidden }; } function logSidebarSummaryMethod(options = {}) { const snapshot = this.getSidebarSnapshot(options); const message = `Sidebar summary: ${snapshot.validCount}/${snapshot.total} draggable elements`; callLogger(this.logger, 'log', message); if (snapshot.invalidCount > 0) { const sample = snapshot.invalid.slice(0, 5).map(result => result.info?.name || '(unknown)'); callLogger(this.logger, 'warn', 'Invalid sidebar elements detected', { count: snapshot.invalidCount, sample }); } return snapshot; } function isGameReadyMethod() { return Boolean(this.getSidebarContainer() && this.getPlayAreaContainer()); } function getPlayAreaContainerMethod() { if (!this.document) { callLogger(this.logger, 'warn', 'Document not available, play area lookup skipped'); return null; } const container = selectFirst(this.document, SELECTORS.playArea); if (!container) { callLogger(this.logger, 'warn', 'Play area container not found', { selectorsTried: SELECTORS.playArea }); } return container; } function getAvailableElementDataMethod(options = {}) { return this.getSidebarElements(options).map(info => ({ id: info.id, name: info.name, emoji: info.emoji })); } function logGameStateMethod(options = {}) { const count = this.getElementCount(options); const names = this.getAvailableElementNames(options); callLogger(this.logger, 'log', `Detected ${count} available elements`); if (names.length) { callLogger(this.logger, 'log', 'Sample elements:', names.slice(0, 10)); } } function runBasicTestsMethod() { const sidebarContainer = this.getSidebarContainer(); const sidebarElements = this.getSidebarElements(); const playArea = this.getPlayAreaContainer(); const results = [ makeTestResult('sidebar-container', Boolean(sidebarContainer), 'Sidebar container detected', 'Sidebar container missing'), makeTestResult('sidebar-elements', sidebarElements.length > 0, `Found ${sidebarElements.length} sidebar elements`, 'No sidebar elements detected'), makeTestResult('play-area', Boolean(playArea), 'Play area container detected', 'Play area container missing') ]; if (!results.every(result => result.passed)) { callLogger(this.logger, 'warn', 'GameInterface basic tests reported issues', results); } else { callLogger(this.logger, 'log', 'GameInterface basic tests passed'); } return results; } function runSelectionDiagnosticsMethod(options = {}) { const snapshot = this.getSidebarSnapshot({ includeHidden: true, ...options }); const issues = []; if (snapshot.total === 0) { issues.push('No sidebar elements detected'); } const invisible = snapshot.elements.filter(info => !info.isVisible); if (invisible.length) { issues.push(`Found ${invisible.length} hidden sidebar elements`); } const undraggable = snapshot.elements.filter(info => !info.isDraggable); if (undraggable.length) { issues.push(`Found ${undraggable.length} non-draggable sidebar elements`); } const duplicates = findDuplicatesByName(snapshot.elements).slice(0, 5); if (duplicates.length) { issues.push(`Duplicate element names detected (${duplicates.join(', ')})`); } if (issues.length) { callLogger(this.logger, 'warn', 'Sidebar selection diagnostics reported issues', { issues, snapshot }); } else { callLogger(this.logger, 'log', 'Sidebar selection diagnostics passed'); } return { snapshot, issues }; } GameInterface.prototype.getSidebarContainer = getSidebarContainerMethod; GameInterface.prototype.getSidebarElements = getSidebarElementsMethod; GameInterface.prototype.getDraggableElements = getDraggableElementsMethod; GameInterface.prototype.getElementCount = getElementCountMethod; GameInterface.prototype.findElementByName = findElementByNameMethod; GameInterface.prototype.findElementsByPredicate = findElementsByPredicateMethod; GameInterface.prototype.findElementByPredicate = findElementByPredicateMethod; GameInterface.prototype.getAvailableElementNames = getAvailableElementNamesMethod; GameInterface.prototype.isElementDraggable = isElementDraggableMethod; GameInterface.prototype.validateSidebarElement = validateSidebarElementMethod; GameInterface.prototype.getSidebarSnapshot = getSidebarSnapshotMethod; GameInterface.prototype.logSidebarSummary = logSidebarSummaryMethod; GameInterface.prototype.isGameReady = isGameReadyMethod; GameInterface.prototype.getPlayAreaContainer = getPlayAreaContainerMethod; GameInterface.prototype.getAvailableElementData = getAvailableElementDataMethod; GameInterface.prototype.logGameState = logGameStateMethod; GameInterface.prototype.runBasicTests = runBasicTestsMethod; GameInterface.prototype.runSelectionDiagnostics = runSelectionDiagnosticsMethod; function createGameInterface(logger) { return new GameInterface({ logger }); } { GameInterface }; // === core/log-entry.js === /** * Log entry data structure * Represents a single log entry with metadata */ /** * Log entry data structure */ class LogEntry { constructor(level, message, args = []) { this.id = this.generateId(); this.timestamp = new Date(); this.level = level; this.message = message; this.args = args; this.source = 'userscript'; } generateId() { return Date.now().toString(36) + Math.random().toString(36).substring(2); } toString() { const timestamp = this.timestamp.toLocaleTimeString(); return `[${timestamp}] ${this.level.toUpperCase()}: ${this.message}`; } } // === core/log-manager.js === /** * LogManager class for log storage, rotation, and event system * Handles log storage, rotation, and event notifications */ /** * LogManager class for log storage, rotation, and event system */ class LogManager { constructor(maxLogs = 100) { this.logs = []; this.maxLogs = maxLogs; this.listeners = []; // Store original console for internal logging to prevent recursion this.originalConsole = { log: console.log.bind(console), error: console.error.bind(console), warn: console.warn.bind(console) }; this.originalConsole.log('[LogManager] Initialized with maxLogs:', maxLogs); } addLog(level, message, args = []) { const logEntry = new LogEntry(level, message, args); // Add to logs array this.logs.unshift(logEntry); // Add to beginning for newest-first order // Rotate logs if we exceed maxLogs if (this.logs.length > this.maxLogs) { const removed = this.logs.splice(this.maxLogs); this.originalConsole.log('[LogManager] Rotated logs, removed', removed.length, 'old entries'); } // Notify listeners this.notifyListeners('logAdded', logEntry); return logEntry; } clearLogs() { const clearedCount = this.logs.length; this.logs = []; // Notify listeners this.notifyListeners('logsCleared', { clearedCount }); this.originalConsole.log('[LogManager] Cleared', clearedCount, 'logs'); return clearedCount; } getLogs() { return [...this.logs]; // Return copy to prevent external modification } getLogCount() { return this.logs.length; } getLogsByLevel(level) { return this.logs.filter(log => log.level === level); } subscribe(callback) { if (typeof callback !== 'function') { this.originalConsole.error('[LogManager] Subscribe callback must be a function'); return null; } this.listeners.push(callback); this.originalConsole.log('[LogManager] Added listener, total listeners:', this.listeners.length); // Return unsubscribe function return () => { const index = this.listeners.indexOf(callback); if (index > -1) { this.listeners.splice(index, 1); this.originalConsole.log('[LogManager] Removed listener, total listeners:', this.listeners.length); } }; } notifyListeners(event, data) { this.listeners.forEach(callback => { try { callback(event, data); } catch (error) { this.originalConsole.error('[LogManager] Error in listener callback:', error); } }); } // Utility methods for testing and debugging getLogStats() { const stats = { total: this.logs.length, byLevel: {} }; // Count logs by level this.logs.forEach(log => { stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1; }); return stats; } } /** * Create Logger API connected to LogManager * @param {LogManager} logManager - The log manager instance * @returns {Object} Logger API object */ function createLogger(logManager) { return { log(message) { logManager.addLog('info', message); }, warn(message) { logManager.addLog('warn', message); }, error(message) { logManager.addLog('error', message); } }; } // === core/version.js === /** * Version management utilities * Handles version information and display formatting */ /** * Get version info from userscript metadata * @returns {Object} Version information object */ function getVersionInfo() { const gmInfo = typeof globalThis !== 'undefined' ? globalThis.GM_info : undefined; const gmVersion = gmInfo && gmInfo.script && gmInfo.script.version ? gmInfo.script.version : null; const injectedVersion = typeof window !== 'undefined' && window.__INFINITE_CRAFT_HELPER_VERSION__ ? window.__INFINITE_CRAFT_HELPER_VERSION__ : null; let version = gmVersion || injectedVersion || '{{VERSION}}'; if (version === '{{VERSION}}') { version = 'dev-local'; } const isDevVersion = /dev|test|-/.test(version); return { version, isDev: isDevVersion, displayVersion: isDevVersion ? version : `v${version}`, tag: isDevVersion ? 'DEV' : null }; } // === ui/panel-styles.js === /** * Panel styles * CSS styles for the main control panel structure and header */ /** * Get panel CSS styles * @returns {string} CSS styles for the panel */ function getPanelStyles() { return ` #infinite-craft-control-panel .panel-header { background: linear-gradient(135deg, #4a90e2, #357abd); padding: 16px 20px; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center; cursor: move; } #infinite-craft-control-panel .panel-header h3 { margin: 0; font-size: 20px; font-weight: bold; } #infinite-craft-control-panel .version { background: rgba(255, 255, 255, 0.2); padding: 3px 10px; border-radius: 14px; font-size: 14px; font-weight: bold; display: flex; align-items: center; gap: 6px; } #infinite-craft-control-panel .version.dev-version { background: rgba(255, 165, 0, 0.25); border: 1px solid rgba(255, 165, 0, 0.4); } #infinite-craft-control-panel .dev-tag { background: #ff6b35; color: white; padding: 1px 6px; border-radius: 9px; font-size: 12px; font-weight: bold; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } #infinite-craft-control-panel .panel-content { padding: 20px; border-radius: 0; } /* When panel-content is the last element (no logs section) */ #infinite-craft-control-panel .panel-content:last-child { border-radius: 0 0 6px 6px; } #infinite-craft-control-panel .panel-content p { margin: 0 0 14px 0; font-size: 15px; color: #e0e0e0; } #infinite-craft-control-panel .panel-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } #infinite-craft-control-panel .panel-button { background: #4a90e2; color: white; border: none; border-radius: 6px; padding: 8px 14px; font-size: 14px; font-weight: 600; cursor: pointer; transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } #infinite-craft-control-panel .panel-button:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(74, 144, 226, 0.35); background: #5aa0f0; } #infinite-craft-control-panel .panel-button:active { transform: translateY(0); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); } #infinite-craft-control-panel .panel-button:disabled { background: rgba(255, 255, 255, 0.2); cursor: default; box-shadow: none; transform: none; } #infinite-craft-control-panel .panel-actions-hint { font-size: 12px; color: rgba(224, 224, 224, 0.7); } `; } // === ui/log-styles/section.js === /** * Core layout styles for the logs section container, header, controls, and scroll area. * @returns {string} CSS rules covering structural layout. */ function buildLogSectionStyles() { return ` /* Logs Section Styles */ #infinite-craft-control-panel .logs-section { border-top: 1px solid rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.2); border-radius: 0 0 6px 6px; } /* Ensure logs section completes the panel's rounded corners */ #infinite-craft-control-panel .logs-section:last-child { border-radius: 0 0 6px 6px; } #infinite-craft-control-panel .logs-section .logs-content:last-child { border-radius: 0 0 6px 6px; } #infinite-craft-control-panel .logs-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: rgba(0, 0, 0, 0.3); border-bottom: 1px solid rgba(255, 255, 255, 0.1); } #infinite-craft-control-panel .logs-header h4 { margin: 0; font-size: 14px; color: #e0e0e0; font-weight: bold; } #infinite-craft-control-panel .logs-controls { display: flex; gap: 6px; } #infinite-craft-control-panel .logs-controls button { background: rgba(74, 144, 226, 0.8); border: none; color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; transition: background 0.2s ease; font-family: Arial, sans-serif; font-weight: normal; } #infinite-craft-control-panel .logs-controls button:hover { background: rgba(74, 144, 226, 1); transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } #infinite-craft-control-panel .logs-controls button:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } #infinite-craft-control-panel .logs-controls button:disabled { background: rgba(100, 100, 100, 0.5); cursor: not-allowed; transform: none; box-shadow: none; } #infinite-craft-control-panel .logs-controls button:disabled:hover { background: rgba(100, 100, 100, 0.5); transform: none; box-shadow: none; } #infinite-craft-control-panel .logs-toggle { width: 20px; padding: 4px 2px !important; font-size: 10px !important; transition: transform 0.2s ease; } #infinite-craft-control-panel .logs-toggle.collapsed { transform: rotate(-90deg); } #infinite-craft-control-panel .logs-content { max-height: 220px; overflow-y: auto; overflow-x: hidden; transition: max-height 0.3s ease, opacity 0.2s ease; scroll-behavior: smooth; scrollbar-width: thin; scrollbar-color: rgba(74, 144, 226, 0.6) rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.1); } /* Webkit scrollbar styling - consistent with panel theme */ #infinite-craft-control-panel .logs-content::-webkit-scrollbar { width: 6px; } #infinite-craft-control-panel .logs-content::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.2); border-radius: 3px; } #infinite-craft-control-panel .logs-content::-webkit-scrollbar-thumb { background: rgba(74, 144, 226, 0.6); border-radius: 3px; transition: background 0.2s ease; } #infinite-craft-control-panel .logs-content::-webkit-scrollbar-thumb:hover { background: rgba(74, 144, 226, 0.8); } #infinite-craft-control-panel .logs-content.collapsed { max-height: 0; overflow: hidden; opacity: 0; } #infinite-craft-control-panel .logs-list { padding: 8px; min-height: 40px; font-family: Arial, sans-serif; } #infinite-craft-control-panel .logs-empty { color: #888; font-style: italic; text-align: center; padding: 20px; font-size: 12px; background: rgba(255, 255, 255, 0.02); border-radius: 4px; margin: 4px; } `; } // === ui/log-styles/entries.js === /** * Styles that shape the visual presentation for log entries and message content. * @returns {string} CSS rules for individual log rows. */ function buildLogEntryStyles() { return ` #infinite-craft-control-panel .log-entry { display: flex; align-items: flex-start; gap: 8px; padding: 6px 8px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); font-size: 11px; line-height: 1.4; border-radius: 3px; margin: 1px 0; transition: background 0.1s ease; } #infinite-craft-control-panel .log-entry:hover { background: rgba(255, 255, 255, 0.03); } #infinite-craft-control-panel .log-entry:last-child { border-bottom: none; } #infinite-craft-control-panel .log-timestamp { color: #888; font-size: 10px; white-space: nowrap; min-width: 60px; } #infinite-craft-control-panel .log-level { font-size: 12px; min-width: 16px; text-align: center; } #infinite-craft-control-panel .log-message { flex: 1; word-break: break-word; color: #e0e0e0; } /* Log Level Colors - consistent with panel theme */ #infinite-craft-control-panel .log-entry.error { border-left: 2px solid #ff6b6b; background: rgba(255, 107, 107, 0.05); } #infinite-craft-control-panel .log-entry.error .log-level { color: #ff6b6b; } #infinite-craft-control-panel .log-entry.error .log-message { color: #ffcccb; } #infinite-craft-control-panel .log-entry.warn { border-left: 2px solid #ffa726; background: rgba(255, 167, 38, 0.05); } #infinite-craft-control-panel .log-entry.warn .log-level { color: #ffa726; } #infinite-craft-control-panel .log-entry.warn .log-message { color: #ffe0b3; } #infinite-craft-control-panel .log-entry.info { border-left: 2px solid #4a90e2; background: rgba(74, 144, 226, 0.05); } #infinite-craft-control-panel .log-entry.info .log-level { color: #4a90e2; } #infinite-craft-control-panel .log-entry.info .log-message { color: #cce7ff; } #infinite-craft-control-panel .log-entry.debug { border-left: 2px solid #9e9e9e; background: rgba(158, 158, 158, 0.05); } #infinite-craft-control-panel .log-entry.debug .log-level { color: #9e9e9e; } #infinite-craft-control-panel .log-entry.debug .log-message { color: #bbb; } #infinite-craft-control-panel .log-entry.log { border-left: 2px solid #e0e0e0; background: rgba(224, 224, 224, 0.03); } #infinite-craft-control-panel .log-entry.log .log-level { color: #e0e0e0; } #infinite-craft-control-panel .log-entry.log .log-message { color: #e0e0e0; } `; } // === ui/log-styles/activity.js === /** * Activity indicator styling and animation for log updates. * @returns {string} CSS covering indicator state. */ function buildLogActivityStyles() { return ` /* Activity indicator styles - consistent with panel theme */ #infinite-craft-control-panel .logs-activity-indicator { color: #ffa726; font-size: 12px; font-weight: normal; animation: pulse-activity 2s infinite; background: rgba(255, 167, 38, 0.1); padding: 2px 6px; border-radius: 10px; border: 1px solid rgba(255, 167, 38, 0.3); } @keyframes pulse-activity { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(0.98); } } `; } // === ui/log-styles/index.js === /** * Get aggregated log CSS styles for the control panel. * @returns {string} Complete CSS payload for log UI components. */ function getLogStyles() { return [ buildLogSectionStyles(), buildLogEntryStyles(), buildLogActivityStyles() ].join('\n'); } // === utils/dom.js === /** * DOM utilities and helpers * Common DOM manipulation functions used throughout the application */ /** * Wait for DOM to be ready * @param {Function} callback - Function to call when DOM is ready */ function onDOMReady(callback) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback); } else { callback(); } } /** * Create and append a style element to the document head * @param {string} cssText - CSS text content * @returns {HTMLStyleElement} The created style element */ function addStyleSheet(cssText) { const style = document.createElement('style'); style.textContent = cssText; document.head.appendChild(style); return style; } /** * Safely append element to document body * @param {HTMLElement} element - Element to append * @returns {boolean} True if successful, false otherwise */ function appendToBody(element) { if (document.body) { document.body.appendChild(element); return true; } else { console.warn('[DOM] document.body not available, element not added to DOM'); return false; } } /** * Create a DOM element with optional properties * @param {string} tagName - HTML tag name * @param {Object} options - Optional properties * @param {string} options.className - CSS class name * @param {string} options.id - Element ID * @param {string} options.innerHTML - Inner HTML content * @param {Object} options.style - Style properties * @param {Object} options.dataset - Data attributes * @returns {HTMLElement} Created element */ function createElement(tagName, options = {}) { const element = document.createElement(tagName); if (options.className) { element.className = options.className; } if (options.id) { element.id = options.id; } if (options.innerHTML) { element.innerHTML = options.innerHTML; } if (options.style) { Object.assign(element.style, options.style); } if (options.dataset) { Object.assign(element.dataset, options.dataset); } return element; } /** * Escape HTML text to prevent XSS * @param {string} text - Text to escape * @returns {string} Escaped HTML */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Query selector with error handling * @param {string|HTMLElement} container - Container element or selector * @param {string} selector - CSS selector * @returns {HTMLElement|null} Found element or null */ function safeQuerySelector(container, selector) { try { const containerElement = typeof container === 'string' ? document.querySelector(container) : container; if (!containerElement) { return null; } return containerElement.querySelector(selector); } catch (error) { console.warn('[DOM] Query selector failed:', selector, error); return null; } } /** * Add event listener with error handling * @param {HTMLElement} element - Target element * @param {string} event - Event name * @param {Function} handler - Event handler * @param {Object} options - Event listener options * @returns {Function|null} Cleanup function or null if failed */ function addEventListenerSafe(element, event, handler, options = {}) { try { if (!element || typeof handler !== 'function') { console.warn('[DOM] Invalid element or handler for event listener'); return null; } element.addEventListener(event, handler, options); // Return cleanup function return () => { element.removeEventListener(event, handler, options); }; } catch (error) { console.warn('[DOM] Failed to add event listener:', event, error); return null; } } /** * Create temporary element for clipboard operations * @param {string} text - Text to copy * @returns {HTMLTextAreaElement} Temporary textarea element */ function createTempTextarea(text) { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; textarea.style.left = '-9999px'; textarea.style.top = '-9999px'; return textarea; } /** * Remove element safely * @param {HTMLElement} element - Element to remove * @returns {boolean} True if removed successfully */ function removeElement(element) { try { if (element && element.parentNode) { element.parentNode.removeChild(element); return true; } return false; } catch (error) { console.warn('[DOM] Failed to remove element:', error); return false; } } // === ui/styles.js === /** * CSS styles injection * Combines panel and log styles and injects them into the document */ /** * Add CSS styles to the document */ function addStyles() { const cssText = getPanelStyles() + getLogStyles(); addStyleSheet(cssText); } // === ui/control-panel.js === /** * Main control panel creation * Creates and configures the draggable control panel overlay */ /** * Create the overlay control panel * @returns {HTMLElement} The created panel element */ function createControlPanel() { const versionInfo = getVersionInfo(); const versionDisplay = versionInfo.tag ? `${versionInfo.displayVersion} ${versionInfo.tag}` : `${versionInfo.displayVersion}`; const panelHTML = `
Control panel ready!