Bloxd Code Content Assist

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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(); });
})();