Bloxd Code Content Assist

Bloxd Code Content Assist! Have 13 objects and 20 Callbacks!

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Bloxd Code Content Assist
// @namespace    http://tampermonkey.net/
// @version      1.2.0
// @description  Bloxd Code Content Assist! Have 13 objects and 20 Callbacks!
// @author       Vicaloser
// @match        https://bloxd.io/*
// @grant        none
// ==/UserScript==
(function() {
    'use strict';
    let menu = null;
    let selectedIndex = 0;
    let currentFilteredList = [];
    let activeTextArea = null;
    //環境狀態偵測
    function getEditorMode() {
        const wrapper = document.querySelector('.WriteOnBoardWrapper');
        if (!wrapper) return "BLOCK";
        const style = window.getComputedStyle(wrapper, '::after').getPropertyValue('content');
        return (style && style.includes("World Code")) ? "WORLD" : "BLOCK";
    }
    function getFunctionNameAtCursor(text, pos) {
        const textBefore = text.substring(0, pos);
        const lastOpenBracket = textBefore.lastIndexOf('(');
        if (lastOpenBracket === -1) return null;
        const textAfterBracket = textBefore.substring(lastOpenBracket);
        if (textAfterBracket.includes(')')) return null;
        const funcMatch = textBefore.substring(0, lastOpenBracket).match(/[\w.]+$/);
        return funcMatch ? funcMatch[0] : null;
    }
    //字典
    function getDict() {
        const mode = getEditorMode();
        const idVar = (mode === "WORLD") ? "playerId" : "myId";
        //code推薦
        let base = [
            { label: "const", replace: "const $0 = " },
            { label: "let", replace: "let $0 = " },
            { label: "var", replace: "var $0 = " },
            { label: "function", replace: "function $0() {\n    \n}" },
            { label: "if", replace: "if ($0) {\n    \n}" },
            { label: "else", replace: "else {\n    $0\n}" },
            { label: "console.log", replace: "console.log($0)" },
            { label: "api.sendMessage", replace: `api.sendMessage(${idVar}, "$0")` },
            { label: "api.getPosition", replace: `api.getPosition(${idVar})` },
            { label: "eval()", replace: 'eval($0)' },
            { label: "return", replace: 'return' },
            { label: "true", replace: 'true' },
            { label: "false", replace: 'false' },
            { label: "api.setPosition", replace: `api.setPosition(${idVar}, x, y, z)` },
        ];
        //world code區
        if (mode === "WORLD") {
            base.push(
                /*事件*/
                { label: "onPlayerJoin", replace: "onPlayerJoin = (playerId, fromGameReset) => {\n    $0\n}" },
                { label: "onPlayerLeave", replace: "onPlayerLeave = (playerId, serverIsShuttingDown) => {\n    $0\n}" },
                { label: "onPlayerChat", replace: "onPlayerChat = (playerId, message) => {\n    $0\n}" },
                { label: "tick", replace: "tick = () => {\n    $0\n}" },
                { label: "onClose", replace: "onClose = (serverIsShuttingDown) => {\n    $0\n}" },
                { label: "onPlayerJump", replace: "onPlayerJump = (playerId) => {\n    $0\n}" },
                { label: "onRespawnRequest", replace: "onRespawnRequest = (playerId) => {\n    $0\n}" },
                { label: "playerCommand", replace: "playerCommand = (playerId, command) => {\n    $0\n}" },
                { label: "onPlayerChangeBlock", replace: "onPlayerChangeBlock = (playerId, x, y, z, fromBlock, toBlock, droppedItem) => {\n    $0\n}" },
                { label: "onPlayerDropItem", replace: "onPlayerDropItem = (playerId, x, y, z, itemName, itemAmount, fromIdx) => {\n    $0\n}" },
                { label: "onPlayerPickedUpItem", replace: "onPlayerPickedUpItem = (playerId, itemName, itemAmount) => {\n    $0\n}" },
                { label: "onPlayerSelectInventorySlot", replace: "onPlayerSelectInventorySlot = (playerId, slotIndex) => {\n    $0\n}" },
                { label: "onBlockStand", replace: "onBlockStand = (playerId, x, y, z, blockName) => {\n    $0\n}" },
                { label: "onPlayerAttemptCraft", replace: "onPlayerAttemptCraft = (playerId, itemName, craftingIdx, craftTimes) => {\n    $0\n}" },
                { label: "onPlayerCraft", replace: "onPlayerCraft = (playerId, itemName, craftingIdx, recipe, craftTimes) => {\n    $0\n}" },
                { label: "onPlayerAttemptOpenChest", replace: "onPlayerAttemptOpenChest = (playerId, x, y, z, isMoonstoneChest, isIronChest) => {\n    $0\n}" },
                { label: "onPlayerOpenedChest", replace: "onPlayerOpenedChest = (playerId, x, y, z, isMoonstoneChest, isIronChest) => {\n    $0\n}" },
                { label: "onPlayerMoveItemOutOfInventory", replace: "onPlayerMoveItemOutOfInventory = (playerId, itemName, itemAmount, fromIdx, movementType) => {\n    $0\n}" },
                { label: "onPlayerMoveInvenItem", replace: "onPlayerMoveInvenItem = (playerId, fromIdx, toStartIdx, toEndIdx, amt) => {\n    $0\n}" },
                { label: "onPlayerMoveItemIntoIdxs", replace: "onPlayerMoveItemIntoIdxs = (playerId, start, end, moveIdx, itemAmount) => {\n    $0\n}" },
                { label: "onPlayerSwapInvenSlots", replace: "onPlayerSwapInvenSlots = (playerId, i, j) => {\n    $0\n}" },
                { label: "onPlayerMoveInvenItemWithAmt", replace: "onPlayerMoveInvenItemWithAmt = (playerId, i, j, amt) => {\n    $0\n}" },
                { label: "onPlayerAttemptAltAction", replace: "onPlayerAttemptAltAction = (playerId, x, y, z, block, targetEId) => {\n    $0\n}" },
                { label: "onPlayerAltAction", replace: "onPlayerAltAction = (playerId, x, y, z, block, targetEId) => {\n    $0\n}" },
                { label: "onPlayerClick", replace: "onPlayerClick = (playerId, wasAltClick, x, y, z, block, targetEId) => {\n    $0\n}" },
                { label: "onClientOptionUpdated", replace: "onClientOptionUpdated = (playerId, option, value) => {\n    $0\n}" },
                { label: "onMobSettingUpdated", replace: "onMobSettingUpdated = (mobId, setting, value) => {\n    $0\n}" },
                { label: "onInventoryUpdated", replace: "onInventoryUpdated = (playerId) => {\n    $0\n}" },
                { label: "onChestUpdated", replace: "onChestUpdated = (initiatorEId, isMoonstoneChest, x, y, z) => {\n    $0\n}" },
                { label: "onWorldChangeBlock", replace: "onWorldChangeBlock = (x, y, z, fromBlock, toBlock, initiatorDbId, extraInfo) => {\n    $0\n}" },
                { label: "onCreateBloxdMeshEntity", replace: "onCreateBloxdMeshEntity = (eId, type, initiatorId) => {\n    $0\n}" },
                { label: "onPlayerAttemptSpawnMob", replace: "onPlayerAttemptSpawnMob = (playerId, mobType, x, y, z) => {\n    $0\n}" },
                { label: "onEntityCollision", replace: "onEntityCollision = (eId, otherEId) => {\n    $0\n}" },
                { label: "onWorldAttemptSpawnMob", replace: "onWorldAttemptSpawnMob = (mobType, x, y, z) => {\n    $0\n}" },
                { label: "onPlayerSpawnMob", replace: "onPlayerSpawnMob = (playerId, mobId, mobType, x, y, z, mobHerdId, playSoundOnSpawn) => {\n    $0\n}" },
                { label: "onWorldSpawnMob", replace: "onWorldSpawnMob = (mobId, mobType, x, y, z, mobHerdId, playSoundOnSpawn) => {\n    $0\n}" },
                { label: "onWorldAttemptDespawnMob", replace: "onWorldAttemptDespawnMob = (mobId) => {\n    $0\n}" },
                { label: "onMobDespawned", replace: "onMobDespawned = (mobId) => {\n    $0\n}" },
                { label: "onPlayerAttack", replace: "onPlayerAttack = (playerId) => {\n    $0\n}" },
                { label: "onPlayerDamagingOtherPlayer", replace: "onPlayerDamagingOtherPlayer = (attackingPlayer, damagedPlayer, damageDealt, withItem, bodyPartHit, damagerDbId) => {\n    $0\n}" },
                { label: "onPlayerDamagingMob", replace: "onPlayerDamagingMob = (playerId, mobId, damageDealt, withItem, damagerDbId) => {\n    $0\n}" },
                { label: "onMobDamagingPlayer", replace: "onMobDamagingPlayer = (attackingMob, damagedPlayer, damageDealt, withItem) => {\n    $0\n}" },
                { label: "onMobDamagingOtherMob", replace: "onMobDamagingOtherMob = (attackingMob, damagedMob, damageDealt, withItem) => {\n    $0\n}" },
                { label: "onAttemptKillPlayer", replace: "onAttemptKillPlayer = (killedPlayer, attackingLifeform) => {\n    $0\n}" },
                { label: "onPlayerKilledOtherPlayer", replace: "onPlayerKilledOtherPlayer = (attackingPlayer, killedPlayer, damageDealt, withItem) => {\n    $0\n}" },
                { label: "onMobKilledPlayer", replace: "onMobKilledPlayer = (attackingMob, killedPlayer, damageDealt, withItem) => {\n    $0\n}" },
                { label: "onPlayerKilledMob", replace: "onPlayerKilledMob = (playerId, mobId, damageDealt, withItem) => {\n    $0\n}" },
                { label: "onMobKilledOtherMob", replace: "onMobKilledOtherMob = (attackingMob, killedMob, damageDealt, withItem) => {\n    $0\n}" },
                { label: "onPlayerPotionEffect", replace: "onPlayerPotionEffect = (initiatorId, targetId, effectName) => {\n    $0\n}" },
                { label: "onPlayerDamagingMeshEntity", replace: "onPlayerDamagingMeshEntity = (playerId, damagedId, damageDealt, withItem) => {\n    $0\n}" },
                { label: "onPlayerBreakMeshEntity", replace: "onPlayerBreakMeshEntity = (playerId, entityId) => {\n    $0\n}" },
                { label: "onPlayerUsedThrowable", replace: "onPlayerUsedThrowable = (playerId, throwableName, thrownEntityId) => {\n    $0\n}" },
                { label: "onPlayerThrowableHitTerrain", replace: "onPlayerThrowableHitTerrain = (playerId, throwableName, thrownEntityId) => {\n    $0\n}" },
                { label: "onTouchscreenActionButton", replace: "onTouchscreenActionButton = (playerId, touchDown) => {\n    $0\n}" },
                { label: "onTaskClaimed", replace: "onTaskClaimed = (playerId, taskId, isPromoTask, claimedRewards) => {\n    $0\n}" },
                { label: "onChunkLoaded", replace: "onChunkLoaded = (chunkId, chunk, wasPersistedChunk) => {\n    $0\n}" },
                { label: "onPlayerRequestChunk", replace: "onPlayerRequestChunk = (playerId, chunkX, chunkY, chunkZ, chunkId) => {\n    $0\n}" },
                { label: "onItemDropCreated", replace: "onItemDropCreated = (itemEId, itemName, itemAmount, x, y, z) => {\n    $0\n}" },
                { label: "onPlayerStartChargingItem", replace: "onPlayerStartChargingItem = (playerId, itemName) => {\n    $0\n}" },
                { label: "onPlayerFinishChargingItem", replace: "onPlayerFinishChargingItem = (playerId, used, itemName, duration) => {\n    $0\n}" },
                { label: "onPlayerFinishQTE", replace: "onPlayerFinishQTE = (playerId, qteId, result) => {\n    $0\n}" },
                { label: "onPlayerBoughtShopItem", replace: "onPlayerBoughtShopItem = (playerId, categoryKey, itemKey, item, userInput) => {\n    $0\n}" },
                { label: "doPeriodicSave", replace: "doPeriodicSave = () => {\n    $0\n}" }
            );
        }

        return base;
    }
    //邏輯處理
    function confirmSelection() {
        const item = currentFilteredList[selectedIndex];
        if (!item || !activeTextArea) return;

        const el = activeTextArea;
        const pos = el.selectionStart;
        const fullText = el.value;

        const textBeforeCursor = fullText.substring(0, pos);
        const match = textBeforeCursor.match(/[\w.{(]+$/);

        if (match) {
            const wordToReplace = match[0];
            const startPos = pos - wordToReplace.length;

            let replacement = item.replace || item.label;
            const hasPlaceholder = replacement.includes("$0");
            const cursorOffset = hasPlaceholder ? replacement.indexOf("$0") : replacement.length;
            replacement = replacement.replace("$0", "");

            const newText = fullText.substring(0, startPos) + replacement + fullText.substring(pos);

            el.focus();
            el.value = newText;

            const finalPos = startPos + cursorOffset;
            el.setSelectionRange(finalPos, finalPos);

            const eventProps = { bubbles: true, cancelable: true };
            el.dispatchEvent(new InputEvent('input', eventProps));
            el.dispatchEvent(new Event('change', eventProps));

            const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
            nativeInputValueSetter.call(el, newText);
            el.dispatchEvent(new Event('input', { bubbles: true }));
        }

        hideMenu();
        setTimeout(() => el.focus(), 10);
    }
    //UI渲染與監聽

    document.addEventListener('input', (e) => {
        if (!e.target.classList.contains('WriteOnBoardTextArea')) return;
        activeTextArea = e.target;
        const pos = e.target.selectionStart;
        const text = e.target.value;

        const match = text.substring(0, pos).match(/[\w.{]+$/);

        if (match) {
            const currentInput = match[0].toLowerCase();
            let suggestions = [...getDict()];

            if (getFunctionNameAtCursor(text, pos) === "api.sendMessage") {
                suggestions.push({ label: "{color}", replace: '{color: "$0"}' });
            }

            const words = text.match(/[a-zA-Z_]\w+/g) || [];
            const context = [...new Set(words)]
                .filter(w => w.toLowerCase() !== currentInput && w.length > 2)
                .map(w => ({ label: w }));

            currentFilteredList = [...suggestions, ...context].filter(it =>
                it.label.toLowerCase().includes(currentInput) && it.label.toLowerCase() !== currentInput
            );

            if (currentFilteredList.length > 0) {
                const coords = getCaretXY(e.target, pos);
                createMenu();
                renderMenu(currentFilteredList, coords.x, coords.y + 22);
            } else hideMenu();
        } else hideMenu();
    });

    function createMenu() {
        if (menu) return menu;
        menu = document.createElement('div');
        menu.style.cssText = `position: fixed; background: rgba(30,30,33,0.98); color: #d4d4d4; border: 1px solid #454545; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 100000; display: none; font-family: 'JetBrains Mono', monospace; font-size: 13px; border-radius: 5px; overflow: hidden; min-width: 240px;`;
        document.body.appendChild(menu);
        return menu;
    }

    function renderMenu(list, x, y) {
        menu.innerHTML = '';
        list.forEach((item, i) => {
            const row = document.createElement('div');
            row.style.cssText = `padding: 6px 12px; display: flex; align-items: center; cursor: pointer; background: ${i === selectedIndex ? '#094771' : 'transparent'};`;
            row.innerHTML = `<span style="margin-right:8px; font-size:10px;">${item.replace ? '➤' : '✦'}</span><span style="flex-grow:1">${item.label}</span>`;
            row.onmousedown = (e) => { e.preventDefault(); selectedIndex = i; confirmSelection(); };
            menu.appendChild(row);
        });
        menu.style.left = x + 'px'; menu.style.top = y + 'px'; menu.style.display = 'block';
    }

    function getCaretXY(el, pos) {
        const div = document.createElement('div');
        const style = window.getComputedStyle(el);
        div.style.cssText = style.cssText;
        div.style.position = 'absolute'; div.style.visibility = 'hidden'; div.style.whiteSpace = 'pre-wrap';
        div.style.width = el.offsetWidth + 'px';
        div.textContent = el.value.substring(0, pos);
        const span = document.createElement('span');
        span.textContent = el.value.substring(pos, pos+1) || '.';
        div.appendChild(span);
        document.body.appendChild(div);
        const rect = el.getBoundingClientRect();
        const xy = { x: rect.left + span.offsetLeft - el.scrollLeft, y: rect.top + span.offsetTop - el.scrollTop };
        document.body.removeChild(div);
        return xy;
    }

    function hideMenu() { if(menu) menu.style.display = 'none'; selectedIndex = 0; }

    document.addEventListener('keydown', (e) => {
        if (!menu || menu.style.display === 'none') return;
        if (e.key === 'Tab' || e.key === 'Enter') { e.preventDefault(); confirmSelection(); }
        else if (e.key === 'ArrowDown') { e.preventDefault(); selectedIndex = (selectedIndex + 1) % currentFilteredList.length; renderMenu(currentFilteredList, parseInt(menu.style.left), parseInt(menu.style.top)); }
        else if (e.key === 'ArrowUp') { e.preventDefault(); selectedIndex = (selectedIndex - 1 + currentFilteredList.length) % currentFilteredList.length; renderMenu(currentFilteredList, parseInt(menu.style.left), parseInt(menu.style.top)); }
        else if (e.key === 'Escape') hideMenu();
    }, true);

    document.addEventListener('mousedown', (e) => { if(menu && !menu.contains(e.target)) hideMenu(); });
})();