├── .deepsource.toml ├── pack_icon.png ├── scripts ├── lib │ ├── cache.js │ ├── LevelDefine.js │ ├── JsonTagDB.js │ ├── util.js │ ├── ScoresFormat.js │ ├── GameLibrary.js │ ├── WorldDB.js │ └── base64.js ├── system │ ├── spawnTp.js │ ├── warp.js │ ├── tool.js │ ├── tpa.js │ ├── chat.js │ ├── money.js │ ├── level.js │ ├── shop.js │ └── home.js ├── mainMenu │ ├── player.js │ ├── buttons.js │ └── admin.js ├── config.js └── main.js ├── res └── 2022-04-23-19-40-25.png ├── items ├── menu.json └── admin_menu.json ├── manifest.json ├── README.md ├── sider.yml ├── CONTRIBUTING.md └── entities └── player.json /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "javascript" 5 | enabled = true -------------------------------------------------------------------------------- /pack_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dada878/Bedrock-Gametest-Plugin/HEAD/pack_icon.png -------------------------------------------------------------------------------- /scripts/lib/cache.js: -------------------------------------------------------------------------------- 1 | export var worldDB_table_cache = {}; 2 | export var tableDB_cache = {}; -------------------------------------------------------------------------------- /res/2022-04-23-19-40-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dada878/Bedrock-Gametest-Plugin/HEAD/res/2022-04-23-19-40-25.png -------------------------------------------------------------------------------- /items/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.16.100", 3 | "minecraft:item": { 4 | "description": { 5 | "identifier": "mcc:menu", 6 | "category": "items" 7 | }, 8 | "components": { 9 | "minecraft:icon": { 10 | "texture": "clock_item" 11 | }, 12 | "minecraft:max_stack_size": 1, 13 | "minecraft:foil": true, 14 | "minecraft:display_name": { 15 | "value": "§e<<§b玩家選單§e>>" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /items/admin_menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.16.100", 3 | "minecraft:item": { 4 | "description": { 5 | "identifier": "mcc:admin_menu", 6 | "category": "items" 7 | }, 8 | "components": { 9 | "minecraft:icon": { 10 | "texture": "compass_item" 11 | }, 12 | "minecraft:max_stack_size": 1, 13 | "minecraft:foil": true, 14 | "minecraft:display_name": { 15 | "value": "§6<<§c管理員選單§6>>" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /scripts/lib/LevelDefine.js: -------------------------------------------------------------------------------- 1 | import { levelFactor } from "../config.js" 2 | export function DefMaxXp(level){ 3 | return level ** 2 * levelFactor ; 4 | }; 5 | export const levelUpMsg = ">> §b恭喜!您升到了§aLv.%l"; 6 | 7 | export const specialLevelMappings = { 8 | 10: { 9 | text: ">> §a獲得10級特殊獎勵鑽石x3!", 10 | handler: ["give @s diamond 3", "say hello"] 11 | }, 12 | 20: { 13 | text: ">> §a獲得20級特殊獎勵綠寶石x1!", 14 | handler: ["give @s emerald"] 15 | } 16 | } -------------------------------------------------------------------------------- /scripts/system/spawnTp.js: -------------------------------------------------------------------------------- 1 | import { enables, pluginDB } from "../config"; 2 | import { logfor } from "../lib/GameLibrary"; 3 | 4 | const setting = pluginDB.table("spawnTpSetting"); 5 | 6 | export function SpawnTp(player) { 7 | 8 | if (enables.getData("spawnTp") == 1) { return logfor(player, ">> §c無法使用,此功能已被禁用") }; 9 | 10 | const pos = setting.getData("pos"); 11 | 12 | if (pos == null) return logfor(player, ">> §c大廳座標尚未被設定,請找管理員配置"); 13 | 14 | player.runCommand(`tp ${pos}`); 15 | } -------------------------------------------------------------------------------- /scripts/mainMenu/player.js: -------------------------------------------------------------------------------- 1 | import * as ui from 'mojang-minecraft-ui'; 2 | import { enables } from '../config.js'; 3 | import { GetScores, log, logfor } from '../lib/GameLibrary.js'; 4 | import { buttons, color, disableColor, disableIcon, disableText } from "./buttons.js"; 5 | 6 | export function PlayerMenu(player) { 7 | let fm = new ui.ActionFormData(); 8 | fm.title("功能選單"); 9 | fm.body("這是一個功能非常強大的選單"); 10 | 11 | buttons.forEach((data) => { 12 | let buttonText, icon; 13 | if (enables.getData(data.id) == 1) { 14 | buttonText = `§r${color}${data.display}\n§r${disableColor}${disableText}` 15 | icon = disableIcon 16 | } else { 17 | buttonText = `§r${color}${data.display}` 18 | icon = data.icon 19 | } 20 | fm.button(buttonText, icon); 21 | }) 22 | 23 | fm.show(player).then(response => { 24 | if (!response || response.isCanceled) return; 25 | 26 | buttons[response.selection].handler(player); 27 | }) 28 | } -------------------------------------------------------------------------------- /scripts/system/warp.js: -------------------------------------------------------------------------------- 1 | import { world } from "mojang-minecraft"; 2 | import * as ui from 'mojang-minecraft-ui'; 3 | import { enables, pluginDB } from "../config.js"; 4 | import { cmd, log, logfor } from '../lib/GameLibrary.js'; 5 | 6 | export function WarpMenu(player) { 7 | 8 | if (enables.getData("warp") == 1) return logfor(player, ">> §c無法使用,此功能已被禁用") 9 | 10 | let warps = pluginDB.table("warps").getAllData(); 11 | 12 | let fm = new ui.ActionFormData(); 13 | fm.title("世界傳送點"); 14 | fm.body("選擇想傳送到的地方!"); 15 | 16 | let pos = []; 17 | 18 | let count = 0; 19 | for (let key in warps) { 20 | fm.button(key); 21 | pos.push(warps[key]); 22 | count++; 23 | } 24 | 25 | if (count == 0) return logfor(player, ">> §c本世界沒有設定任何傳送點"); 26 | 27 | fm.show(player).then(response => { 28 | if (!response || response.isCanceled) return; 29 | player.runCommand(`tp ${pos[response.selection]}`); 30 | logfor(player, ">> §e傳送成功"); 31 | }) 32 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": 2, 3 | "header": { 4 | "description": "§b作者 §6冰川MCC\n§c僅在Youtube發布!請勿轉載!!", 5 | "name": "§d§l隨便寫的Home系統", 6 | "uuid": "cb4ad01c-0606-11ec-9a03-0242ac130003", 7 | "version": [0, 0, 6], 8 | "min_engine_version": [ 1, 14, 0 ] 9 | }, 10 | "modules": [ 11 | { 12 | "description": "Plugin Module", 13 | "type": "javascript", 14 | "uuid": "cb4ad4b0-0607-11ec-9a03-0242ac130003", 15 | "version": [0, 0, 1], 16 | "entry": "scripts/main.js" 17 | } 18 | ], 19 | "dependencies": [ 20 | { 21 | "uuid": "b26a4d4c-afdf-4690-88f8-931846312678", 22 | "version": [ 0, 1, 0 ] 23 | }, 24 | { 25 | "uuid": "6f4b6893-1bb6-42fd-b458-7fa3d0c89616", 26 | "version": [ 0, 1, 0 ] 27 | }, 28 | { 29 | "uuid": "2BD50A27-AB5F-4F40-A596-3641627C635E", 30 | "version": [ 0, 1, 0 ] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /scripts/system/tool.js: -------------------------------------------------------------------------------- 1 | import * as Minecraft from 'mojang-minecraft'; 2 | import { ScoreboardDB } from '../lib/WorldDB'; 3 | 4 | const cpsDB = new ScoreboardDB("cps", true); 5 | let playerCPS = {}; 6 | let CpsCache = {}; 7 | 8 | Minecraft.world.events.entityHit.subscribe(eventData => { 9 | const player = eventData.entity; 10 | const target = eventData.hitEntity; 11 | 12 | if (!target) return; 13 | if (player.id != "minecraft:player") return; 14 | 15 | let cps = playerCPS[player.name] ?? 0; 16 | cps++; 17 | playerCPS[player.name] = cps; 18 | }) 19 | 20 | let tick = 0; 21 | Minecraft.world.events.tick.subscribe(eventData => { 22 | tick ++; 23 | 24 | for (let player of Minecraft.world.getPlayers()) { 25 | const playerName = player.name; 26 | 27 | let cps = playerCPS[player.name] ?? 0; 28 | const cache = CpsCache[playerName] ?? 0; 29 | if (cache > cps) { 30 | cps = cache - 1; 31 | } 32 | 33 | CpsCache[playerName] = cps; 34 | 35 | cpsDB.setScore(player, cps); 36 | } 37 | 38 | if (tick % 20 > 0) return; 39 | playerCPS = {}; 40 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bedrock-Gametest-Plugin 2 | [![version](https://img.shields.io/badge/Minecraft_Version-Bedrock_1.18.30+-brightgreen.svg)](https://youtu.be/oHg5SJYRHA0) 3 | [![downloads](https://img.shields.io/github/downloads/dada909090/Bedrock-Gametest-Plugin/total?color=blue)](https://github.com/dada909090/Bedrock-Gametest-Plugin/releases/tag/Alpha) 4 | [![forks](https://img.shields.io/github/forks/dada909090/Bedrock-Gametest-Plugin?style=flat)](https://github.com/dada909090/Bedrock-Gametest-Plugin/network/members) 5 | [![stars](https://img.shields.io/github/stars/dada909090/Bedrock-Gametest-Plugin?style=flat)](https://youtu.be/oHg5SJYRHA0) 6 | ### 簡介 7 | 8 | > 支援手機及Win10! 9 | 10 | 一個基於基岩版Addon腳本的Gametest框架的專案\ 11 | 提供了一些不錯的功能\ 12 | 讓玩家在普通遊戲下也能體驗到伺服器的高級感\ 13 | 預覽圖片: 14 | 15 | ![預覽圖片](res/2022-04-23-19-40-25.png) 16 | ### 效果 17 | 目前最新版可以在這裡看到\ 18 | https://www.youtube.com/watch?v=PUEsoWuERq4 19 | ### 貢獻 20 | 如果有什麼好的想法\ 21 | 或者是發現可以優化的地方\ 22 | 歡迎直接發送 pull request 到本專案\ 23 | 若想長期合作開發請私訊我Discord 24 | ### 已知問題 25 | - 暫無 26 | ### 未來規劃 27 | - 完善經濟系統 28 | - 公會系統 29 | - 好友系統 30 | - 遊戲內自定義指令 31 | - 商店系統 32 | - 防掛設定 33 | ### 版權聲明 34 | 歡迎大家查看或使用本專案\ 35 | 但需遵守**以下規則**: 36 | - 修改或轉載請標註來源及作者 37 | - 勿稱該專案是自己的作品 38 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | import { WorldDB } from "./lib/WorldDB"; 2 | 3 | /** 4 | * @readonly 5 | * @description 命令的觸發符 6 | */ 7 | export const prefix = "-"; 8 | 9 | /** 10 | * @readonly 11 | * @description 預設的玩家稱號 12 | */ 13 | export const default_title = "§a玩家"; 14 | 15 | /** 16 | * @readonly 17 | * @description 給予的經驗範圍 18 | */ 19 | export const baseXP = 5; 20 | 21 | /** 22 | * @readonly 23 | * @description 升級的factor, formula為 level^2*factor 24 | */ 25 | export const levelFactor = 5; 26 | 27 | /** 28 | * @description 聊天格式 %level% = 玩家等級, %title% = 稱號, %player% = 玩家名字, %content% = 内容 29 | */ 30 | export const chatFormat = `[§bLv.%level%§r][%title%§r]%player% §7>>§f %content%` 31 | 32 | /** 33 | * @readonly 34 | * @description 檢查物品簡介 35 | */ 36 | export const checkLore = true 37 | 38 | /** 39 | * @readonly 40 | * @description 檢查違法附魔 41 | */ 42 | export const checkEnchantment = true 43 | 44 | /** 45 | * @readonly 46 | * @description 簽到獎勵 47 | */ 48 | export const signinReward = 599 49 | 50 | /** 51 | * @readonly 52 | * @description Name Checking 53 | */ 54 | export const nameCheckRegex = /[^A-Za-z0-9_]/gm 55 | 56 | export const pluginDB = new WorldDB("plugin_database"); 57 | export const enables = pluginDB.table("enable"); 58 | -------------------------------------------------------------------------------- /scripts/lib/JsonTagDB.js: -------------------------------------------------------------------------------- 1 | import * as Minecraft from 'mojang-minecraft'; 2 | 3 | 4 | /** 5 | * 取得玩家的JsonTagDB資料 6 | * @param {Minecraft.Player} player 玩家 7 | * @param {string} key 用於存取資料的鍵 8 | * @returns {string} 取得到的資料,若無則回傳null 9 | */ 10 | export function getData(player,key) { 11 | let db = checkDB(player); 12 | if (db["tagDB"][key] == undefined) db["tagDB"][key] = null; 13 | return db["tagDB"][key]; 14 | } 15 | 16 | 17 | /** 18 | * 設定玩家的JsonTagDB資料 19 | * @param {Minecraft.Player} player 玩家 20 | * @param {string} key 儲存資料的鍵 21 | * @param {string} value 要儲存的資料 22 | */ 23 | export function setData(player,key,value) { 24 | let db = checkDB(player); 25 | player.removeTag(JSON.stringify(db)); 26 | db["tagDB"][key] = value; 27 | player.addTag(JSON.stringify(db)); 28 | } 29 | 30 | 31 | /** 32 | * 檢查資料庫是否存在,若無則創建空資料庫 33 | * @param {Minecraft.Player} player 玩家 34 | * @returns {Map} 回傳Json(Map)資料庫 35 | */ 36 | function checkDB(player) { 37 | const tags = player.getTags(); 38 | 39 | let found = false; 40 | let DB; 41 | 42 | for (let i in tags) { 43 | if (tags[i].startsWith('{"tagDB":{')) { 44 | DB = JSON.parse(tags[i]); 45 | found = true; 46 | } 47 | } 48 | 49 | if (found) return DB; 50 | 51 | DB = {"tagDB":{}}; 52 | player.addTag(JSON.stringify(DB)); 53 | 54 | return DB; 55 | } -------------------------------------------------------------------------------- /scripts/lib/util.js: -------------------------------------------------------------------------------- 1 | import {rawcmd, cmd, cmds} from "./GameLibrary.js" 2 | // thanks https://stackoverflow.com/a/52551910 and https://stackoverflow.com/a/7224605 3 | /** 4 | * @name snakeToCamel 5 | * @param {string} str - The string to convert 6 | * @example str("minecraft:enchanted_golden_apple"); 7 | * @remarks Converts a snake_case string to camelCase 8 | * @returns {string} str - The converted string 9 | */ 10 | export function snakeToCamel(str) { 11 | str = str.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase()).replace("minecraft", ""); 12 | 13 | return str.charAt(0).toLowerCase() + str.slice(1); 14 | } 15 | 16 | export function clearItem(player, slot){ 17 | slot = slot + 0; 18 | 19 | if (slot >= 0) { 20 | try { 21 | if(slot <= 8) player.runCommand(`replaceitem entity @s slot.hotbar ${slot} air 1`); 22 | else player.runCommand(`replaceitem entity @s slot.inventory ${slot - 9} air 1`); 23 | } catch(error) { } 24 | } 25 | } 26 | 27 | export function getItemCount(player, id, data) { //from WrapperCord, thanks! 28 | let itemCount = []; 29 | const dat = cmd(`clear "${player}" ${id} ${data ? data : '0'} 0`); 30 | if (dat.error) 31 | return itemCount; 32 | dat.forEach(element => { 33 | const count = parseInt(element.match(/(?<=.*?\().+?(?=\))/)[0]); 34 | const player = element.match(/^.*(?= \(\d+\))/)[0]; 35 | itemCount.push({ player, count }); 36 | }); 37 | return itemCount ? itemCount : []; 38 | } -------------------------------------------------------------------------------- /scripts/system/tpa.js: -------------------------------------------------------------------------------- 1 | import { world } from "mojang-minecraft"; 2 | import * as ui from 'mojang-minecraft-ui'; 3 | import { enables } from "../config.js"; 4 | import { cmd, logfor, GetScores } from '../lib/GameLibrary.js'; 5 | 6 | export function TpaSystem(player) { 7 | if (enables.getData("tpa") == 1) { return logfor(player, ">> §c無法使用,此功能已被禁用") }; 8 | 9 | 10 | let fm = new ui.ModalFormData(); 11 | let players = world.getPlayers(); 12 | 13 | let operations = []; 14 | let playerObjects = []; 15 | 16 | for (let player of players) { 17 | operations.push(player.name); 18 | playerObjects.push(player); 19 | } 20 | 21 | fm.title("玩家互傳系統"); 22 | fm.dropdown("選擇玩家", operations); 23 | fm.toggle("請求對方傳送到你這裡"); 24 | fm.show(player).then(response => { 25 | let fm = new ui.MessageFormData(); 26 | let target = playerObjects[response.formValues[0]]; 27 | let tpHere = response.formValues[1]; 28 | if (tpHere) { 29 | fm.body(`${player.name} 希望把你傳送到他那裡`); 30 | } else { 31 | fm.body(`${player.name} 想要傳送來你這裡`); 32 | } 33 | fm.button1(`接受`); 34 | fm.button2(`拒絕`); 35 | logfor(player, `>> §e已對 §b${target.name} §e送出傳送請求`) 36 | fm.show(target).then(response => { 37 | if (response.selection != 1) return logfor(player, ">> §c請求已被拒絕"); 38 | logfor(player, ">> §a對方接受了傳送請求") 39 | if (tpHere) { 40 | cmd(`tp "${target.name}" "${player.name}"`); 41 | 42 | } else { 43 | cmd(`tp "${player.name}" "${target.name}"`); 44 | } 45 | }) 46 | }) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /sider.yml: -------------------------------------------------------------------------------- 1 | # This is a configuration file to customize code analysis by Sider. 2 | # 3 | # For more information, see the documentation: 4 | # https://help.sider.review/getting-started/custom-configuration 5 | 6 | # Customize each tool. If analyses fail, try adjusting each option referencing the following example. 7 | linter: 8 | 9 | # # ESLint example. See https://help.sider.review/tools/javascript/eslint 10 | # eslint: 11 | # root_dir: project/ 12 | # dependencies: 13 | # - my-eslint-plugin@2 14 | # npm_install: false 15 | # target: 16 | # - src/ 17 | # - lib/ 18 | # ext: [.js, .jsx] 19 | # config: config/.eslintrc.js 20 | # ignore-path: config/.eslintignore 21 | # ignore-pattern: "vendor/**" 22 | # no-ignore: true 23 | # global: ["require", "exports:true"] 24 | # quiet: true 25 | 26 | # # Misspell example. See https://help.sider.review/tools/others/misspell 27 | # misspell: 28 | # root_dir: project/ 29 | # target: [src/, test/] 30 | # exclude: ["**/*.min.*"] 31 | # locale: UK 32 | # ignore: [center, behavior] 33 | 34 | # # ShellCheck example. See https://help.sider.review/tools/shellscript/shellcheck 35 | # shellcheck: 36 | # root_dir: project/ 37 | # target: 38 | # - "**/*.{sh,bash}" 39 | # - shebang: true 40 | # include: [SC2104, SC2105] 41 | # exclude: [SC1000, SC1118] 42 | # enable: all 43 | # shell: bash 44 | # severity: error 45 | # norc: true 46 | 47 | # Ignore specific files. Example: 48 | # ignore: 49 | # - "*.pdf" 50 | # - "*.mp4" 51 | # - "*.min.*" 52 | # - "images/**" 53 | 54 | # Exclude specific branches. Example: 55 | # branches: 56 | # exclude: 57 | # - master 58 | # - development 59 | # - /^release-.*$/ 60 | -------------------------------------------------------------------------------- /scripts/system/chat.js: -------------------------------------------------------------------------------- 1 | import * as ui from 'mojang-minecraft-ui'; 2 | import * as Minecraft from 'mojang-minecraft'; 3 | import { log, logfor } from '../lib/GameLibrary.js'; 4 | import { getData, setData } from '../lib/JsonTagDB'; 5 | import { levelTable } from './level.js'; 6 | import { chatFormat, enables } from "../config.js"; 7 | /** 8 | * 對玩家顯示稱號選單 9 | * @param {Minecraft.Player} player 玩家 10 | * @return void 11 | */ 12 | export function ChangeChat(player) { 13 | 14 | if (enables.getData("title") == 1) { return logfor(player, ">> §c無法使用,此功能已被禁用") }; 15 | 16 | checkTitle(player); 17 | 18 | let hasTitles = getData(player, "hasTitles"); 19 | 20 | const tags = player.getTags(); 21 | for (let tag of tags) { 22 | 23 | if (tag.startsWith("RANK:")) { 24 | const rank = tag.substring(5, tag.length); 25 | hasTitles.push(rank); 26 | } 27 | } 28 | 29 | let fm = new ui.ModalFormData(); 30 | fm.title("稱號系統"); 31 | fm.dropdown("選擇要配戴的稱號", hasTitles) 32 | 33 | fm.show(player).then(response => { 34 | if (!response || response.isCanceled) return; 35 | 36 | let tagId = response.formValues[0]; 37 | 38 | setData(player, "selectedTitle", hasTitles[tagId]); 39 | 40 | logfor(player.name, ">> §a配戴成功") 41 | }) 42 | } 43 | 44 | export function sendMessage(player, message) { 45 | checkTitle(player); 46 | 47 | const title = getData(player, "selectedTitle"); 48 | let level = levelTable.getScore(player.name); 49 | 50 | if (level == null) {level = 0} 51 | 52 | log(chatFormat 53 | .replace("%level%", String(level)) 54 | .replace("%title%", title) 55 | .replace("%player%", player.nameTag ?? player.name) 56 | .replace("%content%", message) 57 | ); 58 | } 59 | 60 | function checkTitle(player) { 61 | if (getData(player, "hasTitles") === null) { 62 | setData(player, "hasTitles", ["§a玩家"]); 63 | } 64 | if (getData(player, "selectedTitle") === null) { 65 | setData(player, "selectedTitle", getData(player, "hasTitles")[0]); 66 | } 67 | } -------------------------------------------------------------------------------- /scripts/lib/ScoresFormat.js: -------------------------------------------------------------------------------- 1 | import * as base64 from "./base64" 2 | 3 | var SCORES_DECODE_CHAR = { 4 | 'A': '11', 'B': '12', 'C': '13', 'D': '14', 'E': '15', 'F': '16', 'G': '17', 'H': '18', 'I': '19', 5 | 'J': '20', 'K': '21', 'L': '22', 'M': '23', 'N': '24', 'O': '25', 'P': '26', 'Q': '27', 6 | 'R': '28', 'S': '29', 'T': '30', 'U': '31', 'V': '32', 'W': '33', 'X': '34', 'Y': '35', 7 | 'Z': '36', 'a': '37', 'b': '38', 'c': '39', 'd': '40', 'e': '41', 'f': '42', 'g': '43', 8 | 'h': '44', 'i': '45', 'j': '46', 'k': '47', 'l': '48', 'm': '49', 'n': '50', 'o': '51', 9 | 'p': '52', 'q': '53', 'r': '54', 's': '55', 't': '56', 'u': '57', 'v': '58', 'w': '59', 10 | 'x': '60', 'y': '61', 'z': '62', '0': '63', '1': '64', '2': '65', '3': '66', '4': '67', 11 | '5': '68', '6': '69', '7': '70', '8': '71', '9': '72', '+': '73', '/': '74', '=': '75', 12 | }; 13 | 14 | var REVERSE_SCORES_DECODE_CHAR = {}; 15 | for (let i in SCORES_DECODE_CHAR) { 16 | const key = SCORES_DECODE_CHAR[i]; 17 | 18 | REVERSE_SCORES_DECODE_CHAR[key] = i; 19 | }; 20 | 21 | /** 22 | * 將字串編碼成"記分板編碼" 23 | * @param {string} string 字串 24 | * @returns {string} 編碼後的字串 25 | */ 26 | export function encode(string) { 27 | 28 | const origin = base64.encode(string); 29 | 30 | let result = ""; 31 | 32 | for (let i in origin) { 33 | let char = SCORES_DECODE_CHAR[origin[i]]; 34 | result += char; 35 | }; 36 | 37 | return result; 38 | }; 39 | 40 | /** 41 | * "記分板編碼"解碼成utf-8字串 42 | * @param {string} string 記分板編碼 43 | * @returns {string} 解碼後結果 44 | */ 45 | export function decode(string) { 46 | 47 | string = string.toString(); 48 | 49 | let twoChars = []; 50 | let result = ""; 51 | 52 | for (let i in string) { 53 | const listIndex = Math.trunc(i / 2); 54 | if (!twoChars[listIndex]) { 55 | twoChars[listIndex] = string[i]; 56 | } else { 57 | twoChars[listIndex] += string[i]; 58 | } 59 | } 60 | 61 | for (let i of twoChars) { 62 | result += REVERSE_SCORES_DECODE_CHAR[i]; 63 | } 64 | 65 | return base64.decode(result); 66 | } -------------------------------------------------------------------------------- /scripts/mainMenu/buttons.js: -------------------------------------------------------------------------------- 1 | import { ChangeChat } from '../system/chat.js' 2 | import { HomeSystem } from "../system/home.js" 3 | import { TpaSystem } from "../system/tpa.js"; 4 | import { SpawnTp } from "../system/spawnTp.js"; 5 | import { WarpMenu } from "../system/warp.js"; 6 | import { LevelSystem } from "../system/level.js"; 7 | import { MoneySystem } from "../system/money.js"; 8 | import { ShopSystem } from "../system/shop.js"; 9 | 10 | import { logfor } from '../lib/GameLibrary.js'; 11 | const noone = ((player) => { logfor(player, ">> §c本功能暫未開放!") }) 12 | const disable = ((player) => { logfor(player, ">> §c本功能已被暫時關閉!") }) 13 | 14 | export const buttons = [ 15 | { 16 | id: "spawnTp", 17 | display: "返回大廳", 18 | icon: "textures/ui/village_hero_effect.png", 19 | handler: SpawnTp 20 | }, 21 | { 22 | id: "title", 23 | display: "稱號系統", 24 | icon: "textures/ui/mute_off.png", 25 | handler: ChangeChat 26 | }, 27 | { 28 | id: "warp", 29 | display: "世界傳送點", 30 | icon: "textures/ui/world_glyph_color.png", 31 | handler: WarpMenu 32 | }, 33 | { 34 | id: "home", 35 | display: "家園系統", 36 | icon: "textures/ui/icon_recipe_item.png", 37 | handler: HomeSystem 38 | }, 39 | { 40 | id: "tpa", 41 | display: "玩家互傳", 42 | icon: "textures/ui/icon_multiplayer.png", 43 | handler: TpaSystem 44 | }, 45 | { 46 | id: "money", 47 | display: "經濟系統", 48 | icon: "textures/ui/MCoin.png", 49 | handler: disable 50 | }, 51 | { 52 | id: "shop", 53 | display: "商店系統", 54 | icon: "textures/ui/MCoin.png", 55 | handler: ShopSystem 56 | }, 57 | { 58 | id: "level", 59 | display: "等級系統", 60 | icon: "textures/items/experience_bottle.png", 61 | handler: LevelSystem 62 | }, 63 | ]; 64 | 65 | export const color = "§l§1"; 66 | export const disableColor = "§4"; 67 | export const disableText = "此功能已被管理員禁用"; 68 | export const disableIcon = "textures/ui/realms_red_x.png"; 69 | -------------------------------------------------------------------------------- /scripts/system/money.js: -------------------------------------------------------------------------------- 1 | import { world } from "mojang-minecraft"; 2 | import * as ui from 'mojang-minecraft-ui'; 3 | import { pluginDB } from "../config.js"; 4 | import { cmd, GetScores, log, logfor, SetScores, AddScores } from '../lib/GameLibrary.js'; 5 | import { WorldDB, ScoreboardDB } from '../lib/WorldDB.js'; 6 | 7 | const dbName = pluginDB.table("moneySetting").getData("scoreboard") ?? "money"; 8 | export const moneyTable = new ScoreboardDB(dbName); 9 | 10 | export function MoneySystem(player) { 11 | const worldPlayers = world.getPlayers(); 12 | let players = []; 13 | let playerNames = []; 14 | 15 | for (let i of worldPlayers) { 16 | playerNames.push(i.name); 17 | players.push(i); 18 | } 19 | 20 | let fm = new ui.ActionFormData(); 21 | fm.title("經濟菜單"); 22 | fm.body("noone"); 23 | fm.button('§l§1匯款'); 24 | fm.button('§l§1簽到'); 25 | 26 | fm.show(player).then(response => { 27 | if (!response || response.isCanceled) { return }; 28 | 29 | switch(response.selection){ 30 | case (0): { 31 | let fm = new ui.ModalFormData(); 32 | fm.title("付款"); 33 | fm.dropdown("選擇目標玩家", playerNames); 34 | fm.textField("輸入付款的數額", ""); 35 | 36 | fm.show(player).then(response => { 37 | if (!response || response.isCanceled) return; 38 | if (!response.formValues[1] || isNaN(response.formValues[1])) return logfor(player, ">> §c金額必須為數字!"); 39 | 40 | let money = moneyTable.getScore(player) 41 | if(isNaN(money)) return logfor(player, `>> §c未知錯誤`); 42 | 43 | if(money < +response.formValues[1]) return logfor(player, `>> §c你沒有足夠的金幣!`); 44 | 45 | let target = players[response.formValues[0]]; 46 | 47 | moneyTable.addScore(`"${target.name}"`, +response.formValues[1]) 48 | moneyTable.setScore(`"${player.name}"`, money - response.formValues[1]) 49 | logfor(player.name, ">> §a給予成功!"); 50 | return logfor(target.name, `>> §a你收到了${player.name}的${response.formValues[1]}$!`); 51 | }) 52 | } 53 | case (1): { 54 | return; 55 | } 56 | } 57 | }); 58 | } -------------------------------------------------------------------------------- /scripts/system/level.js: -------------------------------------------------------------------------------- 1 | import * as Minecraft from 'mojang-minecraft'; 2 | import * as ui from 'mojang-minecraft-ui'; 3 | import { enables, pluginDB } from '../config.js'; 4 | import { cmd, cmds, executeCmds, GetScores, log, logfor, SetScores } from '../lib/GameLibrary.js'; 5 | import { DefMaxXp, specialLevelMappings, levelUpMsg } from "../lib/LevelDefine.js"; 6 | import { ScoreboardDB, WorldDB } from '../lib/WorldDB.js'; 7 | 8 | export const expTable = new ScoreboardDB("exp"); 9 | export const levelTable = new ScoreboardDB("level"); 10 | 11 | /** 12 | * 對玩家顯示等即系統選單 13 | * @param {Minecraft.Player} player 玩家 14 | */ 15 | export function LevelSystem(player) { 16 | if (enables.getData("level") == 1) return logfor(player, ">> §c無法使用,此功能已被禁用"); 17 | 18 | let level = levelTable.getScore(player.name); 19 | let exp = expTable.getScore(player.name); 20 | 21 | if (level == null) level = "0"; 22 | 23 | let fm = new ui.ActionFormData(); 24 | fm.title(`等級系統`); 25 | fm.body(`您目前為§aLv.${level}(${exp}/${DefMaxXp(level)})§r\n\n等級獎勵:`); 26 | fm.button(`查看等級排名`); 27 | 28 | fm.show(player).then(response => { 29 | if (!response || response.isCanceled) return; 30 | 31 | if (response.selection == 0) {}; 32 | }); 33 | } 34 | 35 | /** 36 | * 為玩家添加經驗值 37 | * @param {Minecraft.Player} player 目標玩家 38 | * @param {number} exp 經驗值數量 39 | * @returns 40 | */ 41 | export function addXp(player, exp) { 42 | if (enables.getData("level") == 1) return; 43 | // 取得經驗值與等級並定義玩家名稱 44 | const playerName = player.name; 45 | 46 | let player_level = levelTable.getScore(playerName); 47 | let player_exp = expTable.getScore(playerName); 48 | 49 | // 若玩家等即尚未初始化則歸零 50 | if (player_level == null) { 51 | player_level = 0; 52 | levelTable.setScore(playerName, 0); 53 | } 54 | 55 | //添加經驗值 56 | expTable.addScore(playerName, exp); 57 | // 經驗值大於所需經驗,即升級 58 | if (player_exp >= DefMaxXp(player_level)) { 59 | // 歸零經驗值並添加等級 60 | levelTable.addScore(playerName, 1); 61 | expTable.setScore(playerName, 0); 62 | // 升級特效/音效 63 | cmd(`title "${playerName}" title §b恭喜升級`); 64 | cmd(`title "${playerName}" subtitle §e已經升上 §a${player_level} §e等`); 65 | log(`>> §b${playerName} §e成功升到了 §b${player_level} §e等!`); 66 | cmd(`playsound random.levelup "${playerName}"`); 67 | // 玩家等級的特殊等級map 68 | const specialLevelData = specialLevelMappings[player_level]; 69 | // 若無該特殊等級資料或訊息就return 70 | if (!specialLevelData || specialLevelData.text === "") return; 71 | // 發送特殊等級訊息 72 | logfor(player, `${specialLevelData.text}`); 73 | // 如果有就執行特殊等級指令 74 | if (specialLevelData.handler !== []) { 75 | executeCmds(player, specialLevelData.handler); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | ## Before contributing 4 | 5 | Welcome to [dada909090/Bedrock-Gametest-Plugin](https://github.com/dada909090/Bedrock-Gametest-Plugin)! Before sending your pull requests, 6 | make sure that you **read the whole guidelines**. If you have any doubt on the contributing guide, please feel free to 7 | [state it clearly in an issue](https://github.com/dada909090/Bedrock-Gametest-Plugin/issues/new). 8 | 9 | ## Contributing 10 | 11 | ### Contributor 12 | 13 | We are very happy that you consider implementing usage for other users! This repository is 14 | referenced and used by learners from around the globe. Being one of our contributors, you agree and confirm that: 15 | 16 | - You did your work - plagiarism is not allowed. 17 | - Any plagiarized work will not be merged. 18 | - if you used codes of others, and it is open, you may credit as if you really think you have permissions. 19 | - Your submitted work must fulfill our styles and standards. 20 | 21 | **New implementation** is welcome! For example, new solutions to a problem, different representations of a UI 22 | structure or functuon with different complexity. 23 | 24 | **Improving comments** xor **Improving Variables** are also highly welcome. 25 | 26 | ### Contribution 27 | 28 | We appreciate any contribution, from fixing grammar mistakes to implementing complex modules. Please read this 29 | section if you are contributing to your work. 30 | 31 | If you submit a PR that resolves an open issue, please help us to keep our issue list small by adding 32 | `fixes: #{$ISSUE_NO}` to your commit message. GitHub will use this tag to auto-close the issue if your PR is merged. 33 | 34 | ### What is Suitable? 35 | 36 | 37 | #### File Naming Convention 38 | 39 | - filenames should use the UpperCamelCase (PascalCase) style, or using lowercase. 40 | - There should be no spaces in filenames. 41 | - **Example:** `UserProfile.js` is allowed but `userprofile.js`,`Userprofile.js`,`user-Profile.js`,`userProfile.js` are 42 | not. 43 | 44 | #### Module System 45 | 46 | We use the [ES Module](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/) system in minecraft bedrock edition gametest module, which bring an official, standardized module system to JavaScript. 47 | 48 | It roughly means you will need to use `export` and `import` statements instead of `module.exports` and `require()`. 49 | 50 | #### Testing 51 | 52 | Be confident that your code works. When was the last time you committed a code change, your build failed, and half of 53 | your app stopped working? Mine was last week. Writing tests for our systems will help us ensure the implementations 54 | are air tight even after multiple fixes and code changes. 55 | 56 | It is advised that the module does not contain any "live" code but rather just exports the function(s) in dir "./lib", 57 | needed to execute the module. Your test code can import those function(s), call them with the appropriate parameters 58 | and inspect the outcome. 59 | 60 | Please refrain from using `console` in your implementation, but only test codes. 61 | 62 | You can (and should!) test modules that you implement inside game before committing your changes (even if it have zero logic mistake). 63 | 64 | #### Coding Style 65 | 66 | To maximize the readability and correctness of our code, we require that new submissions follow standard js code style. 67 | 68 | no linters config are given, but please make namings good. 69 | 70 | A few (but not all) of the things to keep in mind: 71 | 72 | - Use camelCase with the leading character as lowercase for identifier names (variables and functions). 73 | - Names start with a letter. 74 | 75 | ```js 76 | function sumOfArray(numbers) { 77 | let sum = 0; 78 | for (let i = 0; i < numbers.length; i++){ 79 | sum += numbers[i]; 80 | } 81 | return sum; 82 | } 83 | ``` 84 | 85 | - Avoid using global variables and avoid `==`. 86 | - Please use `let` over `var`. 87 | - Please refrain from using `console.log` or any other console methods. 88 | - **Absolutely** don't use `alert`. 89 | - We strongly recommend the use of ECMAScript 6. 90 | - Avoid importing external libraries for basic algorithms. Only use those libraries for complicated algorithms. 91 | - Most importantly: 92 | - **Be consistent in the use of these guidelines when submitting.** 93 | - Happy coding! -------------------------------------------------------------------------------- /scripts/system/shop.js: -------------------------------------------------------------------------------- 1 | import { world } from "mojang-minecraft"; 2 | import * as ui from 'mojang-minecraft-ui'; 3 | import { pluginDB } from "../config.js"; 4 | import { cmd, log, logfor, cmds } from '../lib/GameLibrary.js'; 5 | import { getItemCount } from '../lib/util.js'; 6 | import { WorldDB, ScoreboardDB } from '../lib/WorldDB.js'; 7 | 8 | export const maxSelect = 128 9 | const dbName = pluginDB.table("moneySetting").getData("scoreboard") ?? "money"; 10 | export const moneyTable = new ScoreboardDB(dbName); 11 | 12 | export const buyableItems = [ 13 | { 14 | display: '§7鐵錠', 15 | id: 'minecraft:iron_ingot', 16 | price: 300 17 | }, 18 | { 19 | display: '§b鑽石', 20 | id: 'minecraft:diamond', 21 | price: 500 22 | }, 23 | { 24 | display: '§a綠寶石', 25 | id: 'minecraft:emerald', 26 | price: 750 27 | }, 28 | { 29 | display: '§d下屆合金', 30 | id: 'minecraft:netherite_ingot', 31 | price: 1000 32 | }, 33 | ] 34 | export const sellableItems = [ 35 | { 36 | display: '§7鐵錠', 37 | id: 'minecraft:iron_ingot', 38 | price: 250 39 | }, 40 | ] 41 | export function ShopSystem(player) { 42 | let fm = new ui.ActionFormData(); 43 | fm.title("noone"); 44 | fm.body("noone"); 45 | fm.button('§l§1購買'); 46 | fm.button('§l§1出售'); 47 | 48 | fm.show(player).then(response => { 49 | if (!response || response.isCanceled) { return } 50 | 51 | switch(response.selection){ 52 | case (0): { 53 | let fm = new ui.ActionFormData(); 54 | fm.title("購買"); 55 | buyableItems.forEach((f) => {fm.button(f.display, f.icon ?? "")}); 56 | 57 | fm.show(player).then(response => { 58 | if (!response || response.isCanceled) return; 59 | 60 | if(buyableItems[response.selection]){ 61 | const item = buyableItems[response.selection]; 62 | let money = moneyTable.getScore(player); 63 | const maxCount = money / item.price; 64 | if(moneyTable.getScore(player) < item.price) return logfor(player, `>> 你沒有足夠的金錢買一個${item.display}!`); 65 | let fm = new ui.ModalFormData(); 66 | fm.slider("你要買多少個?", 0, Math.min(maxCount, maxSelect), 1, maxCount); 67 | 68 | fm.show(player).then((response) => { 69 | let count = response.formValues[0]; 70 | cmds([ 71 | `give ${player.name} ${item.id} ${response.formValues[0]} ` 72 | ]); 73 | logfor(player, `>> 成功購買${count}個${item.display}!`); 74 | moneyTable.removeScore(player, count * item.price); 75 | }) 76 | } 77 | }) 78 | } 79 | case (1): { 80 | let fm = new ui.ActionFormData(); 81 | fm.title("出售"); 82 | sellableItems.forEach((f) => {fm.button(f.display, f.icon ?? "")}); 83 | fm.show(player).then(response => { 84 | if (!response || response.isCanceled) return; 85 | 86 | if(sellableItems[response.selection]){ 87 | const item = sellableItems[response.selection]; 88 | const count = getItemCount(player, item.id, item.data ?? 0)[0]?.count || 0; 89 | if(count) return logfor(player, `>> 你沒有足夠的物品出售${item.display}!`); 90 | let fm = new ui.ModalFormData(); 91 | fm.slider("你要出售多少個?", 0, Math.min(count, maxSelect), 1, count); 92 | fm.show(player).then((response) => { 93 | let count = response.formValues[0]; 94 | cmds([ 95 | `clear ${player.name} ${item.id} ${response.formValues[0]}` 96 | ]); 97 | logfor(player, `>> 成功出售${count}個${item.display}!`); 98 | moneyTable.addScore(player, count * item.price); 99 | }) 100 | } 101 | }) 102 | return; 103 | } 104 | } 105 | }); 106 | } -------------------------------------------------------------------------------- /scripts/lib/GameLibrary.js: -------------------------------------------------------------------------------- 1 | import * as Minecraft from 'mojang-minecraft'; 2 | 3 | 4 | /** 5 | * 在主世界執行一段指令 6 | * @param {string} command 要執行的指令 7 | * @param {string} dimension opt - 執行維度 8 | * @returns {string} 指令執行結果 9 | */ 10 | export function cmd(command, dimension = "overworld") { 11 | return Minecraft.world.getDimension(dimension).runCommand(command).statusMessage 12 | }; 13 | 14 | 15 | /** 16 | * 執行指令並回傳是否成功與結果 17 | * @param {string} command 要執行的指令 18 | * @returns {map} \{ error:bool, result:string \} 19 | */ 20 | export function rawcmd(command) { 21 | try { 22 | return { error: false, ...Minecraft.world.getDimension("overworld").runCommand(command) }; 23 | } catch (error) { 24 | return { error: true }; 25 | } 26 | }; 27 | 28 | 29 | /** 30 | * 以玩家的身分(execute)執行陣列內的指令 31 | * @param {Minecraft.Player | string} player 玩家 32 | * @param {string[]} commands 需要執行的所有指令 33 | * @returns {boolean} 是否執行成功 34 | */ 35 | export function executeCmds(player, commands) { 36 | 37 | if (typeof player != typeof "string") { 38 | player = player.name; 39 | } 40 | 41 | const conditionalRegex = /^%/; 42 | if (conditionalRegex.test(commands[0])) return false; 43 | let error = false; 44 | commands.forEach(cmd => { 45 | if (error && conditionalRegex.test(cmd)) return false; 46 | error = rawcmd(`execute ${player} ~~~ ` + cmd.replace(conditionalRegex, '')).error; 47 | }); 48 | return true; 49 | } 50 | 51 | 52 | /** 53 | * 執行陣列內所有指令 54 | * @param {string[]} commands 所有要執行的指令 55 | * @returns {boolean} 指令是否執行成功 56 | */ 57 | export function cmds(commands) { 58 | const conditionalRegex = /^%/; 59 | if (conditionalRegex.test(commands[0])) return false; 60 | let error = false; 61 | commands.forEach(cmd => { 62 | if (error && conditionalRegex.test(cmd)) return false; 63 | error = rawcmd(cmd.replace(conditionalRegex, '')).error; 64 | }); 65 | return true; 66 | } 67 | 68 | 69 | /** 70 | * 傳送一則訊息給玩家 71 | * @param {Minecraft.Player | string} player 玩家 72 | * @param {string} message 訊息 73 | */ 74 | export function logfor(player, message) { 75 | try { 76 | if (typeof player != typeof "string") { 77 | player = player.name; 78 | } 79 | let okay_message = message.toString().replaceAll('\"', "''").replaceAll('\\', "/") 80 | if (player.includes("@")) { 81 | Minecraft.world.getDimension("overworld").runCommand(`tellraw ${player} {"rawtext":[{"text":"${okay_message}"}]}`) 82 | } else { 83 | Minecraft.world.getDimension("overworld").runCommand(`tellraw "${player}" {"rawtext":[{"text":"${okay_message}"}]}`) 84 | } 85 | } catch { } 86 | }; 87 | 88 | 89 | /** 90 | * 傳送訊息給所有玩家 91 | * @param {string} message 訊息 92 | */ 93 | export function log(message) { 94 | let okay_message = `${message}`.replaceAll('\"', "''").replaceAll('\\', "/") 95 | Minecraft.world.getDimension("overworld").runCommand(`tellraw @a {"rawtext":[{"text":"${okay_message}"}]}`) 96 | } 97 | 98 | 99 | /** 100 | * 取得某計分項在計分板內的值 101 | * @param {string} target 記分項目 102 | * @param {string} scoreboard 記分板 103 | * @returns {number} 執行成功回傳分數,失敗則為null 104 | */ 105 | export function GetScores(target, scoreboard) { 106 | try { 107 | const scoreMessage = cmd(`scoreboard players operation "${target}" "${scoreboard}" = "${target}" "${scoreboard}"`); 108 | const scoresRegEx = [...scoreMessage.matchAll(/\d+|-\d+/g)]; 109 | const scores = scoresRegEx[scoresRegEx.length - 1]; 110 | 111 | return Number(scores); 112 | 113 | } catch { 114 | return null; 115 | } 116 | } 117 | 118 | /** 119 | * 設定某計分項在計分板內的值 120 | * @param {string} target 計分項 121 | * @param {string} scoreboard 記分板 122 | * @param {number} scores 分數 123 | * @returns 124 | */ 125 | export function SetScores(target, scoreboard, scores) { 126 | return cmd(`scoreboard players set "${target}" "${scoreboard}" ${scores}`); 127 | } 128 | 129 | /** 130 | * 增加某計分項在計分板內的值 131 | * @param {string} target 計分項 132 | * @param {string} scoreboard 記分板 133 | * @param {number} scores 分數 134 | * @returns 135 | */ 136 | export function AddScores(target, scoreboard, scores) { 137 | return cmd(`scoreboard players set "${target}" "${scoreboard}" ${scores}`); 138 | } 139 | 140 | /** 141 | * 強制踢出玩家 142 | * @param {Minecraft.Player} player 143 | */ 144 | export function kickPlayer2(player) { 145 | player.triggerEvent("kick"); 146 | } 147 | 148 | /** 149 | * 踢出玩家 150 | * @param {Minecraft.Player} player 151 | */ 152 | export function kickPlayer(player) { 153 | cmd(`kick ${player.name}`); 154 | } -------------------------------------------------------------------------------- /scripts/system/home.js: -------------------------------------------------------------------------------- 1 | import { world } from "mojang-minecraft"; 2 | import * as ui from 'mojang-minecraft-ui'; 3 | import { enables } from "../config.js"; 4 | import { cmd, logfor } from '../lib/GameLibrary.js'; 5 | 6 | /** 7 | * 8 | * @param {Minecraft.Player} player 9 | * @returns 10 | */ 11 | export function HomeSystem(player) { 12 | if (enables.getData("home") == 1) { return logfor(player, ">> §c無法使用,此功能已被禁用") }; 13 | 14 | let tags = player.getTags() 15 | 16 | let fm = new ui.ActionFormData(); 17 | fm.title("Home家園系統") 18 | fm.body("設定Home,並且快速傳送回家吧") 19 | fm.button('§l§5新增Home', 'textures/ui/color_plus.png') 20 | fm.button('§l§4刪除Home', 'textures/ui/icon_trash.png') 21 | 22 | for (let i in tags) { 23 | if (tags[i].startsWith('{"Home":{')) { 24 | let homeData = JSON.parse(tags[i]) 25 | let homeName = homeData['Home']['Name'] 26 | let homePos = homeData['Home']['Pos'] 27 | fm.button(`§1${homeName}\n§r§9${homePos}`) 28 | } 29 | } 30 | 31 | fm.show(player).then(response => { 32 | if (!response || response.isCanceled) { return } 33 | if (response.selection == 0) { 34 | if (player.dimension != world.getDimension("overworld")) { 35 | cmd(`playsound note.pling "${player.nameTag}"`) 36 | logfor(player.nameTag, `>> §c你只能在主世界創建Home`) 37 | return 38 | } 39 | else { 40 | 41 | let fm = new ui.ModalFormData() 42 | 43 | fm.title("Home家園系統"); 44 | fm.textField("輸入Home名稱", "MyHome") 45 | 46 | fm.show(player).then(response => { 47 | if (!response || response.isCanceled) { return } 48 | let homes = getHomes(player) 49 | if (response.formValues[0] == "") { 50 | cmd(`playsound note.pling "${player.nameTag}"`) 51 | logfor(player.nameTag, `>> §c請輸入Home名稱`) 52 | return 53 | } 54 | if (homes["Homes"].includes(response.formValues[0])) { 55 | cmd(`playsound note.pling "${player.nameTag}"`) 56 | logfor(player.nameTag, `>> §c你已經創過名為 §e${response.formValues[0]} §c的Home了`) 57 | return 58 | } 59 | let pos = player.location 60 | let jsonDB = { 61 | 'Home': { 62 | 'Name': response.formValues[0], 63 | 'Pos': `${Math.trunc(pos.x)} ${Math.trunc(pos.y)} ${Math.trunc(pos.z)}` 64 | } 65 | } 66 | player.addTag(JSON.stringify(jsonDB)) 67 | cmd(`playsound random.orb "${player.nameTag}"`) 68 | logfor(player.nameTag, `>> §a成功設定家園點 §b${response.formValues[0]}`) 69 | 70 | }) 71 | 72 | } 73 | } 74 | else if (response.selection == 1) { 75 | let fm = new ui.ModalFormData() 76 | 77 | let homes = getHomes(player) 78 | 79 | fm.title("Home家園系統"); 80 | fm.dropdown("選擇要刪除的Home", homes['Homes']) 81 | 82 | if (!homes['Homes'].length) { 83 | logfor(player.nameTag, `>> §c你沒有設定任何的Home`) 84 | return 85 | } 86 | 87 | fm.show(player).then(response => { 88 | if (!response || response.isCanceled) { return } 89 | let findJsonDB = { 90 | 'Home': { 91 | 'Name': homes['Homes'][response.formValues[0]], 92 | 'Pos': homes['Pos'][response.formValues[0]] 93 | } 94 | } 95 | player.removeTag(JSON.stringify(findJsonDB)) 96 | cmd(`playsound random.orb "${player.nameTag}"`) 97 | logfor(player.nameTag, `>> §e成功移除家園點 §b${homes['Homes'][response.formValues[0]]}`) 98 | 99 | }) 100 | } 101 | else { 102 | let homes = getHomes(player) 103 | 104 | cmd(`tp "${player.nameTag}" ${homes['Pos'][response.selection - 2]}`) 105 | cmd(`playsound random.orb "${player.nameTag}"`) 106 | logfor(player.nameTag, `>> §a已傳送到Home點 §b${homes['Homes'][response.selection - 2]}`) 107 | } 108 | }); 109 | } 110 | 111 | function getHomes(player) { 112 | let tags = player.getTags() 113 | let homes = { "Homes": [], "Pos": [] } 114 | for (let i in tags) { 115 | if (tags[i].startsWith('{"Home":{')) { 116 | let homeData = JSON.parse(tags[i]) 117 | homes["Homes"].push(homeData['Home']['Name']) 118 | homes["Pos"].push(homeData['Home']['Pos']) 119 | } 120 | } 121 | return homes 122 | } -------------------------------------------------------------------------------- /entities/player.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.17.10", 3 | "minecraft:entity": { 4 | "description": { 5 | "identifier": "minecraft:player", 6 | "is_spawnable": false, 7 | "is_summonable": false, 8 | "is_experimental": false 9 | }, 10 | 11 | "component_groups": { 12 | "kick": { 13 | "minecraft:instant_despawn": { 14 | "remove_child_entities": true 15 | } 16 | }, 17 | "minecraft:add_bad_omen": { 18 | "minecraft:spell_effects": { 19 | "add_effects": [ 20 | { 21 | "effect": "bad_omen", 22 | "duration": 6000, 23 | "display_on_screen_animation": true 24 | } 25 | ] 26 | }, 27 | "minecraft:timer": { 28 | "time": [ 0.0, 0.0 ], 29 | "looping": false, 30 | "time_down_event": { 31 | "event": "minecraft:clear_add_bad_omen", 32 | "target": "self" 33 | } 34 | } 35 | }, 36 | "minecraft:clear_bad_omen_spell_effect": { 37 | "minecraft:spell_effects": { 38 | } 39 | }, 40 | "minecraft:raid_trigger": { 41 | "minecraft:raid_trigger": { 42 | "triggered_event": { 43 | "event": "minecraft:remove_raid_trigger", 44 | "target": "self" 45 | } 46 | }, 47 | "minecraft:spell_effects": { 48 | "remove_effects": "bad_omen" 49 | } 50 | } 51 | }, 52 | 53 | "components": { 54 | "minecraft:experience_reward": { 55 | "on_death": "Math.Min(query.player_level * 7, 100)" 56 | }, 57 | "minecraft:type_family": { 58 | "family": [ "player" ] 59 | }, 60 | "minecraft:is_hidden_when_invisible": { 61 | }, 62 | "minecraft:loot": { 63 | "table": "loot_tables/empty.json" 64 | }, 65 | "minecraft:collision_box": { 66 | "width": 0.6, 67 | "height": 1.8 68 | }, 69 | "minecraft:can_climb": { 70 | }, 71 | "minecraft:movement": { 72 | "value": 0.1 73 | }, 74 | "minecraft:hurt_on_condition": { 75 | "damage_conditions": [ 76 | { 77 | "filters": { "test": "in_lava", "subject": "self", "operator": "==", "value": true }, 78 | "cause": "lava", 79 | "damage_per_tick": 4 80 | } 81 | ] 82 | }, 83 | "minecraft:attack": { 84 | "damage": 1 85 | }, 86 | "minecraft:player.saturation": { 87 | "value": 20 88 | }, 89 | "minecraft:player.exhaustion": { 90 | "value": 0, 91 | "max": 4 92 | }, 93 | "minecraft:player.level": { 94 | "value": 0, 95 | "max": 24791 96 | }, 97 | "minecraft:player.experience": { 98 | "value": 0, 99 | "max": 1 100 | }, 101 | "minecraft:breathable": { 102 | "total_supply": 15, 103 | "suffocate_time": -1, 104 | "inhale_time": 3.75, 105 | "generates_bubbles": false 106 | }, 107 | "minecraft:nameable": { 108 | "always_show": true, 109 | "allow_name_tag_renaming": false 110 | }, 111 | "minecraft:physics": { 112 | }, 113 | "minecraft:pushable": { 114 | "is_pushable": false, 115 | "is_pushable_by_piston": true 116 | }, 117 | "minecraft:insomnia": { 118 | "days_until_insomnia": 3 119 | }, 120 | "minecraft:rideable": { 121 | "seat_count": 2, 122 | "family_types": [ 123 | "parrot_tame" 124 | ], 125 | "pull_in_entities": true, 126 | "seats": [ 127 | { 128 | "position": [ 0.4, -0.2, -0.1 ], 129 | "min_rider_count": 0, 130 | "max_rider_count": 0, 131 | "lock_rider_rotation": 0 132 | }, 133 | { 134 | "position": [ -0.4, -0.2, -0.1 ], 135 | "min_rider_count": 1, 136 | "max_rider_count": 2, 137 | "lock_rider_rotation": 0 138 | } 139 | ] 140 | }, 141 | "minecraft:conditional_bandwidth_optimization": { 142 | }, 143 | "minecraft:block_climber": {}, 144 | "minecraft:environment_sensor": { 145 | "triggers": { 146 | "filters": { 147 | "all_of": [ 148 | { 149 | "test": "has_mob_effect", 150 | "subject": "self", 151 | "value": "bad_omen" 152 | }, 153 | { 154 | "test": "is_in_village", 155 | "subject": "self", 156 | "value": true 157 | } 158 | ] 159 | }, 160 | "event": "minecraft:trigger_raid" 161 | } 162 | } 163 | }, 164 | 165 | "events": { 166 | "kick": { 167 | "add": { 168 | "component_groups": [ 169 | "kick" 170 | ] 171 | } 172 | }, 173 | "minecraft:gain_bad_omen": { 174 | "add": { 175 | "component_groups": [ 176 | "minecraft:add_bad_omen" 177 | ] 178 | } 179 | }, 180 | "minecraft:clear_add_bad_omen": { 181 | "remove": { 182 | "component_groups": [ 183 | "minecraft:add_bad_omen" 184 | ] 185 | }, 186 | "add": { 187 | "component_groups": [ 188 | "minecraft:clear_bad_omen_spell_effect" 189 | ] 190 | } 191 | }, 192 | "minecraft:trigger_raid": { 193 | "add": { 194 | "component_groups": [ "minecraft:raid_trigger" ] 195 | } 196 | }, 197 | "minecraft:remove_raid_trigger": { 198 | "remove": { 199 | "component_groups": [ "minecraft:raid_trigger" ] 200 | } 201 | } 202 | } 203 | } 204 | } 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /scripts/main.js: -------------------------------------------------------------------------------- 1 | import { world } from "mojang-minecraft"; 2 | import * as Minecraft from 'mojang-minecraft'; 3 | import { cmd, cmds, log, logfor, rawcmd, kickPlayer2, kickPlayer } from './lib/GameLibrary.js'; 4 | import { sendMessage } from './system/chat.js' 5 | import { AdminMenu } from "./mainMenu/admin.js"; 6 | import { PlayerMenu } from "./mainMenu/player.js"; 7 | import { addXp, levelTable, expTable } from "./system/level.js"; 8 | import { pluginDB, prefix, baseXP, checkLore, checkEnchantment, enables, nameCheckRegex } from "./config.js"; 9 | import { WorldDB, ScoreboardDB } from "./lib/WorldDB.js"; 10 | import { clearItem, snakeToCamel, getItemCount } from './lib/util.js'; 11 | import { DefMaxXp } from "./lib/LevelDefine.js"; 12 | import * as detect from './system/tool.js' 13 | 14 | const antiCheatSetting = pluginDB.table("antiCheatSetting"); 15 | 16 | //當傳送訊息 17 | world.events.beforeChat.subscribe(eventData => { 18 | eventData.cancel = true; 19 | const {sender: player, message} = eventData; 20 | 21 | 22 | if(message.includes("the best minecraft bedrock utility mod")) return; 23 | 24 | // 發送訊息 25 | if (!message.startsWith(prefix)) return sendMessage(player, message); 26 | 27 | 28 | //發送指令 29 | let command = message 30 | .trim() //去除兩邊空格 31 | .slice(prefix.length) //刪除prefix 32 | .split(/ +/)[0] //取得主指令 33 | .toLowerCase(); //轉成小寫 34 | 35 | switch (command) { 36 | case "help": { 37 | logfor(player, "======§b<§e指令清單§b>§r======\n§e-menu §a-取得玩家選單\n§e-admin_menu §a-取得管理員選單"); 38 | break; 39 | } 40 | case "menu": { 41 | cmd(`give ${player.name} mcc:menu 1 0`); 42 | break; 43 | } 44 | case "admin_menu": { 45 | if (!player.hasTag("admin")) return logfor(player.name, '§c您沒有權限! 需要 "admin" Tag'); 46 | cmd(`give ${player.name} mcc:admin_menu 1 0`); 47 | break; 48 | } 49 | case "testlevel": { 50 | levelTable.addScore(player, 1); 51 | break; 52 | } 53 | case "testexp": { 54 | expTable.addScore(player, 1); 55 | break; 56 | } 57 | case "testitem": { 58 | log(String(getItemCount(player, "minecraft:apple", 0)[0]?.count || 0)) 59 | break; 60 | } 61 | // case "getjoinmotd": { 62 | // logfor(player.name, db.getData("JoinMessage")); 63 | // break; 64 | // } 65 | default: { 66 | logfor(player, ">> §c未知的指令"); 67 | break; 68 | } 69 | 70 | } 71 | }); 72 | 73 | //當玩家加入 74 | world.events.playerJoin.subscribe(eventData => { 75 | const {player} = eventData; 76 | 77 | if (player.nameTag.length > 13 || player.nameTag.length < 3) kickPlayer(player); 78 | 79 | const enable = enables.getData("JoinMsgOption"); 80 | const msg = pluginDB.table("joinSetting").getData("message"); 81 | 82 | if (enables.getData("") == 1) { 83 | logfor(player, msg); 84 | } 85 | 86 | }); 87 | 88 | //當方塊破壞 89 | world.events.blockBreak.subscribe(eventData => { 90 | const {player, block} = eventData; 91 | 92 | let exp = Math.round(Math.random() * baseXP) 93 | 94 | addXp(player, exp); 95 | 96 | }) 97 | 98 | //物品使用 99 | world.events.itemUse.subscribe(eventData => { 100 | const {source: player, item} = eventData; 101 | 102 | if (item.id == "mcc:menu") PlayerMenu(player); 103 | else if (item.id == "mcc:admin_menu") AdminMenu(player); 104 | }); 105 | 106 | //檢測擊殺生物 107 | Minecraft.world.events.entityHit.subscribe(eventData => { 108 | const {entity: player, hitEntity: target} = eventData 109 | 110 | if (!target) return; 111 | 112 | const targetPos = target.location; 113 | const playerPos = player.location; 114 | const distance = Math.sqrt((targetPos.x - playerPos.x) ** 2 + (targetPos.y - playerPos.y) ** 2 + (targetPos.z - playerPos.z) ** 2); 115 | 116 | if (distance > 5 && !player.hasTag("admin") && antiCheatSetting.getData("aura")) { 117 | logfor("@a[tag=admin]", `>> §6${player.name}§c 攻擊距離異常(distance=${distance})`); 118 | if (antiCheatSetting.getData("kick")) kickPlayer(player); 119 | }; 120 | 121 | let hp = target.getComponent("health"); 122 | 123 | if (!hp) return; 124 | if (hp.current > 0) return; 125 | 126 | let exp = Math.round(Math.random() * baseXP * 5); 127 | 128 | addXp(player, exp); 129 | 130 | const level_now = levelTable.getScore(player.name); 131 | const exp_now = expTable.getScore(player.name); 132 | 133 | logfor(player,`§e您獲得了 §b${exp} §e經驗值! \n目前等級為 §bLv.${level_now} §a(${exp_now}/${DefMaxXp(level_now)})`); 134 | 135 | }) 136 | 137 | world.events.tick.subscribe(() => { 138 | for (let player of world.getPlayers()) { 139 | 140 | if (player.location.x > 99999999 || player.location.y > 99999999 || player.location.z > 99999999 ) { 141 | logfor("@a[tag=admin]", `>> §6${player.name} §c嘗試使用Crasher崩圖`); 142 | cmd(`tp ${player.name} 0 -999 0`); 143 | if (antiCheatSetting.getData("kick")) kickPlayer(player); 144 | } 145 | 146 | let container = player.getComponent('inventory').container; 147 | for (let i = 0; i < container.size; i++) if (container.getItem(i)) { 148 | let item = container.getItem(i); 149 | if (item.amount > 64) clearItem(i) 150 | 151 | if(player.hasTag("admin")) continue; 152 | 153 | //TODO:item.nameTag 疑似取得不到,原因待釐清 154 | // if(item.nameTag.length > 32) clearItem(i) 155 | 156 | if (antiCheatSetting.getData("lore") && item.getLore().length) { 157 | logfor("@a[tag=admin]", `>> §6${player.name} §c持有非法物品(id=${item.id},lore=${item.getLore()})`) 158 | clearItem(player, i); 159 | if (antiCheatSetting.getData("kick")) kickPlayer(player); 160 | continue; 161 | } 162 | 163 | const banList = [ 164 | "minecraft:beehive", 165 | "minecraft:bee_nest", 166 | "minecraft:moving_block", 167 | ]; 168 | 169 | if (antiCheatSetting.getData("item") && banList.includes(item.id)) { 170 | logfor("@a[tag=admin]", `>> §6${player.name} §c持有非法物品(id=${item.id}})`) 171 | clearItem(player, i); 172 | if (antiCheatSetting.getData("kick")) kickPlayer(player); 173 | continue; 174 | } 175 | 176 | if (antiCheatSetting.getData("entity")) { 177 | rawcmd("kill @e[type=npc]"); 178 | rawcmd("kill @e[type=bee]"); 179 | rawcmd("kill @e[type=command_block_minecart]"); 180 | } 181 | 182 | if (antiCheatSetting.getData("enchant")) { 183 | let itemEnchants = item.getComponent("enchantments").enchantments; 184 | for (let enchantment in Minecraft.MinecraftEnchantmentTypes) { 185 | let enchantData = itemEnchants.getEnchantment(Minecraft.MinecraftEnchantmentTypes[enchantment]); 186 | 187 | if (enchantData) { 188 | if (enchantData.level > Minecraft.MinecraftEnchantmentTypes[enchantment].maxLevel || enchantData.level > 5) { 189 | logfor("@a[tag=admin]", `>> §6${player.name}§c 物品附魔等級異常(id=${item.id},enchant=${enchantData.type.id},level=${enchantData.level})`); 190 | clearItem(player, i); 191 | if (antiCheatSetting.getData("kick")) kickPlayer(player); 192 | continue; 193 | } 194 | 195 | let item2 = new Minecraft.ItemStack(Minecraft.MinecraftItemTypes[snakeToCamel(item.id)], 1, item.data); 196 | if (!item2.getComponent("enchantments").enchantments.canAddEnchantment(new Minecraft.Enchantment(Minecraft.MinecraftEnchantmentTypes[enchantment], 1))) { 197 | logfor("@a[tag=admin]", `>> §6${player.name}§c 附魔物品類型異常(id=${item.id},enchant=${enchantData.type.id},level=${enchantData.level})`); 198 | clearItem(player, i); 199 | if (antiCheatSetting.getData("kick")) kickPlayer(player); 200 | continue; 201 | } 202 | } 203 | } 204 | } 205 | } 206 | } 207 | }) 208 | /* 209 | 210 | ___====-_ _-====___ 211 | _--^^^ // \\ ^^^--_ 212 | _-^ // ( ) \\ ^-_ 213 | - // |\^^/| \\ - 214 | _/ // (@::@) \\ \_ 215 | / (( \\// )) \ 216 | - \\ (oo) // - 217 | - \\ / VV \ // - 218 | - \\/ \// - 219 | _ /| /\ ( /\ ) /\ |\ _ 220 | |/ | /\ /\ /\/ \ /\ \ | | / /\ / \/\ /\ /\ | \| 221 | ` |/ V V ` V \ \| | | |/ / V ' V V \| ' 222 | ` ` ` ` / | | | | \ ' ' ' ' 223 | ( | | | | ) 224 | __\ | | | | /__ 225 | (vvv(VVV)(VVV)vvv) 226 | 神獸保佑,程式碼沒Bug! 227 | 228 | */ -------------------------------------------------------------------------------- /scripts/lib/WorldDB.js: -------------------------------------------------------------------------------- 1 | import * as scores from "./ScoresFormat.js" 2 | import * as base64 from "./base64.js" 3 | import { cmd, GetScores, log } from "./GameLibrary.js"; 4 | import { tableDB_cache, worldDB_table_cache } from "./cache.js"; 5 | 6 | /** 7 | * 表格資料庫系統 8 | */ 9 | export class WorldDB { 10 | /** 11 | * 建立一個世界資料庫 12 | * @param {string} name 資料庫名稱 13 | */ 14 | constructor(name) { 15 | this.name = name; 16 | try { cmd(`scoreboard objectives add "${name}" dummy`); } catch { }; 17 | } 18 | 19 | /** 20 | * 在資料世界庫使用一個表格 21 | * @param {string} tableName 表格名稱 22 | * @returns 創建/取得到的表格 23 | */ 24 | table(tableName) { 25 | return new WorldDB_Table(this.name, tableName); 26 | } 27 | /** 28 | * 在資料世界庫使用一個分數處理器 29 | * @param {string} tableName 表格名稱 30 | * @returns 創建/取得到的表格 31 | */ 32 | raw() { 33 | return new ScoreboardDB(this.name, tableName); 34 | } 35 | } 36 | 37 | /** 38 | * 世界資料庫表格 39 | */ 40 | class WorldDB_Table { 41 | constructor(dbName, tableName) { 42 | this.dbName = dbName; 43 | this.tableName = tableName; 44 | 45 | worldDB_table_cache[`${dbName}:${tableName}`] = null; 46 | // this.cache = null; 47 | // setTickTimeout(()=>{log(`${tableName} obj created`);},400) 48 | } 49 | 50 | /** 51 | * 從表格中取得對應該鍵的內容 52 | * @param {string} key 鍵 53 | * @returns {number | string | Map | null} 取得到的資料,若無則回傳null 54 | */ 55 | getData(key) { 56 | 57 | key = key.toLowerCase(); 58 | 59 | //check cache 60 | if (worldDB_table_cache[`${this.dbName}:${this.tableName}`] != null) { 61 | return worldDB_table_cache[`${this.dbName}:${this.tableName}`][key]; 62 | } 63 | 64 | //load 65 | const tableData = this.#getDataFromDB(this.tableName); 66 | let mapData; 67 | if (tableData == null) { 68 | mapData = {}; 69 | } else { 70 | mapData = JSON.parse(tableData); 71 | } 72 | //save 73 | this.#setDataFromDB(this.tableName, JSON.stringify(mapData)); 74 | //main 75 | const result = mapData[key] ?? null; 76 | worldDB_table_cache[`${this.dbName}:${this.tableName}`] = mapData; 77 | return result; 78 | } 79 | 80 | /** 81 | * 在表格中設定對應鍵的資料 82 | * @param {string} key 鍵 83 | * @param {number | string | Map | null} value 值(要設定的資料) 84 | */ 85 | setData(key, value) { 86 | 87 | key = key.toLowerCase(); 88 | 89 | //load 90 | const tableData = this.#getDataFromDB(this.tableName); 91 | let mapData; 92 | if (tableData == null) { 93 | mapData = {}; 94 | } else { 95 | mapData = JSON.parse(tableData); 96 | } 97 | //main 98 | mapData[key] = value; 99 | //save 100 | this.#setDataFromDB(this.tableName, JSON.stringify(mapData)); 101 | worldDB_table_cache[`${this.dbName}:${this.tableName}`] = mapData; 102 | } 103 | 104 | /** 105 | * 取得表格內所有資料 106 | * @returns {Map} 鍵對值的Map 107 | */ 108 | getAllData() { 109 | 110 | //check cache 111 | // if (this.cache != null) { 112 | // return this.cache; 113 | // } 114 | 115 | //load 116 | const tableData = this.#getDataFromDB(this.tableName); 117 | let mapData; 118 | if (tableData == null) { 119 | mapData = {}; 120 | } else { 121 | mapData = JSON.parse(tableData); 122 | } 123 | //save 124 | this.#setDataFromDB(this.tableName, JSON.stringify(mapData)); 125 | // this.cache = mapData; 126 | 127 | //main 128 | return mapData; 129 | } 130 | 131 | /** 132 | * 將指定資料項從表格內移除 133 | * @param {string} key 鍵 134 | */ 135 | deleteData(key) { 136 | 137 | key = key.toLowerCase(); 138 | 139 | //load 140 | const tableData = this.#getDataFromDB(this.tableName); 141 | let mapData; 142 | if (tableData == null) { 143 | mapData = {}; 144 | } else { 145 | mapData = JSON.parse(tableData); 146 | } 147 | //main 148 | delete mapData[key]; 149 | //save 150 | this.#setDataFromDB(this.tableName, JSON.stringify(mapData)); 151 | } 152 | 153 | #getDataFromDB(key) { 154 | 155 | key = key.toLowerCase(); 156 | 157 | let result = ""; 158 | try { 159 | let i = 0; 160 | while (true) { 161 | const score = GetScores(`${base64.encode(`${key}[${i}]`)}`, this.dbName); 162 | result += scores.decode(score.toString()); 163 | i++; 164 | } 165 | } catch (e) { 166 | if (result == "" || result == null) { 167 | return null; 168 | } else { 169 | return result; 170 | } 171 | } 172 | } 173 | 174 | #setDataFromDB(key, value) { 175 | 176 | key = key.toLowerCase(); 177 | 178 | value = `${value}`; 179 | let i = 0; 180 | for (let j in value) { 181 | const dataName = `${base64.encode(`${key}[${i}]`)}`; 182 | cmd(`scoreboard players set "${dataName}" "${this.dbName}" ${scores.encode(value[i])}`); 183 | i++; 184 | } 185 | while (true) { 186 | try { 187 | const dataName = `${base64.encode(`${key}[${i}]`)}`; 188 | cmd(`scoreboard players reset "${dataName}" "${this.dbName}"`) 189 | i++; 190 | } catch { break } 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * 表格資料庫 197 | */ 198 | export class TableDB { 199 | constructor(dbName) { 200 | this.dbName = dbName; 201 | try { cmd(`scoreboard objectives add "${dbName}" dummy`); } catch { }; 202 | } 203 | 204 | /** 205 | * 從表格中取得對應該鍵的內容 206 | * @param {string} key 鍵 207 | * @returns {number | string | Map | null} 取得到的資料,若無則回傳null 208 | */ 209 | getData(key) { 210 | 211 | key = key.toLowerCase(); 212 | 213 | //check cache 214 | if (tableDB_cache[`${this.dbName}:${key}`] != null) { 215 | return tableDB_cache[`${this.dbName}:${key}`]; 216 | } 217 | 218 | //load 219 | const data = this.#getDataFromDB(key); 220 | 221 | let result; 222 | if (data == null) { 223 | result = null; 224 | } else { 225 | result = JSON.parse(data); 226 | } 227 | 228 | tableDB_cache[`${this.dbName}:${key}`] = result; 229 | return result; 230 | } 231 | 232 | /** 233 | * 在表格中設定對應鍵的資料 234 | * @param {string} key 鍵 235 | * @param {number | string | Map | null} value 值(要設定的資料) 236 | */ 237 | setData(key, value) { 238 | 239 | key = key.toLowerCase(); 240 | 241 | //save 242 | this.#setDataFromDB(key, JSON.stringify(value)); 243 | tableDB_cache[`${this.dbName}:${key}`] = value; 244 | } 245 | 246 | #getDataFromDB(key) { 247 | let result = ""; 248 | try { 249 | let i = 0; 250 | while (true) { 251 | const score = GetScores(`${base64.encode(`${key}[${i}]`)}`, this.dbName); 252 | result += scores.decode(score.toString()); 253 | i++; 254 | } 255 | } catch (e) { 256 | if (result == "" || result == null) { 257 | return null; 258 | } else { 259 | return result; 260 | } 261 | } 262 | } 263 | 264 | #setDataFromDB(key, value) { 265 | value = `${value}`; 266 | let i = 0; 267 | for (let j in value) { 268 | const dataName = `${base64.encode(`${key}[${i}]`)}`; 269 | cmd(`scoreboard players set "${dataName}" "${this.dbName}" ${scores.encode(value[i])}`); 270 | i++; 271 | } 272 | while (true) { 273 | try { 274 | const dataName = `${base64.encode(`${key}[${i}]`)}`; 275 | cmd(`scoreboard players reset "${dataName}" "${this.dbName}"`) 276 | i++; 277 | } catch { break } 278 | } 279 | } 280 | } 281 | 282 | /** 283 | * 純記分板資料庫系統 284 | */ 285 | export class ScoreboardDB { 286 | constructor(DB_Name, isPlayerScoreboard = false) { 287 | this.name = DB_Name; 288 | this.isPlayerScoreboard = isPlayerScoreboard; 289 | try { cmd(`scoreboard objectives add "${DB_Name}" dummy`); } catch { }; 290 | } 291 | 292 | setScore(target, value) { 293 | if (this.isPlayerScoreboard) { 294 | if (target.name.includes(" ")) { 295 | cmd(`scoreboard players set "${target.name}" "${this.name}" ${value}`); 296 | } else { 297 | cmd(`scoreboard players set ${target.name} "${this.name}" ${value}`); 298 | } 299 | } else { 300 | cmd(`scoreboard players set "${target}" "${this.name}" ${value}`); 301 | } 302 | } 303 | 304 | addScore(target, value) { 305 | if (this.isPlayerScoreboard) { 306 | if (target.name.includes(" ")) { 307 | cmd(`scoreboard players add "${target.name}" "${this.name}" ${value}`); 308 | } else { 309 | cmd(`scoreboard players add ${target.name} "${this.name}" ${value}`); 310 | } 311 | } else { 312 | cmd(`scoreboard players add "${target}" "${this.name}" ${value}`); 313 | } 314 | } 315 | 316 | removeScore(target, value) { 317 | if (this.isPlayerScoreboard) { 318 | if (target.name.includes(" ")) { 319 | cmd(`scoreboard players remove "${target.name}" "${this.name}" ${value}`); 320 | } else { 321 | cmd(`scoreboard players remove ${target.name} "${this.name}" ${value}`); 322 | } 323 | } else { 324 | cmd(`scoreboard players remove "${target}" "${this.name}" ${value}`); 325 | } 326 | } 327 | 328 | getScore(target) { 329 | try { 330 | if (this.isPlayerScoreboard) { 331 | return GetScores(target.name, this.name); 332 | } else { 333 | return GetScores(target, this.name); 334 | } 335 | } catch (e) { return null } 336 | } 337 | } -------------------------------------------------------------------------------- /scripts/mainMenu/admin.js: -------------------------------------------------------------------------------- 1 | import { world } from "mojang-minecraft"; 2 | import * as ui from 'mojang-minecraft-ui'; 3 | import { enables, pluginDB } from "../config.js"; 4 | import { cmd, GetScores, log, logfor, rawcmd, SetScores } from '../lib/GameLibrary.js'; 5 | import { getData, setData } from '../lib/JsonTagDB'; 6 | 7 | import { buttons } from "./buttons.js"; 8 | 9 | export function AdminMenu(player) { 10 | let fm = new ui.ActionFormData(); 11 | fm.title("管理員選單"); 12 | fm.body("管理員專用,內有許多方便的功能"); 13 | fm.button('§l§1插件設定', 'textures/ui/automation_glyph_color.png'); 14 | fm.button('§l§1給予稱號', 'textures/ui/mute_off.png'); 15 | fm.button('§l§1移除稱號', 'textures/ui/mute_on.png'); 16 | fm.button('§l§1踢出玩家', 'textures/ui/anvil_icon.png'); 17 | fm.button('§l§1管理傳送點', 'textures/ui/worldsIcon.png'); 18 | fm.button('§l§1防掛設定', 'textures/ui/absorption_effect.png'); 19 | 20 | fm.show(player).then(response => { 21 | if (!response || response.isCanceled) return; 22 | 23 | const worldPlayers = world.getPlayers(); 24 | let players = []; 25 | let playerNames = []; 26 | 27 | for (let i of worldPlayers) { 28 | playerNames.push(i.name); 29 | players.push(i); 30 | } 31 | 32 | switch (response.selection) { 33 | case (0): { 34 | 35 | let fm = new ui.ActionFormData(); 36 | fm.title("管理員選單"); 37 | fm.body("管理員專用,內有許多方便的功能"); 38 | fm.button('§l§1功能設置'); 39 | fm.button('§l§1開關功能'); 40 | 41 | fm.show(player).then(response => { 42 | switch (response.selection) { 43 | case (0): { 44 | const setting = pluginDB.table("spawnTpSetting"); 45 | const pos = setting.getData("pos") ?? "0 -60 0"; 46 | const message = pluginDB.table("joinSetting").getData("message") ?? "歡迎加入!"; 47 | const board = pluginDB.table("moneySetting").getData("scoreboard") ?? "money"; 48 | 49 | let fm = new ui.ModalFormData(); 50 | fm.title("功能設置"); 51 | fm.textField("設定大廳座標(以空格隔開xyz)", "x y z", pos); 52 | fm.textField('輸入歡迎訊息(將作為玩家加入時的用語)', '', message); 53 | fm.textField('設置經濟系統的計分板', '', board); 54 | 55 | fm.show(player).then(response => { 56 | if (!response || response.isCanceled) return; 57 | 58 | const pos = response.formValues[0].trim(); 59 | // 座標設定 60 | // 過一次regex看看是不是有效坐標 61 | if(pos.match(/(-)?\d{1,8} (-)?\d{1,8} (-)?\d{1,8}/)) setting.setData("pos", pos); 62 | else logfor(player, ">> §c無效坐標!請重新設置出生點坐標"); 63 | 64 | // 加入訊息設定 65 | pluginDB.table("joinSetting").setData("message", response.formValues[1]); 66 | 67 | // 經濟 68 | pluginDB.table("moneySetting").setData("scoreboard", response.formValues[2]); 69 | 70 | logfor(player, ">> §a設定成功!"); 71 | }); 72 | break; 73 | }; 74 | case (1): { 75 | let isData = function (key) { return enables.getData(key) == 1 ? false : true; }; 76 | let boolCvt = function (key) { return key ? 0 : 1; }; //奇怪的bug,暫時修改 77 | let fm = new ui.ModalFormData(); 78 | fm.title("開關功能"); 79 | let enableList = []; 80 | buttons.forEach((data, index) => { 81 | if (isData(data.id)) { 82 | enableList[index] = true; 83 | } else { 84 | enableList[index] = false; 85 | } 86 | fm.toggle(`啟用${data.display}`, enableList[index]); 87 | }); 88 | 89 | fm.show(player).then(response => { 90 | response.formValues.forEach((data, index) => { 91 | enables.setData(buttons[index].id, boolCvt(data)) 92 | }) 93 | logfor(player, ">> §a設定成功!"); 94 | }); 95 | break; 96 | }; 97 | } 98 | }) 99 | return; 100 | 101 | } 102 | case (1): { 103 | let fm = new ui.ModalFormData(); 104 | fm.title("給予稱號"); 105 | fm.dropdown("選擇目標玩家", playerNames); 106 | fm.textField("輸入稱號", ""); 107 | 108 | fm.show(player).then(response => { 109 | if (!response || response.isCanceled) return; 110 | if (!response.formValues[1]) return logfor(player, ">> §c稱號欄位不能為空"); 111 | 112 | let target = players[response.formValues[0]]; 113 | let hasTitles = getData(target, "hasTitles"); 114 | hasTitles.push(response.formValues[1]); 115 | 116 | setData(target, "hasTitles", hasTitles); 117 | logfor(player.name, ">> §a給予成功"); 118 | }) 119 | break; 120 | } case (2): { 121 | const worldPlayers = world.getPlayers(); 122 | let players = []; 123 | let playerNames = []; 124 | 125 | for (let i of worldPlayers) { 126 | playerNames.push(i.name); 127 | players.push(i); 128 | } 129 | 130 | let fm = new ui.ModalFormData(); 131 | fm.title("移除稱號"); 132 | fm.dropdown("選擇目標玩家", playerNames); 133 | 134 | fm.show(player).then(response => { 135 | if (!response || response.isCanceled) return; 136 | 137 | let target = players[response.formValues[0]]; 138 | let hasTitles = getData(target, "hasTitles"); 139 | 140 | let fm = new ui.ModalFormData(); 141 | fm.title("移除稱號"); 142 | fm.dropdown("選擇要移除的稱號", hasTitles); 143 | 144 | fm.show(player).then(response => { 145 | if (!response || response.isCanceled) return; 146 | 147 | let selectedTitle = getData(player, "selectedTitle"); 148 | 149 | if (hasTitles[response.formValues[0]] == hasTitles[0]) { 150 | return logfor(player.name, ">> §c你不能移除預設稱號"); 151 | } 152 | 153 | if (hasTitles[response.formValues[0]] == selectedTitle) { 154 | setData(player, "selectedTitle", getData(player, "hasTitles")[0]); 155 | } 156 | 157 | hasTitles.pop(response.formValues[0]); 158 | 159 | setData(target, "hasTitles", hasTitles); 160 | 161 | logfor(player.name, ">> §a移除成功"); 162 | }) 163 | }); 164 | break; 165 | } case (3): { 166 | let fm = new ui.ModalFormData(); 167 | fm.title("踢人系統"); 168 | fm.dropdown("選擇要踢出的玩家", playerNames); 169 | fm.textField("輸入理由(可留空)", ""); 170 | 171 | fm.show(player).then(response => { 172 | if (!response || response.isCanceled) return; 173 | const kick_player = playerNames[response.formValues[0]]; 174 | const because = response.formValues[1]; 175 | 176 | if (rawcmd(`kick "${kick_player}" ${because}`).error) { 177 | logfor(player, ">> §c踢出失敗"); 178 | } else { 179 | logfor(player, ">> §a踢出成功"); 180 | } 181 | }) 182 | break; 183 | 184 | } case (4): { 185 | 186 | let fm = new ui.ActionFormData(); 187 | fm.title("管理傳送點"); 188 | fm.body("快速刪除、添加傳送點!") 189 | fm.button("§l§1添加傳送點"); 190 | fm.button("§l§1移除傳送點"); 191 | 192 | fm.show(player).then(response => { 193 | if (!response || response.isCanceled) return; 194 | 195 | const warpsTable = pluginDB.table("warps"); 196 | let warps = warpsTable.getAllData(); 197 | 198 | let warpNames = []; 199 | 200 | for (let i in warps) { warpNames.push(i); } 201 | 202 | if (response.selection == 0) { 203 | 204 | let fm = new ui.ModalFormData(); 205 | fm.title("管理傳送點"); 206 | fm.textField("傳送點名稱", ""); 207 | fm.textField("傳送點座標", "x y z"); 208 | 209 | fm.show(player).then(response => { 210 | if (!response || response.isCanceled) return; 211 | 212 | const warpName = response.formValues[0]; 213 | const warpPos = response.formValues[1]; 214 | 215 | warpsTable.setData(warpName, warpPos); 216 | 217 | logfor(player, ">> §a添加成功"); 218 | }) 219 | } else if (response.selection == 1) { 220 | 221 | if (warpNames.length == 0) { return logfor(player, ">> §c本世界沒有設定任何傳送點"); } 222 | 223 | let fm = new ui.ModalFormData(); 224 | fm.title("移除傳送點"); 225 | fm.dropdown("選擇要移除的傳送點", warpNames); 226 | 227 | fm.show(player).then(response => { 228 | if (!response || response.isCanceled) return; 229 | 230 | const warpName = warpNames[response.formValues[0]]; 231 | 232 | warpsTable.deleteData(warpName); 233 | 234 | logfor(player, ">> §a移除成功"); 235 | }) 236 | } 237 | 238 | const kick_player = playerNames[response.formValues[0]]; 239 | const because = response.formValues[1]; 240 | 241 | cmd(`kick "${kick_player}" ${because}`); 242 | logfor(player, ">> §a踢出成功"); 243 | }) 244 | break; 245 | } case(5): { 246 | 247 | const antiCheatSetting = pluginDB.table("antiCheatSetting"); 248 | 249 | new ui.ModalFormData() 250 | .title("防掛設定選單",) 251 | .toggle("當玩家被防掛系統檢測到則直接踢出",antiCheatSetting.getData("kick")) 252 | .toggle("檢測並清除異常附魔物品",antiCheatSetting.getData("enchant")) 253 | .toggle("檢測並清除含有lore的物品",antiCheatSetting.getData("lore")) 254 | .toggle("檢測並清除物品蜂箱、蜂巢、移動的方塊",antiCheatSetting.getData("item")) 255 | .toggle("清除生物蜜蜂、指令方塊礦車、移動的方塊",antiCheatSetting.getData("entity")) 256 | .toggle("檢測攻擊距離異常玩家",antiCheatSetting.getData("aura")) 257 | .show(player) 258 | .then(response => { 259 | if (response.isCancel) return; 260 | antiCheatSetting.setData("kick", response.formValues[0]); 261 | antiCheatSetting.setData("enchant", response.formValues[1]); 262 | antiCheatSetting.setData("lore", response.formValues[2]); 263 | antiCheatSetting.setData("item", response.formValues[3]); 264 | antiCheatSetting.setData("entity", response.formValues[4]); 265 | antiCheatSetting.setData("aura", response.formValues[5]); 266 | 267 | 268 | logfor(player, ">> §a設定成功"); 269 | }); 270 | } default: { 271 | 272 | } 273 | } 274 | }) 275 | 276 | } 277 | 278 | 279 | -------------------------------------------------------------------------------- /scripts/lib/base64.js: -------------------------------------------------------------------------------- 1 | /* 2 | * [hi-base64]{@link https://github.com/emn178/hi-base64} 3 | * 4 | * @version 0.2.1 5 | * @author Chen, Yi-Cyuan [emn178@gmail.com] 6 | * @copyright Chen, Yi-Cyuan 2014-2017 7 | * @license MIT 8 | */ 9 | /*jslint bitwise: true */ 10 | 11 | 'use strict'; 12 | 13 | var root = typeof window === 'object' ? window : {}; 14 | var NODE_JS = !root.HI_BASE64_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; 15 | if (NODE_JS) { 16 | root = global; 17 | } 18 | var COMMON_JS = !root.HI_BASE64_NO_COMMON_JS && typeof module === 'object' && module.exports; 19 | var AMD = typeof define === 'function' && define.amd; 20 | var BASE64_ENCODE_CHAR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(''); 21 | var BASE64_DECODE_CHAR = { 22 | 'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'I': 8, 23 | 'J': 9, 'K': 10, 'L': 11, 'M': 12, 'N': 13, 'O': 14, 'P': 15, 'Q': 16, 24 | 'R': 17, 'S': 18, 'T': 19, 'U': 20, 'V': 21, 'W': 22, 'X': 23, 'Y': 24, 25 | 'Z': 25, 'a': 26, 'b': 27, 'c': 28, 'd': 29, 'e': 30, 'f': 31, 'g': 32, 26 | 'h': 33, 'i': 34, 'j': 35, 'k': 36, 'l': 37, 'm': 38, 'n': 39, 'o': 40, 27 | 'p': 41, 'q': 42, 'r': 43, 's': 44, 't': 45, 'u': 46, 'v': 47, 'w': 48, 28 | 'x': 49, 'y': 50, 'z': 51, '0': 52, '1': 53, '2': 54, '3': 55, '4': 56, 29 | '5': 57, '6': 58, '7': 59, '8': 60, '9': 61, '+': 62, '/': 63, '-': 62, 30 | '_': 63 31 | }; 32 | 33 | var utf8ToBytes = function (str) { 34 | var bytes = []; 35 | for (var i = 0; i < str.length; i++) { 36 | var c = str.charCodeAt(i); 37 | if (c < 0x80) { 38 | bytes[bytes.length] = c; 39 | } else if (c < 0x800) { 40 | bytes[bytes.length] = 0xc0 | (c >> 6); 41 | bytes[bytes.length] = 0x80 | (c & 0x3f); 42 | } else if (c < 0xd800 || c >= 0xe000) { 43 | bytes[bytes.length] = 0xe0 | (c >> 12); 44 | bytes[bytes.length] = 0x80 | ((c >> 6) & 0x3f); 45 | bytes[bytes.length] = 0x80 | (c & 0x3f); 46 | } else { 47 | c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(++i) & 0x3ff)); 48 | bytes[bytes.length] = 0xf0 | (c >> 18); 49 | bytes[bytes.length] = 0x80 | ((c >> 12) & 0x3f); 50 | bytes[bytes.length] = 0x80 | ((c >> 6) & 0x3f); 51 | bytes[bytes.length] = 0x80 | (c & 0x3f); 52 | } 53 | } 54 | return bytes; 55 | }; 56 | 57 | var decodeAsBytes = function (base64Str) { 58 | var v1, v2, v3, v4, bytes = [], index = 0, length = base64Str.length; 59 | if (base64Str.charAt(length - 2) === '=') { 60 | length -= 2; 61 | } else if (base64Str.charAt(length - 1) === '=') { 62 | length -= 1; 63 | } 64 | 65 | // 4 char to 3 bytes 66 | for (var i = 0, count = length >> 2 << 2; i < count;) { 67 | v1 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 68 | v2 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 69 | v3 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 70 | v4 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 71 | bytes[index++] = (v1 << 2 | v2 >>> 4) & 255; 72 | bytes[index++] = (v2 << 4 | v3 >>> 2) & 255; 73 | bytes[index++] = (v3 << 6 | v4) & 255; 74 | } 75 | 76 | // remain bytes 77 | var remain = length - count; 78 | if (remain === 2) { 79 | v1 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 80 | v2 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 81 | bytes[index++] = (v1 << 2 | v2 >>> 4) & 255; 82 | } else if (remain === 3) { 83 | v1 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 84 | v2 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 85 | v3 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 86 | bytes[index++] = (v1 << 2 | v2 >>> 4) & 255; 87 | bytes[index++] = (v2 << 4 | v3 >>> 2) & 255; 88 | } 89 | return bytes; 90 | }; 91 | 92 | var encodeFromBytes = function (bytes) { 93 | var v1, v2, v3, base64Str = '', length = bytes.length; 94 | for (var i = 0, count = parseInt(length / 3) * 3; i < count;) { 95 | v1 = bytes[i++]; 96 | v2 = bytes[i++]; 97 | v3 = bytes[i++]; 98 | base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + 99 | BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] + 100 | BASE64_ENCODE_CHAR[(v2 << 2 | v3 >>> 6) & 63] + 101 | BASE64_ENCODE_CHAR[v3 & 63]; 102 | } 103 | 104 | // remain char 105 | var remain = length - count; 106 | if (remain === 1) { 107 | v1 = bytes[i]; 108 | base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + 109 | BASE64_ENCODE_CHAR[(v1 << 4) & 63] + 110 | '=='; 111 | } else if (remain === 2) { 112 | v1 = bytes[i++]; 113 | v2 = bytes[i]; 114 | base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + 115 | BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] + 116 | BASE64_ENCODE_CHAR[(v2 << 2) & 63] + 117 | '='; 118 | } 119 | return base64Str; 120 | }; 121 | 122 | var btoa = root.btoa, atob = root.atob, utf8Base64Encode, utf8Base64Decode; 123 | if (NODE_JS) { 124 | var Buffer = require('buffer').Buffer; 125 | btoa = function (str) { 126 | return new Buffer(str, 'ascii').toString('base64'); 127 | }; 128 | 129 | utf8Base64Encode = function (str) { 130 | return new Buffer(str).toString('base64'); 131 | }; 132 | 133 | encodeFromBytes = utf8Base64Encode; 134 | 135 | atob = function (base64Str) { 136 | return new Buffer(base64Str, 'base64').toString('ascii'); 137 | }; 138 | 139 | utf8Base64Decode = function (base64Str) { 140 | return new Buffer(base64Str, 'base64').toString(); 141 | }; 142 | } else if (!btoa) { 143 | btoa = function (str) { 144 | var v1, v2, v3, base64Str = '', length = str.length; 145 | for (var i = 0, count = parseInt(length / 3) * 3; i < count;) { 146 | v1 = str.charCodeAt(i++); 147 | v2 = str.charCodeAt(i++); 148 | v3 = str.charCodeAt(i++); 149 | base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + 150 | BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] + 151 | BASE64_ENCODE_CHAR[(v2 << 2 | v3 >>> 6) & 63] + 152 | BASE64_ENCODE_CHAR[v3 & 63]; 153 | } 154 | 155 | // remain char 156 | var remain = length - count; 157 | if (remain === 1) { 158 | v1 = str.charCodeAt(i); 159 | base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + 160 | BASE64_ENCODE_CHAR[(v1 << 4) & 63] + 161 | '=='; 162 | } else if (remain === 2) { 163 | v1 = str.charCodeAt(i++); 164 | v2 = str.charCodeAt(i); 165 | base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + 166 | BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] + 167 | BASE64_ENCODE_CHAR[(v2 << 2) & 63] + 168 | '='; 169 | } 170 | return base64Str; 171 | }; 172 | 173 | utf8Base64Encode = function (str) { 174 | var v1, v2, v3, base64Str = '', bytes = utf8ToBytes(str), length = bytes.length; 175 | for (var i = 0, count = parseInt(length / 3) * 3; i < count;) { 176 | v1 = bytes[i++]; 177 | v2 = bytes[i++]; 178 | v3 = bytes[i++]; 179 | base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + 180 | BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] + 181 | BASE64_ENCODE_CHAR[(v2 << 2 | v3 >>> 6) & 63] + 182 | BASE64_ENCODE_CHAR[v3 & 63]; 183 | } 184 | 185 | // remain char 186 | var remain = length - count; 187 | if (remain === 1) { 188 | v1 = bytes[i]; 189 | base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + 190 | BASE64_ENCODE_CHAR[(v1 << 4) & 63] + 191 | '=='; 192 | } else if (remain === 2) { 193 | v1 = bytes[i++]; 194 | v2 = bytes[i]; 195 | base64Str += BASE64_ENCODE_CHAR[v1 >>> 2] + 196 | BASE64_ENCODE_CHAR[(v1 << 4 | v2 >>> 4) & 63] + 197 | BASE64_ENCODE_CHAR[(v2 << 2) & 63] + 198 | '='; 199 | } 200 | return base64Str; 201 | }; 202 | 203 | atob = function (base64Str) { 204 | var v1, v2, v3, v4, str = '', length = base64Str.length; 205 | if (base64Str.charAt(length - 2) === '=') { 206 | length -= 2; 207 | } else if (base64Str.charAt(length - 1) === '=') { 208 | length -= 1; 209 | } 210 | 211 | // 4 char to 3 bytes 212 | for (var i = 0, count = length >> 2 << 2; i < count;) { 213 | v1 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 214 | v2 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 215 | v3 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 216 | v4 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 217 | str += String.fromCharCode((v1 << 2 | v2 >>> 4) & 255) + 218 | String.fromCharCode((v2 << 4 | v3 >>> 2) & 255) + 219 | String.fromCharCode((v3 << 6 | v4) & 255); 220 | } 221 | 222 | // remain bytes 223 | var remain = length - count; 224 | if (remain === 2) { 225 | v1 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 226 | v2 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 227 | str += String.fromCharCode((v1 << 2 | v2 >>> 4) & 255); 228 | } else if (remain === 3) { 229 | v1 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 230 | v2 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 231 | v3 = BASE64_DECODE_CHAR[base64Str.charAt(i++)]; 232 | str += String.fromCharCode((v1 << 2 | v2 >>> 4) & 255) + 233 | String.fromCharCode((v2 << 4 | v3 >>> 2) & 255); 234 | } 235 | return str; 236 | }; 237 | 238 | utf8Base64Decode = function (base64Str) { 239 | var str = '', bytes = decodeAsBytes(base64Str), length = bytes.length; 240 | var i = 0, followingChars = 0, b, c; 241 | while (i < length) { 242 | b = bytes[i++]; 243 | if (b <= 0x7F) { 244 | str += String.fromCharCode(b); 245 | continue; 246 | } else if (b > 0xBF && b <= 0xDF) { 247 | c = b & 0x1F; 248 | followingChars = 1; 249 | } else if (b <= 0xEF) { 250 | c = b & 0x0F; 251 | followingChars = 2; 252 | } else if (b <= 0xF7) { 253 | c = b & 0x07; 254 | followingChars = 3; 255 | } else { 256 | throw 'not a UTF-8 string'; 257 | } 258 | 259 | for (var j = 0; j < followingChars; ++j) { 260 | b = bytes[i++]; 261 | if (b < 0x80 || b > 0xBF) { 262 | throw 'not a UTF-8 string'; 263 | } 264 | c <<= 6; 265 | c += b & 0x3F; 266 | } 267 | if (c >= 0xD800 && c <= 0xDFFF) { 268 | throw 'not a UTF-8 string'; 269 | } 270 | if (c > 0x10FFFF) { 271 | throw 'not a UTF-8 string'; 272 | } 273 | 274 | if (c <= 0xFFFF) { 275 | str += String.fromCharCode(c); 276 | } else { 277 | c -= 0x10000; 278 | str += String.fromCharCode((c >> 10) + 0xD800); 279 | str += String.fromCharCode((c & 0x3FF) + 0xDC00); 280 | } 281 | } 282 | return str; 283 | }; 284 | } else { 285 | utf8Base64Encode = function (str) { 286 | var result = ''; 287 | for (var i = 0; i < str.length; i++) { 288 | var charcode = str.charCodeAt(i); 289 | if (charcode < 0x80) { 290 | result += String.fromCharCode(charcode); 291 | } else if (charcode < 0x800) { 292 | result += String.fromCharCode(0xc0 | (charcode >> 6)) + 293 | String.fromCharCode(0x80 | (charcode & 0x3f)); 294 | } else if (charcode < 0xd800 || charcode >= 0xe000) { 295 | result += String.fromCharCode(0xe0 | (charcode >> 12)) + 296 | String.fromCharCode(0x80 | ((charcode >> 6) & 0x3f)) + 297 | String.fromCharCode(0x80 | (charcode & 0x3f)); 298 | } else { 299 | charcode = 0x10000 + (((charcode & 0x3ff) << 10) | (str.charCodeAt(++i) & 0x3ff)); 300 | result += String.fromCharCode(0xf0 | (charcode >> 18)) + 301 | String.fromCharCode(0x80 | ((charcode >> 12) & 0x3f)) + 302 | String.fromCharCode(0x80 | ((charcode >> 6) & 0x3f)) + 303 | String.fromCharCode(0x80 | (charcode & 0x3f)); 304 | } 305 | } 306 | return btoa(result); 307 | }; 308 | 309 | utf8Base64Decode = function (base64Str) { 310 | var tmpStr = atob(base64Str.trim('=').replace(/-/g, '+').replace(/_/g, '/')); 311 | if (!/[^\x00-\x7F]/.test(tmpStr)) { 312 | return tmpStr; 313 | } 314 | var str = '', i = 0, length = tmpStr.length, followingChars = 0, b, c; 315 | while (i < length) { 316 | b = tmpStr.charCodeAt(i++); 317 | if (b <= 0x7F) { 318 | str += String.fromCharCode(b); 319 | continue; 320 | } else if (b > 0xBF && b <= 0xDF) { 321 | c = b & 0x1F; 322 | followingChars = 1; 323 | } else if (b <= 0xEF) { 324 | c = b & 0x0F; 325 | followingChars = 2; 326 | } else if (b <= 0xF7) { 327 | c = b & 0x07; 328 | followingChars = 3; 329 | } else { 330 | throw 'not a UTF-8 string'; 331 | } 332 | 333 | for (var j = 0; j < followingChars; ++j) { 334 | b = tmpStr.charCodeAt(i++); 335 | if (b < 0x80 || b > 0xBF) { 336 | throw 'not a UTF-8 string'; 337 | } 338 | c <<= 6; 339 | c += b & 0x3F; 340 | } 341 | if (c >= 0xD800 && c <= 0xDFFF) { 342 | throw 'not a UTF-8 string'; 343 | } 344 | if (c > 0x10FFFF) { 345 | throw 'not a UTF-8 string'; 346 | } 347 | 348 | if (c <= 0xFFFF) { 349 | str += String.fromCharCode(c); 350 | } else { 351 | c -= 0x10000; 352 | str += String.fromCharCode((c >> 10) + 0xD800); 353 | str += String.fromCharCode((c & 0x3FF) + 0xDC00); 354 | } 355 | } 356 | return str; 357 | }; 358 | } 359 | 360 | export function encode (str, asciiOnly) { 361 | var notString = typeof (str) != 'string'; 362 | if (notString && str.constructor === root.ArrayBuffer) { 363 | str = new Uint8Array(str); 364 | } 365 | if (notString) { 366 | return encodeFromBytes(str); 367 | } else { 368 | return !asciiOnly && /[^\x00-\x7F]/.test(str) ? utf8Base64Encode(str) : btoa(str); 369 | } 370 | }; 371 | 372 | export function decode (base64Str, asciiOnly) { 373 | return asciiOnly ? atob(base64Str) : utf8Base64Decode(base64Str); 374 | }; 375 | 376 | var exports = { 377 | encode: encode, 378 | decode: decode, 379 | atob: atob, 380 | btoa: btoa 381 | }; 382 | decode.bytes = decodeAsBytes; 383 | decode.string = decode; 384 | 385 | if (COMMON_JS) { 386 | module.exports = exports; 387 | } else { 388 | root.base64 = exports; 389 | if (AMD) { 390 | define(function () { 391 | return exports; 392 | }); 393 | } 394 | } 395 | --------------------------------------------------------------------------------