Phoenix - SPNATI

A modern, modular, and resilient cheat/UI layer for SPNATI

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         Phoenix - SPNATI
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  A modern, modular, and resilient cheat/UI layer for SPNATI
// @author       ARandomGuyWithAComputer
// @match        https://spnati.net/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    /**
     * ==========================================
     * PHOENIX NAMESPACE
     * ==========================================
     */
    const Phoenix = window.Phoenix = {
        version: '1.0.1',
        modules: {},
        isGameReady: false,
        state: {}, // Centralized state storage

        register(module) {
            if (!module || !module.name) {
                console.error("[Phoenix] Attempted to register an invalid module.");
                return;
            }
            this.modules[module.name] = module;
        },

        safeExecute(fn, context = 'Unknown') {
            try {
                return fn();
            } catch (error) {
                if (this.modules.Logger) {
                    this.modules.Logger.error(`[${context}] Critical Failure:`, error);
                } else {
                    console.error(`[Phoenix] [${context}] Critical Failure:`, error);
                }
                return null;
            }
        },

        async bootstrap() {
            const coreModules = Object.values(this.modules).filter(m => m.category === 'Core');
            for (const mod of coreModules) {
                this.safeExecute(() => mod.init?.(), `Core Init: ${mod.name}`);
            }

            this.modules.Logger?.info("Waiting for SPNATI game objects...");
            await this.modules.Utils.waitForGameReady();
            this.isGameReady = true;
            this.modules.Logger?.info("Game objects detected. Initializing features...");

            const featureModules = Object.values(this.modules).filter(m => m.category !== 'Core');
            for (const mod of featureModules) {
                this.safeExecute(() => mod.init?.(), `Feature Init: ${mod.name}`);
            }

            this.modules.Logger?.info(`Phoenix v${this.version} fully operational.`);
        }
    };

    /**
     * ==========================================
     * CORE: LOGGER
     * ==========================================
     */
    const Logger = {
        name: 'Logger',
        category: 'Core',
        prefix: '[Phoenix]',

        info: (...args) => console.log(Logger.prefix, ...args),
        warn: (...args) => console.warn(Logger.prefix, ...args),
        error: (...args) => console.error(Logger.prefix, ...args),
        debug: (...args) => console.debug(Logger.prefix, ...args),

        init() {}
    };
    Phoenix.register(Logger);

    /**
     * ==========================================
     * CORE: STATE
     * ==========================================
     */
    const State = {
        name: 'State',
        category: 'Core',
        _data: {},

        get(key, defaultValue = null) {
            return this._data.hasOwnProperty(key) ? this._data[key] : defaultValue;
        },

        set(key, value) {
            this._data[key] = value;
        },

        init() {
            Logger.info("State manager initialized.");
        }
    };
    Phoenix.register(State);

    /**
     * ==========================================
     * CORE: SETTINGS
     * ==========================================
     */
    const Settings = {
        name: 'Settings',
        category: 'Core',
        STORAGE_KEY: 'phoenix_settings',
        defaults: {
            persistUnlocks: false,
            uiScale: 1.0,
            themeColor: '#ff3b3b'
        },
        current: {},

        load() {
            try {
                const saved = localStorage.getItem(this.STORAGE_KEY);
                this.current = saved ? JSON.parse(saved) : { ...this.defaults };
            } catch (e) {
                this.current = { ...this.defaults };
            }
        },
        save() {
            try { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.current)); } catch (e) {}
        },
        get(key, defaultValue = null) {
            return this.current.hasOwnProperty(key) ? this.current[key] : (defaultValue !== null ? defaultValue : this.defaults[key]);
        },
        set(key, value) {
            this.current[key] = value;
            this.save();
        },
        init() { this.load(); Logger.info("Settings manager initialized."); }
    };
    Phoenix.register(Settings);

    /**
     * ==========================================
     * CORE: UTILS
     * ==========================================
     */
    const Utils = {
        name: 'Utils',
        category: 'Core',

        waitForGameReady(timeout = 30000) {
            return new Promise((resolve, reject) => {
                const startTime = Date.now();
                const interval = setInterval(() => {
                    if (typeof window.players !== "undefined" && typeof window.Card !== "undefined") {
                        clearInterval(interval);
                        resolve();
                    } else if (Date.now() - startTime > timeout) {
                        clearInterval(interval);
                        reject(new Error("Timeout waiting for SPNATI game objects."));
                    }
                }, 100);
            });
        },

        createElement(tag, props = {}) {
            const el = document.createElement(tag);
            Object.entries(props).forEach(([key, val]) => {
                if (key === 'style' && typeof val === 'object') {
                    Object.assign(el.style, val);
                } else if (key === 'class') {
                    el.className = val;
                } else if (key === 'textContent') {
                    el.textContent = val;
                } else if (key === 'innerHTML') {
                    el.innerHTML = val;
                } else if (key.startsWith('on') && typeof val === 'function') {
                    el.addEventListener(key.substring(2), val);
                } else if (key === 'dataset' && typeof val === 'object') {
                    Object.assign(el.dataset, val);
                } else {
                    el.setAttribute(key, val);
                }
            });
            return el;
        },

        init() {}
    };
    Phoenix.register(Utils);

    /**
     * ==========================================
     * CORE: HOOKS
     * ==========================================
     */
    const Hooks = {
        name: 'Hooks',
        category: 'Core',
        _registry: new Map(),

        intercept(context, fnName, callback) {
            if (!context || typeof context[fnName] !== 'function') {
                return false;
            }

            const originalFn = context[fnName];
            const hookId = `${context.constructor?.name || 'Global'}_${fnName}`;

            if (this._registry.has(hookId)) {
                return false;
            }

            context[fnName] = function(...args) {
                return callback.call(this, originalFn, args);
            };

            this._registry.set(hookId, originalFn);
            Logger.debug(`Hooked: ${hookId}`);
            return true;
        },

        init() {
            Logger.info("Hook manager initialized.");
        }
    };
    Phoenix.register(Hooks);

    /**
     * ==========================================
     * CORE: GAME ABSTRACTION
     * ==========================================
     */
    const Game = {
        name: 'Game',
        category: 'Core',

        SUITS: { SPADE: 0, HEART: 1, DIAMOND: 2, CLUB: 3 },
        SUIT_NAMES: ['spade', 'heart', 'diamo', 'clubs'],
        RANKS: {
            TWO: 2, THREE: 3, FOUR: 4, FIVE: 5, SIX: 6,
            SEVEN: 7, EIGHT: 8, NINE: 9, TEN: 10,
            JACK: 11, QUEEN: 12, KING: 13, ACE: 14
        },
        HAND_TYPES: {
            HIGH_CARD: 0, PAIR: 1, TWO_PAIR: 2, THREE_OF_A_KIND: 3,
            STRAIGHT: 4, FLUSH: 5, FULL_HOUSE: 6, FOUR_OF_A_KIND: 7,
            STRAIGHT_FLUSH: 8, ROYAL_FLUSH: 9
        },

        players() { return window.players || []; },
        getHuman() { return this.players()[0]; },
        getOpponents() { return this.players().slice(1); },

        getOpponent(identifier) {
            const opponents = this.getOpponents();
            if (!opponents) return null;
            if (typeof identifier === 'number') {
                return opponents[identifier >= 0 ? identifier : opponents.length + identifier] || null;
            }
            if (typeof identifier === 'string') {
                return opponents.find(p => p && p.name && p.name.toLowerCase().includes(identifier.toLowerCase())) || null;
            }
            return null;
        },

        // Robust name fetching: Checks memory first, then falls back to DOM IDs
        getPlayerName(player, playerIndex) {
            if (player && player.name) return player.name;

            // Fallback 1: Check specific ID (e.g., game-name-label-3)
            const labelById = document.getElementById(`game-name-label-${playerIndex}`);
            if (labelById && labelById.innerText.trim()) return labelById.innerText.trim();

            // Fallback 2: Query all labels and map by index
            const labels = document.querySelectorAll(".bordered.name-label");
            if (labels.length > playerIndex) {
                return labels[playerIndex].innerText.trim();
            }
            return `Player ${playerIndex}`;
        },

        save() { return window.save || null; },

        createCard(suit, rank) {
            if (!window.Card) return null;
            return new window.Card(suit, rank);
        },

        generateUniqueHand(cardRanks, targetSuit = null) {
            const deck = [];
            for (let r = 2; r <= 14; r++) {
                for (let s = 0; s < 4; s++) {
                    deck.push({ rank: r, suit: s });
                }
            }

            const hand = [];
            const usedCards = new Set();

            for (const rank of cardRanks) {
                let validCards = deck.filter(c => c.rank === rank && !usedCards.has(`${c.rank}_${c.suit}`));
                if (targetSuit !== null) validCards = validCards.filter(c => c.suit === targetSuit);
                if (validCards.length === 0) validCards = deck.filter(c => c.rank === rank && !usedCards.has(`${c.rank}_${c.suit}`));

                if (validCards.length > 0) {
                    const chosen = validCards[Math.floor(Math.random() * validCards.length)];
                    hand.push(this.createCard(chosen.suit, chosen.rank));
                    usedCards.add(`${chosen.rank}_${chosen.suit}`);
                }
            }
            return hand;
        },

        injectHand(player, cardRanks, targetSuit = null) {
            if (!player || !player.hand || !Array.isArray(cardRanks)) return;
            player.hand.cards = this.generateUniqueHand(cardRanks, targetSuit);
            if (typeof player.evaluateHand === 'function') player.evaluateHand();
            if (typeof window.evaluateHands === 'function') window.evaluateHands();
        },

        init() { Logger.info("Game abstraction initialized."); }
    };
    Phoenix.register(Game);
    /**
     * ==========================================
     * CORE: UI SYSTEM
     * ==========================================
     */
    const UI = {
        name: 'UI',
        category: 'Core',
        stylesInjected: false,

        styles: `
            :root {
                --phoenix-primary: #ff3b3b;
                --phoenix-accent: #ff7b7b;
            }
            .phoenix-window {
                position: fixed; background: rgba(25, 25, 25, 0.95); border: 1px solid #444;
                border-radius: 8px; box-shadow: 0 8px 16px rgba(0,0,0,0.5); color: #eee;
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; z-index: 99999;
                width: 240px; min-width: 200px; min-height: 80px; display: flex; flex-direction: column;
                backdrop-filter: blur(5px); resize: both; overflow: hidden;
            }
            .phoenix-header {
                padding: 6px 12px; background: rgba(40, 40, 40, 0.8); cursor: move;
                border-bottom: 1px solid #444; border-radius: 8px 8px 0 0; display: flex;
                justify-content: space-between; align-items: center; user-select: none;
            }
            .phoenix-title { font-weight: bold; font-size: 13px; color: var(--phoenix-accent); }
            .phoenix-close { cursor: pointer; font-size: 18px; line-height: 1; color: #aaa; transition: color 0.2s; }
            .phoenix-close:hover { color: #fff; }
            .phoenix-body { padding: 10px; overflow-y: auto; flex: 1; font-size: 12px; }
            .phoenix-btn {
                width: 100%; padding: 6px 10px; margin: 3px 0; background: #3a3a3a; border: 1px solid #555;
                color: #ccc; border-radius: 4px; cursor: pointer; transition: all 0.2s; font-size: 12px;
            }
            .phoenix-btn:hover { background: #4a4a4a; color: white; }
            .phoenix-btn.primary { background: var(--phoenix-primary); border-color: var(--phoenix-primary); color: white; }
            .phoenix-btn.primary:hover { filter: brightness(1.2); }
            #phoenix-notifications {
                position: fixed; bottom: 80px; right: 20px; z-index: 100000; display: flex;
                flex-direction: column-reverse; gap: 8px; pointer-events: none;
            }
            .phoenix-toast {
                background: rgba(30, 30, 30, 0.95); color: white; padding: 8px 15px; border-radius: 6px;
                border-left: 4px solid var(--phoenix-accent); box-shadow: 0 4px 12px rgba(0,0,0,0.3);
                animation: phoenix-slide-in 0.3s ease-out forwards; font-family: sans-serif; font-size: 12px; pointer-events: auto;
            }
            @keyframes phoenix-slide-in {
                from { transform: translateX(100%); opacity: 0; }
                to { transform: translateX(0); opacity: 1; }
            }
        `,

        injectStyles() {
            if (this.stylesInjected) return;
            const style = Utils.createElement('style', { textContent: this.styles });
            document.head.appendChild(style);
            this.stylesInjected = true;
            this.applyTheme(Phoenix.modules.Settings.get('themeColor', '#ff3b3b'));
        },

        applyTheme(hex) {
            document.documentElement.style.setProperty('--phoenix-primary', hex);
            document.documentElement.style.setProperty('--phoenix-accent', this.lightenHex(hex, 30));
        },

        lightenHex(hex, percent) {
            let r = parseInt(hex.slice(1, 3), 16);
            let g = parseInt(hex.slice(3, 5), 16);
            let b = parseInt(hex.slice(5, 7), 16);
            r = Math.min(255, Math.floor(r + (255 - r) * (percent / 100)));
            g = Math.min(255, Math.floor(g + (255 - g) * (percent / 100)));
            b = Math.min(255, Math.floor(b + (255 - b) * (percent / 100)));
            return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
        },

        createWindow(id, title, options = {}) {
            const existing = document.getElementById(id);
            if (existing) existing.remove();

            const winStyles = { top: options.top || '100px' };
            if (options.right !== undefined) { winStyles.right = options.right; winStyles.left = 'auto'; }
            else { winStyles.left = options.left || '100px'; }

            const win = Utils.createElement('div', { id: id, class: 'phoenix-window', style: winStyles });
            const header = Utils.createElement('div', { class: 'phoenix-header' });
            const titleEl = Utils.createElement('span', { class: 'phoenix-title', textContent: title });
            const closeBtn = Utils.createElement('span', { class: 'phoenix-close', textContent: '×' });

            header.appendChild(titleEl);
            header.appendChild(closeBtn);
            const body = Utils.createElement('div', { class: 'phoenix-body' });
            win.appendChild(header);
            win.appendChild(body);
            document.body.appendChild(win);

            let isDragging = false, offsetX, offsetY;
            const startDrag = (e) => { isDragging = true; offsetX = e.clientX - win.getBoundingClientRect().left; offsetY = e.clientY - win.getBoundingClientRect().top; };
            const drag = (e) => {
                if (!isDragging) return;
                e.preventDefault();
                let newLeft = e.clientX - offsetX;
                let newTop = e.clientY - offsetY;

                // BOUNDARY CLAMPING: Prevents dragging off-screen or behind browser UI
                const maxLeft = window.innerWidth - win.offsetWidth;
                const maxTop = window.innerHeight - win.offsetHeight;
                newLeft = Math.max(0, Math.min(newLeft, maxLeft));
                newTop = Math.max(0, Math.min(newTop, maxTop));

                win.style.left = newLeft + 'px';
                win.style.top = newTop + 'px';
                win.style.right = 'auto';
            };
            const stopDrag = () => { isDragging = false; };

            header.addEventListener('mousedown', startDrag);
            document.addEventListener('mousemove', drag);
            document.addEventListener('mouseup', stopDrag);

            closeBtn.addEventListener('click', () => {
                win.style.display = 'none';
                if (typeof options.onClose === 'function') options.onClose();
            });

            return { element: win, body: body, show: () => win.style.display = 'flex', hide: () => win.style.display = 'none' };
        },

        notify(message, type = 'info', duration = 3000) {
            const container = document.getElementById('phoenix-notifications');
            if (!container) return;
            const toast = Utils.createElement('div', { class: `phoenix-toast ${type}`, textContent: message });
            container.appendChild(toast);
            setTimeout(() => {
                toast.style.animation = 'phoenix-slide-in 0.3s ease-out reverse forwards';
                setTimeout(() => toast.remove(), 300);
            }, duration);
        },

        init() {
            this.injectStyles();
            const notifContainer = Utils.createElement('div', { id: 'phoenix-notifications' });
            document.body.appendChild(notifContainer);
            Logger.info("UI System initialized.");
        }
    };
    Phoenix.register(UI);

    /**
     * ==========================================
     * UI: MAIN MENU
     * ==========================================
     */
    const MainMenu = {
        name: 'MainMenu',
        category: 'UI',
        window: null,
        fab: null,

        buildUI() {
            const existingFab = document.querySelector('#phoenix-fab');
            if (existingFab) existingFab.remove();

            this.fab = Utils.createElement('div', {
                id: 'phoenix-fab', textContent: '🔥',
                style: {
                    position: 'fixed', bottom: '20px', right: '20px', width: '50px', height: '50px',
                    background: 'var(--phoenix-primary)', color: 'white', borderRadius: '50%', display: 'flex',
                    alignItems: 'center', justifyContent: 'center', fontSize: '24px', cursor: 'pointer',
                    zIndex: '100001', boxShadow: '0 4px 10px rgba(0,0,0,0.5)', userSelect: 'none'
                },
                onclick: () => this.toggleWindow()
            });
            document.body.appendChild(this.fab);

            this.window = UI.createWindow('phoenix-main-menu', 'Phoenix Control Panel', { top: '100px', right: '20px' });

            const features = [
                { name: 'Hand Builder', action: () => Phoenix.modules.HandBuilder.toggle(), type: 'button' },
                { name: 'Card Inspector', action: () => Phoenix.modules.CardInspector.toggle(), type: 'toggle' },
                { name: 'Loser Picker', action: () => Phoenix.modules.LoserPicker.toggle(), type: 'toggle' },
                { name: 'Wardrobe Manager', action: () => Phoenix.modules.Wardrobe.toggle(), type: 'button' },
                { name: 'Trigger Epilogue', action: () => Phoenix.modules.Epilogue.triggerEpilogue(), type: 'button' },
                { name: 'Temporary Unlocks', action: () => Phoenix.modules.Unlocks.applyTemporaryPatches(), type: 'button' },
                { name: 'Permanent Unlocks', action: () => Phoenix.modules.PermanentUnlocks.inject(), type: 'button' }
            ];

            features.forEach(feat => {
                if (feat.type === 'button') {
                    const btn = Utils.createElement('button', { class: 'phoenix-btn primary', textContent: feat.name, onclick: feat.action });
                    this.window.body.appendChild(btn);
                } else {
                    const row = Utils.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', margin: '8px 0' } });
                    const label = Utils.createElement('span', { textContent: feat.name, style: { fontSize: '13px' } });

                    // ADDED ID for syncing state across modules
                    const toggleId = `phoenix-toggle-${feat.name.replace(/\s+/g, '-').toLowerCase()}`;
                    const toggle = Utils.createElement('input', { type: 'checkbox', id: toggleId, onchange: feat.action });
                    if (feat.checked) toggle.checked = true;

                    row.appendChild(label); row.appendChild(toggle);
                    this.window.body.appendChild(row);
                }
            });

            const themeRow = Utils.createElement('div', { style: { display: 'flex', gap: '10px', alignItems: 'center', margin: '15px 0 5px 0', borderTop: '1px solid #444', paddingTop: '10px' } });
            const colorPicker = Utils.createElement('input', { type: 'color', value: Phoenix.modules.Settings.get('themeColor', '#ff3b3b'), style: { width: '30px', height: '25px', padding: 0, border: 'none', background: 'none', cursor: 'pointer' } });
            colorPicker.addEventListener('input', (e) => {
                Phoenix.modules.Settings.set('themeColor', e.target.value);
                UI.applyTheme(e.target.value);
            });
            const resetThemeBtn = Utils.createElement('button', { class: 'phoenix-btn', textContent: 'Reset', style: { width: 'auto', padding: '4px 8px', margin: 0, fontSize: '11px' } });
            resetThemeBtn.onclick = () => {
                Phoenix.modules.Settings.set('themeColor', '#ff3b3b');
                UI.applyTheme('#ff3b3b');
                colorPicker.value = '#ff3b3b';
            };
            themeRow.appendChild(Utils.createElement('span', { textContent: 'Theme:', style: { fontSize: '12px' } }));
            themeRow.appendChild(colorPicker);
            themeRow.appendChild(resetThemeBtn);
            this.window.body.appendChild(themeRow);

            this.window.hide();
        },

        toggleWindow() {
            if (!this.window || !this.window.element) return;
            if (this.window.element.style.display === 'none' || this.window.element.style.display === '') this.window.show();
            else this.window.hide();
        },

        init() { this.buildUI(); Logger.info("Main Menu initialized."); }
    };
    Phoenix.register(MainMenu);

    /**
     * ==========================================
     * GAMEPLAY: HAND BUILDER
     * ==========================================
     */
    const HandBuilder = {
        name: 'HandBuilder',
        category: 'Gameplay',
        window: null,
        isActive: false,

        presets: {
            'Royal Flush': { ranks: [10, 11, 12, 13, 14], type: Game.HAND_TYPES.ROYAL_FLUSH },
            'Straight Flush': { ranks: [9, 10, 11, 12, 13], type: Game.HAND_TYPES.STRAIGHT_FLUSH },
            'Four of a Kind': { ranks: [14, 14, 14, 14, 13], type: Game.HAND_TYPES.FOUR_OF_A_KIND },
            'Full House': { ranks: [14, 14, 14, 13, 13], type: Game.HAND_TYPES.FULL_HOUSE },
            'Flush': { ranks: [14, 13, 11, 8, 6], type: Game.HAND_TYPES.FLUSH },
            'Straight': { ranks: [10, 11, 12, 13, 14], type: Game.HAND_TYPES.STRAIGHT },
            'Three of a Kind': { ranks: [14, 14, 14, 13, 12], type: Game.HAND_TYPES.THREE_OF_A_KIND },
            'Two Pair': { ranks: [14, 14, 13, 13, 12], type: Game.HAND_TYPES.TWO_PAIR },
            'Pair': { ranks: [14, 14, 13, 12, 11], type: Game.HAND_TYPES.PAIR },
            'High Card (Ace)': { ranks: [14, 13, 12, 11, 9], type: Game.HAND_TYPES.HIGH_CARD }
        },

        buildUI() {
            this.window = UI.createWindow('phoenix-win-hand', 'Hand Builder', { top: '150px', left: '150px' });
            const suitLabel = Utils.createElement('div', { textContent: 'Force Suit (Optional):', style: { marginBottom: '5px', fontSize: '12px' } });
            const suitSelect = Utils.createElement('select', { class: 'phoenix-btn', id: 'phoenix-suit-select' });
            suitSelect.innerHTML = `<option value="-1">Random / Mixed</option><option value="${Game.SUITS.SPADE}">Spades (♠)</option><option value="${Game.SUITS.HEART}">Hearts (♥)</option><option value="${Game.SUITS.DIAMOND}">Diamonds (♦)</option><option value="${Game.SUITS.CLUB}">Clubs (♣)</option>`;
            this.window.body.appendChild(suitLabel);
            this.window.body.appendChild(suitSelect);

            const randomizeRow = Utils.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', margin: '10px 0' } });
            const randomizeCheck = Utils.createElement('input', { type: 'checkbox', id: 'phoenix-randomize-ranks' });
            const randomizeLabel = Utils.createElement('label', { textContent: 'Randomize Ranks', for: 'phoenix-randomize-ranks', style: { fontSize: '12px', cursor: 'pointer' } });
            randomizeRow.appendChild(randomizeCheck);
            randomizeRow.appendChild(randomizeLabel);
            this.window.body.appendChild(randomizeRow);

            const presetLabel = Utils.createElement('div', { textContent: 'Presets (Applies to You):', style: { marginTop: '10px', marginBottom: '5px', fontSize: '12px' } });
            this.window.body.appendChild(presetLabel);

            Object.entries(this.presets).forEach(([name, data]) => {
                const btn = Utils.createElement('button', { class: 'phoenix-btn primary', textContent: name, onclick: () => this.applyPreset({ ...data, name }) });
                this.window.body.appendChild(btn);
            });
            this.window.hide();
        },

        getRandomizedRanks(handType) {
            const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
            switch(handType) {
                case Game.HAND_TYPES.STRAIGHT_FLUSH:
                case Game.HAND_TYPES.STRAIGHT: {
                    const start = randInt(2, 10);
                    return [start, start+1, start+2, start+3, start+4];
                }
                case Game.HAND_TYPES.FOUR_OF_A_KIND: {
                    const quad = randInt(2, 14);
                    let kicker = randInt(2, 14);
                    while(kicker === quad) kicker = randInt(2, 14);
                    return [quad, quad, quad, quad, kicker];
                }
                case Game.HAND_TYPES.FULL_HOUSE: {
                    const trip = randInt(2, 14);
                    let pair = randInt(2, 14);
                    while(pair === trip) pair = randInt(2, 14);
                    return [trip, trip, trip, pair, pair];
                }
                case Game.HAND_TYPES.THREE_OF_A_KIND: {
                    const trip = randInt(2, 14);
                    let k1 = randInt(2, 14), k2 = randInt(2, 14);
                    while(k1 === trip) k1 = randInt(2, 14);
                    while(k2 === trip || k2 === k1) k2 = randInt(2, 14);
                    return [trip, trip, trip, k1, k2];
                }
                case Game.HAND_TYPES.TWO_PAIR: {
                    let p1 = randInt(2, 14), p2 = randInt(2, 14);
                    while(p2 === p1) p2 = randInt(2, 14);
                    let k = randInt(2, 14);
                    while(k === p1 || k === p2) k = randInt(2, 14);
                    return [p1, p1, p2, p2, k];
                }
                case Game.HAND_TYPES.PAIR: {
                    const p = randInt(2, 14);
                    const kickers = new Set();
                    while(kickers.size < 3) {
                        let k = randInt(2, 14);
                        if(k !== p) kickers.add(k);
                    }
                    return [p, p, ...Array.from(kickers)];
                }
                case Game.HAND_TYPES.FLUSH:
                case Game.HAND_TYPES.HIGH_CARD: {
                    const ranks = new Set();
                    while(ranks.size < 5) ranks.add(randInt(2, 14));
                    return Array.from(ranks).sort((a,b) => b-a);
                }
                default: return [10, 11, 12, 13, 14]; // Royal Flush stays specific
            }
        },

        applyPreset(presetData) {
            const human = Game.getHuman();
            if (!human) { UI.notify('Cannot find human player!', 'error'); return; }

            const suitVal = parseInt(document.getElementById('phoenix-suit-select').value);
            let targetSuit = suitVal === -1 ? null : suitVal;
            const randomize = document.getElementById('phoenix-randomize-ranks')?.checked || false;

            let ranks = randomize ? this.getRandomizedRanks(presetData.type) : [...presetData.ranks];

            const flushTypes = [Game.HAND_TYPES.FLUSH, Game.HAND_TYPES.STRAIGHT_FLUSH, Game.HAND_TYPES.ROYAL_FLUSH];
            if (targetSuit === null && flushTypes.includes(presetData.type)) targetSuit = Math.floor(Math.random() * 4);

            Game.injectHand(human, ranks, targetSuit);
            UI.notify(`Injected ${randomize ? 'Random ' : ''}${presetData.name}!`, 'success');
        },

        toggle() {
            if (!this.window) this.buildUI();
            if (this.window.element.style.display === 'none' || this.window.element.style.display === '') { this.window.show(); this.isActive = true; }
            else { this.window.hide(); this.isActive = false; }
        },
        init() { this.buildUI(); Logger.info("Hand Builder module ready."); }
    };
    Phoenix.register(HandBuilder);

    /**
     * ==========================================
     * GAMEPLAY: CARD INSPECTOR
     * ==========================================
     */
    const CardInspector = {
        name: 'CardInspector',
        category: 'Gameplay',
        window: null,
        intervalId: null,
        isActive: false,
        RANK_NAMES: ['', '', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'],
        SUIT_SYMBOLS: ['♠', '♥', '♦', '♣'],

        buildUI() {
            this.window = UI.createWindow('phoenix-win-inspect', 'Card Inspector', {
                top: '150px',
                right: '80px',
                onClose: () => {
                    if (this.isActive) this.stop();
                    // SYNC: Uncheck the Main Menu toggle when closed via 'X'
                    const cb = document.getElementById('phoenix-toggle-card-inspector');
                    if (cb) cb.checked = false;
                }
            });
            this.window.hide();
        },

        start() {
            if (this.isActive) return;
            this.isActive = true;
            if (!this.window) this.buildUI();
            this.window.show();

            this.intervalId = setInterval(() => {
                if (!Phoenix.isGameReady) return;
                Phoenix.safeExecute(() => this.update(), 'CardInspector.Update');
            }, 500);
            UI.notify('Card Inspector Started', 'success');
        },

        update() {
            if (!this.window || !this.window.body) return;
            let html = '<div style="font-family: monospace; font-size: 13px; line-height: 1.4;">';
            let hasData = false;

            for (let i = 1; i <= 4; i++) {
                const p = window.players?.[i];
                const displayName = Game.getPlayerName(p, i);

                if (p && p.hand && p.hand.cards && p.hand.cards.length > 0) {
                    hasData = true;
                    const cards = p.hand.cards.map(c => {
                        const rank = c.rank !== undefined ? c.rank : c;
                        const suit = c.suit !== undefined ? c.suit : -1;

                        const r = this.RANK_NAMES[rank] || rank;
                        const s = suit >= 0 ? this.SUIT_SYMBOLS[suit] : '';
                        const color = (suit === 1 || suit === 2) ? '#ff4444' : '#ffffff';
                        return `<span style="color: ${color}">${r}${s}</span>`;
                    }).join(' ');
                    html += `<div style="margin-bottom: 6px;"><strong style="color: var(--phoenix-accent);">${displayName}:</strong> ${cards}</div>`;
                } else {
                    html += `<div style="margin-bottom: 6px; color: #888;"><strong>${displayName}:</strong> Hidden</div>`;
                }
            }

            if (!hasData && !window.players) html += 'Waiting for game data...';
            html += '</div>';
            this.window.body.innerHTML = html;
        },

        stop() {
            this.isActive = false;
            if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; }
            if (this.window && this.window.element.style.display !== 'none') {
                this.window.hide();
            }
            // SYNC: Uncheck the Main Menu toggle when stopped via the toggle switch
            const cb = document.getElementById('phoenix-toggle-card-inspector');
            if (cb) cb.checked = false;

            UI.notify('Card Inspector Stopped', 'info');
        },

        toggle() { if (this.isActive) this.stop(); else this.start(); },
        init() { this.buildUI(); Logger.info("Card Inspector module initialized."); }
    };
    Phoenix.register(CardInspector);

    /**
     * ==========================================
     * GAMEPLAY: LOSER PICKER
     * ==========================================
     */
    const LoserPicker = {
        name: 'LoserPicker',
        category: 'Gameplay',
        isActive: false,
        boundElements: new WeakSet(),
        bindInterval: null,
        badHandRanks: [2, 3, 4, 5, 7],

        enable() {
            if (this.isActive) return;
            this.isActive = true;
            this.bindLabels();
            this.bindInterval = setInterval(() => { if (this.isActive) this.bindLabels(); }, 2000);
            UI.notify('Loser Picker Enabled', 'success');
        },

        disable() {
            this.isActive = false;
            if (this.bindInterval) { clearInterval(this.bindInterval); this.bindInterval = null; }
            document.querySelectorAll('.phoenix-loser-target').forEach(el => {
                el.classList.remove('phoenix-loser-target');
                el.style.cursor = '';
                el.style.outline = '';
                const clone = el.cloneNode(true);
                el.parentNode.replaceChild(clone, el);
            });
            UI.notify('Loser Picker Disabled', 'info');
        },

        bindLabels() {
            const labels = document.querySelectorAll(".bordered.name-label");
            labels.forEach((label) => {
                if (this.boundElements.has(label)) return;

                let targetPlayer = null;
                let playerIndex = -1;

                const idMatch = label.id.match(/(\d+)/);
                if (idMatch) {
                    playerIndex = parseInt(idMatch[1], 10);
                    targetPlayer = Game.players()[playerIndex];
                }

                if (!targetPlayer) {
                    let name = label.innerText.trim().split('\n')[0].trim();
                    targetPlayer = Game.getOpponent(name);
                    if (!targetPlayer) {
                        const opponents = Game.getOpponents();
                        targetPlayer = opponents.find(p => p && p.name && (name.includes(p.name) || p.name.includes(name)));
                    }
                }

                if (!targetPlayer || !targetPlayer.hand) {
                    label.style.cursor = 'not-allowed';
                    label.addEventListener('click', (e) => {
                        e.stopPropagation();
                        UI.notify(`Debug: Clicked ${label.innerText.trim()} but no player found!`, 'error');
                    });
                    this.boundElements.add(label);
                    return;
                }

                this.boundElements.add(label);
                label.classList.add('phoenix-loser-target');
                label.style.cursor = 'crosshair';

                const highlight = () => { if (this.isActive) label.style.outline = '2px solid #ff3b3b'; };
                const unhighlight = () => { label.style.outline = ''; };

                const handleClick = (e) => {
                    if (!this.isActive) return;
                    e.stopPropagation();

                    const displayName = label.innerText.trim().split('\n')[0].trim() || "Opponent";
                    Logger.debug(`Loser Picker: Injecting bad hand for ${displayName}`);
                    const garbageCards = Game.generateUniqueHand(this.badHandRanks);

                    // NATIVE INTEGRATION: Replace cards and let spniPoker.js do the math
                    targetPlayer.hand.cards = garbageCards;

                    if (typeof targetPlayer.hand.determine === 'function') {
                        targetPlayer.hand.determine(); // Calculates strength (1 = High Card) and value natively
                    }
                    if (typeof targetPlayer.hand.sort === 'function') {
                        targetPlayer.hand.sort(); // Sorts cards for the UI display
                    }

                    // Force UI refresh
                    if (typeof window.displayHand === 'function') {
                        const pIdx = Game.players().indexOf(targetPlayer);
                        if (pIdx !== -1) window.displayHand(pIdx, null);
                    }

                    UI.notify(`Forced ${displayName} to fold!`, 'success');
                };

                label.addEventListener('mouseenter', highlight);
                label.addEventListener('mouseleave', unhighlight);
                label.addEventListener('click', handleClick);
            });
        },

        toggle() { if (this.isActive) this.disable(); else this.enable(); },
        init() { Logger.info("Loser Picker module initialized."); }
    };
    Phoenix.register(LoserPicker);

    /**
     * ==========================================
     * GAMEPLAY: WARDROBE MANAGER
     * ==========================================
     */
    const Wardrobe = {
        name: 'Wardrobe',
        category: 'Gameplay',
        window: null,
        isActive: false,

        buildUI() {
            this.window = UI.createWindow('phoenix-win-wardrobe', 'Wardrobe Manager', { top: '200px', left: '200px' });
            this.updateUI();
            this.window.hide();
        },

        updateUI() {
            if (!this.window || !this.window.body) return;

            // FIX: Reset height to 'auto' so it naturally expands to fit the opponent list,
            // overriding any previous manual user resizing that made it too small.
            this.window.element.style.height = 'auto';

            this.window.body.innerHTML = '';
            const opponents = Game.getOpponents();
            if (opponents.length === 0) { this.window.body.textContent = 'No opponents found.'; return; }

            opponents.forEach((p, idx) => {
                const pIdx = Game.players().indexOf(p);
                const name = Game.getPlayerName(p, pIdx);
                const row = Utils.createElement('div', { style: { marginBottom: '10px', borderBottom: '1px solid #444', paddingBottom: '5px' } });
                row.appendChild(Utils.createElement('div', { textContent: name, style: { fontWeight: 'bold', color: 'var(--phoenix-accent)', marginBottom: '5px' } }));

                const btnStrip = Utils.createElement('button', { class: 'phoenix-btn', textContent: 'Strip 1 Layer', onclick: () => this.stripLayer(pIdx) });
                const btnNaked = Utils.createElement('button', { class: 'phoenix-btn', textContent: 'Force Naked (0 Layers)', onclick: () => this.forceNaked(pIdx) });
                const btnForfeit = Utils.createElement('button', { class: 'phoenix-btn primary', textContent: 'Force Forfeit (Out)', onclick: () => this.forceForfeit(pIdx) });

                row.appendChild(btnStrip); row.appendChild(btnNaked); row.appendChild(btnForfeit);
                this.window.body.appendChild(row);
            });

            const globalRow = Utils.createElement('div', { style: { marginTop: '15px' } });
            globalRow.appendChild(Utils.createElement('button', { class: 'phoenix-btn primary', textContent: 'All NPCs Naked', onclick: () => this.allNaked() }));
            globalRow.appendChild(Utils.createElement('button', { class: 'phoenix-btn primary', textContent: 'All NPCs Forfeit', onclick: () => this.allForfeit() }));
            this.window.body.appendChild(globalRow);
        },

        stripLayer(pIdx) {
            if (typeof window.stripAIPlayer === 'function') {
                Phoenix.safeExecute(() => window.stripAIPlayer(pIdx), 'Wardrobe.stripAIPlayer');
                UI.notify('Stripped 1 layer.', 'success');
                this.updateUI();
            }
        },

        forceNaked(pIdx) {
            const p = Game.players()[pIdx];
            if (!p) return;
            let safety = 20;
            while (typeof p.countLayers === 'function' && p.countLayers() > 0 && safety-- > 0) {
                if (typeof window.stripAIPlayer === 'function') window.stripAIPlayer(pIdx);
                else break;
            }
            UI.notify('Forced Naked.', 'success');
            this.updateUI();
        },

        forceForfeit(pIdx) {
            const p = Game.players()[pIdx];
            if (!p) return;

            let safety = 20;
            while (typeof p.countLayers === 'function' && p.countLayers() > 0 && safety-- > 0) {
                if (typeof window.stripAIPlayer === 'function') window.stripAIPlayer(pIdx);
                else break;
            }

            if (typeof window.startMasturbation === 'function') {
                Phoenix.safeExecute(() => window.startMasturbation(pIdx), 'Wardrobe.startMasturbation');
            } else if (typeof window.forfeit === 'function') {
                Phoenix.safeExecute(() => window.forfeit(pIdx), 'Wardrobe.forfeit');
            } else {
                UI.notify('Native forfeit function not found.', 'error');
                return;
            }

            UI.notify('Forced Forfeit (Naked + Out).', 'success');
            this.updateUI();
        },

        allNaked() { Game.getOpponents().forEach((p) => { const pIdx = Game.players().indexOf(p); if (pIdx !== -1) this.forceNaked(pIdx); }); },
        allForfeit() { Game.getOpponents().forEach((p) => { const pIdx = Game.players().indexOf(p); if (pIdx !== -1) this.forceForfeit(pIdx); }); },

        toggle() {
            if (!this.window) this.buildUI(); else this.updateUI();
            if (this.window.element.style.display === 'none' || this.window.element.style.display === '') { this.window.show(); this.isActive = true; }
            else { this.window.hide(); this.isActive = false; }
        },
        init() { this.buildUI(); Logger.info("Wardrobe module initialized."); }
    };
    Phoenix.register(Wardrobe);
    /**
     * ==========================================
     * GAMEPLAY: EPILOGUE
     * ==========================================
     */
    const Epilogue = {
        name: 'Epilogue',
        category: 'Gameplay',

        triggerEpilogue() {
            const fn = window.doEpilogueModal || window.game?.doEpilogueModal || window.triggerEpilogue || window.endGame;
            if (typeof fn === 'function') {
                Phoenix.safeExecute(() => fn.call(window), 'Epilogue.triggerEpilogue');
                UI.notify('Triggered Epilogue/End Game Function', 'success');
            } else {
                UI.notify('Epilogue function not found. Are you at the end of a match?', 'error');
            }
        },
        init() { Logger.info("Epilogue module ready."); }
    };
    Phoenix.register(Epilogue);

    /**
     * ==========================================
     * UNLOCKS: TEMPORARY
     * ==========================================
     */
    const Unlocks = {
        name: 'Unlocks',
        category: 'Unlocks',
        patched: false,

        // 1. Temporary Unlocks (Runtime Prototype Patch)
        // Tricks the Gallery UI by intercepting "is unlocked?" checks.
        applyTemporaryPatches() {
            if (this.patched) return;
            const SaveConstructor = window.Save || window.game?.Save;
            if (!SaveConstructor?.prototype) {
                Logger.warn("Could not find Save prototype.");
                UI.notify("Save engine not found.", "error");
                return;
            }

            const proto = SaveConstructor.prototype;
            const trueFns = ['hasEnding', 'hasEpilogue', 'isEndingUnlocked', 'hasCollectible', 'isCollectibleUnlocked', 'hasClothing', 'hasUnlockedClothing', 'isClothingUnlocked'];
            const falseFns = ['isClothingLocked', 'isWardrobeLocked', 'isOutfitLocked'];
            const highNumFns = ['getCollectibleCounter', 'getCollectibleProgress', 'getCollectibleCount'];

            trueFns.forEach(fn => { if (typeof proto[fn] === 'function') proto[fn] = () => true; });
            falseFns.forEach(fn => { if (typeof proto[fn] === 'function') proto[fn] = () => false; });
            highNumFns.forEach(fn => { if (typeof proto[fn] === 'function') proto[fn] = () => 9999; });

            this.patched = true;
            Logger.info("Temporary Runtime Patches applied.");
            UI.notify("Temporary Unlocks Active (Session Only)", "success");
        },

        init() {
            Logger.info("Unlocks module ready.");
        }
    };
    Phoenix.register(Unlocks);

    /**
     * ==========================================
     * UNLOCKS: PERMANENT
     * ==========================================
     * SPNATI stores epilogues as raw XML DOM Nodes in memory.
     * This module extracts the textContent (the epilogue title)
     * and passes it to the native addEnding() function.
     */
    const PermanentUnlocks = {
        name: 'PermanentUnlocks',
        category: 'Unlocks',

        inject() {
            const saveObj = Game.save();
            if (!saveObj) {
                UI.notify('Could not find native Save engine.', 'error');
                return;
            }

            UI.notify('Scanning XML nodes & injecting endings...', 'info');

            // 1. Deep Memory Scanner
            const charDefs = [];
            const seen = new Set();
            const scanObject = (obj, depth = 0) => {
                if (!obj || typeof obj !== 'object' || depth > 5 || seen.has(obj)) return;
                seen.add(obj);

                const hasId = obj.id || obj.characterId || (obj.getAttribute && obj.getAttribute('id'));
                const hasData = obj.epilogues || obj.endings || obj.collectibles || obj.clothing;

                if (hasData && hasId) charDefs.push(obj);

                for (let key in obj) {
                    try {
                        const val = obj[key];
                        if (val && typeof val === 'object' && !(val instanceof Window)) {
                            // Allow scanning into NodeLists/HTMLCollections but skip raw text nodes
                            if (val instanceof Node && !(val instanceof Document) && !(val instanceof Element)) continue;
                            scanObject(val, depth + 1);
                        }
                    } catch(e) {}
                }
            };

            [window, window.game, window.spnati, window.save, window.players, window.opponents, window.loadedOpponents].forEach(root => {
                try { scanObject(root, 0); } catch(e) {}
            });

            let endingsInjected = 0;
            let collectiblesInjected = 0;
            const processedChars = new Set();

            let endingsBlob = {};
            try { endingsBlob = JSON.parse(localStorage.getItem('SPNatI.endings') || '{}'); } catch(e) { endingsBlob = {}; }
            if (typeof endingsBlob !== 'object' || Array.isArray(endingsBlob)) endingsBlob = {};

            charDefs.forEach(charObj => {
                if (!charObj) return;

                // Extract Character ID (handle XML nodes and JS objects)
                let charId = charObj.id || charObj.characterId;
                if (!charId && charObj.getAttribute) {
                    charId = charObj.getAttribute('id') || charObj.getAttribute('characterId');
                }

                if (!charId || typeof charId !== 'string' || processedChars.has(charId)) return;
                processedChars.add(charId);

                // Collectibles
                const cols = charObj.collectibles || charObj.collectible || [];
                let colList = [];
                if (Array.isArray(cols)) colList = cols;
                else if (cols instanceof NodeList || cols instanceof HTMLCollection) colList = Array.from(cols);
                else if (typeof cols === 'object') colList = Object.values(cols);

                colList.forEach(col => {
                    if (!col) return;
                    let colId = col.id || col.name;
                    if (!colId && col.getAttribute) colId = col.getAttribute('id') || col.getAttribute('name');
                    if (!colId && typeof col === 'string') colId = col;

                    if (colId && typeof colId === 'string') {
                        try {
                            saveObj.setItem('collectibles.' + charId + '.' + colId, 999);
                            collectiblesInjected++;
                        } catch(e) {}
                    }
                });

                // Epilogues
                const epis = charObj.epilogues || charObj.epilogue || charObj.endings || [];
                let epiList = [];
                if (Array.isArray(epis)) epiList = epis;
                else if (epis instanceof NodeList || epis instanceof HTMLCollection) epiList = Array.from(epis);
                else if (typeof epis === 'object') epiList = Object.values(epis);

                epiList.forEach(epi => {
                    if (!epi) return;

                    const epiIdentifiers = [];

                    // Handle XML DOM Nodes (The actual format SPNATI uses)
                    if (epi instanceof Element || epi instanceof Node) {
                        if (epi.textContent) epiIdentifiers.push(epi.textContent.trim());
                        if (epi.innerText && epi.innerText !== epi.textContent) epiIdentifiers.push(epi.innerText.trim());
                        if (epi.getAttribute) {
                            const idAttr = epi.getAttribute('id');
                            const titleAttr = epi.getAttribute('title');
                            const nameAttr = epi.getAttribute('name');
                            if (idAttr) epiIdentifiers.push(idAttr);
                            if (titleAttr) epiIdentifiers.push(titleAttr);
                            if (nameAttr) epiIdentifiers.push(nameAttr);
                        }
                    }
                    // Handle standard JS objects
                    else if (typeof epi === 'object') {
                        if (epi.id) epiIdentifiers.push(epi.id);
                        if (epi.title) epiIdentifiers.push(epi.title);
                        if (epi.name) epiIdentifiers.push(epi.name);
                        if (epi.key) epiIdentifiers.push(epi.key);
                    }
                    // Handle raw strings
                    else if (typeof epi === 'string') {
                        epiIdentifiers.push(epi);
                    }

                    // Filter out empty strings and duplicates
                    const uniqueIds = [...new Set(epiIdentifiers.filter(id => id && typeof id === 'string' && id.length > 0))];

                    uniqueIds.forEach(epiId => {
                        // Call native addEnding
                        try {
                            if (typeof saveObj.addEnding === 'function') {
                                saveObj.addEnding(charId, epiId);
                                endingsInjected++;
                            }
                        } catch(e) {}

                        // Fallback direct localStorage injection
                        if (!endingsBlob[charId]) endingsBlob[charId] = [];
                        if (!endingsBlob[charId].includes(epiId)) {
                            endingsBlob[charId].push(epiId);
                        }
                    });
                });
            });

            // Write the JSON blob back to localStorage
            try { localStorage.setItem('SPNatI.endings', JSON.stringify(endingsBlob)); } catch(e) {}

            if (endingsInjected > 0 || collectiblesInjected > 0) {
                UI.notify(`Injected ${endingsInjected} endings & ${collectiblesInjected} collectibles!`, 'success', 6000);
            } else {
                UI.notify(`No data found. Open Gallery first!`, 'info');
            }
            Logger.info(`Permanent Unlocks: Injected ${endingsInjected} endings and ${collectiblesInjected} collectibles.`);
        },

        init() { Logger.info("Permanent Unlocks module ready."); }
    };
    Phoenix.register(PermanentUnlocks);

    /**
     * ==========================================
     * BOOTSTRAP EXECUTION
     * ==========================================
     */
    Phoenix.bootstrap().catch(err => {
        console.error("[Phoenix] Bootstrap failed:", err);
    });

})();