From 4df6cad70ac014a146dde3a946daf3142eefc2a3 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Wed, 13 May 2026 18:26:34 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(core):=20=E7=A7=BB=E9=99=A4=20remoteCh?= =?UTF-8?q?annel=20=E7=9A=84=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E8=A6=81=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: 已移除 `hasBox3JSClient()` 和 `hasBox3JSClientMod()` 方法,不再需要手动检测客户端。远程通道事件现在使用可选负载,无 Box3JS 的普通客户端会自动忽略且不会报错。 实现方案改用原版箱子菜单类型替代自定义容器,无需注册客户端界面及 MenuType 定义。 --- Box3JS-NeoForge-1.21.1/docs/api/server.md | 6 +- Box3JS-NeoForge-1.21.1/docs/api/server_en.md | 6 +- Box3JS-NeoForge-1.21.1/docs/guide/faq.md | 10 +-- Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md | 10 +-- .../docs/tutorial/06-client-scripting.md | 11 +--- .../docs/tutorial/06-client-scripting_en.md | 11 +--- .../main/java/com/box3lab/box3js/Box3JS.java | 25 -------- .../screen/Box3JSScriptContainerScreen.java | 63 ------------------- .../box3js/script/Box3JSGuiServerHandler.java | 15 ++++- .../script/Box3JSScriptContainerMenu.java | 24 ------- .../assets/box3js/template/eslint.config.mjs | 4 +- .../assets/box3js/template/src/server/app.ts | 10 ++- .../box3js/template/types/server/entity.d.ts | 2 +- .../box3js/template/types/server/player.d.ts | 9 --- 14 files changed, 25 insertions(+), 181 deletions(-) delete mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server.md b/Box3JS-NeoForge-1.21.1/docs/api/server.md index 658dc09..bc05668 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/server.md @@ -157,14 +157,10 @@ globalConfig.set("season", "spring"); ## 跨端通信 -服务端使用 `remoteChannel` 与客户端脚本通信。发送前建议检查玩家是否安装了 Box3JS 客户端: +服务端使用 `remoteChannel` 与客户端脚本通信。数据包为可选的(optional),未安装 Box3JS 的客户端会自动忽略,无需手动检测: ```ts world.onPlayerJoin((entity) => { - if (!entity.hasBox3JSClient()) { - return; - } - remoteChannel.sendClientEvent(entity, { type: "welcome", text: "欢迎来到服务器", diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md b/Box3JS-NeoForge-1.21.1/docs/api/server_en.md index 8345f37..7925e03 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/server_en.md @@ -157,14 +157,10 @@ globalConfig.set("season", "spring"); ## Cross-Side Events -Use `remoteChannel` to communicate with client scripts. Before sending to a player, check whether their client supports Box3JS: +Use `remoteChannel` to communicate with client scripts. Packets are optional — vanilla clients silently ignore them, no manual check needed: ```ts world.onPlayerJoin((entity) => { - if (!entity.hasBox3JSClient()) { - return; - } - remoteChannel.sendClientEvent(entity, { type: "welcome", text: "Welcome to the server", diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/faq.md b/Box3JS-NeoForge-1.21.1/docs/guide/faq.md index ce07ea4..f0de694 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/faq.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/faq.md @@ -143,15 +143,7 @@ db.sql("SELECT * FROM t WHERE name = '" + userInput + "'"); ### Q: 如何检测玩家是否安装了 Box3JS 客户端? -```js -// 服务端检测 -if (entity.player.hasBox3JSClientMod()) { - // 可以发送客户端事件 -} else { - // 降级到聊天消息 - entity.player.directMessage("安装 Box3JS 客户端获得完整体验"); -} -``` +无需手动检测。`remoteChannel.sendClientEvent()` 使用可选数据包(optional payload),未安装 Box3JS 客户端的玩家会自动忽略这些数据包,不会产生任何错误或断线。可以直接发送,安全无副作用。 ### Q: 客户端和服务端可以同时使用 `remoteChannel` 吗? diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md index 15a88bb..40bf166 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md @@ -143,15 +143,7 @@ db.sql("SELECT * FROM t WHERE name = '" + userInput + "'"); ### Q: How to detect if a player has Box3JS client mod installed? -```js -// Server-side check -if (entity.player.hasBox3JSClientMod()) { - // Can send client events -} else { - // Fallback to chat messages - entity.player.directMessage("Install Box3JS client for full experience"); -} -``` +No manual detection is needed. `remoteChannel.sendClientEvent()` uses optional payloads — players without the Box3JS client mod will silently ignore these packets without errors or disconnects. You can safely send events to all players. ### Q: Can client and server use `remoteChannel` at the same time? diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md index 3c0ad66..b68cb6e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md @@ -361,16 +361,7 @@ remoteChannel.onServerEvent((event) => { ### 检测客户端兼容性 -```js -// 服务端检测玩家是否安装了 Box3JS 客户端 -if (entity.player.hasBox3JSClientMod()) { - // 可以发送客户端事件 - remoteChannel.sendClientEvent(entity, { type: "custom_ui", ... }); -} else { - // 降级到聊天消息 - entity.player.directMessage("请安装 Box3JS 客户端以获得完整体验"); -} -``` +无需手动检测。`remoteChannel.sendClientEvent()` 使用可选数据包,未安装 Box3JS 客户端的玩家会自动忽略,不会报错或断线。可以放心向所有玩家发送。 ### 通讯数据格式 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md index 1cdc153..cd1cc99 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md @@ -361,16 +361,7 @@ remoteChannel.onServerEvent((event) => { ### Detecting Client Compatibility -```js -// Server-side: check if a player has Box3JS client mod installed -if (entity.player.hasBox3JSClientMod()) { - // Can send client events - remoteChannel.sendClientEvent(entity, { type: "custom_ui", ... }); -} else { - // Fallback to chat messages - entity.player.directMessage("Install Box3JS client for the full experience"); -} -``` +No manual detection is needed. `remoteChannel.sendClientEvent()` uses optional payloads — players without the Box3JS client mod will silently ignore them without errors or disconnects. You can safely send events to all players. ### Data Format diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java index e450ba4..94e2cea 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java @@ -2,25 +2,17 @@ import com.box3lab.box3js.client.Box3JSClientEngine; import com.box3lab.box3js.client.Box3JSGuiProxy; -import com.box3lab.box3js.client.screen.Box3JSScriptContainerScreen; import com.box3lab.box3js.registries.Box3JSRecipeManager; import com.box3lab.box3js.script.Box3ScriptCommand; import com.box3lab.box3js.script.Box3ScriptEngine; import com.box3lab.box3js.script.Box3JSGuiServerHandler; -import com.box3lab.box3js.script.Box3JSScriptContainerMenu; import com.mojang.logging.LogUtils; -import net.minecraft.core.registries.Registries; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.flag.FeatureFlags; -import net.minecraft.world.inventory.MenuType; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; -import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; -import net.neoforged.neoforge.registries.DeferredRegister; -import java.util.function.Supplier; import java.nio.file.Path; import java.util.Set; @@ -45,20 +37,7 @@ public class Box3JS { /** Tracks which connected players have Box3JS installed on their client. */ public static final Set clientsWithBox3JS = ConcurrentHashMap.newKeySet(); - // ── Registries ── - - private static final DeferredRegister> MENU_TYPES = - DeferredRegister.create(Registries.MENU, MODID); - - /** Single MenuType for all script container sizes (1-6 rows). */ - public static final Supplier> SCRIPT_CONTAINER_MENU = - MENU_TYPES.register("script_container", - () -> new MenuType<>(new Box3JSScriptContainerMenu.Factory(), FeatureFlags.DEFAULT_FLAGS)); - public Box3JS(IEventBus modEventBus, ModContainer modContainer) { - // Register registries - MENU_TYPES.register(modEventBus); - // Register custom payloads modEventBus.addListener(RegisterPayloadHandlersEvent.class, event -> { var registrar = event.registrar("1"); @@ -128,10 +107,6 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) { ); }); - // Register client-side screen for the script container - modEventBus.addListener(RegisterMenuScreensEvent.class, event -> - event.register(SCRIPT_CONTAINER_MENU.get(), Box3JSScriptContainerScreen::new)); - // Script commands NeoForge.EVENT_BUS.addListener(Box3ScriptCommand::register); diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java deleted file mode 100644 index 22bfd61..0000000 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.box3lab.box3js.client.screen; - -import com.box3lab.box3js.script.Box3JSScriptContainerMenu; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.player.Inventory; - -public class Box3JSScriptContainerScreen extends AbstractContainerScreen { - - private static final ResourceLocation CONTAINER_BACKGROUND = - ResourceLocation.fromNamespaceAndPath("minecraft", "textures/gui/container/generic_54.png"); - - private final int rows; - - public Box3JSScriptContainerScreen(Box3JSScriptContainerMenu menu, Inventory playerInventory, - Component title) { - super(menu, playerInventory, title); - this.rows = menu.getRows(); - this.imageHeight = 114 + this.rows * 18; - this.inventoryLabelY = this.imageHeight - 94; - } - - @Override - public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - super.render(guiGraphics, mouseX, mouseY, partialTick); - this.renderTooltip(guiGraphics, mouseX, mouseY); - } - - @Override - protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) { - int x = (this.width - this.imageWidth) / 2; - int y = (this.height - this.imageHeight) / 2; - - // Draw chest background — only render the top part (dynamic rows) - int containerRows = this.rows; - int texHeight = 222; // Full 6-row texture height - int renderHeight = 18 + containerRows * 18; // Top border + rows + gap above inventory - - // Top of the container (including top border) - guiGraphics.blit(CONTAINER_BACKGROUND, x, y, 0, 0, this.imageWidth, 17); - // Middle rows - for (int row = 0; row < containerRows; row++) { - guiGraphics.blit(CONTAINER_BACKGROUND, x, y + 17 + row * 18, 0, 17, this.imageWidth, 18); - } - // Gap + player inventory area - int bottomStart = 17 + containerRows * 18; - int bottomTexY = 215 - 96; // Transition area above player inventory in texture - // Actually render the bottom inventory section from the 6-row texture - int invSectionStart = 17 + containerRows * 18; - // Copy the player inventory from the fixed texture - for (int row = 0; row < 4; row++) { - int srcY = 17 + 5 * 18 + row * 18; // Start from row 5 in the 6-row texture (skip rows) - // Actually use the bottom inventory strip from the texture - guiGraphics.blit(CONTAINER_BACKGROUND, x, y + invSectionStart + row * 18, - 0, 125 + row * 18, this.imageWidth, 18); - } - // Render the remaining lines below - guiGraphics.blit(CONTAINER_BACKGROUND, x, y + invSectionStart + 4 * 18, - 0, 215 - 7, this.imageWidth, 7); - } -} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java index a2aba6e..5cba3aa 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java @@ -1,11 +1,11 @@ package com.box3lab.box3js.script; -import com.box3lab.box3js.Box3JS; import com.box3lab.box3js.Box3JSNetwork; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.MenuProvider; import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.MenuType; import net.neoforged.neoforge.network.PacketDistributor; import java.util.Map; import java.util.UUID; @@ -19,6 +19,17 @@ public final class Box3JSGuiServerHandler { private Box3JSGuiServerHandler() {} + /** Vanilla generic chest menu types mapped by row count (1-6). */ + private static final MenuType[] CHEST_TYPES = { + MenuType.GENERIC_9x1, MenuType.GENERIC_9x2, MenuType.GENERIC_9x3, + MenuType.GENERIC_9x4, MenuType.GENERIC_9x5, MenuType.GENERIC_9x6 + }; + + private static MenuType chestType(int rows) { + int idx = Math.clamp(rows, 1, 6) - 1; + return CHEST_TYPES[idx]; + } + private static final Map activeGuis = new ConcurrentHashMap<>(); private static class ActiveGui { @@ -43,7 +54,7 @@ public static void handleOpen(ServerPlayer player, String title, int rows, Strin MenuProvider provider = new SimpleMenuProvider((containerId, inv, p) -> { Box3JSScriptContainerMenu menu = new Box3JSScriptContainerMenu( - Box3JS.SCRIPT_CONTAINER_MENU.get(), containerId, inv, rows, controller); + chestType(rows), containerId, inv, rows, controller); controller.setMenu(menu); // Populate initial slots from JSON string like {"0":"minecraft:diamond","1":"minecraft:stone"} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java index a58c7a9..d472ba9 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java @@ -1,14 +1,10 @@ package com.box3lab.box3js.script; -import com.box3lab.box3js.Box3JS; -import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.*; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.flag.FeatureFlags; -import net.neoforged.neoforge.network.IContainerFactory; public class Box3JSScriptContainerMenu extends AbstractContainerMenu { @@ -106,26 +102,6 @@ public ItemStack quickMoveStack(Player player, int index) { return copy; } - // ---- Factory for MenuType registration ---- - - public static class Factory implements MenuType.MenuSupplier, - IContainerFactory { - @Override - public Box3JSScriptContainerMenu create(int containerId, Inventory playerInventory) { - // Default 3 rows — used as fallback if IContainerFactory path fails - return new Box3JSScriptContainerMenu( - Box3JS.SCRIPT_CONTAINER_MENU.get(), containerId, playerInventory, 3, null); - } - - @Override - public Box3JSScriptContainerMenu create(int containerId, Inventory playerInventory, - RegistryFriendlyByteBuf extraData) { - int rows = extraData.readVarInt(); - return new Box3JSScriptContainerMenu( - Box3JS.SCRIPT_CONTAINER_MENU.get(), containerId, playerInventory, rows, null); - } - } - // ---- Player inventory helper ---- private void addPlayerInventory(Inventory playerInventory, int x, int y) { diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/eslint.config.mjs b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/eslint.config.mjs index 064f9f5..9019cf3 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/eslint.config.mjs +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/eslint.config.mjs @@ -95,9 +95,7 @@ export default [ languageOptions: { parser: tseslint.parser, parserOptions: { - project: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + project: ['./tsconfig.server.json', './tsconfig.client.json'], tsconfigRootDir: import.meta.dirname, }, }, diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts index c884b42..39e46e3 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts @@ -6,12 +6,10 @@ world.onPlayerJoin((entity: GamePlayerEntity) => { p.directMessage("§6Welcome to §ePROJECT_NAME§6!"); p.directMessage("§7Type §e!hello §7to say hi"); - if (entity.hasBox3JSClient()) { - remoteChannel.sendClientEvent(entity, { - type: "welcome", - message: `Welcome, ${p.name}`, - }); - } + remoteChannel.sendClientEvent(entity, { + type: "welcome", + message: `Welcome, ${p.name}`, + }); }); world.onChat((entity: GamePlayerEntity, message: string, _tick: number) => { diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts index 56590a7..6380bd1 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts @@ -323,4 +323,4 @@ interface GameEntity { * @zh 玩家实体 — `GameEntity` 的子类型,保证 `player` 属性非 null。 * @en A player entity — subtype of `GameEntity` with a guaranteed non‑null `player`. */ -type GamePlayerEntity = GameEntity & { player: GamePlayer; hasBox3JSClient(): boolean }; +type GamePlayerEntity = GameEntity & { player: GamePlayer }; diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/player.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/player.d.ts index 0de3231..a3f8176 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/player.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/player.d.ts @@ -430,13 +430,4 @@ interface GamePlayer { */ revokeAdvancement(advancementId: string): void; - // ── @zh 客户端 Mod 检测 @en Client Mod Detection ── - - /** - * @zh 检查该玩家的客户端是否安装了 Box3JS mod。 - * @en Returns true if this player's client has the Box3JS mod installed. - * @remarks 用于在调用 `remoteChannel.sendClientEvent()` 前检测,避免向未安装的客户端发送。 - * Use before calling `remoteChannel.sendClientEvent()` to avoid sending to unsupported clients. - */ - hasBox3JSClientMod(): boolean; } From e6e5dedf704ddf333d4594ea5ae729d1c0613abd Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 14 May 2026 12:38:31 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(build):=20=E6=B7=BB=E5=8A=A0=20TypeScr?= =?UTF-8?q?ipt=20=E9=AA=8C=E8=AF=81=E4=BB=BB=E5=8A=A1=E5=92=8C=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E5=99=A8=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 verifyBox3JSProject 任务,用于检查模板文件、运行时 DTS 拆分以及事件 API Token 的一致性。将该任务注册到 check 生命周期。启用废弃 API 和未检查操作的编译器警告,提升代码质量。 docs(api): 添加 API 风格规范并改进文档 新增 API 风格规则章节,涵盖 GameEventHandlerToken 使用、环境特定类型、跨端数据通信以及坐标 API 模式。更新 client.onTick 文档,反映其返回 GameEventHandlerToken。补充 db.isAvailable() 文档。记录 player 的 position、velocity、bounds 和 onGround 属性。在服务端 API 文档中添加 broadcastClientEvent 示例。说明 storage.key 属性。更新架构文档,反映 server.js 入口点。 feat(client): 为客户端事件实现 GameEventHandlerToken 修改 client.onTick 使其返回 GameEventHandlerToken,实现事件监听器的规范管理。更新按键映射,包含无前缀的修饰键(shift、ctrl、alt)。使用解释执行上下文进行 Rhino 操作。 refactor(storage): 集中存储路径解析与错误处理 将存储文件操作移至 Box3StorageSupport 工具类,实现更好的错误处理和一致的路径解析。用集中式实现替代手动的路径清理。 refactor(database): 改进错误处理与日志记录 将数据库目录创建失败时的静默异常捕获替换为适当的警告日志。 refactor(gui): 改进错误处理与日志记录 将 GUI 槽位条目无效时的静默异常捕获替换为调试日志。 refactor(entity): 改进错误处理与日志记录 将设置文本展示实体属性失败时的静默异常捕获替换为警告日志。使用 BuiltInRegistries 进行实体类型查找,替代已废弃的方法。 refactor(response): 移除静默异常处理 移除解析 JSON 响应体时被忽略的异常处理。 refactor(player): 为玩家事件实现 GameEventHandlerToken 更新 player.onChat 使其返回 GameEventHandlerToken,实现事件监听器的规范管理。改进设置玩家列表名称时的错误处理。 --- Box3JS-NeoForge-1.21.1/build.gradle | 11 + Box3JS-NeoForge-1.21.1/docs/api/README.md | 7 + Box3JS-NeoForge-1.21.1/docs/api/README_en.md | 7 + Box3JS-NeoForge-1.21.1/docs/api/client.md | 8 +- Box3JS-NeoForge-1.21.1/docs/api/client_en.md | 8 +- Box3JS-NeoForge-1.21.1/docs/api/database.md | 4 + .../docs/api/database_en.md | 4 + Box3JS-NeoForge-1.21.1/docs/api/player.md | 16 + Box3JS-NeoForge-1.21.1/docs/api/player_en.md | 16 + Box3JS-NeoForge-1.21.1/docs/api/server.md | 10 +- Box3JS-NeoForge-1.21.1/docs/api/server_en.md | 10 +- Box3JS-NeoForge-1.21.1/docs/api/storage.md | 4 + Box3JS-NeoForge-1.21.1/docs/api/storage_en.md | 4 + .../docs/guide/architecture.md | 4 +- .../docs/guide/architecture_en.md | 4 +- .../docs/guide/getting-started.md | 3 +- .../docs/guide/getting-started_en.md | 3 +- .../docs/tutorial/06-client-scripting.md | 2 + .../docs/tutorial/06-client-scripting_en.md | 2 + .../main/java/com/box3lab/box3js/Box3JS.java | 3 +- .../com/box3lab/box3js/Box3JSNetwork.java | 4 +- .../box3js/client/Box3JSClientDatabase.java | 6 +- .../box3js/client/Box3JSClientEngine.java | 19 +- .../box3js/client/Box3JSClientStorage.java | 56 +- .../box3lab/box3js/script/Box3JSConsole.java | 52 ++ .../box3lab/box3js/script/Box3JSDatabase.java | 3 +- .../box3lab/box3js/script/Box3JSEntity.java | 25 +- .../box3js/script/Box3JSGuiServerHandler.java | 8 +- .../box3lab/box3js/script/Box3JSPlayer.java | 13 +- .../box3js/script/Box3JSRemoteChannel.java | 2 +- .../box3lab/box3js/script/Box3JSResponse.java | 4 +- .../box3lab/box3js/script/Box3JSStorage.java | 55 +- .../com/box3lab/box3js/script/Box3Rhino.java | 18 + .../box3js/script/Box3ScriptConfig.java | 12 +- .../box3js/script/Box3ScriptEngine.java | 79 +-- .../box3js/script/Box3ScriptSandbox.java | 1 + .../box3js/script/Box3ScriptTemplate.java | 8 +- .../box3js/script/Box3ScriptWatcher.java | 6 +- .../box3js/script/Box3StorageSupport.java | 81 +++ .../box3js/script/Box3StorageTypes.java | 7 +- .../box3js/template/types/client/client.d.ts | 14 +- .../box3js/template/types/client/input.d.ts | 5 +- .../box3js/template/types/server/entity.d.ts | 3 + .../box3js/template/types/server/server.d.ts | 9 + .../assets/box3js/template/types/shared.d.ts | 7 + .../tools/box3js-api-manifest.json | 273 +++++++++ .../tools/verify-box3js-project.mjs | 547 ++++++++++++++++++ 47 files changed, 1257 insertions(+), 190 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSConsole.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3Rhino.java create mode 100644 Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageSupport.java create mode 100644 Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json create mode 100644 Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs diff --git a/Box3JS-NeoForge-1.21.1/build.gradle b/Box3JS-NeoForge-1.21.1/build.gradle index e366251..db49f9c 100644 --- a/Box3JS-NeoForge-1.21.1/build.gradle +++ b/Box3JS-NeoForge-1.21.1/build.gradle @@ -209,6 +209,17 @@ tasks.named('jar', Jar).configure { tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation + options.compilerArgs.addAll(['-Xlint:deprecation', '-Xlint:unchecked']) +} + +tasks.register('verifyBox3JSProject', Exec) { + group = 'verification' + description = 'Verifies Box3JS template files, runtime DTS split, and event API token consistency.' + commandLine 'node', 'tools/verify-box3js-project.mjs' +} + +tasks.named('check').configure { + dependsOn tasks.named('verifyBox3JSProject') } // IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README.md b/Box3JS-NeoForge-1.21.1/docs/api/README.md index 54f0f0e..986563e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -60,6 +60,13 @@ Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服 | [客户端 API 总览](client.md) | 写本地 UI、输入、音效、聊天辅助、本地数据、客户端到服务端事件 | `client`、`audio`、`input`、`ui`、`chat`、`gui`、`storage`、`db`、`http` | | [共享工具](math.md) | 写双端都可用的数学、颜色、空间计算代码 | `GameVector3`、`GameBounds3`、`GameRGBColor`、`GameRGBAColor`、`GameQuaternion` | +## API 风格约定 + +- 所有 `onXxx(...)` 事件注册 API 都返回 `GameEventHandlerToken`,使用 `token.cancel()` 取消监听,使用 `token.active()` 检查是否仍有效。 +- 服务端 API 只出现在 `src/server/app.ts` 的类型环境中;客户端 API 只出现在 `src/client/app.ts` 的类型环境中。双端共享 API 为 `storage`、`db`、`http`、`remoteChannel`、`console` 和数学类型。 +- 跨端数据通过 `remoteChannel` 发送 JSON 可序列化对象:客户端使用 `sendServerEvent` / `onClientEvent`,服务端使用 `sendClientEvent` / `broadcastClientEvent` / `onServerEvent`。 +- 能用 `GameVector3` 的坐标 API 通常同时支持 `x, y, z` 重载;服务端方块坐标按整数处理。 + ## 功能速查 — 我想... 按你想做的事情查找对应 API,而非按全局对象记。 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md index fb8de02..901fa40 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md @@ -60,6 +60,13 @@ Box3JS APIs are split into server-side, client-side, and shared runtimes. The ty | [Client API Overview](client_en.md) | Local UI, input, audio, chat helpers, local data, client-to-server events | `client`, `audio`, `input`, `ui`, `chat`, `gui`, `storage`, `db`, `http` | | [Shared Utilities](math_en.md) | Math, color, and spatial code usable on both sides | `GameVector3`, `GameBounds3`, `GameRGBColor`, `GameRGBAColor`, `GameQuaternion` | +## API Style Rules + +- Every `onXxx(...)` event registration API returns a `GameEventHandlerToken`; call `token.cancel()` to unsubscribe and `token.active()` to check whether it is still live. +- Server APIs are only typed in `src/server/app.ts`; client APIs are only typed in `src/client/app.ts`. Shared APIs are `storage`, `db`, `http`, `remoteChannel`, `console`, and the math classes. +- Cross-side data travels through `remoteChannel` as JSON-serializable objects: clients use `sendServerEvent` / `onClientEvent`; servers use `sendClientEvent` / `broadcastClientEvent` / `onServerEvent`. +- Coordinate APIs that accept `GameVector3` usually also support an `x, y, z` overload; server block coordinates are handled as integers. + ## Find by Task — I want to... Find APIs by what you want to do, not by which global object they live on. diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client.md b/Box3JS-NeoForge-1.21.1/docs/api/client.md index c0ce264..2fa08f0 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client.md @@ -104,12 +104,14 @@ audio.setVolume("player", 0.8); ### client.onTick(callback) -注册客户端每 tick 回调(每秒 20 次)。无参数,无返回值。 +注册客户端每 tick 回调(每秒 20 次)。无参数,返回 `GameEventHandlerToken`,可用 `cancel()` 取消监听。 ```js -client.onTick(() => { +const token = client.onTick(() => { // 每帧更新逻辑 }); + +// token.cancel(); ``` > **注意:** 服务端也有 `world.onTick()`,但参数为 `TickInfo` 对象。客户端 `client.onTick()` 无参数。 @@ -240,7 +242,7 @@ token.cancel(); | 功能键 | `f1`–`f12` | | 方向键 | `up`, `down`, `left`, `right` | | 特殊键 | `space`, `enter`, `escape`, `tab`, `backspace`, `delete` | -| 修饰键 | `left_shift`, `right_shift`, `left_ctrl`, `right_ctrl`, `left_alt`, `right_alt` | +| 修饰键 | `shift`, `left_shift`, `right_shift`, `ctrl`, `left_ctrl`, `right_ctrl`, `alt`, `left_alt`, `right_alt` | ## ui — 屏幕 UI diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md b/Box3JS-NeoForge-1.21.1/docs/api/client_en.md index 10b91d3..b01a224 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client_en.md @@ -104,12 +104,14 @@ audio.setVolume("player", 0.8); ### client.onTick(callback) -Registers a callback invoked every client tick (20 times/sec). No parameters, no return value. +Registers a callback invoked every client tick (20 times/sec). It receives no parameters and returns a `GameEventHandlerToken`; call `cancel()` to unsubscribe. ```js -client.onTick(() => { +const token = client.onTick(() => { // Per-frame logic }); + +// token.cancel(); ``` > **Note:** Server-side `world.onTick()` receives a `TickInfo` object. Client-side `client.onTick()` receives nothing. @@ -240,7 +242,7 @@ token.cancel(); | Function keys | `f1`–`f12` | | Arrow keys | `up`, `down`, `left`, `right` | | Special keys | `space`, `enter`, `escape`, `tab`, `backspace`, `delete` | -| Modifiers | `left_shift`, `right_shift`, `left_ctrl`, `right_ctrl`, `left_alt`, `right_alt` | +| Modifiers | `shift`, `left_shift`, `right_shift`, `ctrl`, `left_ctrl`, `right_ctrl`, `alt`, `left_alt`, `right_alt` | ## ui — Screen UI diff --git a/Box3JS-NeoForge-1.21.1/docs/api/database.md b/Box3JS-NeoForge-1.21.1/docs/api/database.md index 2ff883e..5bcf02f 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/database.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/database.md @@ -21,6 +21,10 @@ db API requires SQLite JDBC driver. Install the minecraft-sqlite-jdbc mod, then > - 请将 `minecraft-sqlite-jdbc` 放到 `run/mods/`。 > - 模组文件必须是 `.jar`(例如 `xxx.jar`),不要使用 `.zip`,否则不会被 NeoForge 加载。 +## `db.isAvailable()` + +检查 SQLite JDBC 驱动是否可用。不可用时,`db.sql(...)` 会返回安全的空错误结果或显示清晰提示,脚本可用该方法提前降级。 + ## `db.sql(sql, ...params)` 执行 SQL 查询或更新,返回 `GameQueryResult`。 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/database_en.md b/Box3JS-NeoForge-1.21.1/docs/api/database_en.md index bde65ce..9cb46af 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/database_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/database_en.md @@ -21,6 +21,10 @@ After installing `minecraft-sqlite-jdbc` and restarting the server, the `db` API > - Put `minecraft-sqlite-jdbc` under `run/mods/`. > - The file must be a `.jar` (for example, `xxx.jar`), not `.zip`, otherwise NeoForge will not load it. +## `db.isAvailable()` + +Checks whether the SQLite JDBC driver is available. When unavailable, `db.sql(...)` returns a safe empty error result or shows a clear hint; scripts can call this first to degrade gracefully. + ## `db.sql(sql, ...params)` Executes a SQL query/update and returns `GameQueryResult`. diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player.md b/Box3JS-NeoForge-1.21.1/docs/api/player.md index 649f38e..4825dea 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -57,6 +57,22 @@ console.log("玩家缩放: " + player.scale); ## 移动 +### player.position + +只读引用。玩家当前世界坐标,可通过 `player.position.set(x, y, z)` 修改向量值;如需传送玩家,优先使用 `player.teleport(pos)`。 + +### player.velocity + +只读引用。玩家当前速度向量,可通过 `.set()` 修改。 + +### player.bounds + +只读。玩家包围盒半尺寸。 + +### player.onGround + +只读。玩家当前是否站在方块上。 + ### player.walkSpeed diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md index 98b4ec4..d76bb5f 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md @@ -57,6 +57,22 @@ console.log("Player scale: " + player.scale); ## Movement +### player.position + +Readonly reference. Current world position. You can mutate the vector with `player.position.set(x, y, z)`; prefer `player.teleport(pos)` when moving the player. + +### player.velocity + +Readonly reference. Current velocity vector; mutate with `.set()`. + +### player.bounds + +Readonly. Player bounding-box half extents. + +### player.onGround + +Readonly. Whether the player is currently standing on a block. + ### player.walkSpeed diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server.md b/Box3JS-NeoForge-1.21.1/docs/api/server.md index bc05668..84bad67 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/server.md @@ -168,6 +168,15 @@ world.onPlayerJoin((entity) => { }); ``` +向所有玩家广播客户端事件: + +```ts +remoteChannel.broadcastClientEvent({ + type: "serverNotice", + text: "服务器事件已触发", +}); +``` + 接收客户端事件: ```ts @@ -223,4 +232,3 @@ if (registries) { - 对所有长期事件监听保存 token,需要关闭玩法或重载模块时调用 `cancel()`。 - 大范围 `voxels.fillVoxel()`、大量实体生成、同步 HTTP 请求都应控制频率,避免卡住服务器 tick。 - 共享数据优先明确命名空间,例如 `storage.getDataStorage("arena/scores")` 或 `storage.getGroupStorage("global/season")`。 - diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md b/Box3JS-NeoForge-1.21.1/docs/api/server_en.md index 7925e03..be69033 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/server_en.md @@ -168,6 +168,15 @@ world.onPlayerJoin((entity) => { }); ``` +Broadcast a client event to every player: + +```ts +remoteChannel.broadcastClientEvent({ + type: "serverNotice", + text: "A server event was triggered", +}); +``` + Receive client events: ```ts @@ -223,4 +232,3 @@ Custom content files: - Keep tokens for long-lived event listeners and call `cancel()` when disabling a game mode or subsystem. - Rate-limit large `voxels.fillVoxel()` calls, mass entity spawning, and synchronous HTTP requests to avoid blocking server ticks. - Give storage namespaces explicit names, such as `storage.getDataStorage("arena/scores")` or `storage.getGroupStorage("global/season")`. - diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage.md b/Box3JS-NeoForge-1.21.1/docs/api/storage.md index a69057a..6b575a6 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -6,6 +6,10 @@ ## 获取存储实例 +### storage.key + +只读。根 `storage` 对象始终返回空字符串;具体命名空间请读取 `store.key`。 + ### storage.getDataStorage(name) 获取或创建一个命名存储。同名存储返回同一实例。 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md b/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md index 4e46b1b..2765df9 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md @@ -6,6 +6,10 @@ ## Getting a Storage Instance +### storage.key + +Readonly. The root `storage` object always returns an empty string; read `store.key` for a concrete namespace name. + ### storage.getDataStorage(name) Gets or creates a named storage. Same name returns the same instance. diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md b/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md index f8159ec..f31b990 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md @@ -117,8 +117,8 @@ scope.put("storage", scope, storageApi); // 2. 初始化 console JS 代码 cx.evaluateString(scope, Box3ScriptUtils.CONSOLE_INIT_JS, "console-init", 1, null); -// 3. 加载用户脚本 -cx.evaluateReader(scope, scriptReader, "app.js", 1, null); +// 3. 加载服务端入口脚本 +cx.evaluateReader(scope, scriptReader, "server.js", 1, null); ``` ### 类型桥接 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md index a9734bf..c26fc78 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md @@ -114,8 +114,8 @@ scope.put("storage", scope, storageApi); // 2. Initialize console JS bridge cx.evaluateString(scope, Box3ScriptUtils.CONSOLE_INIT_JS, "console-init", 1, null); -// 3. Load user script -cx.evaluateReader(scope, scriptReader, "app.js", 1, null); +// 3. Load server entry script +cx.evaluateReader(scope, scriptReader, "server.js", 1, null); ``` ### Type Bridging diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md index 00bab8c..0e537ee 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md @@ -126,7 +126,8 @@ config/box3/script/mygame/ │ │ └── app.ts ← ★ 服务端入口(你写代码的地方) │ └── client/ │ └── app.ts ← 客户端入口 -└── registries/ ← 自定义内容(方块/物品/音效 JSON) +├── registries/ ← 自定义内容(方块/物品/音效 JSON) +└── assets/lang/ ← 自定义内容本地化文本 ``` ### 安装依赖 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md index 0272332..27320d4 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md @@ -111,7 +111,8 @@ config/box3/script/mygame/ │ │ └── app.ts ← ★ Server entry point (where you write code) │ └── client/ │ └── app.ts ← Client entry point -└── registries/ ← Custom content (blocks/items/sounds JSON) +├── registries/ ← Custom content (blocks/items/sounds JSON) +└── assets/lang/ ← Custom content localization text ``` ### Install Dependencies diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md index b68cb6e..3461885 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md @@ -50,6 +50,8 @@ client.onTick(() => { }); ``` +与其他事件 API 一样,`client.onTick()` 会返回 `GameEventHandlerToken`;不再需要监听时调用 `token.cancel()`。 + **性能提示:** 客户端 onTick 也在主线程执行。避免密集循环,用取模运算降低实际执行频率。 ## 6.4 input — 键盘输入 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md index cd1cc99..61b95ff 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md @@ -50,6 +50,8 @@ client.onTick(() => { }); ``` +Like other event APIs, `client.onTick()` returns a `GameEventHandlerToken`; call `token.cancel()` when you no longer need the listener. + **Performance tip:** Client onTick also runs on the main thread. Avoid tight loops; use modulo to reduce the effective execution rate. ## 6.4 input — Keyboard Input diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java index 94e2cea..50216e1 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java @@ -14,7 +14,6 @@ import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; -import java.nio.file.Path; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -209,7 +208,7 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) { event.getSource().getEntity()); }); - // Auto-load scripts from config/box3/script//app.js on server start + // Auto-load server scripts from config/box3/script//dist/server.js on server start NeoForge.EVENT_BUS.addListener((ServerStartedEvent event) -> { Box3ScriptEngine.get().autoLoad(event.getServer()); Box3JSRecipeManager.init(event.getServer()); diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java index e4f3976..8d6ff3c 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java @@ -208,6 +208,8 @@ public static void sendClientScripts(ServerPlayer player) { Box3JS.LOGGER.error("Failed to send client script '{}': {}", name, e.getMessage()); } }); - } catch (IOException ignored) {} + } catch (IOException e) { + Box3JS.LOGGER.warn("Failed to scan client script directory: {}", scriptDir, e); + } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientDatabase.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientDatabase.java index 4108a4d..6cfd972 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientDatabase.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientDatabase.java @@ -25,7 +25,11 @@ public class Box3JSClientDatabase extends Box3DatabaseBase { public Box3JSClientDatabase(java.io.File gameDir) { this.dataDir = gameDir.toPath().resolve("box3").resolve("client-db"); - try { Files.createDirectories(dataDir); } catch (IOException ignored) {} + try { + Files.createDirectories(dataDir); + } catch (IOException e) { + LOGGER.warn("Failed to create client database directory: {}", dataDir, e); + } } public void setProjectName(String name) { diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java index 4f57e6d..9cd85ce 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java @@ -2,6 +2,7 @@ import com.box3lab.box3js.Box3JSNetwork; import com.box3lab.box3js.script.Box3JSQueryResult; +import com.box3lab.box3js.script.Box3Rhino; import com.box3lab.box3js.script.GameBounds3; import com.box3lab.box3js.script.GameEventHandlerToken; import com.box3lab.box3js.script.GameQuaternion; @@ -105,10 +106,13 @@ public class Box3JSClientEngine { KEY_MAP.put("delete", InputConstants.KEY_DELETE); KEY_MAP.put("left_shift", InputConstants.KEY_LSHIFT); KEY_MAP.put("right_shift", InputConstants.KEY_RSHIFT); + KEY_MAP.put("shift", InputConstants.KEY_LSHIFT); KEY_MAP.put("left_ctrl", InputConstants.KEY_LCONTROL); KEY_MAP.put("right_ctrl", InputConstants.KEY_RCONTROL); + KEY_MAP.put("ctrl", InputConstants.KEY_LCONTROL); KEY_MAP.put("left_alt", InputConstants.KEY_LALT); KEY_MAP.put("right_alt", InputConstants.KEY_RALT); + KEY_MAP.put("alt", InputConstants.KEY_LALT); KEY_MAP.put("up", InputConstants.KEY_UP); KEY_MAP.put("down", InputConstants.KEY_DOWN); KEY_MAP.put("left", InputConstants.KEY_LEFT); @@ -126,8 +130,7 @@ private Box3JSClientEngine() {} private void init() { if (initialized) return; - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); + Context cx = Box3Rhino.enterInterpretedContext(); try { scope = cx.initStandardObjects(); @@ -162,7 +165,7 @@ private void init() { public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { if (args.length > 0 && args[0] instanceof Function fn) { - tickCallbacks.add(() -> { + Runnable callback = () -> { Context cx2 = Context.enter(); try { fn.call(cx2, scope, scope, new Object[0]); @@ -171,7 +174,9 @@ public Object call(Context cx, Scriptable scope, } finally { Context.exit(); } - }); + }; + tickCallbacks.add(callback); + return new GameEventHandlerToken(() -> tickCallbacks.remove(callback)); } return Undefined.instance; } @@ -775,8 +780,7 @@ public Object call(Context cx, Scriptable scope, public void loadScript(String projectName, String scriptSource) { if (!initialized) init(); - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); + Context cx = Box3Rhino.enterInterpretedContext(); try { if (!projectName.equals(this.currentProject)) { this.currentProject = projectName; @@ -891,8 +895,7 @@ public void fireClientEvent(String projectName, long tick, String eventJson) { List handlers = clientEventHandlers.getOrDefault(projectName, List.of()); if (handlers.isEmpty()) return; - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); + Context cx = Box3Rhino.enterInterpretedContext(); try { scope.put("_arg", scope, eventJson); Object args = cx.evaluateString(scope, diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java index ec1907c..57766c2 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java @@ -1,13 +1,13 @@ package com.box3lab.box3js.client; import com.box3lab.box3js.script.Box3StorageTypes; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; +import com.box3lab.box3js.script.Box3StorageSupport; +import com.mojang.logging.LogUtils; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; +import org.slf4j.Logger; import java.io.IOException; -import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -20,8 +20,7 @@ */ public class Box3JSClientStorage { - private static final Gson GSON = new Gson(); - private static final Type MAP_TYPE = new TypeToken>() {}.getType(); + private static final Logger LOGGER = LogUtils.getLogger(); private final Path baseDir; private final String projectName; @@ -30,7 +29,11 @@ public class Box3JSClientStorage { public Box3JSClientStorage(java.io.File gameDir, String projectName) { this.baseDir = gameDir.toPath().resolve("box3").resolve("client-storage"); this.projectName = projectName; - try { Files.createDirectories(baseDir); } catch (IOException ignored) {} + try { + Files.createDirectories(baseDir); + } catch (IOException e) { + LOGGER.warn("Failed to create client storage directory: {}", baseDir, e); + } } public String getKey() { return ""; } @@ -53,41 +56,14 @@ public class GameDataStorage { GameDataStorage(String name) { this.name = name; - String[] parts = name.split("/"); - Path dir = baseDir; - for (int i = 0; i < parts.length - 1; i++) { - String seg = sanitize(parts[i]); - if (!seg.isEmpty()) dir = dir.resolve(seg); - } - String file = sanitize(parts[parts.length - 1]); - if (file.isEmpty()) file = "default"; - this.path = dir.resolve(file + ".json"); - this.data = cache.computeIfAbsent(path, p -> { - if (Files.exists(p)) { - try { - String json = Files.readString(p); - Map map = GSON.fromJson(json, MAP_TYPE); - return map != null ? Collections.synchronizedMap(new LinkedHashMap<>(map)) - : Collections.synchronizedMap(new LinkedHashMap<>()); - } catch (IOException e) { - return Collections.synchronizedMap(new LinkedHashMap<>()); - } - } - return Collections.synchronizedMap(new LinkedHashMap<>()); - }); - } - - private String sanitize(String s) { - return s.replaceAll("[^a-zA-Z0-9_.\\-]", "_"); + this.path = Box3StorageSupport.resolveStoragePath(baseDir, name); + this.data = cache.computeIfAbsent(path, p -> Box3StorageSupport.readData(p, "client")); } public String getKey() { return name; } private void persist() { - try { - Files.createDirectories(path.getParent()); - Files.writeString(path, GSON.toJson(data)); - } catch (IOException ignored) {} + Box3StorageSupport.writeData(path, data, "client"); } // ── Basic API ── @@ -100,7 +76,7 @@ public void set(String key, Object value) { if (existing != null) { existing.value = value; existing.updateTime = now; - existing.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + existing.version = Box3StorageTypes.newVersion(now); } else { data.put(key, new Box3StorageTypes.ValueEntry(value, now)); } @@ -135,7 +111,7 @@ public void update(String key, Function handler) { Context.exit(); } entry.updateTime = now; - entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + entry.version = Box3StorageTypes.newVersion(now); persist(); } } @@ -165,7 +141,7 @@ public double increment(String key, double value) { entry.value = delta; } entry.updateTime = now; - entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + entry.version = Box3StorageTypes.newVersion(now); } else { entry = new Box3StorageTypes.ValueEntry(delta, now); data.put(key, entry); @@ -255,7 +231,7 @@ private double extractSortValue(Object value, String target) { public void destroy() { synchronized (data) { cache.remove(path); - try { Files.deleteIfExists(path); } catch (IOException ignored) {} + Box3StorageSupport.deleteData(path, "client"); } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSConsole.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSConsole.java new file mode 100644 index 0000000..07535d1 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSConsole.java @@ -0,0 +1,52 @@ +package com.box3lab.box3js.script; + +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +import java.util.function.Supplier; + +/** + * Console backend exposed to server-side scripts. + */ +public class Box3JSConsole { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private final Supplier projectName; + + public Box3JSConsole(Supplier projectName) { + this.projectName = projectName; + } + + private String format(Object... args) { + StringBuilder sb = new StringBuilder(); + String project = projectName.get(); + if (project != null && !project.isEmpty()) { + sb.append('[').append(project).append("] "); + } + for (Object arg : args) { + sb.append(arg).append(' '); + } + return sb.toString().trim(); + } + + public void log(Object... args) { + LOGGER.info("[Box3JS] {}", format(args)); + } + + public void debug(Object... args) { + LOGGER.debug("[Box3JS] {}", format(args)); + } + + public void warn(Object... args) { + LOGGER.warn("[Box3JS] {}", format(args)); + } + + public void error(Object... args) { + LOGGER.error("[Box3JS] {}", format(args)); + } + + public void clear() { + // Server logs cannot be cleared reliably; keep console.clear() as a safe no-op. + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSDatabase.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSDatabase.java index f749af6..f142597 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSDatabase.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSDatabase.java @@ -62,7 +62,8 @@ public Box3JSDatabase(Path configDir, Box3ScriptEngine engine) { this.engine = engine; try { Files.createDirectories(dataDir); - } catch (java.io.IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to create database directory: {}", dataDir, e); } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java index 6fe690c..5aba6f3 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java @@ -2,6 +2,7 @@ import net.minecraft.ChatFormatting; import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; @@ -16,13 +17,17 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.scores.PlayerTeam; import net.minecraft.world.scores.Scoreboard; +import com.mojang.logging.LogUtils; import org.mozilla.javascript.Function; +import org.slf4j.Logger; import java.util.Map; import java.util.function.Consumer; public class Box3JSEntity { + private static final Logger LOGGER = LogUtils.getLogger(); + private final Entity entity; private final MinecraftServer server; private final Box3ScriptEngine engine; @@ -50,8 +55,8 @@ public String getId() { public boolean isPlayer() { return entity instanceof ServerPlayer; } public String getEntityType() { - var key = entity.getType().builtInRegistryHolder().key(); - return key != null ? key.location().toString() : "unknown"; + var key = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()); + return key != null ? key.toString() : "unknown"; } public Box3JSPlayer getPlayer() { @@ -387,7 +392,11 @@ public void setPersistent(boolean v) { public void setText(String text) { if (entity instanceof net.minecraft.world.entity.Display.TextDisplay td) { - try { _tdSetText.invoke(td, Component.literal(text)); } catch (Exception ignored) {} + try { + _tdSetText.invoke(td, Component.literal(text)); + } catch (Exception e) { + LOGGER.warn("Failed to set TextDisplay text for entity {}", entity.getStringUUID(), e); + } } } @@ -401,7 +410,9 @@ public void setTextColor(GameRGBColor color) { int b = (int) (Math.max(0, Math.min(1, color.b)) * 255); int rgb = (r << 16) | (g << 8) | b; _tdSetText.invoke(td, Component.literal(text).withColor(rgb)); - } catch (Exception ignored) {} + } catch (Exception e) { + LOGGER.warn("Failed to set TextDisplay text color for entity {}", entity.getStringUUID(), e); + } } } @@ -411,7 +422,11 @@ public void setTextBackgroundColor(GameRGBAColor color) { int g = (int) (Math.max(0, Math.min(1, color.g)) * 255); int b = (int) (Math.max(0, Math.min(1, color.b)) * 255); int a = (int) (Math.max(0, Math.min(1, color.a)) * 255); - try { _tdSetBgColor.invoke(td, (a << 24) | (r << 16) | (g << 8) | b); } catch (Exception ignored) {} + try { + _tdSetBgColor.invoke(td, (a << 24) | (r << 16) | (g << 8) | b); + } catch (Exception e) { + LOGGER.warn("Failed to set TextDisplay background color for entity {}", entity.getStringUUID(), e); + } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java index 5cba3aa..31d4aea 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java @@ -1,6 +1,7 @@ package com.box3lab.box3js.script; import com.box3lab.box3js.Box3JSNetwork; +import com.mojang.logging.LogUtils; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.MenuProvider; @@ -10,6 +11,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; /** * Handles client→server GUI packets on the server thread. @@ -17,6 +19,8 @@ */ public final class Box3JSGuiServerHandler { + private static final Logger LOGGER = LogUtils.getLogger(); + private Box3JSGuiServerHandler() {} /** Vanilla generic chest menu types mapped by row count (1-6). */ @@ -79,7 +83,9 @@ public static void handleOpen(ServerPlayer player, String title, int rows, Strin if (item != null && slot >= 0 && slot < rows * 9) { menu.getContainer().setItem(slot, new net.minecraft.world.item.ItemStack(item, 1)); } - } catch (NumberFormatException | IndexOutOfBoundsException ignored) {} + } catch (NumberFormatException | IndexOutOfBoundsException e) { + LOGGER.debug("Ignoring invalid GUI slot entry '{}'", pair, e); + } } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java index 0686430..88ca22f 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java @@ -17,15 +17,20 @@ import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.GameType; +import com.mojang.logging.LogUtils; import org.mozilla.javascript.Function; import org.mozilla.javascript.NativeObject; import org.mozilla.javascript.ScriptableObject; +import org.slf4j.Logger; import java.util.Map; import java.util.function.Consumer; +@SuppressWarnings("deprecation") public class Box3JSPlayer { + private static final Logger LOGGER = LogUtils.getLogger(); + private final ServerPlayer player; private final MinecraftServer server; private final Box3ScriptEngine engine; @@ -361,8 +366,8 @@ public Object dialog(NativeObject config) { // ---- Chat (player-level) ---- - public void onChat(Function handler) { - engine.setPlayerChatHandler(player.getUUID(), handler); + public GameEventHandlerToken onChat(Function handler) { + return new GameEventHandlerToken(engine.setPlayerChatHandler(player.getUUID(), handler)); } // ---- Link ---- @@ -387,7 +392,9 @@ public void setPlayerListName(String name) { new net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket( java.util.EnumSet.of(net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), java.util.List.of(player))); - } catch (Exception ignored) {} + } catch (Exception e) { + LOGGER.warn("Failed to set player list name for {}", player.getGameProfile().getName(), e); + } } // ---- Look at (MC extension) ---- diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java index b938108..c69ada3 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java @@ -71,7 +71,7 @@ public void broadcastClientEvent(Object event) { // ── onServerEvent(handler) ── - public Object onServerEvent(Function handler) { + public GameEventHandlerToken onServerEvent(Function handler) { String project = engine.getCurrentProject(); Function stored = engine.bus.addServerEventHandler(project, handler); return new GameEventHandlerToken(() -> diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSResponse.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSResponse.java index 9f274f0..fc66ef2 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSResponse.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSResponse.java @@ -60,9 +60,7 @@ public Box3JSResponse(HttpResponse r, String responseType, long maxBodyS if (responseType != null && ok && body != null && body.length > 0) { switch (responseType) { - case "json" -> { - try { this.parsedBody = json(); } catch (Exception ignored) {} - } + case "json" -> this.parsedBody = json(); case "text" -> this.parsedBody = text(); case "arrayBuffer" -> this.parsedBody = arrayBuffer(); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java index b255cd4..ffe8963 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java @@ -1,11 +1,10 @@ package com.box3lab.box3js.script; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; import org.mozilla.javascript.Function; +import org.slf4j.Logger; import java.io.IOException; -import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -13,8 +12,7 @@ public class Box3JSStorage { - private static final Gson GSON = new Gson(); - private static final Type MAP_TYPE = new TypeToken>() {}.getType(); + private static final Logger LOGGER = LogUtils.getLogger(); private final Path baseDir; private final Box3ScriptEngine engine; @@ -23,7 +21,11 @@ public class Box3JSStorage { public Box3JSStorage(Path configDir, Box3ScriptEngine engine) { this.baseDir = configDir.resolve("box3").resolve("storage"); this.engine = engine; - try { Files.createDirectories(baseDir); } catch (IOException ignored) {} + try { + Files.createDirectories(baseDir); + } catch (IOException e) { + LOGGER.warn("Failed to create storage directory: {}", baseDir, e); + } } // ---- GameStorage ---- @@ -55,32 +57,8 @@ public class GameDataStorage { GameDataStorage(String name) { this.name = name; - String[] parts = name.split("/"); - Path dir = baseDir; - for (int i = 0; i < parts.length - 1; i++) { - String seg = sanitize(parts[i]); - if (!seg.isEmpty()) dir = dir.resolve(seg); - } - String file = sanitize(parts[parts.length - 1]); - if (file.isEmpty()) file = "default"; - this.path = dir.resolve(file + ".json"); - this.data = cache.computeIfAbsent(path, p -> { - if (Files.exists(p)) { - try { - String json = Files.readString(p); - Map map = GSON.fromJson(json, MAP_TYPE); - return map != null ? Collections.synchronizedMap(new LinkedHashMap<>(map)) - : Collections.synchronizedMap(new LinkedHashMap<>()); - } catch (IOException e) { - return Collections.synchronizedMap(new LinkedHashMap<>()); - } - } - return Collections.synchronizedMap(new LinkedHashMap<>()); - }); - } - - private String sanitize(String s) { - return s.replaceAll("[^a-zA-Z0-9_.\\-]", "_"); + this.path = Box3StorageSupport.resolveStoragePath(baseDir, name); + this.data = cache.computeIfAbsent(path, p -> Box3StorageSupport.readData(p, "server")); } public String getKey() { return name; } @@ -88,10 +66,7 @@ private String sanitize(String s) { // ---- Persist ---- private void persist() { - try { - Files.createDirectories(path.getParent()); - Files.writeString(path, GSON.toJson(data)); - } catch (IOException ignored) {} + Box3StorageSupport.writeData(path, data, "server"); } // ---- Public API ---- @@ -104,7 +79,7 @@ public void set(String key, Object value) { if (existing != null) { existing.value = value; existing.updateTime = now; - existing.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + existing.version = Box3StorageTypes.newVersion(now); } else { data.put(key, new Box3StorageTypes.ValueEntry(value, now)); } @@ -134,7 +109,7 @@ public void update(String key, Function handler) { long now = System.currentTimeMillis(); entry.value = engine.callFunction(handler, entry.value); entry.updateTime = now; - entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + entry.version = Box3StorageTypes.newVersion(now); persist(); } } @@ -164,7 +139,7 @@ public double increment(String key, double value) { entry.value = delta; } entry.updateTime = now; - entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + entry.version = Box3StorageTypes.newVersion(now); } else { entry = new Box3StorageTypes.ValueEntry(delta, now); data.put(key, entry); @@ -254,7 +229,7 @@ private double extractSortValue(Object value, String target) { public void destroy() { synchronized (data) { cache.remove(path); - try { Files.deleteIfExists(path); } catch (IOException ignored) {} + Box3StorageSupport.deleteData(path, "server"); } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3Rhino.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3Rhino.java new file mode 100644 index 0000000..9636e42 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3Rhino.java @@ -0,0 +1,18 @@ +package com.box3lab.box3js.script; + +import org.mozilla.javascript.Context; + +/** + * Shared Rhino helpers for server and client script engines. + */ +public final class Box3Rhino { + + private Box3Rhino() {} + + @SuppressWarnings("deprecation") + public static Context enterInterpretedContext() { + Context cx = Context.enter(); + cx.setOptimizationLevel(-1); + return cx; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java index 0632bec..e862da7 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java @@ -9,11 +9,14 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; import net.minecraft.server.MinecraftServer; +import org.slf4j.Logger; public class Box3ScriptConfig { + private static final Logger LOGGER = LogUtils.getLogger(); private static final Gson GSON = new Gson(); private static final Type MAP_TYPE = new TypeToken>() { }.getType(); @@ -41,7 +44,8 @@ public void load(MinecraftServer server) { Map loaded = GSON.fromJson(json, MAP_TYPE); if (loaded != null) projects = new LinkedHashMap<>(loaded); - } catch (IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to load script config: {}", configFile, e); } } } @@ -53,7 +57,8 @@ private void save() { try { Files.createDirectories(configFile.getParent()); Files.writeString(configFile, GSON.toJson(projects)); - } catch (IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to save script config: {}", configFile, e); } } @@ -89,7 +94,8 @@ public void discover(MinecraftServer server) { projects.putIfAbsent(name, false); } }); - } catch (IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to discover script projects in: {}", scriptDir, e); } save(); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java index 32184ea..e98ec7e 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java @@ -1,6 +1,7 @@ package com.box3lab.box3js.script; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.state.BlockState; @@ -15,7 +16,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import com.mojang.logging.LogUtils; @@ -91,16 +91,16 @@ public ScriptableObject getScope() { } /** Exposed for remoteChannel payload handler. */ - public MinecraftServer getServer() { + MinecraftServer getServer() { return server; } /** Exposed for remoteChannel. */ - public long getCurrentTick() { + long getCurrentTick() { return currentTick; } - public boolean isInitialized() { + boolean isInitialized() { return initialized; } @@ -121,7 +121,7 @@ public void handleClientEvent(ServerPlayer sender, String projectName, String ev }); } - /** Execute app.js for enabled projects under config/box3/script/ */ + /** Execute server.js for enabled projects under config/box3/script/ */ public void autoLoad(MinecraftServer server) { init(server); Box3ScriptConfig config = Box3ScriptConfig.get(); @@ -152,15 +152,15 @@ public void autoLoad(MinecraftServer server) { } } }); - } catch (IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to scan script directory for auto-load: {}", scriptDir, e); } } public Object eval(String code) { if (!initialized) throw new IllegalStateException("ScriptEngine not initialized"); - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); // interpreter mode avoids regex classloader issues + Context cx = Box3Rhino.enterInterpretedContext(); try { return cx.evaluateString(scope, code, "script", 1, null); } finally { @@ -323,9 +323,15 @@ public Runnable addMessageCallback(String project, MessageCallback cb) { return () -> bus.removeMessage(project, wrapped); } - public void setPlayerChatHandler(UUID uuid, Function handler) { + public Runnable setPlayerChatHandler(UUID uuid, Function handler) { String project = currentProject; bus.chatHandlersFor(project).put(uuid, handler); + return () -> { + Map handlers = bus.chatHandlersFor(project); + if (handlers.get(uuid) == handler) { + handlers.remove(uuid); + } + }; } private Runnable wrapContext(String project, Runnable cb) { @@ -350,12 +356,12 @@ public void runInContext(String project, Runnable action) { } } - public void setCurrentProject(String name) { + void setCurrentProject(String name) { currentProject = name; worldBinding.setProjectName(name); } - public String getCurrentProject() { + String getCurrentProject() { return currentProject; } @@ -645,8 +651,8 @@ private void tickFluid(String project, List enter, List currentProject), scope)); cx.evaluateString(scope, Box3ScriptUtils.CONSOLE_INIT_JS, "console-init", 1, null); ScriptableObject.putProperty(scope, "require", new BaseFunction() { @@ -1027,49 +1033,10 @@ protected String getCharacterEncoding(java.net.URLConnection c) { } } - public Box3JSVoxels getVoxelsBinding() { + Box3JSVoxels getVoxelsBinding() { return voxelsBinding; } - public class Box3JSConsole { - private void print(String level, Object... args) { - StringBuilder sb = new StringBuilder(); - String proj = currentProject; - if (proj != null) - sb.append('[').append(proj).append("] "); - for (Object a : args) - sb.append(a).append(' '); - System.out.println("[Box3JS]" + level + " " + sb.toString().trim()); - } - - public void log(Object... args) { - print("", args); - } - - public void debug(Object... args) { - print("[DEBUG]", args); - } - - public void warn(Object... args) { - print("[WARN]", args); - } - - public void error(Object... args) { - StringBuilder sb = new StringBuilder(); - String proj = currentProject; - if (proj != null) - sb.append('[').append(proj).append("] "); - for (Object a : args) - sb.append(a).append(' '); - System.err.println("[Box3JS][ERROR] " + sb.toString().trim()); - } - - public void clear() { - System.out.print("\033[H\033[2J"); - System.out.flush(); - } - } - static class TimerEntry { final int id; final Function handler; diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java index 67b1457..01e62ca 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java @@ -25,6 +25,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; +@SuppressWarnings("deprecation") class Box3ScriptSandbox { private static final Logger LOGGER = LogUtils.getLogger(); diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java index e023377..3b166cc 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java @@ -32,6 +32,12 @@ public class Box3ScriptTemplate { "types/client/ui.d.ts", "types/client/chat.d.ts", "types/client/gui.d.ts", + "registries/blocks.json", + "registries/items.json", + "registries/sounds.json", + "registries/creativeTabs.json", + "assets/lang/en_us.json", + "assets/lang/zh_cn.json", }; public static void copyTo(Path projectDir, String projectName) throws IOException { @@ -46,7 +52,7 @@ public static void copyTo(Path projectDir, String projectName) throws IOExceptio throw new IOException("Template file not found: " + resourcePath); Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); } - if (relPath.equals("package.json") || relPath.equals("src/server/app.ts")) { + if (relPath.endsWith(".json") || relPath.endsWith(".ts") || relPath.endsWith(".mjs")) { String content = Files.readString(dest); Files.writeString(dest, content.replace("PROJECT_NAME", projectName)); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java index 71b97fd..1d4249f 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java @@ -93,7 +93,8 @@ private void closeWatchService() { if (watchService != null) { try { watchService.close(); - } catch (IOException ignored) { + } catch (IOException e) { + Box3JS.LOGGER.warn("Failed to close script watch service", e); } watchService = null; } @@ -165,7 +166,8 @@ private void retryRegister(String project) { } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - } catch (IOException ignored) { + } catch (IOException e) { + Box3JS.LOGGER.warn("Failed to re-register watch for project '{}'", project, e); } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageSupport.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageSupport.java new file mode 100644 index 0000000..a09d9c5 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageSupport.java @@ -0,0 +1,81 @@ +package com.box3lab.box3js.script; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Shared JSON storage helpers for server and client storage implementations. + */ +public final class Box3StorageSupport { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Gson GSON = new Gson(); + private static final Type MAP_TYPE = new TypeToken>() {}.getType(); + + private Box3StorageSupport() {} + + public static String sanitize(String value) { + return value.replaceAll("[^a-zA-Z0-9_.\\-]", "_"); + } + + public static Path resolveStoragePath(Path baseDir, String name) { + String[] parts = name.split("/"); + Path dir = baseDir; + for (int i = 0; i < parts.length - 1; i++) { + String segment = sanitize(parts[i]); + if (!segment.isEmpty()) { + dir = dir.resolve(segment); + } + } + String file = sanitize(parts[parts.length - 1]); + if (file.isEmpty()) { + file = "default"; + } + return dir.resolve(file + ".json"); + } + + public static Map emptyDataMap() { + return Collections.synchronizedMap(new LinkedHashMap<>()); + } + + public static Map readData(Path path, String label) { + if (!Files.exists(path)) { + return emptyDataMap(); + } + try { + String json = Files.readString(path); + Map map = GSON.fromJson(json, MAP_TYPE); + return map != null ? Collections.synchronizedMap(new LinkedHashMap<>(map)) : emptyDataMap(); + } catch (IOException e) { + LOGGER.warn("Failed to read {} storage file: {}", label, path, e); + return emptyDataMap(); + } + } + + public static void writeData(Path path, Map data, String label) { + try { + Files.createDirectories(path.getParent()); + Files.writeString(path, GSON.toJson(data)); + } catch (IOException e) { + LOGGER.warn("Failed to persist {} storage file: {}", label, path, e); + } + } + + public static void deleteData(Path path, String label) { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + LOGGER.warn("Failed to delete {} storage file: {}", label, path, e); + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageTypes.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageTypes.java index 056e8b2..400e078 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageTypes.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageTypes.java @@ -1,6 +1,7 @@ package com.box3lab.box3js.script; import java.util.*; +import java.util.concurrent.ThreadLocalRandom; /** * Shared storage data types used by both server-side and client-side storage. @@ -21,10 +22,14 @@ public ValueEntry(Object value, long createTime) { this.value = value; this.createTime = createTime; this.updateTime = createTime; - this.version = Long.toHexString(createTime) + "-" + Integer.toHexString(new Random().nextInt()); + this.version = newVersion(createTime); } } + public static String newVersion(long timestamp) { + return Long.toHexString(timestamp) + "-" + Integer.toHexString(ThreadLocalRandom.current().nextInt()); + } + // ── ReturnValue ── public static class ReturnValue { diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts index fa048a5..ee5268e 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts @@ -35,8 +35,12 @@ interface RemoteChannel { /** @zh 通过 `client` 访问:生命周期回调 @en Accessed via `client`: lifecycle callbacks */ interface GameClient { - /** @zh 注册客户端每 tick 回调(每秒 20 次)。 @en Registers a callback invoked every client tick (20/sec). */ - onTick(callback: () => void): void; + /** + * @zh 注册客户端每 tick 回调(每秒 20 次)。 + * @en Registers a callback invoked every client tick (20/sec). + * @returns @zh GameEventHandlerToken — 调用 .cancel() 取消 @en GameEventHandlerToken — call .cancel() to unsubscribe + */ + onTick(callback: () => void): GameEventHandlerToken; /** * @zh 获取当前帧率 (FPS)。 @@ -68,7 +72,7 @@ interface GameClient { * @returns @zh `{ type: "block"|"entity", position, entity?, blockPos?, direction? }` 或 null @en `{ type: "block"|"entity", position, entity?, blockPos?, direction? }` or null */ getLookingAt(): { - type: string; + type: "block" | "entity"; position: { x: number; y: number; z: number }; entity?: { name: string; @@ -88,8 +92,8 @@ interface GameClient { ip: string; name: string; isLocal: boolean; - playerCount: number; - maxPlayers: number; + playerCount?: number; + maxPlayers?: number; }; } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts index 213117b..105733d 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts @@ -8,8 +8,9 @@ */ type KeyName = | "space" | "enter" | "escape" | "tab" | "backspace" | "delete" - | "left_shift" | "right_shift" | "left_ctrl" | "right_ctrl" - | "left_alt" | "right_alt" + | "shift" | "left_shift" | "right_shift" + | "ctrl" | "left_ctrl" | "right_ctrl" + | "alt" | "left_alt" | "right_alt" | "up" | "down" | "left" | "right" | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts index 6380bd1..0ae1bf6 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts @@ -135,6 +135,8 @@ interface GameEntity { * @en Custom name tag text (empty string = none). */ nameTag: string; + + /** @zh 设置名称标签文本。 @en Sets the custom name tag text. */ setNameTag(name: string): void; // ── @zh 物理 @en Physics ── @@ -308,6 +310,7 @@ interface GameEntity { */ destroy(): void; + /** @zh 注册实体销毁回调。 @en Registers a callback invoked when this entity is destroyed. */ setOnDestroy(handler: (entity: GameEntity) => void): void; // ── @zh 玩家代理 @en Player proxy ── diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts index 50ef9a7..5ce92c0 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts @@ -266,30 +266,39 @@ type GameMapColor = * @en Query interface for registered content — only available in JARs built via `/box3script compile`. */ interface GameRegistries { + /** @zh 获取已注册方块信息。 @en Gets registered block information. */ getBlock(id: string): { block: any; itemId: string; } | null; + /** @zh 检查方块是否已注册。 @en Checks whether a block is registered. */ hasBlock(id: string): boolean; + /** @zh 列出所有已注册方块 ID。 @en Lists all registered block IDs. */ listBlocks(): string[]; + /** @zh 获取已注册物品信息。 @en Gets registered item information. */ getItem(id: string): { item: any; itemId: string; } | null; + /** @zh 检查物品是否已注册。 @en Checks whether an item is registered. */ hasItem(id: string): boolean; + /** @zh 列出所有已注册物品 ID。 @en Lists all registered item IDs. */ listItems(): string[]; + /** @zh 获取已注册音效信息。 @en Gets registered sound information. */ getSound(id: string): { soundId: string; } | null; + /** @zh 检查音效是否已注册。 @en Checks whether a sound is registered. */ hasSound(id: string): boolean; + /** @zh 列出所有已注册音效 ID。 @en Lists all registered sound IDs. */ listSounds(): string[]; } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts index a8d79e3..8cfe49c 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts @@ -161,6 +161,9 @@ declare class GameVector3 { static fromPolar(mag: number, phi: number, theta: number): GameVector3; /** @zh 返回 "(x, y, z)" 格式的字符串表示。 @en Returns a string in "(x, y, z)" format. */ + /** @zh 返回 "(lo, hi)" 格式的字符串表示。 @en Returns a string representation in "(lo, hi)" format. */ + /** @zh 返回四元数字符串表示。 @en Returns a string representation of the quaternion. */ + /** @zh 返回四元数字符串表示。 @en Returns a string representation of the quaternion. */ toString(): string; } @@ -230,6 +233,7 @@ declare class GameBounds3 { /** @zh 从 GameVector3 数组创建最小包围盒。 @en Creates bounds from an array of GameVector3. */ static fromPoints(points: GameVector3[]): GameBounds3 | null; + /** @zh 返回颜色字符串表示。 @en Returns a string representation of the color. */ toString(): string; } @@ -301,6 +305,7 @@ declare class GameRGBColor { /** @zh 生成一个随机 RGB 颜色(每个通道 0–1)。 @en Generates a random RGB color (each channel 0–1). */ static random(): GameRGBColor; + /** @zh 返回颜色字符串表示。 @en Returns a string representation of the color. */ toString(): string; } @@ -373,6 +378,7 @@ declare class GameRGBAColor { */ blendEq(rgb: GameRGBColor): GameRGBColor; + /** @zh 返回四元数字符串表示。 @en Returns a string representation of the quaternion. */ toString(): string; } @@ -491,6 +497,7 @@ declare class GameQuaternion { /** @zh 近似相等检查(容差 1e‑6)。 @en Approximate equality check within 1e‑6 tolerance. */ equals(v: GameQuaternion): boolean; + /** @zh 返回四元数字符串表示。 @en Returns a string representation of the quaternion. */ toString(): string; } diff --git a/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json b/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json new file mode 100644 index 0000000..dfe2a8d --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json @@ -0,0 +1,273 @@ +{ + "globals": [ + { "side": "server", "name": "world", "type": "GameWorld", "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "voxels", "type": "GameVoxels", "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "storage", "type": "GameStorage", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "remoteChannel", "type": "RemoteChannel", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "db", "type": "GameDatabase", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "http", "type": "GameHttpAPI", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "registries", "type": "GameRegistries | undefined", "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", "runtime": "src/main/java/com/box3lab/box3js/standalone/Box3StandaloneBootstrap.java" }, + { "side": "client", "name": "audio", "type": "GameAudio", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "client", "type": "GameClient", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "input", "type": "GameInput", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "ui", "type": "GameUI", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "chat", "type": "GameChat", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "gui", "type": "GameGUI", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "storage", "type": "GameStorage", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "remoteChannel", "type": "RemoteChannel", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "db", "type": "GameDatabase", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "http", "type": "GameHttpAPI", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" } + ], + "apis": [ + { + "name": "world", + "prefix": "world", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSWorld.java", + "dts": "src/main/resources/assets/box3js/template/types/server/world.d.ts", + "iface": "GameWorld", + "accessorProperties": ["serverId", "rainDensity", "thunderDensity", "time", "timeScale", "difficulty", "spawnPoint", "ambientSound", "playerJoinSound", "playerLeaveSound", "placeVoxelSound", "breakVoxelSound", "borderSize"], + "keepMethodNames": ["setTime"], + "ignoreJava": ["setProjectName", "removeProject", "resetAll", "getProjectName", "getCurrentTick"], + "docs": ["docs/api/world.md", "docs/api/world_en.md"] + }, + { + "name": "GameVector3", + "java": "src/main/java/com/box3lab/box3js/script/GameVector3.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameVector3" + }, + { + "name": "GameBounds3", + "java": "src/main/java/com/box3lab/box3js/script/GameBounds3.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameBounds3" + }, + { + "name": "GameRGBColor", + "java": "src/main/java/com/box3lab/box3js/script/GameRGBColor.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameRGBColor" + }, + { + "name": "GameRGBAColor", + "java": "src/main/java/com/box3lab/box3js/script/GameRGBAColor.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameRGBAColor" + }, + { + "name": "GameQuaternion", + "java": "src/main/java/com/box3lab/box3js/script/GameQuaternion.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameQuaternion" + }, + { + "name": "GameEventHandlerToken", + "java": "src/main/java/com/box3lab/box3js/script/GameEventHandlerToken.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameEventHandlerToken" + }, + { + "name": "console", + "expectedMembers": ["log", "debug", "warn", "error", "clear", "assert"], + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameConsole" + }, + { + "name": "entity", + "prefix": "entity", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSEntity.java", + "dts": "src/main/resources/assets/box3js/template/types/server/entity.d.ts", + "iface": "GameEntity", + "ignoreJava": ["getEntity"], + "accessorProperties": ["id", "entityType", "player", "position", "velocity", "bounds", "meshInvisible", "glowing", "nameTag", "onGround", "eyePosition", "destroyed", "hp", "maxHp", "collides", "fixed", "gravity", "friction", "mass", "restitution", "invulnerable"], + "keepMethodNames": ["isPlayer", "setNameTag"], + "docs": ["docs/api/entity.md", "docs/api/entity_en.md"] + }, + { + "name": "player", + "prefix": "player", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java", + "dts": "src/main/resources/assets/box3js/template/types/server/player.d.ts", + "iface": "GamePlayer", + "ignoreJava": ["getPlayer"], + "accessorProperties": ["position", "velocity", "bounds", "onGround", "name", "userId", "opLevel", "invisible", "scale", "walkSpeed", "runSpeed", "jumpPower", "moveState", "walkState", "enableJump", "crouchSpeed", "swimSpeed", "canFly", "flying", "collision", "spectator", "flySpeed", "gameMode", "dimension", "disableFly", "cameraMode", "cameraEntity", "cameraPitch", "cameraYaw", "facingDirection", "cameraTarget", "dead", "spawnPoint", "hp", "maxHp", "xp", "food", "saturation"], + "docs": ["docs/api/player.md", "docs/api/player_en.md"] + }, + { + "name": "voxels", + "prefix": "voxels", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java", + "dts": "src/main/resources/assets/box3js/template/types/server/voxels.d.ts", + "iface": "GameVoxels", + "ignoreJava": ["getId"], + "docs": ["docs/api/voxels.md", "docs/api/voxels_en.md"] + }, + { + "name": "server storage", + "prefix": "storage", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSStorage.java", + "dts": ["src/main/resources/assets/box3js/template/types/shared.d.ts", "src/main/resources/assets/box3js/template/types/server/server.d.ts"], + "iface": "GameStorage", + "accessorProperties": ["key"], + "docs": ["docs/api/storage.md", "docs/api/storage_en.md"] + }, + { + "name": "server data storage", + "prefix": "store", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSStorage.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameDataStorage", + "nestedClass": "GameDataStorage", + "accessorProperties": ["key"], + "docs": ["docs/api/storage.md", "docs/api/storage_en.md"] + }, + { + "name": "client storage", + "java": "src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameStorage", + "accessorProperties": ["key"] + }, + { + "name": "client data storage", + "java": "src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameDataStorage", + "nestedClass": "GameDataStorage", + "accessorProperties": ["key"] + }, + { + "name": "server remoteChannel", + "prefix": "remoteChannel", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java", + "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", + "iface": "RemoteChannel", + "docs": ["docs/api/server.md", "docs/api/server_en.md"] + }, + { + "name": "http", + "prefix": "http", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSHttp.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameHttpAPI", + "docs": ["docs/api/http.md", "docs/api/http_en.md"] + }, + { + "name": "client http", + "java": "src/main/java/com/box3lab/box3js/client/Box3JSClientHttp.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameHttpAPI" + }, + { + "name": "database", + "prefix": "db", + "java": "src/main/java/com/box3lab/box3js/script/Box3DatabaseBase.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameDatabase", + "docs": ["docs/api/database.md", "docs/api/database_en.md"] + }, + { + "name": "query result", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSQueryResult.java", + "className": "Box3JSQueryResult", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameQueryResult", + "ignoreJava": ["toString"], + "accessorProperties": ["rows", "firstRow", "columnNames", "columnCount", "rowCount", "affectedRows"] + }, + { + "name": "http response", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSResponse.java", + "className": "Box3JSResponse", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameHttpFetchResponse", + "ignoreJava": ["timeout", "error"], + "accessorProperties": ["status", "statusText", "ok", "errorMessage", "headers", "data", "truncated"] + }, + { + "name": "registries", + "prefix": "registries", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/standalone/Box3StandaloneBootstrap.java", "variable": "registriesObj" }, + "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", + "iface": "GameRegistries", + "docs": ["docs/api/registries.md", "docs/api/registries_en.md"] + }, + { + "name": "client", + "prefix": "client", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "clientObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", + "iface": "GameClient", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "audio", + "prefix": "audio", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "audioObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/audio.d.ts", + "iface": "GameAudio", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "input", + "prefix": "input", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "inputObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/input.d.ts", + "iface": "GameInput", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "ui", + "prefix": "ui", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "uiObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/ui.d.ts", + "iface": "GameUI", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "chat", + "prefix": "chat", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "chatObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/chat.d.ts", + "iface": "GameChat", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "client remoteChannel", + "prefix": "remoteChannel", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "rcObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", + "iface": "RemoteChannel", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "client database", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "dbObj" }, + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameDatabase" + }, + { + "name": "client http global", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "httpObj" }, + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameHttpAPI" + }, + { + "name": "gui", + "prefix": "gui", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "guiObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/gui.d.ts", + "iface": "GameGUI", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "gui controller", + "prefix": "ctrl", + "java": "src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java", + "dts": "src/main/resources/assets/box3js/template/types/client/gui.d.ts", + "iface": "GuiController", + "ignoreJava": ["fireSlotClick", "fireClose"], + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + } + ] +} diff --git a/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs b/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs new file mode 100644 index 0000000..b9a9545 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs @@ -0,0 +1,547 @@ +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join, relative } from "node:path"; + +const root = new URL("..", import.meta.url).pathname.replace(/\/$/, ""); +const failures = []; + +function read(path) { + return readFileSync(join(root, path), "utf8"); +} + +function readJson(path) { + return JSON.parse(read(path)); +} + +function fail(message) { + failures.push(message); +} + +const apiManifest = readJson("tools/box3js-api-manifest.json"); + +function listFiles(dir) { + const base = join(root, dir); + const out = []; + function walk(abs) { + for (const name of readdirSync(abs)) { + const file = join(abs, name); + if (statSync(file).isDirectory()) { + walk(file); + } else { + out.push(relative(base, file).replaceAll("\\", "/")); + } + } + } + walk(base); + return out.sort(); +} + +function extractTemplateFiles() { + const source = read("src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java"); + const match = source.match(/private static final String\[\] FILES = \{([\s\S]*?)\};/); + if (!match) { + fail("Box3ScriptTemplate FILES list was not found"); + return []; + } + return [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]).sort(); +} + +function normalizeTemplatePath(path) { + return path === "gitignore.template" ? "gitignore.template" : path; +} + +function verifyTemplateFiles() { + const declared = extractTemplateFiles().map(normalizeTemplatePath); + const actual = listFiles("src/main/resources/assets/box3js/template") + .filter((path) => !path.startsWith("dist/")) + .filter((path) => !path.endsWith(".map")); + + for (const path of declared) { + if (!actual.includes(path)) { + fail(`Template file declared in Box3ScriptTemplate but missing from resources: ${path}`); + } + } + + for (const path of actual) { + if (!declared.includes(path)) { + fail(`Template resource is not copied by Box3ScriptTemplate: ${path}`); + } + } +} + +function verifyRuntimeTypeSplit() { + const serverIndex = read("src/main/resources/assets/box3js/template/types/server/index.d.ts"); + const clientIndex = read("src/main/resources/assets/box3js/template/types/client/index.d.ts"); + const serverConfig = read("src/main/resources/assets/box3js/template/tsconfig.server.json"); + const clientConfig = read("src/main/resources/assets/box3js/template/tsconfig.client.json"); + + if (serverIndex.includes("../client") || serverIndex.includes("./client")) { + fail("Server DTS index references client types"); + } + if (clientIndex.includes("../server") || clientIndex.includes("./server")) { + fail("Client DTS index references server types"); + } + if (serverConfig.includes("src/client") || serverConfig.includes("types/client")) { + fail("tsconfig.server.json includes client sources or types"); + } + if (clientConfig.includes("src/server") || clientConfig.includes("types/server")) { + fail("tsconfig.client.json includes server sources or types"); + } +} + +function verifyEventTokens() { + const javaFiles = [ + "src/main/java/com/box3lab/box3js/script/Box3JSWorld.java", + "src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java", + "src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java", + "src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java", + ]; + + for (const file of javaFiles) { + const source = read(file); + for (const match of source.matchAll(/public\s+([A-Za-z0-9_<>, ?]+)\s+(on[A-Z]\w*)\s*\(/g)) { + const returnType = match[1].trim(); + const method = match[2]; + if (returnType !== "GameEventHandlerToken") { + fail(`${file}: ${method} returns ${returnType}, expected GameEventHandlerToken`); + } + } + } + + for (const file of listFiles("src/main/resources/assets/box3js/template/types")) { + if (!file.endsWith(".d.ts")) continue; + const path = `src/main/resources/assets/box3js/template/types/${file}`; + const source = read(path); + for (const match of source.matchAll(/^\s*(on[A-Z]\w*)\s*\([\s\S]*?\):\s*([^;\n]+);/gm)) { + const method = match[1]; + const returnType = match[2].trim(); + if (returnType !== "GameEventHandlerToken") { + fail(`${path}: ${method} returns ${returnType}, expected GameEventHandlerToken`); + } + } + } +} + +function findMatchingBrace(source, openIndex) { + let depth = 0; + for (let i = openIndex; i < source.length; i++) { + const ch = source[i]; + if (ch === "{") { + depth++; + } else if (ch === "}") { + depth--; + if (depth === 0) { + return i; + } + } + } + return -1; +} + +function stripBlockAndLineComments(source) { + return source + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/\/\/.*$/gm, ""); +} + +function stripCommentsPreserveLength(source) { + return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => " ".repeat(comment.length)) + .replace(/\/\/.*$/gm, (comment) => " ".repeat(comment.length)); +} + +function extractDtsInterfaceMembers(paths, interfaceName) { + const files = Array.isArray(paths) ? paths : [paths]; + const members = new Set(); + for (const path of files) { + const source = stripBlockAndLineComments(read(path)); + const interfaceRe = new RegExp(`(?:interface|declare\\s+class|class)\\s+${interfaceName}\\b[^\\{]*\\{`, "g"); + let match; + while ((match = interfaceRe.exec(source)) !== null) { + const open = source.indexOf("{", match.index); + const close = findMatchingBrace(source, open); + if (close === -1) { + fail(`${path}: interface ${interfaceName} has no closing brace`); + continue; + } + const body = source.slice(open + 1, close); + let parenDepth = 0; + let braceDepth = 0; + let bracketDepth = 0; + for (const line of body.split(/\r?\n/)) { + if (parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) { + const member = line.trim().match(/^(?:static\s+)?(?:readonly\s+)?([A-Za-z_$]\w*)\??\s*(?:<[^;{(]*>)?\s*[:(]/); + if (member) { + if (member[1] !== "constructor") { + members.add(member[1]); + } + } + } + for (const ch of line) { + if (ch === "(") parenDepth++; + if (ch === ")") parenDepth = Math.max(0, parenDepth - 1); + if (ch === "{") braceDepth++; + if (ch === "}") braceDepth = Math.max(0, braceDepth - 1); + if (ch === "[") bracketDepth++; + if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1); + } + } + } + } + return members; +} + +function updateDepths(line, state) { + let inString = null; + let escaped = false; + for (const ch of line) { + if (inString) { + if (escaped) { + escaped = false; + } else if (ch === "\\") { + escaped = true; + } else if (ch === inString) { + inString = null; + } + continue; + } + if (ch === "\"" || ch === "'" || ch === "`") { + inString = ch; + } else if (ch === "(") { + state.parenDepth++; + } else if (ch === ")") { + state.parenDepth = Math.max(0, state.parenDepth - 1); + } else if (ch === "{") { + state.braceDepth++; + } else if (ch === "}") { + state.braceDepth = Math.max(0, state.braceDepth - 1); + } else if (ch === "[") { + state.bracketDepth++; + } else if (ch === "]") { + state.bracketDepth = Math.max(0, state.bracketDepth - 1); + } + } +} + +function extractDtsInterfaceMemberDetails(paths, interfaceName) { + const files = Array.isArray(paths) ? paths : [paths]; + const members = new Map(); + for (const path of files) { + const source = read(path); + const searchable = stripCommentsPreserveLength(source); + const interfaceRe = new RegExp(`(?:interface|declare\\s+class|class)\\s+${interfaceName}\\b[^\\{]*\\{`, "g"); + let match; + while ((match = interfaceRe.exec(searchable)) !== null) { + const open = searchable.indexOf("{", match.index); + const close = findMatchingBrace(searchable, open); + if (close === -1) { + fail(`${path}: interface ${interfaceName} has no closing brace`); + continue; + } + const body = source.slice(open + 1, close); + const state = { parenDepth: 0, braceDepth: 0, bracketDepth: 0 }; + let pendingComment = ""; + let commentBuffer = ""; + let inDocComment = false; + + for (const line of body.split(/\r?\n/)) { + const trimmed = line.trim(); + if (trimmed.startsWith("/**")) { + inDocComment = true; + commentBuffer = `${line}\n`; + if (trimmed.includes("*/")) { + inDocComment = false; + pendingComment = commentBuffer; + commentBuffer = ""; + } + continue; + } + if (inDocComment) { + commentBuffer += `${line}\n`; + if (trimmed.includes("*/")) { + inDocComment = false; + pendingComment = commentBuffer; + commentBuffer = ""; + } + continue; + } + + if (state.parenDepth === 0 && state.braceDepth === 0 && state.bracketDepth === 0) { + const lineWithoutComment = line.replace(/\/\/.*$/, ""); + const member = lineWithoutComment.trim().match(/^(?:static\s+)?(?:readonly\s+)?([A-Za-z_$]\w*)\??\s*(?:<[^;{(]*>)?\s*[:(]/); + if (member) { + const name = member[1]; + if (name === "constructor") { + pendingComment = ""; + continue; + } + const documented = pendingComment.includes("@zh") && pendingComment.includes("@en"); + const current = members.get(name) ?? { documented: false, locations: [] }; + current.documented ||= documented; + current.locations.push(path); + members.set(name, current); + pendingComment = ""; + } else if (trimmed !== "" && !trimmed.startsWith("//")) { + pendingComment = ""; + } + } + + updateDepths(stripBlockAndLineComments(line), state); + } + } + } + return members; +} + +function extractClassBody(source, className) { + const classRe = new RegExp(`\\bclass\\s+${className}\\b[^\\{]*\\{`); + const match = source.match(classRe); + if (!match) { + return null; + } + const open = source.indexOf("{", match.index); + const close = findMatchingBrace(source, open); + return close === -1 ? null : source.slice(open + 1, close); +} + +function javaPropertyName(method) { + if (/^(get|set)[A-Z]/.test(method)) { + return method.slice(3, 4).toLowerCase() + method.slice(4); + } + if (/^is[A-Z]/.test(method)) { + return method.slice(2, 3).toLowerCase() + method.slice(3); + } + return method; +} + +function extractJavaApiMembers(path, options = {}) { + const source = read(path); + const ignore = new Set(options.ignore ?? []); + const accessorProperties = new Set(options.accessorProperties ?? []); + const keepMethodNames = new Set(options.keepMethodNames ?? []); + const members = new Set(); + const defaultClassName = path.split("/").pop().replace(/\.java$/, ""); + const javaBody = options.nestedClass + ? extractClassBody(source, options.nestedClass) + : extractClassBody(source, options.className ?? defaultClassName); + + if (!javaBody) { + fail(`${path}: Java API body was not found`); + return members; + } + + const fieldRe = /^\s*public\s+(?:static\s+)?(?:final\s+)?[A-Za-z0-9_.<>\[\]?]+\s+([A-Za-z_$]\w*(?:\s*,\s*[A-Za-z_$]\w*)*)\s*(?:=|;)/gm; + let field; + while ((field = fieldRe.exec(javaBody)) !== null) { + const before = javaBody.slice(0, field.index); + const depth = (before.match(/\{/g) ?? []).length - (before.match(/\}/g) ?? []).length; + if (depth === 0) { + for (const name of field[1].split(",").map((part) => part.trim())) { + if (!ignore.has(name)) { + members.add(name); + } + } + } + } + + const methodRe = /^\s*public\s+(?:static\s+)?(?!class\b|interface\b)([A-Za-z0-9_.<>\[\], ?]+)\s+([A-Za-z_$]\w*)\s*\(/gm; + let match; + while ((match = methodRe.exec(javaBody)) !== null) { + const before = javaBody.slice(0, match.index); + const depth = (before.match(/\{/g) ?? []).length - (before.match(/\}/g) ?? []).length; + if (depth !== 0) { + continue; + } + const method = match[2]; + if (ignore.has(method)) { + continue; + } + const property = javaPropertyName(method); + if (accessorProperties.has(property)) { + members.add(property); + if (keepMethodNames.has(method)) { + members.add(method); + } + } else { + members.add(method); + } + } + return members; +} + +function extractJavaScriptableMembers(path, objectVariable) { + const source = read(path); + const members = new Set(); + const escapedVariable = objectVariable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const propertyRe = new RegExp( + `ScriptableObject\\.putProperty\\(\\s*${escapedVariable}\\s*,\\s*"([^"]+)"`, + "g", + ); + let match; + while ((match = propertyRe.exec(source)) !== null) { + members.add(match[1]); + } + return members; +} + +function hasDtsConst(paths, name, type) { + const files = Array.isArray(paths) ? paths : [paths]; + const escapedType = type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s*"); + const declarationRe = new RegExp(`declare\\s+const\\s+${name}\\s*:\\s*${escapedType}\\s*;`); + return files.some((path) => declarationRe.test(read(path))); +} + +function hasRuntimeScopeBinding(path, name) { + const source = read(path); + const bindingRe = new RegExp(`ScriptableObject\\.putProperty\\(\\s*scope\\s*,\\s*"${name}"`); + return bindingRe.test(source); +} + +function verifyGlobalDeclarations() { + for (const global of apiManifest.globals) { + if (!hasDtsConst(global.dts, global.name, global.type)) { + fail(`${global.side}: DTS missing global declaration 'declare const ${global.name}: ${global.type};'`); + } + if (global.runtimeContains && !read(global.runtime).includes(global.runtimeContains)) { + fail(`${global.side}: runtime does not initialize global '${global.name}' via ${global.runtimeContains}`); + } else if (!global.runtimeContains && !hasRuntimeScopeBinding(global.runtime, global.name)) { + fail(`${global.side}: runtime does not bind global '${global.name}'`); + } + } +} + +function extractDtsPublicTypeNames() { + const names = new Set(); + for (const file of listFiles("src/main/resources/assets/box3js/template/types")) { + if (!file.endsWith(".d.ts")) continue; + const source = read(`src/main/resources/assets/box3js/template/types/${file}`); + for (const match of source.matchAll(/^(?:declare\s+class|interface)\s+([A-Za-z_$]\w*)/gm)) { + names.add(match[1]); + } + } + return names; +} + +function extractDtsGlobalNames() { + const names = new Set(); + for (const file of listFiles("src/main/resources/assets/box3js/template/types")) { + if (!file.endsWith(".d.ts")) continue; + const source = read(`src/main/resources/assets/box3js/template/types/${file}`); + for (const match of source.matchAll(/^declare\s+const\s+([A-Za-z_$]\w*)\s*:/gm)) { + names.add(match[1]); + } + } + return names; +} + +function verifyManifestCoverage() { + const coveredTypes = new Set(apiManifest.apis.map((api) => api.iface)); + const ignoredTypes = new Set(apiManifest.ignoredDtsTypes ?? []); + for (const typeName of extractDtsPublicTypeNames()) { + if (!coveredTypes.has(typeName) && !ignoredTypes.has(typeName)) { + fail(`manifest: public DTS type '${typeName}' is not covered by tools/box3js-api-manifest.json`); + } + } + + const coveredGlobals = new Set(apiManifest.globals.map((global) => global.name)); + const ignoredGlobals = new Set(apiManifest.ignoredGlobals ?? []); + for (const globalName of extractDtsGlobalNames()) { + if (!coveredGlobals.has(globalName) && !ignoredGlobals.has(globalName)) { + fail(`manifest: global DTS const '${globalName}' is not covered by tools/box3js-api-manifest.json`); + } + } +} + +function verifyJavaDtsApiParity() { + for (const mapping of apiManifest.apis) { + const javaMembers = mapping.expectedMembers + ? new Set(mapping.expectedMembers) + : mapping.javaObject + ? extractJavaScriptableMembers(mapping.javaObject.path, mapping.javaObject.variable) + : extractJavaApiMembers(mapping.java, { + ignore: mapping.ignoreJava, + nestedClass: mapping.nestedClass, + className: mapping.className, + accessorProperties: mapping.accessorProperties, + keepMethodNames: mapping.keepMethodNames, + }); + const dtsMembers = extractDtsInterfaceMembers(mapping.dts, mapping.iface); + + for (const member of javaMembers) { + if (!dtsMembers.has(member)) { + fail(`${mapping.name}: Java exposes '${member}' but ${mapping.iface} DTS does not declare it`); + } + } + + for (const member of dtsMembers) { + if (!javaMembers.has(member)) { + fail(`${mapping.name}: ${mapping.iface} DTS declares '${member}' but Java does not expose it`); + } + } + } +} + +function docsContainApiMember(docs, prefix, member) { + const token = `${prefix}.${member}`; + const headingRe = new RegExp(`^#{2,4}\\s+\`?${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "m"); + return docs.some((path) => { + const source = read(path); + return source.includes(token) || headingRe.test(source); + }); +} + +function verifyDtsDocumentation() { + for (const mapping of apiManifest.apis) { + const details = extractDtsInterfaceMemberDetails(mapping.dts, mapping.iface); + for (const [member, info] of details.entries()) { + if (!info.documented) { + fail(`${mapping.name}: ${mapping.iface}.${member} DTS member must include @zh and @en documentation`); + } + } + } +} + +function verifyDocsApiSync() { + for (const mapping of apiManifest.apis) { + if (!mapping.docs || !mapping.prefix) { + continue; + } + const members = extractDtsInterfaceMembers(mapping.dts, mapping.iface); + for (const member of members) { + if (!docsContainApiMember(mapping.docs, mapping.prefix, member)) { + fail(`${mapping.name}: docs/api missing ${mapping.prefix}.${member} from ${mapping.docs.join(", ")}`); + } + } + } +} + +function verifyEntrypoints() { + const build = read("src/main/resources/assets/box3js/template/build.mjs"); + const engine = read("src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java"); + if (!build.includes("src/server/app.ts") || !build.includes("dist/server.js")) { + fail("build.mjs must build src/server/app.ts to dist/server.js"); + } + if (!build.includes("src/client/app.ts") || !build.includes("dist/client.js")) { + fail("build.mjs must build src/client/app.ts to dist/client.js"); + } + if (engine.includes("app.js")) { + fail("Box3ScriptEngine still references legacy app.js"); + } +} + +verifyTemplateFiles(); +verifyRuntimeTypeSplit(); +verifyEventTokens(); +verifyGlobalDeclarations(); +verifyJavaDtsApiParity(); +verifyDtsDocumentation(); +verifyDocsApiSync(); +verifyEntrypoints(); + +if (failures.length > 0) { + console.error("Box3JS project verification failed:"); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exit(1); +} + +console.log("Box3JS project verification passed."); From d93e69f48bb21f2410d0b73e7d09e222f16db0f2 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 14 May 2026 12:45:23 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(box3js):=20=E6=B7=BB=E5=8A=A0=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF/=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=8F=8C?= =?UTF-8?q?=E7=AB=AF=E8=84=9A=E6=9C=AC=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 Box3JS 添加基于 Rhino 引擎的双端(客户端 + 服务端)TypeScript/JavaScript 脚本支持,取代了之前仅限服务端的实现。包含全面的文档更新、新增的分离式客户端/服务端 API 包结构,以及支持服务端和客户端脚本编译的增强构建流程。 BREAKING CHANGE: Box3JS 现已支持客户端和服务端脚本,取代了之前的仅服务端脚本模式。项目需要更新脚本结构以适配新的双端架构。 --- CLAUDE.md | 178 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 128 insertions(+), 50 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0c1e16a..da72907 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Box3Blocks is a Minecraft mod that imports 372 decorative blocks from the Box3 platform into Minecraft, supporting terrain file import/export and model items. It also includes **Box3JS**, a server-side TypeScript/JavaScript scripting engine (Rhino) for creating custom gameplay, mini-games, and world interactions. +Box3Blocks is a Minecraft mod that imports 372 decorative blocks from the Box3 platform into Minecraft, supporting terrain file import/export and model items. It also includes **Box3JS**, a dual-side (server + client) TypeScript/JavaScript scripting engine (Rhino) for creating custom gameplay, mini-games, GUIs, and world interactions. The repository is a **multi-project monorepo** with 7 independent subprojects targeting different mod loaders and Minecraft versions. There is no root build system — each subproject has its own Gradle wrapper and `build.gradle`. @@ -31,9 +31,12 @@ cd NeoForge-1.21.1 && ./gradlew build # Clean build artifacts cd NeoForge-1.21.1 && ./gradlew clean -# Build Box3JS script (in run/config/box3/script//) -cd run/config/box3/script/colorzone -npm install && npm run build # esbuild → Babel → Rhino target +# Build Box3JS scripts +cd run/config/box3/script/colorzone && npm run build +cd run/config/box3/script/mygame && npm run build + +# Run project verification (Java ↔ DTS ↔ docs consistency) +cd NeoForge-1.21.1 && node tools/verify-box3js-project.mjs ``` **Important:** Forge-1.20.1 requires Java 17. All other subprojects use Java 21+. NeoForge-26.1 uses Java 25. @@ -58,70 +61,143 @@ All subprojects (including NeoForge-1.21.1) share the block mod's runtime genera ## Box3JS Scripting Engine (NeoForge-1.21.1 only) -Box3JS uses Mozilla Rhino to run server-side JavaScript/TypeScript. Scripts live in `run/config/box3/script//`. Each project has its own isolated scope, callbacks, and tracked state. +Box3JS uses Mozilla Rhino to run **dual-side** JavaScript/TypeScript — server scripts (`src/server/`) run on the server thread, client scripts (`src/client/`) run on each player's client. Scripts live in `run/config/box3/script//`. Each project has its own isolated scope, callbacks, and tracked state. + +### Package Structure + +| Package | Role | +|---------|------| +| `script/` | Server-side API bindings, engine, event bus, sandbox | +| `client/` | Client-side engine, API bindings (input, ui, gui, audio, chat, storage, db, http) | +| `registries/` | Recipe manager | +| `standalone/` | JS→JAR compiler, standalone bootstrap, registry code-gen | -### Java Package: `com.box3lab.box3js` +### Server-side Java (`script/`) + +| File | Role | +|------|------| +| `Box3ScriptEngine.java` | Singleton Rhino engine: load/reload/stop scripts, fire events, manage scopes | +| `Box3ScriptCommand.java` | `/box3script` command handler | +| `Box3ScriptConfig.java` | Config: enabled projects, sandbox state, file watcher | +| `Box3ScriptSandbox.java` | Tracks block/entity/player/world mutations for rollback | +| `Box3ScriptTemplate.java` | Template for `/box3script create` | +| `Box3ScriptWatcher.java` | File watching + auto-reload on `.js` change | +| `Box3JSEventBus.java` | Per-project callback storage with isolation | +| `Box3JSCallbacks.java` | Callback interface definitions | +| `Box3JSWorld.java` | `world.*` API: events, entity queries, scoreboard, BossBar, teams, border, particles, fireworks, recipes, structures | +| `Box3JSEntity.java` | `entity.*` API: position, velocity, HP, tags, AI, equipment, effects | +| `Box3JSPlayer.java` | `player.*` API: inventory, flight, game mode, teleport, XP, food, advancements, tab list | +| `Box3JSVoxels.java` | `voxels.*` API: get/set voxel, fill region, spawner control | +| `Box3JSRemoteChannel.java` | `remoteChannel.*` cross-side event communication | +| `Box3JSStorage.java` | Per-project JSON file persistence + `GameDataStorage` inner class | +| `Box3JSHttp.java` | Server-side HTTP API (`http.*`) | +| `Box3DatabaseBase.java` | Shared SQLite database base class (used by server + client db) | +| `Box3JSQueryResult.java` | SQL query result wrapper exposed to JS | +| `Box3JSResponse.java` | HTTP response wrapper (`GameHttpFetchResponse`) | +| `Box3JSConsole.java` | `console.*` (log/warn/error/debug/clear/assert) | +| `Box3JSGuiServerHandler.java` | Handles C→S GUI packets (slot clicks, close) on server thread | +| `Box3JSGuiController.java` | Side-agnostic GUI controller (callbacks via Consumer/Runnable) | +| `Box3JSScriptContainerMenu.java` | `AbstractContainerMenu` subclass using vanilla `MenuType.GENERIC_9xN` | +| `Box3ScriptUtils.java` | Shared helpers: sound, raycast, lookAt, stringify | +| `Box3JSScoreboard.java` / `Box3JSBossbar.java` / `Box3JSTeam.java` | Scoreboard / BossBar / Team CRUD | +| `GameVector3.java` / `GameBounds3.java` / `GameRGBColor.java` / `GameRGBAColor.java` / `GameQuaternion.java` | Math types exposed to JS | +| `GameEventHandlerToken.java` | Returned by all `onXxx()` — has `cancel()` and `active()` | + +### Client-side Java (`client/`) | File | Role | |------|------| -| `Box3JS.java` | `@Mod` entry point, subscribes to NeoForge events, fires callbacks into JS | -| `script/Box3ScriptEngine.java` | Singleton Rhino engine: load/reload/stop scripts, fire events, manage scopes | -| `script/Box3ScriptCommand.java` | `/box3script` command handler | -| `script/Box3ScriptConfig.java` | Config: enabled projects, sandbox state, file watcher | -| `script/Box3ScriptSandbox.java` | Tracks block/entity/player/world mutations for rollback | -| `script/Box3ScriptTemplate.java` | Template for `/box3script create` | -| `script/Box3ScriptWatcher.java` | File watching + auto-reload on `.js` change | -| `script/Box3JSWorld.java` | `world.*` API: events, entity queries, scoreboard, BossBar, teams, border, particles, fireworks, recipes, structures, custom items | -| `script/Box3JSEntity.java` | `entity.*` API: position, velocity, HP, tags, AI, equipment, effects | -| `script/Box3JSPlayer.java` | `player.*` API: inventory, flight, game mode, teleport, XP, food, advancements, tab list | -| `script/Box3JSVoxels.java` | `voxels.*` API: get/set voxel, fill region, spawner control | -| `script/Box3JSQuery.java` | `world.querySelectorAll()` / `entitiesInRadius()` etc. | -| `script/Box3JSEventBus.java` | Per-project callback storage with isolation | -| `script/Box3JSCallbacks.java` | Callback interface definitions | -| `script/Box3JSScoreboard.java` | Scoreboard CRUD | -| `script/Box3JSBossbar.java` | BossBar CRUD | -| `script/Box3JSTeam.java` | Team CRUD | -| `script/Box3JSStorage.java` | Per-project JSON file persistence | -| `script/Box3ScriptUtils.java` | Shared helpers: sound playing, raycast, entity lookAt | -| `script/GameVector3.java` | 3D vector exposed to JS (`new GameVector3(x, y, z)`) | -| `script/GameBounds3.java` | AABB bounds | -| `script/GameRGBColor.java` / `GameRGBAColor.java` | Color types | -| `script/GameQuaternion.java` | Quaternion math | -| `script/GameEventHandlerToken.java` | Returned by `world.onXxx()` — has `cancel()` and `active()` | -| `registries/Box3JSCustomItems.java` | Custom items via Minecraft data components on `minecraft:paper` carrier | -| `registries/Box3JSRecipeManager.java` | Recipe blacklist via `RecipeManager.replaceRecipes()` | - -### DTS Type Constraints - -`world.currentTick` and `world.projectName` are **methods** in `globals.d.ts`, not properties: -```ts -world.currentTick() // ✅ returns number -world.projectName() // ✅ returns string +| `Box3JSClientEngine.java` | Client-side Rhino engine; wires `client`, `audio`, `input`, `ui`, `chat`, `gui`, `remoteChannel`, `storage`, `db`, `http` globals | +| `Box3JSGuiProxy.java` | Returned by `gui.openGUI()` — stores callbacks, sends C→S packets | +| `Box3JSClientStorage.java` | Client-side JSON storage + `GameDataStorage` | +| `Box3JSClientDatabase.java` | Client-side SQLite with graceful-fallback reminder | +| `Box3JSClientHttp.java` | Client-side HTTP API | +| `screen/Box3JSScriptContainerScreen.java` | **DELETED** — vanilla `ChestScreen` renders the container now | + +### Network Layer + +`Box3JSNetwork.java` defines all custom payloads (C↔S): +- **GUI**: `GUIServerboundPayload` (open/click/close), `GUIClientboundPayload` (slot update/close) +- **RemoteChannel**: `RemoteChannelPayload` (server→client event), `RemoteChannelServerboundPayload` (client→server event) + +Payloads are registered with `optional()` — vanilla clients silently ignore them. + +`Box3JS.java` (`@Mod` class) registers all payload handlers, subscribes to NeoForge events, and fires callbacks into JS. + +### key constraints + +- **No custom `MenuType`**: GUI uses vanilla `MenuType.GENERIC_9x1` through `GENERIC_9x6`. This means vanilla clients never see unknown registry keys and can connect safely. +- `world.currentTick` and `world.projectName` are **methods**, not properties: `world.currentTick()`, `world.projectName()` +- All `onXxx()` event registration methods return `GameEventHandlerToken` (has `.cancel()` and `.active()`) + +### DTS Structure + +Template types live in `src/main/resources/assets/box3js/template/types/`: + ``` +types/ + shared.d.ts — math types, console, storage, db, http, remoteChannel + server/ + index.d.ts — references: shared, world, voxels, entity, player + server.d.ts — world, voxels, registries, server-specific remoteChannel/storage + world.d.ts — GameWorld interface + entity.d.ts — GameEntity + GamePlayerEntity type + player.d.ts — GamePlayer interface + voxels.d.ts — GameVoxels interface + client/ + index.d.ts — references: shared, client, audio, input, ui, chat, gui + client.d.ts — GameClient, RemoteChannel (client-side) + audio.d.ts — GameAudio + input.d.ts — GameInput + ui.d.ts — GameUI + chat.d.ts — GameChat + gui.d.ts — GameGUI + GuiController +``` + +**Template sync rule**: When changing template DTS, also sync to `colorzone/types/` and `mygame/types/`. ### Script Build Pipeline -`build.mjs` in each script project does: `esbuild bundle` → `Babel` (target Rhino 1.9.1) → regex sanitize for Rhino. Entry is always `src/app.ts`, output is `dist/app.js`. Supports `--watch` for hot reload. +`build.mjs` in each project: `esbuild bundle` → `Babel` (target Rhino 1.9.1) → regex sanitize. Two entry points: -### Custom Items System +``` +src/server/app.ts → dist/server.js (server-side, runs on server thread) +src/client/app.ts → dist/client.js (client-side, runs on each player's client) +``` + +Supports `--watch` for hot reload. ESLint uses split tsconfig: `tsconfig.server.json` + `tsconfig.client.json` (no root tsconfig). + +### ESLint Config -Uses `minecraft:paper` as carrier with `DataComponents` (CUSTOM_NAME, LORE, CUSTOM_MODEL_DATA, MAX_STACK_SIZE, ENCHANTMENT_GLINT_OVERRIDE, RARITY, FOOD). Client-side textures via resource pack `paper.json` with `custom_model_data` overrides. **No DeferredRegister** — no registry sync needed. +Projects with split tsconfig (no root `tsconfig.json`) must explicitly list them: + +```js +// eslint.config.mjs +export default [ + { + languageOptions: { + parserOptions: { + project: ['./tsconfig.server.json', './tsconfig.client.json'], + }, + }, + }, +]; +``` -Config: `resourcepacks/box3js-items/items.json` + textures + model JSONs. Loaded via `world.loadCustomItems("box3js-items")`. +### Registries System (Standalone/JAR mode) -Consumable/Cooldown/Enchantable/JukeboxPlayable components are NOT available in NeoForge 21.1.220 (need MC 1.21.2+). +`Box3JSRegistryGen.java` reads JSON config files (`registries/blocks.json`, `items.json`, `creativeTabs.json`, `sounds.json`) and generates Java registration code injected into the compiled `@Mod` class. `Box3ScriptCompiler.java` bundles JS source into a JAR; `Box3StandaloneBootstrap.java` launches it. ### Recipe Manager `Box3JSRecipeManager` uses `RecipeManager.replaceRecipes()` (public API, no reflection): -- `removeRecipe(id)` — filters via replaceRecipes -- `clearRecipes()` — restores full original list -- `listRecipes(filter)` — searches by keyword +- `removeRecipe(id)` / `clearRecipes()` / `listRecipes(filter)` ### Documentation -- `docs/api/` — Full API reference for world, entity, player, voxels, storage, math, commands (Chinese + English) -- `docs/tutorial/` — 5-part tutorial series (01-basics → 05-examples) with complete PvP arena and parkour game examples +- `docs/api/` — Full API reference: world, entity, player, voxels, storage, database, http, server, client, math, commands (CN + EN) +- `docs/guide/` — Getting started, architecture, JS vs Java comparison, FAQ, cookbook +- `docs/tutorial/` — 6-part tutorial (01-basics → 06-client-scripting) ## Version Differences @@ -132,5 +208,7 @@ Consumable/Cooldown/Enchantable/JukeboxPlayable components are NOT available in ## Tools -- **`tools/generate_blocks_fabric.py`** / **`tools/generate_blocks_forge.py`** — generates block registration code +- **`tools/verify-box3js-project.mjs`** — 7-check project integrity: template files, type split, event tokens, globals, Java↔DTS parity, DTS docs, docs↔API sync +- **`tools/box3js-api-manifest.json`** — Source of truth: 17 globals + 32 API groups with Java/DTS/docs paths, accessor property rules, ignore lists +- **`tools/generate_blocks_fabric.py`** / **`tools/generate_blocks_forge.py`** — Block registration code generators - **`tools/box3-texture-cut/`** — TypeScript tool for cutting sprite sheets into textures From ed735c601f3ba6e788d52173f8fc377ee31ad1f7 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 14 May 2026 17:26:28 +0800 Subject: [PATCH 4/6] =?UTF-8?q?docs(README):=20=E5=A2=9E=E5=BC=BA=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=8C=E8=A1=A5=E5=85=85=20Box3JS=20=E4=B8=8E=20Box?= =?UTF-8?q?3=20=E5=85=B3=E7=B3=BB=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新主要描述,明确 Box3JS 是社区驱动的 Minecraft 模组 - 添加 Box3JS 与 Box3 关系文档的参考链接 - 扩展 API 设计灵感来源于 Box3 平台的说明 - 新增 about-box3js 指南文档路径 - 修复安装示例代码块的格式问题 - 调整表格列宽,提升可读性 - 优化功能表格和 API 概览表格的文本对齐 --- Box3JS-NeoForge-1.21.1/README.md | 71 +- Box3JS-NeoForge-1.21.1/README_en.md | 5 +- .../docs/BOX3_API_COMPARISON.md | 1686 ++++++++++------- Box3JS-NeoForge-1.21.1/docs/api/README.md | 7 +- Box3JS-NeoForge-1.21.1/docs/api/README_en.md | 7 +- Box3JS-NeoForge-1.21.1/docs/api/client.md | 64 + Box3JS-NeoForge-1.21.1/docs/api/client_en.md | 64 + Box3JS-NeoForge-1.21.1/docs/api/server.md | 2 +- Box3JS-NeoForge-1.21.1/docs/api/server_en.md | 2 +- Box3JS-NeoForge-1.21.1/docs/api/world.md | 30 +- Box3JS-NeoForge-1.21.1/docs/api/world_en.md | 30 +- Box3JS-NeoForge-1.21.1/docs/guide/README.md | 25 +- .../docs/guide/README_en.md | 25 +- .../docs/guide/about-box3js.md | 137 ++ .../docs/guide/about-box3js_en.md | 139 ++ .../docs/guide/getting-started.md | 867 ++++++++- .../docs/guide/getting-started_en.md | 837 +++++++- Box3JS-NeoForge-1.21.1/docs/guide/recipes.md | 8 +- .../docs/guide/recipes_en.md | 8 +- .../docs/tutorial/01-basics.md | 8 +- .../docs/tutorial/01-basics_en.md | 8 +- .../docs/tutorial/03-events-entities.md | 13 +- .../docs/tutorial/03-events-entities_en.md | 13 +- .../docs/tutorial/04-advanced-systems.md | 8 +- .../docs/tutorial/04-advanced-systems_en.md | 8 +- .../docs/tutorial/05-examples.md | 40 +- .../docs/tutorial/05-examples_en.md | 40 +- .../docs/tutorial/06-client-scripting.md | 34 +- .../docs/tutorial/06-client-scripting_en.md | 34 +- .../docs/tutorial/README.md | 2 +- .../docs/tutorial/README_en.md | 2 +- .../box3js/client/Box3JSClientEngine.java | 191 ++ .../box3lab/box3js/script/Box3JSWorld.java | 7 - .../box3js/script/Box3ScriptEngine.java | 20 + .../box3js/template/types/client/client.d.ts | 53 +- .../box3js/template/types/server/world.d.ts | 26 - .../assets/box3js/template/types/shared.d.ts | 33 + .../tools/box3js-api-manifest.json | 4 + .../tools/verify-box3js-project.mjs | 5 + 39 files changed, 3428 insertions(+), 1135 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/guide/about-box3js_en.md diff --git a/Box3JS-NeoForge-1.21.1/README.md b/Box3JS-NeoForge-1.21.1/README.md index 4f41d40..faecf9b 100644 --- a/Box3JS-NeoForge-1.21.1/README.md +++ b/Box3JS-NeoForge-1.21.1/README.md @@ -6,7 +6,9 @@ **无需 Java 知识,用 TypeScript 为你的 Minecraft 服务器创造无限玩法。** -Box3JS 是一个内置于 NeoForge 模组的服务端脚本引擎(Mozilla Rhino),延续了神奇代码岛的 API 风格。告别复杂的 Java 模组开发——写 TypeScript,一键热重载,即时生效。PvP 竞技场、RPG 副本、派对小游戏、世界管理、社交工具,全能用脚本快速实现。 +Box3JS 是一个社区驱动的 Minecraft 模组,在服务端嵌入 Mozilla Rhino JavaScript 引擎。它的 API 设计延续了[神奇代码岛](https://box3.fun)(深圳奇梦岛科技有限公司)的风格——把 Box3 平台简洁高效的开发体验带进 Minecraft。告别复杂的 Java 模组开发:写 TypeScript,一键热重载,即时生效。PvP 竞技场、RPG 副本、派对小游戏、世界管理、社交工具,全能用脚本快速实现。 + +> 了解 Box3JS 与神奇代码岛的关系?→ [Box3JS 与神奇代码岛](docs/guide/about-box3js.md) ## 安装 @@ -24,7 +26,7 @@ Box3JS 是一个内置于 NeoForge 模组的服务端脚本引擎(Mozilla Rhin 这会创建 TypeScript 项目: -``` +```` config/box3/script/mygame/ ├── package.json ← npm 依赖(esbuild、Babel、TypeScript) ├── tsconfig.base.json ← 公共 TS 编译选项 @@ -47,7 +49,7 @@ config/box3/script/mygame/ ```bash cd config/box3/script/mygame npm install && npm run build -``` +```` ``` /box3script sandbox mygame # (推荐) 开启沙盒,放心测试 @@ -58,19 +60,19 @@ npm install && npm run build ## 为什么选择 Box3JS? -| 特性 | 说明 | -| -------------- | -------------------------------------------------------------------------------- | -| **零门槛** | 会 JS/TS 就能写,无需 Gradle、无需 IDE、无需重启 | -| **热重载** | 改代码 → build → reload,秒级生效。开启 `watch` 自动重载 | -| **沙盒保护** | 开启沙盒自动追踪所有脚本修改,关闭时完整回滚,服务器不留痕迹 | -| **TypeScript** | 完整 `.d.ts` 类型声明,esbuild + Babel 编译管线,享受智能提示 | -| **20+ 种事件** | onTick、onPlayerJoin、onChat、onEntityDeath、onBlockActivate、onButtonPressed... | -| **视觉效果** | 13+ 粒子、烟花、闪电、爆炸、音效 | -| **客户端 API** | 键盘输入、屏幕 UI、聊天拦截、音效/音乐控制、客户端存储、SQLite、HTTP、双向事件通道 | -| **游戏系统** | 计分板、BossBar、队伍、世界边界、跨脚本通信 | -| **自定义注册表** | JSON 配置注册方块、物品(食物/工具/盔甲)、音效与创造标签页,编译为独立 JAR | -| **数据持久化** | JSON 存储 + SQLite 数据库(排行榜、经济、玩家数据) | -| **独立打包** | `/box3script compile` 将脚本编译为独立 JAR 模组,便于分发部署 | +| 特性 | 说明 | +| ---------------- | ---------------------------------------------------------------------------------- | +| **零门槛** | 会 JS/TS 就能写,无需 Gradle、无需 IDE、无需重启 | +| **热重载** | 改代码 → build → reload,秒级生效。开启 `watch` 自动重载 | +| **沙盒保护** | 开启沙盒自动追踪所有脚本修改,关闭时完整回滚,服务器不留痕迹 | +| **TypeScript** | 完整 `.d.ts` 类型声明,esbuild + Babel 编译管线,享受智能提示 | +| **20+ 种事件** | onTick、onPlayerJoin、onChat、onEntityDeath、onBlockActivate、onButtonPressed... | +| **视觉效果** | 13+ 粒子、烟花、闪电、爆炸、音效 | +| **客户端 API** | 键盘输入、屏幕 UI、聊天拦截、音效/音乐控制、客户端存储、SQLite、HTTP、双向事件通道 | +| **游戏系统** | 计分板、BossBar、队伍、世界边界、跨脚本通信 | +| **自定义注册表** | JSON 配置注册方块、物品(食物/工具/盔甲)、音效与创造标签页,编译为独立 JAR | +| **数据持久化** | JSON 存储 + SQLite 数据库(排行榜、经济、玩家数据) | +| **独立打包** | `/box3script compile` 将脚本编译为独立 JAR 模组,便于分发部署 | ## 命令 @@ -89,23 +91,23 @@ npm install && npm run build ## API 速览 -| 全局对象 | 用途 | -| -------------------------------- | ----------------------------------------------------------------------------------- | -| `world` | 世界状态、事件回调、粒子、烟花、闪电、音效、计分板、BossBar、队伍、边界 | -| `entity` | 实体属性、AI 寻路、装备、药水效果、标签、导航 | -| `player` | 背包、飞行、游戏模式、传送、消息、经验、音效 | -| `voxels` | 方块读写、区域填充、刷怪笼 | -| `http` | HTTP 网络请求(同步 + 异步,GET/POST/JSON) | -| `remoteChannel` | 服务端 ↔ 客户端双向事件通讯 | -| `registries` | 自定义方块/物品/音效(编译 JAR 模式),见 [registries.md](docs/api/registries.md) | -| `client` · `input` · `ui` · `chat` · `audio` | 客户端脚本:生命周期、键盘、屏幕文字、聊天、音频控制 | -| `storage` | JSON 数据持久化(服务端 & 客户端) | -| `db` | SQLite 数据库(服务端 & 客户端) | -| `console` | 控制台日志输出(`log`/`warn`/`error`/`debug`/`assert`/`clear`) | -| `GameVector3` | 三维向量(坐标运算) | -| `GameBounds3` | 包围盒 | -| `GameRGBColor` / `GameRGBAColor` | RGB/RGBA 颜色 | -| `GameQuaternion` | 四元数(旋转运算) | +| 全局对象 | 用途 | +| -------------------------------------------- | --------------------------------------------------------------------------------- | +| `world` | 世界状态、事件回调、粒子、烟花、闪电、音效、计分板、BossBar、队伍、边界 | +| `entity` | 实体属性、AI 寻路、装备、药水效果、标签、导航 | +| `player` | 背包、飞行、游戏模式、传送、消息、经验、音效 | +| `voxels` | 方块读写、区域填充、刷怪笼 | +| `http` | HTTP 网络请求(同步 + 异步,GET/POST/JSON) | +| `remoteChannel` | 服务端 ↔ 客户端双向事件通讯 | +| `registries` | 自定义方块/物品/音效(编译 JAR 模式),见 [registries.md](docs/api/registries.md) | +| `client` · `input` · `ui` · `chat` · `audio` | 客户端脚本:生命周期、键盘、屏幕文字、聊天、音频控制 | +| `storage` | JSON 数据持久化(服务端 & 客户端) | +| `db` | SQLite 数据库(服务端 & 客户端) | +| `console` | 控制台日志输出(`log`/`warn`/`error`/`debug`/`assert`/`clear`) | +| `GameVector3` | 三维向量(坐标运算) | +| `GameBounds3` | 包围盒 | +| `GameRGBColor` / `GameRGBAColor` | RGB/RGBA 颜色 | +| `GameQuaternion` | 四元数(旋转运算) | [文档首页 →](docs/README.md) · [API 总览 →](docs/api/README.md) · [按任务速查 →](docs/api/README.md#功能速查---我想) @@ -116,7 +118,7 @@ npm install && npm run build | # | 教程 | 时长 | 学什么 | | --- | ----------------------------------------------------- | ------ | ---------------------------------------- | | 1 | [从零开始](docs/tutorial/01-basics.md) | 10 min | 创建项目、第一个脚本、聊天命令、定时任务 | -| 2 | [玩家操控与物品](docs/tutorial/02-player-items.md) | 15 min | 传送、飞行、物品、附魔、药水 | +| 2 | [玩家操控与物品](docs/tutorial/02-player-items.md) | 15 min | 传送、飞行、物品、附魔、药水 | | 3 | [事件系统与实体](docs/tutorial/03-events-entities.md) | 15 min | 事件回调、实体生成、AI、战斗、巡逻 | | 4 | [高级游戏系统](docs/tutorial/04-advanced-systems.md) | 15 min | 计分板、BossBar、队伍、边界、跨脚本通信 | | 5 | [实战小游戏](docs/tutorial/05-examples.md) | 20 min | PvP 竞技场、粒子烟花、波次刷怪、特效大全 | @@ -129,6 +131,7 @@ npm install && npm run build docs/ ├── guide/ ← 入门指南 │ ├── README.md 指南总览 +│ ├── about-box3js.md Box3JS 与神奇代码岛(起源、关系、优势) │ ├── getting-started.md 从零开始(环境、第一个脚本、调试、发布) │ ├── architecture.md 运行原理(Rhino 引擎、作用域、构建管线) │ └── js-vs-java.md JS vs Java 模组开发对比 diff --git a/Box3JS-NeoForge-1.21.1/README_en.md b/Box3JS-NeoForge-1.21.1/README_en.md index d881602..4d91210 100644 --- a/Box3JS-NeoForge-1.21.1/README_en.md +++ b/Box3JS-NeoForge-1.21.1/README_en.md @@ -6,7 +6,9 @@ **No Java knowledge required. Build unlimited Minecraft gameplay with TypeScript.** -Box3JS is a server-side scripting engine (Mozilla Rhino) built into a NeoForge mod. Forget complex Java mod development — write TypeScript, hot-reload instantly, see changes live. PvP arenas, RPG dungeons, party games, world management, social tools — all achievable with scripts. +Box3JS is a community-driven Minecraft mod (NeoForge 1.21.1) that embeds the Mozilla Rhino JavaScript engine in the server. Its API design is inspired by [Box3](https://box3.fun) (Shenzhen Qimengdao Technology Co., Ltd.) — bringing Box3's clean, efficient developer experience into Minecraft. Forget complex Java mod development: write TypeScript, hot-reload instantly, see changes live. PvP arenas, RPG dungeons, party games, world management, social tools — all achievable with scripts. + +> Curious about Box3JS's relationship with the Box3 platform? → [Box3JS & Box3](docs/guide/about-box3js_en.md) ## Installation @@ -129,6 +131,7 @@ From zero to full mini-games. Every example is TypeScript-compiled and ESLint-ve docs/ ├── guide/ ← Getting Started │ ├── README.md Guide overview +│ ├── about-box3js.md Box3JS & Box3 (origin, relationship, advantages) │ ├── getting-started.md From zero (setup, first script, debug, deploy) │ ├── architecture.md Internals (Rhino engine, scopes, build pipeline) │ └── js-vs-java.md JS vs Java modding comparison diff --git a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md index 14bfc72..73b834d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md +++ b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md @@ -1,6 +1,6 @@ # Box3 API vs Box3JS 实现对比 -本文档详细对比官方 Box3 平台 API 与 Box3JS 模组(NeoForge 1.21.1)的实现差异。仅涉及**服务端 API**,因为客户端 API(ClientUI、ClientAudio、ClientMedia 等)在 Box3JS 中完全不可用——Minecraft 模组运行在服务端,没有 Box3 平台的客户端渲染环境。 +本文档详细对比官方 Box3 平台 API 与 Box3JS 模组的实现差异。 > **图例**: ✅ 已实现 | ⚠️ 部分实现 | ❌ 未实现 | ⬆ 独有扩展 @@ -15,7 +15,7 @@ 5. [GameDataStorage (storage)](#5-gamedatastorage-storage) 6. [Math 类型](#6-math-类型) 7. [其他服务端 API](#7-其他服务端-api) -8. [客户端 API(不适用)](#8-客户端-api不适用) +8. [客户端 API](#8-客户端-api) 9. [Box3JS 独有 MC 扩展](#9-box3js-独有-mc-扩展) 10. [总结](#10-总结) @@ -25,111 +25,112 @@ ### 1.1 基础属性 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.projectName()` (只读方法) | `world.projectName` (属性) | ✅ | 返回当前脚本项目名;服务器 MOTD 使用 `world.serverId` | -| `world.serverId` (属性) | `world.serverId` (读写属性) | ✅ | 一致。映射到服务端 MOTD(get/set) | -| `world.currentTick()` (只读方法) | `world.currentTick` (属性) | ✅ | 返回服务器总 tick 数 | -| `world.url` (只读属性) | — | ❌ | 未实现。MC 无地图 URL 概念 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------- | --------------------------- | ---- | ----------------------------------------------------- | +| `world.projectName()` (只读方法) | `world.projectName` (属性) | ✅ | 返回当前脚本项目名;服务器 MOTD 使用 `world.serverId` | +| `world.serverId` (属性) | `world.serverId` (读写属性) | ✅ | 一致。映射到服务端 MOTD(get/set) | +| `world.currentTick()` (只读方法) | `world.currentTick` (属性) | ✅ | 返回服务器总 tick 数 | +| `world.url` (只读属性) | — | ❌ | 未实现。MC 无地图 URL 概念 | ### 1.2 天气 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.rainDensity` (属性 0-1) | `world.rainDensity` (属性) | ✅ | 一致 | -| — | `world.thunderDensity` (属性) | ⬆ | MC 扩展。Box3 无打雷概念 | -| — | `world.clearWeather()` | ⬆ | MC 扩展。清除雨和雷 | -| `world.maxFog` | — | ❌ | | -| `world.fogColor` | — | ❌ | | -| `world.fogStartDistance` | — | ❌ | | -| `world.fogHeightOffset` | — | ❌ | | -| `world.fogUniformDensity` | — | ❌ | | -| `world.fogHeightFalloff` | — | ❌ | | -| `world.rainSpeed` | — | ❌ | | -| `world.rainColor` | — | ❌ | | -| `world.rainDirection` | — | ❌ | | -| `world.rainInterference` | — | ❌ | | -| `world.rainSizeLo/Hi` | — | ❌ | | -| `world.snowColor` | — | ❌ | Box3 有独立雪花系统,MC 降雪依附于原版天气 | -| `world.snowTexture` | — | ❌ | | -| `world.snowDensity` | — | ❌ | | -| `world.snowFallSpeed` | — | ❌ | | -| `world.snowSpinSpeed` | — | ❌ | | -| `world.snowSizeLo/Hi` | — | ❌ | | -| `world.lightMode` | — | ❌ | Box3 的手动/自然光照模式 | -| `world.sunFrequency/Phase/Direction/Light` | — | ❌ | | -| `world.skyLeftLight/RightLight/...` | — | ❌ | Box3 六个方向环境光 | -| `world.lunarPhase` | — | ❌ | | - -**原因**: Box3 拥有独立的天气/光照渲染引擎,可以精细控制雾、雨、雪、光照参数。MC 的天气和光照系统由原版引擎控制,模组无法在不安装客户端 mod 的情况下改变这些视觉效果。要实现这些需要客户端侧渲染 hook,超出了服务端脚本引擎的范围。 +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------ | ----------------------------- | ---- | ------------------------------------------ | +| `world.rainDensity` (属性 0-1) | `world.rainDensity` (属性) | ✅ | 一致 | +| — | `world.thunderDensity` (属性) | ⬆ | MC 扩展。Box3 无打雷概念 | +| — | `world.clearWeather()` | ⬆ | MC 扩展。清除雨和雷 | +| `world.maxFog` | `client.setFogEndDistance(d)` | ✅ | 客户端 API。设置雾完全遮挡距离(方块) | +| `world.fogColor` | `client.setFogColor(r, g, b)` | ✅ | 客户端 API。RGB 0-255 | +| `world.fogStartDistance` | `client.setFogStartDistance(d)` | ✅ | 客户端 API。雾开始距离(方块) | +| `world.fogHeightOffset` | — | ❌ | | +| `world.fogUniformDensity` | — | ❌ | | +| `world.fogHeightFalloff` | — | ❌ | | +| `world.rainSpeed` | — | ❌ | | +| `world.rainColor` | — | ❌ | | +| `world.rainDirection` | — | ❌ | | +| `world.rainInterference` | — | ❌ | | +| `world.rainSizeLo/Hi` | — | ❌ | | +| `world.snowColor` | — | ❌ | Box3 有独立雪花系统,MC 降雪依附于原版天气 | +| `world.snowTexture` | — | ❌ | | +| `world.snowDensity` | — | ❌ | | +| `world.snowFallSpeed` | — | ❌ | | +| `world.snowSpinSpeed` | — | ❌ | | +| `world.snowSizeLo/Hi` | — | ❌ | | +| `world.lightMode` | — | ❌ | Box3 的手动/自然光照模式 | +| `world.sunFrequency/Phase/Direction/Light` | — | ❌ | | +| `world.skyLeftLight/RightLight/...` | — | ❌ | Box3 六个方向环境光 | +| `world.lunarPhase` | — | ❌ | | + +**原因**: Box3 拥有独立的天气/光照渲染引擎,可以精细控制雾、雨、雪、光照参数。MC 的天气和光照系统由原版引擎控制,这些视觉效果需要客户端侧渲染 hook。Box3JS 客户端脚本引擎**已暴露雾颜色和距离 API**(`client.setFogColor` / `client.setFogStartDistance` / `client.setFogEndDistance` / `client.resetFog`)。雪花/光照等高级渲染参数 API 未来可继续扩展。 ### 1.3 时间 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.time` (属性) | `world.time` (属性, 本质是 getTime/setTime) | ✅ | 一致。Box3 一天 = 24000 tick | -| `world.setTime(tick)` | `world.setTime(tick)` | ✅ | 便捷方法,一致 | -| `world.timeScale` (属性 0-1) | `world.timeScale` (属性) | ✅ | 一致。底层操作 doDaylightCycle 规则 | -| — | `world.setTime(tick)` | ⬆ | 等同于 `world.time = tick` | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ---------------------------- | ------------------------------------------- | ---- | ----------------------------------- | +| `world.time` (属性) | `world.time` (属性, 本质是 getTime/setTime) | ✅ | 一致。Box3 一天 = 24000 tick | +| `world.setTime(tick)` | `world.setTime(tick)` | ✅ | 便捷方法,一致 | +| `world.timeScale` (属性 0-1) | `world.timeScale` (属性) | ✅ | 一致。底层操作 doDaylightCycle 规则 | +| — | `world.setTime(tick)` | ⬆ | 等同于 `world.time = tick` | ### 1.4 难度 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.difficulty` (属性) | `world.difficulty` (属性) | ✅ | 一致。get 返回名称字符串,set 接受名称或数字 0-3 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------- | ------------------------- | ---- | ------------------------------------------------ | +| `world.difficulty` (属性) | `world.difficulty` (属性) | ✅ | 一致。get 返回名称字符串,set 接受名称或数字 0-3 | ### 1.5 出生点 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.spawnPoint` (只读属性) | `world.spawnPoint` (只读属性) | ✅ | 一致,返回 GameVector3 | -| `world.setWorldSpawn(pos)` | `world.setWorldSpawn(pos)` | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | ----------------------------- | ---- | ---------------------- | +| `world.spawnPoint` (只读属性) | `world.spawnPoint` (只读属性) | ✅ | 一致,返回 GameVector3 | +| `world.setWorldSpawn(pos)` | `world.setWorldSpawn(pos)` | ✅ | 一致 | ### 1.6 游戏规则 Box3 **没有**游戏规则 API。Box3JS 完全为 MC 扩展: -| Box3JS API | 说明 | -|------------|------| -| `world.getGameRule(name)` | MC 扩展。获取游戏规则布尔值 | +| Box3JS API | 说明 | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `world.getGameRule(name)` | MC 扩展。获取游戏规则布尔值 | | `world.setGameRule(name, value)` | MC 扩展。支持 7 种规则:doDaylightCycle, doWeatherCycle, keepInventory, doMobSpawning, doFireTick, mobGriefing, doImmediateRespawn | ### 1.7 物理系统 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.useOBB` | — | ❌ | OBB 碰撞检测 | -| `world.gravity` | — | ❌ | 世界重力 | -| `world.airFriction` | — | ❌ | 空气阻力 | -| `world.addCollisionFilter(a, b)` | — | ❌ | 碰撞过滤 | -| `world.removeCollisionFilter(a, b)` | — | ❌ | | -| `world.clearCollisionFilters()` | — | ❌ | | -| `world.collisionFilters()` | — | ❌ | | -| `world.testSelector(sel, ent)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------------- | ----------- | ---- | ------------ | +| `world.useOBB` | — | ❌ | OBB 碰撞检测 | +| `world.gravity` | — | ❌ | 世界重力 | +| `world.airFriction` | — | ❌ | 空气阻力 | +| `world.addCollisionFilter(a, b)` | — | ❌ | 碰撞过滤 | +| `world.removeCollisionFilter(a, b)` | — | ❌ | | +| `world.clearCollisionFilters()` | — | ❌ | | +| `world.collisionFilters()` | — | ❌ | | +| `world.testSelector(sel, ent)` | — | ❌ | | **原因**: MC 的物理系统由原版引擎控制,重力/碰撞/摩擦力是全局常量,无法通过脚本动态修改。 ### 1.8 实体生成 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.createEntity(config)` | `world.createEntity(config)` | ✅ | 一致。接受 NativeObject 配置对象,支持 type/position/velocity/fixed/gravity/friction/mass/restitution/collides/meshInvisible/hp/maxHp/tags 字段 | -| `world.spawnEntity(type, pos)` | `world.spawnEntity(type, pos)` | ⬆ | 便捷方法。简化版生成,仅接受类型+位置 | -| `world.entityQuota()` | — | ❌ | 实体配额查询 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | ------------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `world.createEntity(config)` | `world.createEntity(config)` | ✅ | 一致。接受 NativeObject 配置对象,支持 type/position/velocity/fixed/gravity/friction/mass/restitution/collides/meshInvisible/hp/maxHp/tags 字段 | +| `world.spawnEntity(type, pos)` | `world.spawnEntity(type, pos)` | ⬆ | 便捷方法。简化版生成,仅接受类型+位置 | +| `world.entityQuota()` | — | ❌ | 实体配额查询 | ### 1.9 查询 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.querySelector(selector)` | `world.querySelector(selector)` | ✅ | 一致 | -| `world.querySelectorAll(selector)` | `world.querySelectorAll(selector)` | ✅ | 一致 | -| `world.searchBox(bounds)` | `world.searchBox(bounds)` | ✅ | 一致。接受 GameBounds3,内部委托 entitiesInArea | -| `world.raycast(origin, dir, options?)` | `world.raycast(origin, dir)` / `world.raycast(origin, dir, maxDist)` | ⚠️ | Box3JS 无 options 对象(无 ignoreFluid/ignoreVoxel/ignoreEntities 等),仅支持 maxDistance | -| — | `world.entitiesInArea(pos1, pos2)` | ⬆ | MC 扩展 | -| — | `world.entitiesInRadius(x, y, z, r)` / `world.entitiesInRadius(pos, r)` | ⬆ | MC 扩展 | -| — | `world.getBiome(x, y, z)` / `world.getBiome(pos)` | ⬆ | MC 扩展。返回生物群系 ID(如 "minecraft:plains") | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------------- | ----------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------ | +| `world.querySelector(selector)` | `world.querySelector(selector)` | ✅ | 一致 | +| `world.querySelectorAll(selector)` | `world.querySelectorAll(selector)` | ✅ | 一致 | +| `world.searchBox(bounds)` | `world.searchBox(bounds)` | ✅ | 一致。接受 GameBounds3,内部委托 entitiesInArea | +| `world.raycast(origin, dir, options?)` | `world.raycast(origin, dir)` / `world.raycast(origin, dir, maxDist)` | ⚠️ | Box3JS 无 options 对象(无 ignoreFluid/ignoreVoxel/ignoreEntities 等),仅支持 maxDistance | +| — | `world.entitiesInArea(pos1, pos2)` | ⬆ | MC 扩展 | +| — | `world.entitiesInRadius(x, y, z, r)` / `world.entitiesInRadius(pos, r)` | ⬆ | MC 扩展 | +| — | `world.getBiome(x, y, z)` / `world.getBiome(pos)` | ⬆ | MC 扩展。返回生物群系 ID(如 "minecraft:plains") | **Raycast 返回值差异**: + - Box3 返回: `{origin, direction, distance, hit, hitEntity, hitPosition, hitVoxel, voxelIndex, normal}` - Box3JS 返回: `{hit, x, y, z, normalX, normalY, normalZ, distance, entity, voxel}` - 差异: 字段命名不同 (`normal` → `normalX/Y/Z`, `hitEntity` → `entity`),无 `origin/direction/hitPosition/voxelIndex` @@ -138,59 +139,60 @@ Box3 **没有**游戏规则 API。Box3JS 完全为 MC 扩展: Box3 的区域系统完全未实现: -| Box3 API | 状态 | -|----------|------| -| `world.addZone(config)` | ❌ | -| `world.removeZone(trigger)` | ❌ | -| `world.zones()` | ❌ | -| `zone.onEnter / zone.onLeave` | ❌ | -| `zone.entities` | ❌ | -| `zone.remove()` | ❌ | +| Box3 API | 状态 | +| ----------------------------- | ---- | +| `world.addZone(config)` | ❌ | +| `world.removeZone(trigger)` | ❌ | +| `world.zones()` | ❌ | +| `zone.onEnter / zone.onLeave` | ❌ | +| `zone.entities` | ❌ | +| `zone.remove()` | ❌ | -Zone 可以设置局部天气/光照/力场参数,这在 MC 服务端无法实现(需要客户端渲染支持)。 +Zone 可以设置局部天气/光照/力场参数,需要客户端渲染支持(Box3JS 客户端引擎已可用,但具体的 Zone 渲染 API 尚未暴露)。 ### 1.11 聊天 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.say(message)` | `world.say(message)` | ✅ | 一致。全服广播 | -| `world.createTempChat(chatId?)` | — | ❌ | 临时聊天频道 | -| `world.destroyTempChat(chatId)` | — | ❌ | | -| `world.addTempChatPlayer(chatId, player)` | — | ❌ | | -| `world.removeTempChatPlayer(chatId, player)` | — | ❌ | | -| `world.getTempChats()` | — | ❌ | | -| `world.getTempChatUsers(chatId)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------------------- | -------------------- | ---- | -------------- | +| `world.say(message)` | `world.say(message)` | ✅ | 一致。全服广播 | +| `world.createTempChat(chatId?)` | — | ❌ | 临时聊天频道 | +| `world.destroyTempChat(chatId)` | — | ❌ | | +| `world.addTempChatPlayer(chatId, player)` | — | ❌ | | +| `world.removeTempChatPlayer(chatId, player)` | — | ❌ | | +| `world.getTempChats()` | — | ❌ | | +| `world.getTempChatUsers(chatId)` | — | ❌ | | ### 1.12 事件回调 -| Box3 API | Box3JS 实现 | 状态 | 回调签名差异 | -|----------|-------------|------|-------------| -| `world.onTick(fn)` | `world.onTick(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{tick, prevTick, elapsedTimeMS, skip}`;Box3JS 已对齐传入 NativeObject | -| `world.onPlayerJoin(fn)` | `world.onPlayerJoin(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, tick}`;Box3JS 传入 `(entity, tick)` | -| `world.onPlayerLeave(fn)` | `world.onPlayerLeave(fn)` → GameEventHandlerToken | ✅ | 同上 | -| `world.onChat(fn)` | `world.onChat(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, message, tick}`;Box3JS 传入 `(entity, message, tick)` — 展开参数 | -| `world.onInteract(fn)` | `world.onInteract(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, targetEntity, tick}`;Box3JS 传入 `(entity, target, tick)` — 展开参数 | -| `world.onEntityContact(fn)` | `world.onEntityContact(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{axis, entity, force, other, tick}`;Box3JS 传入 `(entity, other, tick)` — 缺少 axis/force | -| `world.onEntitySeparate(fn)` | `world.onEntitySeparate(fn)` → GameEventHandlerToken | ✅ | 同上 | -| `world.onVoxelContact(fn)` | `world.onVoxelContact(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{axis, entity, force, voxel, tick, x, y, z}`;Box3JS 传入 `(entity, voxelId, x, y, z, contactType, force, tick)` | -| `world.onFluidEnter(fn)` | `world.onFluidEnter(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, voxel, tick}`;Box3JS 传入 `(entity, fluid, x, y, z, tick)` | -| `world.onFluidLeave(fn)` | `world.onFluidLeave(fn)` → GameEventHandlerToken | ✅ | 同上 | -| `world.onEntityCreate(fn)` | — | ❌ | | -| `world.onEntityDestroy(fn)` | — | ❌ | 实体层面有 `entity.onDestroy` 属性 | -| `world.onClick(fn)` | — | ❌ | Box3 鼠标点击事件,MC 无对应 | -| `world.onPress(fn)` | `world.onButtonPressed(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, button, raycast, tick, position, pressed}`;Box3JS 传入 `(entity, button, tick)` — 简化版 | -| `world.onRelease(fn)` | — | ❌ | 按键释放事件 | -| `world.onTakeDamage(fn)` | `world.onEntityDamage(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, attacker, damage, damageType, tick}`;Box3JS 传入 `(entity, amount, source, attacker, tick)` — 展开参数 | -| `world.onDie(fn)` | `world.onEntityDeath(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, attacker, damageType, tick}`;Box3JS 传入 `(entity, killer, tick)`,killer 可能为 null | -| `world.onRespawn(fn)` | `world.onPlayerRespawn(fn)` → GameEventHandlerToken | ⚠️ | 命名不同(Respawn → PlayerRespawn),Box3 传入 `{entity, tick}`;Box3JS 传入 `(entity, tick)` | -| `world.onPlayerPurchaseSuccess(fn)` | — | ❌ | MC 无商城系统 | -| — | `world.onVoxelDestroy(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, tick)` | -| — | `world.onBlockPlace(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, voxelId, tick)` | -| — | `world.onBlockActivate(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, tick)` | -| — | `world.onEntityDamage(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, amount, source, attacker, tick)` | -| — | `world.onMessage(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。跨脚本消息 | +| Box3 API | Box3JS 实现 | 状态 | 回调签名差异 | +| ----------------------------------- | ---------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------- | +| `world.onTick(fn)` | `world.onTick(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{tick, prevTick, elapsedTimeMS, skip}`;Box3JS 已对齐传入 NativeObject | +| `world.onPlayerJoin(fn)` | `world.onPlayerJoin(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, tick}`;Box3JS 传入 `(entity, tick)` | +| `world.onPlayerLeave(fn)` | `world.onPlayerLeave(fn)` → GameEventHandlerToken | ✅ | 同上 | +| `world.onChat(fn)` | `world.onChat(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, message, tick}`;Box3JS 传入 `(entity, message, tick)` — 展开参数 | +| `world.onInteract(fn)` | `world.onInteract(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, targetEntity, tick}`;Box3JS 传入 `(entity, target, tick)` — 展开参数 | +| `world.onEntityContact(fn)` | `world.onEntityContact(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{axis, entity, force, other, tick}`;Box3JS 传入 `(entity, other, tick)` — 缺少 axis/force | +| `world.onEntitySeparate(fn)` | `world.onEntitySeparate(fn)` → GameEventHandlerToken | ✅ | 同上 | +| `world.onVoxelContact(fn)` | `world.onVoxelContact(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{axis, entity, force, voxel, tick, x, y, z}`;Box3JS 传入 `(entity, voxelId, x, y, z, contactType, force, tick)` | +| `world.onFluidEnter(fn)` | `world.onFluidEnter(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, voxel, tick}`;Box3JS 传入 `(entity, fluid, x, y, z, tick)` | +| `world.onFluidLeave(fn)` | `world.onFluidLeave(fn)` → GameEventHandlerToken | ✅ | 同上 | +| `world.onEntityCreate(fn)` | — | ❌ | | +| `world.onEntityDestroy(fn)` | — | ❌ | 实体层面有 `entity.onDestroy` 属性 | +| `world.onClick(fn)` | — | ❌ | Box3 鼠标点击事件,MC 无对应 | +| `world.onPress(fn)` | `world.onButtonPressed(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, button, raycast, tick, position, pressed}`;Box3JS 传入 `(entity, button, tick)` — 简化版 | +| `world.onRelease(fn)` | — | ❌ | 按键释放事件 | +| `world.onTakeDamage(fn)` | `world.onEntityDamage(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, attacker, damage, damageType, tick}`;Box3JS 传入 `(entity, amount, source, attacker, tick)` — 展开参数 | +| `world.onDie(fn)` | `world.onEntityDeath(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, attacker, damageType, tick}`;Box3JS 传入 `(entity, killer, tick)`,killer 可能为 null | +| `world.onRespawn(fn)` | `world.onPlayerRespawn(fn)` → GameEventHandlerToken | ⚠️ | 命名不同(Respawn → PlayerRespawn),Box3 传入 `{entity, tick}`;Box3JS 传入 `(entity, tick)` | +| `world.onPlayerPurchaseSuccess(fn)` | — | ❌ | MC 无商城系统 | +| — | `world.onVoxelDestroy(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, tick)` | +| — | `world.onBlockPlace(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, voxelId, tick)` | +| — | `world.onBlockActivate(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, tick)` | +| — | `world.onEntityDamage(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, amount, source, attacker, tick)` | +| — | `world.onMessage(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。跨脚本消息 | **关键差异总结**: + 1. 所有事件注册**已返回 GameEventHandlerToken**,支持 `.cancel()` 和 `.active()` 2. Box3 事件回调接收**事件对象**,Box3JS 回调接收**展开的参数列表**(JS 展开参数风格) 3. `onTick` 已对齐传入 `{tick, prevTick, elapsedTimeMS, skip}`;`onPlayerJoin/Leave/Respawn` 已添加 tick 参数 @@ -198,162 +200,160 @@ Zone 可以设置局部天气/光照/力场参数,这在 MC 服务端无法实 ### 1.13 音效 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.ambientSound` (属性) | `world.ambientSound` (属性) | ✅ | 一致。存储音效路径字符串 | -| `world.playerJoinSound` (属性) | `world.playerJoinSound` (属性) | ✅ | 一致 | -| `world.playerLeaveSound` (属性) | `world.playerLeaveSound` (属性) | ✅ | 一致 | -| `world.placeVoxelSound` (属性) | `world.placeVoxelSound` (属性) | ✅ | 一致 | -| `world.breakVoxelSound` (属性) | `world.breakVoxelSound` (属性) | ✅ | 一致 | -| `world.sound(config)` | `world.sound(config)` | ✅ | 一致。接受 String 路径或 `{path, position, volume, pitch}` 对象。内部委托 playSound | -| — | `world.playSound(path, x, y, z, volume, pitch)` | ⬆ | MC 扩展。展开参数版本 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | ----------------------------------------------- | ---- | ----------------------------------------------------------------------------------- | +| `world.ambientSound` (属性) | `world.ambientSound` (属性) | ✅ | 一致。存储音效路径字符串 | +| `world.playerJoinSound` (属性) | `world.playerJoinSound` (属性) | ✅ | 一致 | +| `world.playerLeaveSound` (属性) | `world.playerLeaveSound` (属性) | ✅ | 一致 | +| `world.placeVoxelSound` (属性) | `world.placeVoxelSound` (属性) | ✅ | 一致 | +| `world.breakVoxelSound` (属性) | `world.breakVoxelSound` (属性) | ✅ | 一致 | +| `world.sound(config)` | `world.sound(config)` | ✅ | 一致。接受 String 路径或 `{path, position, volume, pitch}` 对象。内部委托 playSound | +| — | `world.playSound(path, x, y, z, volume, pitch)` | ⬆ | MC 扩展。展开参数版本 | ### 1.14 计时器 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.setTimeout(fn, ticks)` | `world.setTimeout(fn, ticks)` | ✅ | 一致 | -| `world.setInterval(fn, ticks)` | `world.setInterval(fn, ticks)` | ✅ | 一致 | -| `world.clearTimeout(id)` | `world.clearTimeout(id)` | ✅ | 一致 | -| `world.clearInterval(id)` | `world.clearInterval(id)` | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------ | ------------------------ | ---- | -------- | +| `setTimeout(fn, ticks)` | `setTimeout(fn, ticks)` | ✅ | 一致 | +| `setInterval(fn, ticks)` | `setInterval(fn, ticks)` | ✅ | 一致 | -**注意**: Box3 的 setTimeout/setInterval 在官方文档中标注为 ,但 Box3JS 在 `world` 全局对象上直接提供,用法一致。 +**注意**: Box3 的 setTimeout/setInterval 在官方文档中标注为 `world.xxx`,但 Box3JS 以全局函数直接提供(返回 `GameEventHandlerToken`,调用 `.cancel()` 取消)。 ### 1.15 视觉效果 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.strikeLightning(x, y, z)` | `world.strikeLightning(x, y, z)` | ✅ | 一致。额外支持 damage 参数 | -| `world.strikeLightning(pos)` | `world.strikeLightning(pos)` | ✅ | GameVector3 重载 | -| — | `world.strikeLightning(x, y, z, damage)` | ⬆ | 传自定义伤害值 | -| `world.launchFirework(x, y, z, color, shape)` | `world.launchFirework(x, y, z, color, shape)` | ✅ | 颜色和形状枚举值一致 | -| `world.launchFirework(pos, color, shape)` | `world.launchFirework(pos, color, shape)` | ✅ | GameVector3 重载 | -| `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | ✅ | 一致 | -| `world.spawnParticle(type, pos, count, dx, dy, dz, speed)` | `world.spawnParticle(type, pos, count, dx, dy, dz, speed)` | ✅ | GameVector3 重载 | -| — | `world.spawnParticleCircle(x, y, z, radius, type, count)` | ⬆ | MC 扩展。圆形粒子圈 | -| — | `world.spawnParticleCircle(pos, radius, type, count)` | ⬆ | GameVector3 重载 | -| — | `world.spawnParticle(x, y, z, color, count, dx, dy, dz, speed)` | ⬆ | MC 扩展。使用 GameRGBColor 生成彩色 dust 粒子 | -| — | `world.spawnParticle(pos, color, count, dx, dy, dz, speed)` | ⬆ | GameVector3 + GameRGBColor 重载 | -| — | `world.launchFirework(x, y, z, colors, shape)` | ⬆ | MC 扩展。colors 为 GameRGBColor[] 数组 | -| — | `world.launchFirework(pos, colors, shape)` | ⬆ | GameVector3 + GameRGBColor[] 重载 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------------------------------------- | --------------------------------------------------------------- | ---- | --------------------------------------------- | +| `world.strikeLightning(x, y, z)` | `world.strikeLightning(x, y, z)` | ✅ | 一致。额外支持 damage 参数 | +| `world.strikeLightning(pos)` | `world.strikeLightning(pos)` | ✅ | GameVector3 重载 | +| — | `world.strikeLightning(x, y, z, damage)` | ⬆ | 传自定义伤害值 | +| `world.launchFirework(x, y, z, color, shape)` | `world.launchFirework(x, y, z, color, shape)` | ✅ | 颜色和形状枚举值一致 | +| `world.launchFirework(pos, color, shape)` | `world.launchFirework(pos, color, shape)` | ✅ | GameVector3 重载 | +| `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | ✅ | 一致 | +| `world.spawnParticle(type, pos, count, dx, dy, dz, speed)` | `world.spawnParticle(type, pos, count, dx, dy, dz, speed)` | ✅ | GameVector3 重载 | +| — | `world.spawnParticleCircle(x, y, z, radius, type, count)` | ⬆ | MC 扩展。圆形粒子圈 | +| — | `world.spawnParticleCircle(pos, radius, type, count)` | ⬆ | GameVector3 重载 | +| — | `world.spawnParticle(x, y, z, color, count, dx, dy, dz, speed)` | ⬆ | MC 扩展。使用 GameRGBColor 生成彩色 dust 粒子 | +| — | `world.spawnParticle(pos, color, count, dx, dy, dz, speed)` | ⬆ | GameVector3 + GameRGBColor 重载 | +| — | `world.launchFirework(x, y, z, colors, shape)` | ⬆ | MC 扩展。colors 为 GameRGBColor[] 数组 | +| — | `world.launchFirework(pos, colors, shape)` | ⬆ | GameVector3 + GameRGBColor[] 重载 | ### 1.16 物品与抛射物 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.dropItem(x, y, z, itemId, count)` | `world.dropItem(x, y, z, itemId, count)` | ✅ | 一致 | -| `world.dropItem(pos, itemId, count)` | `world.dropItem(pos, itemId, count)` | ✅ | GameVector3 重载 | -| `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | ✅ | 一致 | -| `world.launchProjectile(type, pos, target, speed)` | `world.launchProjectile(type, pos, target, speed)` | ✅ | GameVector3 重载 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ---------------------------------------------------------- | ---------------------------------------------------------- | ---- | ---------------- | +| `world.dropItem(x, y, z, itemId, count)` | `world.dropItem(x, y, z, itemId, count)` | ✅ | 一致 | +| `world.dropItem(pos, itemId, count)` | `world.dropItem(pos, itemId, count)` | ✅ | GameVector3 重载 | +| `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | ✅ | 一致 | +| `world.launchProjectile(type, pos, target, speed)` | `world.launchProjectile(type, pos, target, speed)` | ✅ | GameVector3 重载 | ### 1.17 爆炸 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.explode(x, y, z, power)` | `world.explode(x, y, z, power)` | ✅ | 一致 | -| `world.explode(pos, power)` | `world.explode(pos, power)` | ✅ | GameVector3 重载 | -| — | `world.explode(x, y, z, power, fire)` | ⬆ | MC 扩展。fire=true 可引燃方块 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | ------------------------------------- | ---- | ----------------------------- | +| `world.explode(x, y, z, power)` | `world.explode(x, y, z, power)` | ✅ | 一致 | +| `world.explode(pos, power)` | `world.explode(pos, power)` | ✅ | GameVector3 重载 | +| — | `world.explode(x, y, z, power, fire)` | ⬆ | MC 扩展。fire=true 可引燃方块 | ### 1.18 动画 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.animate(keyframes, playback?)` | — | ❌ | 世界级关键帧动画 | -| `world.getAnimations()` | — | ❌ | | -| `world.getEntityAnimations()` | — | ❌ | | -| `world.getPlayerAnimations()` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------- | ----------- | ---- | ---------------- | +| `world.animate(keyframes, playback?)` | — | ❌ | 世界级关键帧动画 | +| `world.getAnimations()` | — | ❌ | | +| `world.getEntityAnimations()` | — | ❌ | | +| `world.getPlayerAnimations()` | — | ❌ | | ### 1.19 传送 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.teleport(mapId, players, serverId?)` | — | ❌ | 地图组间传送。MC 无地图组概念 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------- | ----------- | ---- | ----------------------------- | +| `world.teleport(mapId, players, serverId?)` | — | ❌ | 地图组间传送。MC 无地图组概念 | ### 1.20 跨脚本消息 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| — | `world.sendMessage(target, data)` | ⬆ | MC 扩展。target 为 `"*"` 或项目名 | -| — | `world.onMessage(fn)` | ⬆ | MC 扩展。回调 `(from, data)` | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------- | --------------------------------- | ---- | --------------------------------- | +| — | `world.sendMessage(target, data)` | ⬆ | MC 扩展。target 为 `"*"` 或项目名 | +| — | `world.onMessage(fn)` | ⬆ | MC 扩展。回调 `(from, data)` | ### 1.21 控制台命令 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| — | `world.runCommand(cmd)` | ⬆ | MC 扩展。以控制台身份执行命令 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------- | ----------------------- | ---- | ----------------------------- | +| — | `world.runCommand(cmd)` | ⬆ | MC 扩展。以控制台身份执行命令 | ### 1.22 记分板 (全部 MC 扩展) Box3 无记分板系统。 -| Box3JS API | 说明 | -|------------|------| -| `world.addScoreboard(name)` | 创建 dummy 类型记分项 | -| `world.addScoreboard(name, criteria)` | 指定标准(dummy/deathCount 等) | -| `world.removeScoreboard(name)` | 删除记分项 | -| `world.setScore(entityOrName, objective, value)` | 设置分数 | -| `world.getScore(entityOrName, objective)` | 获取分数 | -| `world.showScoreboard(slot, objective)` | 在 sidebar/list/belowname 显示 | -| `world.hideScoreboard(slot)` | 隐藏槽位 | -| `world.listScores(objective)` | 列出所有条目 | +| Box3JS API | 说明 | +| ------------------------------------------------ | ------------------------------- | +| `world.addScoreboard(name)` | 创建 dummy 类型记分项 | +| `world.addScoreboard(name, criteria)` | 指定标准(dummy/deathCount 等) | +| `world.removeScoreboard(name)` | 删除记分项 | +| `world.setScore(entityOrName, objective, value)` | 设置分数 | +| `world.getScore(entityOrName, objective)` | 获取分数 | +| `world.showScoreboard(slot, objective)` | 在 sidebar/list/belowname 显示 | +| `world.hideScoreboard(slot)` | 隐藏槽位 | +| `world.listScores(objective)` | 列出所有条目 | ### 1.23 Boss 血条 (全部 MC 扩展) Box3 无 Bossbar 系统。 -| Box3JS API | 说明 | -|------------|------| +| Box3JS API | 说明 | +| ------------------------------------------------ | -------------- | | `world.showBossbar(name, text, progress, color)` | 显示或更新血条 | -| `world.removeBossbar(name)` | 移除血条 | +| `world.removeBossbar(name)` | 移除血条 | ### 1.24 队伍 (全部 MC 扩展) Box3 无队伍系统。 -| Box3JS API | 说明 | -|------------|------| -| `world.createTeam(name, color)` | 创建队伍 | -| `world.removeTeam(name)` | 删除队伍 | +| Box3JS API | 说明 | +| ---------------------------------- | -------- | +| `world.createTeam(name, color)` | 创建队伍 | +| `world.removeTeam(name)` | 删除队伍 | | `world.joinTeam(entity, teamName)` | 加入队伍 | -| `world.leaveTeam(entity)` | 离开队伍 | -| `world.getTeamOf(entity)` | 查询队伍 | +| `world.leaveTeam(entity)` | 离开队伍 | +| `world.getTeamOf(entity)` | 查询队伍 | ### 1.25 世界边界 (全部 MC 扩展) Box3 无世界边界概念。 -| Box3JS API | 说明 | -|------------|------| -| `world.borderSize` (属性) | 获取/设置边界大小 | -| `world.setBorderCenter(x, z)` | 设置边界中心 | -| `world.shrinkBorder(targetSize, seconds)` | 平滑缩圈 | -| `world.setBorderDamage(damage)` | 边界外伤害值 | -| `world.setBorderWarning(blocks)` | 警告距离 | +| Box3JS API | 说明 | +| ----------------------------------------- | ----------------- | +| `world.borderSize` (属性) | 获取/设置边界大小 | +| `world.setBorderCenter(x, z)` | 设置边界中心 | +| `world.shrinkBorder(targetSize, seconds)` | 平滑缩圈 | +| `world.setBorderDamage(damage)` | 边界外伤害值 | +| `world.setBorderWarning(blocks)` | 警告距离 | ### 1.26 合成管理 (全部 MC 扩展) -| Box3JS API | 说明 | -|------------|------| -| `world.listRecipes(filter)` | 搜索匹配 filter 的合成配方 ID 列表 | -| `world.removeRecipe(recipeId)` | 禁用指定配方(加入黑名单) | -| `world.clearRecipes()` | 清除黑名单,恢复所有原始配方 | +| Box3JS API | 说明 | +| ------------------------------ | ---------------------------------- | +| `world.listRecipes(filter)` | 搜索匹配 filter 的合成配方 ID 列表 | +| `world.removeRecipe(recipeId)` | 禁用指定配方(加入黑名单) | +| `world.clearRecipes()` | 清除黑名单,恢复所有原始配方 | ### 1.28 结构与成就 (全部 MC 扩展) -| Box3JS API | 说明 | -|------------|------| -| `world.placeStructure(x, y, z, structureId)` | 在指定位置放置数据包结构模板 (NBT) | -| `world.placeStructure(pos, structureId)` | GameVector3 重载 | -| `world.grantAdvancement(playerName, advancementId)` | 按玩家名称授予进度 | +| Box3JS API | 说明 | +| --------------------------------------------------- | ---------------------------------- | +| `world.placeStructure(x, y, z, structureId)` | 在指定位置放置数据包结构模板 (NBT) | +| `world.placeStructure(pos, structureId)` | GameVector3 重载 | +| `world.grantAdvancement(playerName, advancementId)` | 按玩家名称授予进度 | ### 1.29 数据库 (db 全局对象) Box3 平台无独立数据库 API。Box3JS 通过 `db` 全局对象提供 SQLite 能力。 -| Box3JS API | 说明 | -|------------|------| -| `db.sql(sql, ...params)` | 执行 SQL 查询/更新,返回 `GameQueryResult` | -| `db.sql\`SELECT * FROM t WHERE id = ${id}\`` | Tagged template 语法,自动参数化 | +| Box3JS API | 说明 | +| --------------------------------------------- | ------------------------------------------ | +| `db.sql(sql, ...params)` | 执行 SQL 查询/更新,返回 `GameQueryResult` | +| `db.sql\`SELECT \* FROM t WHERE id = ${id}\`` | Tagged template 语法,自动参数化 | **GameQueryResult**: `rows` (数组), `firstRow`, `columnNames`, `columnCount`, `rowCount`, `affectedRows`, `isQuery` **GameQueryResult 方法**: `next()`, `reset()`, `then(resolve, reject?)` @@ -366,67 +366,67 @@ Box3 平台无独立数据库 API。Box3JS 通过 `db` 全局对象提供 SQLite ### 2.1 身份标识 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.id` (只读属性) | `entity.id` (只读属性) | ✅ | 一致。返回 UUID 字符串 | -| `entity.isPlayer` (只读属性) | `entity.isPlayer()` (方法) | ⚠️ | Box3 为属性,Box3JS 为方法 | -| `entity.player` (只读属性) | `entity.player` (只读属性) | ✅ | 一致。非玩家返回 null | -| `entity.entityType` (只读属性) | `entity.entityType` (只读属性) | ✅ | 一致。返回命名空间 ID(如 "minecraft:zombie") | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | ------------------------------ | ---- | ---------------------------------------------- | +| `entity.id` (只读属性) | `entity.id` (只读属性) | ✅ | 一致。返回 UUID 字符串 | +| `entity.isPlayer` (只读属性) | `entity.isPlayer()` (方法) | ⚠️ | Box3 为属性,Box3JS 为方法 | +| `entity.player` (只读属性) | `entity.player` (只读属性) | ✅ | 一致。非玩家返回 null | +| `entity.entityType` (只读属性) | `entity.entityType` (只读属性) | ✅ | 一致。返回命名空间 ID(如 "minecraft:zombie") | ### 2.2 标签 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.addTag(tag)` | `entity.addTag(tag)` | ✅ | 一致 | -| `entity.hasTag(tag)` | `entity.hasTag(tag)` | ✅ | 一致 | -| `entity.removeTag(tag)` | `entity.removeTag(tag)` | ✅ | 一致 | -| `entity.tags()` (返回数组) | `entity.tags()` | ✅ | 一致。返回所有标签的字符串数组 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------- | ----------------------- | ---- | ------------------------------ | +| `entity.addTag(tag)` | `entity.addTag(tag)` | ✅ | 一致 | +| `entity.hasTag(tag)` | `entity.hasTag(tag)` | ✅ | 一致 | +| `entity.removeTag(tag)` | `entity.removeTag(tag)` | ✅ | 一致 | +| `entity.tags()` (返回数组) | `entity.tags()` | ✅ | 一致。返回所有标签的字符串数组 | ### 2.3 位置/速度/包围盒 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.position` (属性) | `entity.position` (属性) | ✅ | 一致。LiveVec3 — 赋值即传送 | -| `entity.velocity` (属性) | `entity.velocity` (属性) | ✅ | 一致。赋值即设置速度 | -| `entity.bounds` (只读属性) | `entity.bounds` (只读属性) | ✅ | 一致。返回半尺寸 (half-extents) | -| — | `entity.eyePosition` (只读属性) | ⬆ | MC 扩展。返回眼睛位置的 GameVector3 | -| — | `entity.onGround` (只读属性) | ⬆ | MC 扩展。是否着地 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------- | ------------------------------- | ---- | ----------------------------------- | +| `entity.position` (属性) | `entity.position` (属性) | ✅ | 一致。LiveVec3 — 赋值即传送 | +| `entity.velocity` (属性) | `entity.velocity` (属性) | ✅ | 一致。赋值即设置速度 | +| `entity.bounds` (只读属性) | `entity.bounds` (只读属性) | ✅ | 一致。返回半尺寸 (half-extents) | +| — | `entity.eyePosition` (只读属性) | ⬆ | MC 扩展。返回眼睛位置的 GameVector3 | +| — | `entity.onGround` (只读属性) | ⬆ | MC 扩展。是否着地 | ### 2.4 外观 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.mesh` | — | ❌ | Box3 自定义模型 | -| `entity.meshColor` | — | ❌ | | -| `entity.meshScale` | — | ❌ | | -| `entity.meshOrientation` | — | ❌ | | -| `entity.meshMetalness` | — | ❌ | | -| `entity.meshEmissive` | — | ❌ | | -| `entity.meshShininess` | — | ❌ | | -| `entity.anchor` | — | ❌ | | -| `entity.anchorOffset` | — | ❌ | | -| `entity.meshInvisible` (属性) | `entity.meshInvisible` (属性) | ✅ | 一致。底层设置 MC 隐身 | -| — | `entity.nameTag` (属性) | ⬆ | MC 扩展。获取/设置实体名牌 | -| — | `entity.glowing` (属性) | ⬆ | MC 扩展。获取/设置发光效果 | -| — | `entity.invulnerable` (属性) | ⬆ | MC 扩展。获取/设置无敌状态 | -| — | `entity.fire` (setFire/clearFire) | ⬆ | MC 扩展。设置/清除着火 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | --------------------------------- | ---- | -------------------------- | +| `entity.mesh` | — | ❌ | Box3 自定义模型 | +| `entity.meshColor` | — | ❌ | | +| `entity.meshScale` | — | ❌ | | +| `entity.meshOrientation` | — | ❌ | | +| `entity.meshMetalness` | — | ❌ | | +| `entity.meshEmissive` | — | ❌ | | +| `entity.meshShininess` | — | ❌ | | +| `entity.anchor` | — | ❌ | | +| `entity.anchorOffset` | — | ❌ | | +| `entity.meshInvisible` (属性) | `entity.meshInvisible` (属性) | ✅ | 一致。底层设置 MC 隐身 | +| — | `entity.nameTag` (属性) | ⬆ | MC 扩展。获取/设置实体名牌 | +| — | `entity.glowing` (属性) | ⬆ | MC 扩展。获取/设置发光效果 | +| — | `entity.invulnerable` (属性) | ⬆ | MC 扩展。获取/设置无敌状态 | +| — | `entity.fire` (setFire/clearFire) | ⬆ | MC 扩展。设置/清除着火 | ### 2.5 物理属性 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.collides` (属性) | `entity.collides` (属性) | ✅ | 一致。false 时对 LivingEntity 设置 noGravity | -| `entity.fixed` (属性) | `entity.fixed` (属性) | ✅ | 一致。true 时设置 noGravity 并锁定位置 | -| `entity.friction` (属性) | `entity.friction` (属性) | ✅ | 一致。存储为自定义属性(脚本自行利用) | -| `entity.gravity` (属性) | `entity.gravity` (属性) | ✅ | 一致。false 时设置 noGravity(true) | -| `entity.mass` (属性) | `entity.mass` (属性) | ✅ | 一致。存储为自定义属性,默认 1.0 | -| `entity.restitution` (属性) | `entity.restitution` (属性) | ✅ | 一致。存储为自定义属性,默认 0.0 | -| `entity.contactForce` | — | ❌ | | -| `entity.entityContacts` (只读) | — | ❌ | | -| `entity.voxelContacts` (只读) | — | ❌ | | -| `entity.fluidContacts` (只读) | — | ❌ | | -| `entity.useOBB` | — | ❌ | | -| `entity.ignoreEntityGravity` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | --------------------------- | ---- | -------------------------------------------- | +| `entity.collides` (属性) | `entity.collides` (属性) | ✅ | 一致。false 时对 LivingEntity 设置 noGravity | +| `entity.fixed` (属性) | `entity.fixed` (属性) | ✅ | 一致。true 时设置 noGravity 并锁定位置 | +| `entity.friction` (属性) | `entity.friction` (属性) | ✅ | 一致。存储为自定义属性(脚本自行利用) | +| `entity.gravity` (属性) | `entity.gravity` (属性) | ✅ | 一致。false 时设置 noGravity(true) | +| `entity.mass` (属性) | `entity.mass` (属性) | ✅ | 一致。存储为自定义属性,默认 1.0 | +| `entity.restitution` (属性) | `entity.restitution` (属性) | ✅ | 一致。存储为自定义属性,默认 0.0 | +| `entity.contactForce` | — | ❌ | | +| `entity.entityContacts` (只读) | — | ❌ | | +| `entity.voxelContacts` (只读) | — | ❌ | | +| `entity.fluidContacts` (只读) | — | ❌ | | +| `entity.useOBB` | — | ❌ | | +| `entity.ignoreEntityGravity` | — | ❌ | | **注意**: MC 物理由原版引擎控制,物理属性(friction/mass/restitution)存储为自定义属性供脚本读取,不会改变原版物理行为。collides/fixed/gravity 有对应 MC 副作用(noGravity)。 @@ -434,111 +434,111 @@ Box3 平台无独立数据库 API。Box3JS 通过 `db` 全局对象提供 SQLite Box3 实体可独立发射粒子,Box3JS 完全未实现实体级粒子: -| Box3 API | 状态 | -|----------|------| -| `entity.particleRate` | ❌ | -| `entity.particleRateSpread` | ❌ | -| `entity.particleLimit` | ❌ | -| `entity.particleLifetime` | ❌ | -| `entity.particleLifetimeSpread` | ❌ | -| `entity.particleSize` | ❌ | -| `entity.particleSizeSpread` | ❌ | -| `entity.particleColor` | ❌ | -| `entity.particleVelocity` | ❌ | -| `entity.particleVelocitySpread` | ❌ | -| `entity.particleDamping` | ❌ | -| `entity.particleAcceleration` | ❌ | -| `entity.particleNoise` | ❌ | -| `entity.particleNoiseFrequency` | ❌ | -| `entity.particleTarget` | ❌ | -| `entity.particleTargetOffset` | ❌ | +| Box3 API | 状态 | +| ------------------------------- | ---- | +| `entity.particleRate` | ❌ | +| `entity.particleRateSpread` | ❌ | +| `entity.particleLimit` | ❌ | +| `entity.particleLifetime` | ❌ | +| `entity.particleLifetimeSpread` | ❌ | +| `entity.particleSize` | ❌ | +| `entity.particleSizeSpread` | ❌ | +| `entity.particleColor` | ❌ | +| `entity.particleVelocity` | ❌ | +| `entity.particleVelocitySpread` | ❌ | +| `entity.particleDamping` | ❌ | +| `entity.particleAcceleration` | ❌ | +| `entity.particleNoise` | ❌ | +| `entity.particleNoiseFrequency` | ❌ | +| `entity.particleTarget` | ❌ | +| `entity.particleTargetOffset` | ❌ | ### 2.7 音效 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.chatSound` | — | ❌ | | -| `entity.hurtSound` | — | ❌ | | -| `entity.dieSound` | — | ❌ | | -| `entity.interactSound` | — | ❌ | | -| `entity.sound(config)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ---------------------- | ----------- | ---- | -------- | +| `entity.chatSound` | — | ❌ | | +| `entity.hurtSound` | — | ❌ | | +| `entity.dieSound` | — | ❌ | | +| `entity.interactSound` | — | ❌ | | +| `entity.sound(config)` | — | ❌ | | ### 2.8 交互 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.enableInteract` | — | ❌ | | -| `entity.interactRadius` | — | ❌ | | -| `entity.interactHint` | — | ❌ | | -| `entity.interactColor` | — | ❌ | | -| `entity.say(message)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------- | ----------- | ---- | -------- | +| `entity.enableInteract` | — | ❌ | | +| `entity.interactRadius` | — | ❌ | | +| `entity.interactHint` | — | ❌ | | +| `entity.interactColor` | — | ❌ | | +| `entity.say(message)` | — | ❌ | | ### 2.9 战斗/生命 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.destroyed` (只读属性) | `entity.destroyed` (只读属性) | ✅ | 一致 | -| `entity.hp` (属性) | `entity.hp` (属性) | ✅ | 一致 | -| `entity.maxHp` (属性) | `entity.maxHp` (属性) | ✅ | 一致 | -| `entity.enableDamage` | — | ❌ | | -| `entity.showHealthBar` | — | ❌ | | -| `entity.hurt(amount)` | `entity.hurt(amount)` | ✅ | 一致 | -| — | `entity.heal(amount)` | ⬆ | MC 扩展。治疗实体 | -| `entity.destroy()` | `entity.destroy()` | ✅ | 一致。触发 onDestroy 回调后移除。Box3 中 destroy() 调用后实体立即消失 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | ----------------------------- | ---- | --------------------------------------------------------------------- | +| `entity.destroyed` (只读属性) | `entity.destroyed` (只读属性) | ✅ | 一致 | +| `entity.hp` (属性) | `entity.hp` (属性) | ✅ | 一致 | +| `entity.maxHp` (属性) | `entity.maxHp` (属性) | ✅ | 一致 | +| `entity.enableDamage` | — | ❌ | | +| `entity.showHealthBar` | — | ❌ | | +| `entity.hurt(amount)` | `entity.hurt(amount)` | ✅ | 一致 | +| — | `entity.heal(amount)` | ⬆ | MC 扩展。治疗实体 | +| `entity.destroy()` | `entity.destroy()` | ✅ | 一致。触发 onDestroy 回调后移除。Box3 中 destroy() 调用后实体立即消失 | ### 2.10 实体事件 Box3 的实体级事件非常丰富: -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.onClick(fn)` | — | ❌ | | -| `entity.onInteract(fn)` | — | ❌ | 可用 `world.onInteract` 替代 | -| `entity.onEntityContact(fn)` | — | ❌ | 可用 `world.onEntityContact` 替代 | -| `entity.onEntitySeparate(fn)` | — | ❌ | 可用 `world.onEntitySeparate` 替代 | -| `entity.onFluidEnter(fn)` | — | ❌ | 可用 `world.onFluidEnter` 替代 | -| `entity.onFluidLeave(fn)` | — | ❌ | 可用 `world.onFluidLeave` 替代 | -| `entity.onVoxelContact(fn)` | — | ❌ | 可用 `world.onVoxelContact` 替代 | -| `entity.onVoxelSeparate(fn)` | — | ❌ | | -| `entity.onDestroy(fn)` | `entity.onDestroy` (可赋值属性) | ⚠️ | Box3 为 `onDestroy(handler)` 方法;Box3JS 为可赋值属性 `entity.onDestroy = fn` | -| `entity.onTakeDamage(fn)` | — | ❌ | 可用 `world.onEntityDamage` 替代 | -| `entity.onDie(fn)` | — | ❌ | 可用 `world.onEntityDeath` 替代 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | ------------------------------- | ---- | ------------------------------------------------------------------------------ | +| `entity.onClick(fn)` | — | ❌ | | +| `entity.onInteract(fn)` | — | ❌ | 可用 `world.onInteract` 替代 | +| `entity.onEntityContact(fn)` | — | ❌ | 可用 `world.onEntityContact` 替代 | +| `entity.onEntitySeparate(fn)` | — | ❌ | 可用 `world.onEntitySeparate` 替代 | +| `entity.onFluidEnter(fn)` | — | ❌ | 可用 `world.onFluidEnter` 替代 | +| `entity.onFluidLeave(fn)` | — | ❌ | 可用 `world.onFluidLeave` 替代 | +| `entity.onVoxelContact(fn)` | — | ❌ | 可用 `world.onVoxelContact` 替代 | +| `entity.onVoxelSeparate(fn)` | — | ❌ | | +| `entity.onDestroy(fn)` | `entity.onDestroy` (可赋值属性) | ⚠️ | Box3 为 `onDestroy(handler)` 方法;Box3JS 为可赋值属性 `entity.onDestroy = fn` | +| `entity.onTakeDamage(fn)` | — | ❌ | 可用 `world.onEntityDamage` 替代 | +| `entity.onDie(fn)` | — | ❌ | 可用 `world.onEntityDeath` 替代 | **注意**: 所有 `world.onXxx()` 事件回调注册后返回 `GameEventHandlerToken`,支持 `.cancel()` / `.active()`,与 Box3 一致。实体级事件 (`entity.onDestroy`) 暂不返回 token。 ### 2.11 动画 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.motion` (属性) | — | ❌ | Box3 的 GameMotionController | -| `entity.animate(keyframes, playback?)` | — | ❌ | | -| `entity.getAnimations()` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------------- | ----------- | ---- | ---------------------------- | +| `entity.motion` (属性) | — | ❌ | Box3 的 GameMotionController | +| `entity.animate(keyframes, playback?)` | — | ❌ | | +| `entity.getAnimations()` | — | ❌ | | ### 2.12 变换方法 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.lookAt(target)` | `entity.lookAt(x, y, z)` / `entity.lookAt(pos)` | ✅ | 一致。Box3 接受多种目标类型,Box3JS 接受坐标或 GameVector3 | -| `entity.rotateLocal(quat)` | — | ❌ | 局部旋转 | -| `entity.scaleLocal(vec)` | — | ❌ | 局部缩放 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------- | ----------------------------------------------- | ---- | ---------------------------------------------------------- | +| `entity.lookAt(target)` | `entity.lookAt(x, y, z)` / `entity.lookAt(pos)` | ✅ | 一致。Box3 接受多种目标类型,Box3JS 接受坐标或 GameVector3 | +| `entity.rotateLocal(quat)` | — | ❌ | 局部旋转 | +| `entity.scaleLocal(vec)` | — | ❌ | 局部缩放 | ### 2.13 MC 独有扩展 (Entity) -| Box3JS API | 说明 | -|------------|------| -| `entity.navigateTo(x, y, z, speed)` / `entity.navigateTo(pos, speed)` | 生物寻路到目标位置 | -| `entity.setTarget(targetEntity)` | 设置生物攻击目标 | -| `entity.clearTarget()` | 清除攻击目标 | -| `entity.getTarget()` | 获取当前攻击目标 | -| `entity.setAI(enabled)` | 启用/禁用生物 AI | -| `entity.addEffect(effectId, duration, amplifier)` | 添加药水效果 | -| `entity.addEffect(effectId, duration, amplifier, hideParticles)` | 添加效果(可隐藏粒子) | -| `entity.setEquipment(slot, itemId)` | 设置生物装备(mainhand/offhand/head/chest/legs/feet) | -| `entity.setDropChance(slot, chance)` / `entity.setDropChance("all", chance)` | 设置装备掉落概率 | -| `entity.setPersistent(true)` | 使生物持久化(不自然消失) | -| `entity.getAttribute(attributeId)` | 获取属性值(如 "minecraft:generic.attack_damage") | -| `entity.setAttribute(attributeId, value)` | 设置属性值 | -| `entity.lookAt(x, y, z)` / `entity.lookAt(pos)` | 使实体看向某位置 | +| Box3JS API | 说明 | +| ---------------------------------------------------------------------------- | ----------------------------------------------------- | +| `entity.navigateTo(x, y, z, speed)` / `entity.navigateTo(pos, speed)` | 生物寻路到目标位置 | +| `entity.setTarget(targetEntity)` | 设置生物攻击目标 | +| `entity.clearTarget()` | 清除攻击目标 | +| `entity.getTarget()` | 获取当前攻击目标 | +| `entity.setAI(enabled)` | 启用/禁用生物 AI | +| `entity.addEffect(effectId, duration, amplifier)` | 添加药水效果 | +| `entity.addEffect(effectId, duration, amplifier, hideParticles)` | 添加效果(可隐藏粒子) | +| `entity.setEquipment(slot, itemId)` | 设置生物装备(mainhand/offhand/head/chest/legs/feet) | +| `entity.setDropChance(slot, chance)` / `entity.setDropChance("all", chance)` | 设置装备掉落概率 | +| `entity.setPersistent(true)` | 使生物持久化(不自然消失) | +| `entity.getAttribute(attributeId)` | 获取属性值(如 "minecraft:generic.attack_damage") | +| `entity.setAttribute(attributeId, value)` | 设置属性值 | +| `entity.lookAt(x, y, z)` / `entity.lookAt(pos)` | 使实体看向某位置 | --- @@ -546,210 +546,233 @@ Box3 的实体级事件非常丰富: ### 3.1 基础信息 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.name` (只读属性) | `player.name` (只读属性) | ✅ | 一致 | -| `player.userId` (只读属性) | `player.userId` (只读属性) | ✅ | Box3 返回用户数字 ID;Box3JS 返回 UUID 字符串 | -| `player.boxId` (只读属性) | — | ❌ | 已弃用的 Box ID | -| `player.userKey` (只读属性) | — | ❌ | 已弃用的用户密钥 | -| `player.avatar` (只读属性) | — | ❌ | 头像 URL | -| `player.movementBounds` (属性) | — | ❌ | | -| `player.url` (属性) | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | -------------------------- | ---- | --------------------------------------------- | +| `player.name` (只读属性) | `player.name` (只读属性) | ✅ | 一致 | +| `player.userId` (只读属性) | `player.userId` (只读属性) | ✅ | Box3 返回用户数字 ID;Box3JS 返回 UUID 字符串 | +| `player.boxId` (只读属性) | — | ❌ | 已弃用的 Box ID | +| `player.userKey` (只读属性) | — | ❌ | 已弃用的用户密钥 | +| `player.avatar` (只读属性) | — | ❌ | 头像 URL | +| `player.movementBounds` (属性) | — | ❌ | | +| `player.url` (属性) | — | ❌ | | ### 3.2 社交 -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.querySocial(type)` | — | ❌ | -| `player.querySocialStatistic()` | — | ❌ | -| `player.openUserProfileDialog(userId)` | — | ❌ | +| Box3 API | Box3JS 实现 | 状态 | +| -------------------------------------- | ----------- | ---- | +| `player.querySocial(type)` | — | ❌ | +| `player.querySocialStatistic()` | — | ❌ | +| `player.openUserProfileDialog(userId)` | — | ❌ | ### 3.3 外观 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.color` | — | ❌ | | -| `player.emissive` | — | ❌ | | -| `player.invisible` (属性) | `player.invisible` (属性) | ✅ | 一致 | -| `player.showName` | — | ❌ | | -| `player.showIndicator` | — | ❌ | | -| `player.scale` (属性) | `player.scale` (只读) | ⚠️ | Box3JS 只读 | -| `player.metalness` | — | ❌ | | -| `player.shininess` | — | ❌ | | -| `player.skin` | — | ❌ | | -| `player.skinInvisible` | — | ❌ | | -| `player.setSkinByName(name)` | — | ❌ | | -| `player.resetToDefaultSkin()` | — | ❌ | | -| `player.clearSkin()` | — | ❌ | | -| `player.addWearable(config)` | — | ❌ | | -| `player.removeWearable(config)` | — | ❌ | | -| `player.wearables(bodyPart?)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | ------------------------- | ---- | ----------- | +| `player.color` | — | ❌ | | +| `player.emissive` | — | ❌ | | +| `player.invisible` (属性) | `player.invisible` (属性) | ✅ | 一致 | +| `player.showName` | — | ❌ | | +| `player.showIndicator` | — | ❌ | | +| `player.scale` (属性) | `player.scale` (只读) | ⚠️ | Box3JS 只读 | +| `player.metalness` | — | ❌ | | +| `player.shininess` | — | ❌ | | +| `player.skin` | — | ❌ | | +| `player.skinInvisible` | — | ❌ | | +| `player.setSkinByName(name)` | — | ❌ | | +| `player.resetToDefaultSkin()` | — | ❌ | | +| `player.clearSkin()` | — | ❌ | | +| `player.addWearable(config)` | — | ❌ | | +| `player.removeWearable(config)` | — | ❌ | | +| `player.wearables(bodyPart?)` | — | ❌ | | ### 3.4 相机 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.cameraMode` (属性) | `player.cameraMode` (属性) | ⚠️ | Box3 支持 FIXED/FOLLOW/FPS/RELATIVE 四种模式;Box3JS 仅支持 FPS/FOLLOW | -| `player.cameraEntity` (属性) | `player.cameraEntity` (属性) | ⚠️ | Box3 可设为 null/实体;Box3JS 支持但设为非 null 时自动切换到 FOLLOW | -| `player.cameraPosition` (属性) | — | ❌ | FIXED/RELATIVE 模式用 | -| `player.cameraTarget` (属性) | `player.cameraTarget` (只读属性) | ⚠️ | Box3 可读写;Box3JS 只读,返回玩家视线方向 5 格处的点 | -| `player.cameraUp` (属性) | — | ❌ | | -| `player.cameraFovY` (属性) | — | ❌ | | -| `player.enable3DCursor` | — | ❌ | | -| `player.cameraFreezedAxis` | — | ❌ | | -| `player.freezedForwardDirection` | — | ❌ | | -| `player.cameraDistance` (属性) | — | ❌ | 相机到目标距离 | -| `player.cameraPitch` (只读属性) | `player.cameraPitch` (读/写属性) | ✅ | Box3JS 可写(`setCameraPitch` 方法效果) | -| `player.cameraYaw` (只读属性) | `player.cameraYaw` (读/写属性) | ✅ | Box3JS 可写 | -| `player.setCameraPitch(v)` | `player.setCameraPitch(v)` (作为属性 setter) | ✅ | 通过属性赋值实现 | -| `player.setCameraYaw(v)` | `player.setCameraYaw(v)` (作为属性 setter) | ✅ | 同上 | -| `player.facingDirection` (只读属性) | `player.facingDirection` (只读属性) | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------------- | -------------------------------------------- | ---- | ---------------------------------------------------------------------- | +| `player.cameraMode` (属性) | `player.cameraMode` (属性) | ⚠️ | Box3 支持 FIXED/FOLLOW/FPS/RELATIVE 四种模式;Box3JS 仅支持 FPS/FOLLOW | +| `player.cameraEntity` (属性) | `player.cameraEntity` (属性) | ⚠️ | Box3 可设为 null/实体;Box3JS 支持但设为非 null 时自动切换到 FOLLOW | +| `player.cameraPosition` (属性) | — | ❌ | FIXED/RELATIVE 模式用 | +| `player.cameraTarget` (属性) | `player.cameraTarget` (只读属性) | ⚠️ | Box3 可读写;Box3JS 只读,返回玩家视线方向 5 格处的点 | +| `player.cameraUp` (属性) | — | ❌ | | +| `player.cameraFovY` (属性) | — | ❌ | | +| `player.enable3DCursor` | — | ❌ | | +| `player.cameraFreezedAxis` | — | ❌ | | +| `player.freezedForwardDirection` | — | ❌ | | +| `player.cameraDistance` (属性) | — | ❌ | 相机到目标距离 | +| `player.cameraPitch` (只读属性) | `player.cameraPitch` (读/写属性) | ✅ | Box3JS 可写(`setCameraPitch` 方法效果) | +| `player.cameraYaw` (只读属性) | `player.cameraYaw` (读/写属性) | ✅ | Box3JS 可写 | +| `player.setCameraPitch(v)` | `player.setCameraPitch(v)` (作为属性 setter) | ✅ | 通过属性赋值实现 | +| `player.setCameraYaw(v)` | `player.setCameraYaw(v)` (作为属性 setter) | ✅ | 同上 | +| `player.facingDirection` (只读属性) | `player.facingDirection` (只读属性) | ✅ | 一致 | ### 3.5 画面滤镜 -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.colorLUT` (属性) | — | ❌ | +| Box3 API | Box3JS 实现 | 状态 | +| ------------------------ | ----------- | ---- | +| `player.colorLUT` (属性) | — | ❌ | ### 3.6 聊天/消息 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.directMessage(message)` | `player.directMessage(message)` | ✅ | 一致 | -| `player.dialog(config)` | `player.dialog(config)` | ⚠️ | **大幅简化**。Box3 支持 TEXT/INPUT/SELECT 三种类型 + 丰富样式配置 + 异步返回;Box3JS 仅发送系统消息并返回 `{index: 0, value: "OK"}`,不支持真正的交互对话框 | -| `player.cancelDialogs()` | — | ❌ | | -| `player.share(content)` | — | ❌ | | -| `player.onChat(handler)` | `player.onChat(handler)` | ✅ | Box3 传入 `{entity, message, tick}` 事件对象;Box3JS 直接展开参数 | -| — | `player.actionBar(message)` | ⬆ | MC 扩展。ActionBar 消息 | -| — | `player.title(title, subtitle)` | ⬆ | MC 扩展。标题/副标题 | -| — | `player.title(title, subtitle, fadeIn, stay, fadeOut)` | ⬆ | MC 扩展。带时间的标题 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | ------------------------------------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `player.directMessage(message)` | `player.directMessage(message)` | ✅ | 一致 | +| `player.dialog(config)` | `player.dialog(config)` | ⚠️ | **大幅简化**。Box3 支持 TEXT/INPUT/SELECT 三种类型 + 丰富样式配置 + 异步返回;Box3JS 仅发送系统消息并返回 `{index: 0, value: "OK"}`,不支持真正的交互对话框 | +| `player.cancelDialogs()` | — | ❌ | | +| `player.share(content)` | — | ❌ | | +| `player.onChat(handler)` | `player.onChat(handler)` | ✅ | Box3 传入 `{entity, message, tick}` 事件对象;Box3JS 直接展开参数 | +| — | `player.actionBar(message)` | ⬆ | MC 扩展。ActionBar 消息 | +| — | `player.title(title, subtitle)` | ⬆ | MC 扩展。标题/副标题 | +| — | `player.title(title, subtitle, fadeIn, stay, fadeOut)` | ⬆ | MC 扩展。带时间的标题 | ### 3.7 战斗/生命 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.dead` (只读属性) | `player.dead` (只读属性) | ✅ | 一致。返回 `player.isDeadOrDying()` | -| `player.spawnPoint` (属性) | `player.spawnPoint` (读写属性) | ✅ | 一致。可读可写,写入委托 setRespawnPoint | -| `player.forceRespawn()` | `player.respawn()` | ⚠️ | 方法名不同(forceRespawn → respawn) | -| `player.onRespawn(handler)` | — | ❌ | 可用 `world.onPlayerRespawn` 替代 | -| `player.hp` (属性) | `player.hp` (属性) | ✅ | 一致 | -| `player.maxHp` (属性) | `player.maxHp` (属性) | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| --------------------------- | ------------------------------ | ---- | ---------------------------------------- | +| `player.dead` (只读属性) | `player.dead` (只读属性) | ✅ | 一致。返回 `player.isDeadOrDying()` | +| `player.spawnPoint` (属性) | `player.spawnPoint` (读写属性) | ✅ | 一致。可读可写,写入委托 setRespawnPoint | +| `player.forceRespawn()` | `player.respawn()` | ⚠️ | 方法名不同(forceRespawn → respawn) | +| `player.onRespawn(handler)` | — | ❌ | 可用 `world.onPlayerRespawn` 替代 | +| `player.hp` (属性) | `player.hp` (属性) | ✅ | 一致 | +| `player.maxHp` (属性) | `player.maxHp` (属性) | ✅ | 一致 | ### 3.8 移动/输入 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.gamepad` | — | ❌ | 虚拟按键图片 | -| `player.disableInputDirection` | — | ❌ | | -| `player.enableAction0` | — | ❌ | | -| `player.enableAction1` | — | ❌ | | -| `player.action0Button` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 ACTION0 | -| `player.action1Button` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 ACTION1 | -| `player.jumpButton` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 JUMP | -| `player.walkButton` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 WALK | -| `player.swapInputDirection` | — | ❌ | | -| `player.reverseInputDirection` | — | ❌ | | -| `player.canFly` (属性) | `player.canFly` (属性) | ✅ | 一致 | -| — | `player.flying` (属性) | ⬆ | MC 扩展。当前是否在飞行 | -| `player.spectator` (只读属性) | `player.spectator` (只读属性) | ✅ | 一致 | -| — | `player.collision` (属性) | ⬆ | MC 扩展。实体碰撞开关 | -| `player.enableJump` (属性) | `player.enableJump` (属性) | ✅ | 一致。false 时保存并清零跳跃强度,true 时恢复 | -| `player.enableDoubleJump` | — | ❌ | 二段跳已移除 | -| `player.walkSpeed` (属性) | `player.walkSpeed` (属性) | ✅ | 一致 | -| `player.runSpeed` (属性) | `player.runSpeed` (属性) | ✅ | 一致 | -| `player.runAcceleration` | — | ❌ | | -| `player.jumpPower` (属性) | `player.jumpPower` (属性) | ✅ | 一致 | -| `player.jumpSpeedFactor` | — | ❌ | | -| `player.jumpAccelerationFactor` | — | ❌ | | -| `player.doubleJumpPower` | — | ❌ | | -| `player.crouchSpeed` (属性) | `player.crouchSpeed` (属性) | ✅ | 一致。存储为自定义属性 | -| `player.crouchAcceleration` | — | ❌ | | -| `player.flySpeed` (属性) | `player.flySpeed` (属性) | ✅ | 一致 | -| `player.flyAcceleration` | — | ❌ | | -| `player.swimAcceleration` | — | ❌ | | -| `player.swimSpeed` (属性) | `player.swimSpeed` (属性) | ✅ | 一致。映射到 WATER_MOVEMENT_EFFICIENCY 属性 | -| `player.walkAcceleration` | — | ❌ | | -| `player.moveState` (只读属性) | `player.moveState` (只读属性) | ✅ | 一致。枚举值: FLYING/GROUND/SWIM/FALL/JUMP | -| `player.walkState` (只读属性) | `player.walkState` (只读属性) | ✅ | 一致。枚举值: NONE/CROUCH/WALK/RUN | -| `player.cameraPitch` (只读属性) | `player.cameraPitch` (读/写属性) | ✅ | 如上 | -| `player.cameraYaw` (只读属性) | `player.cameraYaw` (读/写属性) | ✅ | 如上 | -| `player.kick()` | `player.kick()` / `player.kick(reason)` | ✅ | 一致。额外支持自定义踢出原因 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | --------------------------------------- | ---- | --------------------------------------------- | +| `player.gamepad` | — | ❌ | 虚拟按键图片 | +| `player.disableInputDirection` | — | ❌ | | +| `player.enableAction0` | — | ❌ | | +| `player.enableAction1` | — | ❌ | | +| `player.action0Button` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 ACTION0 | +| `player.action1Button` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 ACTION1 | +| `player.jumpButton` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 JUMP | +| `player.walkButton` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 WALK | +| `player.swapInputDirection` | — | ❌ | | +| `player.reverseInputDirection` | — | ❌ | | +| `player.canFly` (属性) | `player.canFly` (属性) | ✅ | 一致 | +| — | `player.flying` (属性) | ⬆ | MC 扩展。当前是否在飞行 | +| `player.spectator` (只读属性) | `player.spectator` (只读属性) | ✅ | 一致 | +| — | `player.collision` (属性) | ⬆ | MC 扩展。实体碰撞开关 | +| `player.enableJump` (属性) | `player.enableJump` (属性) | ✅ | 一致。false 时保存并清零跳跃强度,true 时恢复 | +| `player.enableDoubleJump` | — | ❌ | 二段跳已移除 | +| `player.walkSpeed` (属性) | `player.walkSpeed` (属性) | ✅ | 一致 | +| `player.runSpeed` (属性) | `player.runSpeed` (属性) | ✅ | 一致 | +| `player.runAcceleration` | — | ❌ | | +| `player.jumpPower` (属性) | `player.jumpPower` (属性) | ✅ | 一致 | +| `player.jumpSpeedFactor` | — | ❌ | | +| `player.jumpAccelerationFactor` | — | ❌ | | +| `player.doubleJumpPower` | — | ❌ | | +| `player.crouchSpeed` (属性) | `player.crouchSpeed` (属性) | ✅ | 一致。存储为自定义属性 | +| `player.crouchAcceleration` | — | ❌ | | +| `player.flySpeed` (属性) | `player.flySpeed` (属性) | ✅ | 一致 | +| `player.flyAcceleration` | — | ❌ | | +| `player.swimAcceleration` | — | ❌ | | +| `player.swimSpeed` (属性) | `player.swimSpeed` (属性) | ✅ | 一致。映射到 WATER_MOVEMENT_EFFICIENCY 属性 | +| `player.walkAcceleration` | — | ❌ | | +| `player.moveState` (只读属性) | `player.moveState` (只读属性) | ✅ | 一致。枚举值: FLYING/GROUND/SWIM/FALL/JUMP | +| `player.walkState` (只读属性) | `player.walkState` (只读属性) | ✅ | 一致。枚举值: NONE/CROUCH/WALK/RUN | +| `player.cameraPitch` (只读属性) | `player.cameraPitch` (读/写属性) | ✅ | 如上 | +| `player.cameraYaw` (只读属性) | `player.cameraYaw` (读/写属性) | ✅ | 如上 | +| `player.kick()` | `player.kick()` / `player.kick(reason)` | ✅ | 一致。额外支持自定义踢出原因 | ### 3.9 输入事件 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.onPress(handler)` | — | ❌ | 可用 `world.onButtonPressed` 替代 | -| `player.onRelease(handler)` | — | ❌ | | -| `player.onKeyDown(handler)` | — | ❌ | 键盘事件,MC 服务端无法获取 | -| `player.onKeyUp(handler)` | — | ❌ | | +Box3 的 `player.onPress/onRelease/onKeyDown/onKeyUp` 是客户端事件。Box3JS 通过客户端脚本引擎提供独立的 `input` 全局对象: + +| Box3 API | Box3JS 客户端实现 | 状态 | 差异说明 | +| --------------------------- | -------------------------------- | ---- | ------------------------------------------ | +| `player.onPress(handler)` | `world.onButtonPressed(handler)` | ⚠️ | 服务端可检测按钮点击;客户端用 `input` API | +| `player.onRelease(handler)` | — | ❌ | | +| `player.onKeyDown(handler)` | `input.onKeyPress(key, callback)` | ⚠️ | 客户端 `input` API,按键名如 `"f5"`、`"c"` | +| `player.onKeyUp(handler)` | — | ❌ | | + +**客户端 `input` 全局对象补充能力**: + +| Box3JS 客户端 API | 说明 | +| ------------------------------ | ----------------------------------- | +| `input.onKeyPress(key, fn)` | 注册按键按下回调(如 `"f5"`) | +| `input.isKeyDown(key)` | 检查按键是否正在按住 | +| `input.onMouseClick(fn)` | 鼠标点击回调 `(button, action, x, y)` | +| `input.getMouseX()` / `getMouseY()` | 获取鼠标屏幕坐标 | ### 3.10 音效 -Box3 玩家有 14 种音效属性,Box3JS 仅提供播放方法: - -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.music` | — | ❌ | -| `player.action0Sound` | — | ❌ | -| `player.action1Sound` | — | ❌ | -| `player.crouchSound` | — | ❌ | -| `player.jumpSound` | — | ❌ | -| `player.doubleJumpSound` | — | ❌ | -| `player.landSound` | — | ❌ | -| `player.enterWaterSound` | — | ❌ | -| `player.leaveWaterSound` | — | ❌ | -| `player.swimSound` | — | ❌ | -| `player.spawnSound` | — | ❌ | -| `player.stepSound` | — | ❌ | -| `player.startFlySound` | — | ❌ | -| `player.stopFlySound` | — | ❌ | -| `player.sound(config)` | `player.playSound(path, volume, pitch)` | ⚠️ | Box3 接受完整 Sound 对象或路径;Box3JS 展开参数 | +Box3 玩家有 14 种音效属性(`music`/`jumpSound`/`landSound` 等),这些是客户端音效控制。Box3JS 通过客户端脚本引擎提供 `audio` 全局对象: + +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------ | --------------------------------------- | ---- | --------------------------------------------------------------------------------- | +| `player.music` | `audio.playMusic(path, volume)` | ⚠️ | 客户端 `audio` API,通过背景音乐类别播放 | +| `player.action0Sound` | — | ❌ | | +| `player.action1Sound` | — | ❌ | | +| `player.crouchSound` | — | ❌ | | +| `player.jumpSound` | — | ❌ | | +| `player.doubleJumpSound` | — | ❌ | | +| `player.landSound` | — | ❌ | | +| `player.enterWaterSound` | — | ❌ | | +| `player.leaveWaterSound` | — | ❌ | | +| `player.swimSound` | — | ❌ | | +| `player.spawnSound` | — | ❌ | | +| `player.stepSound` | — | ❌ | | +| `player.startFlySound` | — | ❌ | | +| `player.stopFlySound` | — | ❌ | | +| `player.sound(config)` | `player.playSound(path, volume, pitch)` | ⚠️ | Box3 接受完整 Sound 对象或路径;Box3JS 展开参数 | + +**客户端 `audio` 全局对象补充能力**: + +| Box3JS 客户端 API | 说明 | +| -------------------------------------- | ------------------------------------------- | +| `audio.playSound(path, volume, pitch)` | 播放音效(支持原版和自定义音效) | +| `audio.playMusic(path, volume)` | 播放背景音乐(MUSIC 类别) | +| `audio.stopAll()` | 停止所有正在播放的音效和音乐 | +| `audio.getVolume(category)` | 获取指定音频类别的音量 | +| `audio.setVolume(category, value)` | 设置指定音频类别的音量(0.0–1.0) | + +> **注意**: Box3 的逐事件音效属性(如 `player.jumpSound`)需要 hook 每个游戏事件,Box3JS 目前提供的是主动播放 API。如需自动触发,可在 `client.onTick()` 中检测玩家状态变化后调用 `audio.playSound()`。 ### 3.11 链接/跳转 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.link(href, options?)` | `player.link(href)` | ⚠️ | Box3JS 简化版,无 isConfirm/isNewTab 选项。通过发送可点击的聊天组件实现 | -| `player.teleport(pos)` | `player.teleport(pos)` | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | ---------------------- | ---- | ----------------------------------------------------------------------- | +| `player.link(href, options?)` | `player.link(href)` | ⚠️ | Box3JS 简化版,无 isConfirm/isNewTab 选项。通过发送可点击的聊天组件实现 | +| `player.teleport(pos)` | `player.teleport(pos)` | ✅ | 一致 | ### 3.12 商城 -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.openMarketplace(productIds)` | — | ❌ | -| `player.getMiaoShells()` | — | ❌ | +| Box3 API | Box3JS 实现 | 状态 | +| ------------------------------------ | ----------- | ---- | +| `player.openMarketplace(productIds)` | — | ❌ | +| `player.getMiaoShells()` | — | ❌ | ### 3.13 玩家动画 -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.animate(keyframes, playback?)` | — | ❌ | -| `player.getAnimations()` | — | ❌ | +| Box3 API | Box3JS 实现 | 状态 | +| -------------------------------------- | ----------- | ---- | +| `player.animate(keyframes, playback?)` | — | ❌ | +| `player.getAnimations()` | — | ❌ | ### 3.14 MC 独有扩展 (Player) -| Box3JS API | 说明 | -|------------|------| -| `player.opLevel` (属性) | 获取/设置玩家 OP 权限级别 | -| `player.gameMode` (属性) | 获取/设置游戏模式(survival/creative/adventure/spectator) | -| `player.dimension` (属性) | 获取/设置玩家所在维度(如 "minecraft:overworld") | -| `player.disableFly` (属性) | 禁用飞行(退出飞行状态并阻止重新开启) | -| `player.giveItem(itemId, count)` | 给予物品 | -| `player.giveEnchantedItem(itemId, count, enchants)` | 给予附魔物品 | -| `player.giveNamedItem(itemId, count, name, lore)` | 给予自定义名称/描述物品 | -| `player.getHeldItem()` | 获取手持物品 `{id, count}` | -| `player.clearInventory()` | 清空背包 | -| `player.addEffect(effectId, duration, amplifier)` | 添加药水效果 | -| `player.addEffect(effectId, duration, amplifier, hideParticles)` | 添加效果(可隐藏粒子) | -| `player.clearEffects()` | 清除所有效果 | -| `player.xp` (属性) | 获取/设置经验等级 | -| `player.addExperienceLevels(levels)` | 增加经验等级 | -| `player.food` (属性) | 获取/设置饥饿值 | -| `player.saturation` (属性) | 获取/设置饱和值 | -| `player.runCommand(cmd)` | 以玩家身份执行命令 | -| `player.lookAt(x, y, z)` / `player.lookAt(pos)` | 使玩家看向某位置 | -| `player.setPlayerListName(name)` | 设置 TAB 列表显示名称 | +| Box3JS API | 说明 | +| ---------------------------------------------------------------- | ---------------------------------------------------------- | +| `player.opLevel` (属性) | 获取/设置玩家 OP 权限级别 | +| `player.gameMode` (属性) | 获取/设置游戏模式(survival/creative/adventure/spectator) | +| `player.dimension` (属性) | 获取/设置玩家所在维度(如 "minecraft:overworld") | +| `player.disableFly` (属性) | 禁用飞行(退出飞行状态并阻止重新开启) | +| `player.giveItem(itemId, count)` | 给予物品 | +| `player.giveEnchantedItem(itemId, count, enchants)` | 给予附魔物品 | +| `player.giveNamedItem(itemId, count, name, lore)` | 给予自定义名称/描述物品 | +| `player.getHeldItem()` | 获取手持物品 `{id, count}` | +| `player.clearInventory()` | 清空背包 | +| `player.addEffect(effectId, duration, amplifier)` | 添加药水效果 | +| `player.addEffect(effectId, duration, amplifier, hideParticles)` | 添加效果(可隐藏粒子) | +| `player.clearEffects()` | 清除所有效果 | +| `player.xp` (属性) | 获取/设置经验等级 | +| `player.addExperienceLevels(levels)` | 增加经验等级 | +| `player.food` (属性) | 获取/设置饥饿值 | +| `player.saturation` (属性) | 获取/设置饱和值 | +| `player.runCommand(cmd)` | 以玩家身份执行命令 | +| `player.lookAt(x, y, z)` / `player.lookAt(pos)` | 使玩家看向某位置 | +| `player.setPlayerListName(name)` | 设置 TAB 列表显示名称 | --- @@ -759,50 +782,50 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 ### 4.1 属性 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `voxels.shape` (只读属性) | `voxels.shape` (只读属性) | ✅ | 一致。返回世界最大尺寸 | -| `voxels.VoxelTypes` (只读属性) | `voxels.VoxelTypes` (只读属性) | ✅ | 一致。所有可用方块名称数组 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | ------------------------------ | ---- | -------------------------- | +| `voxels.shape` (只读属性) | `voxels.shape` (只读属性) | ✅ | 一致。返回世界最大尺寸 | +| `voxels.VoxelTypes` (只读属性) | `voxels.VoxelTypes` (只读属性) | ✅ | 一致。所有可用方块名称数组 | ### 4.2 名称/ID 映射 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `voxels.id(name)` | `voxels.id(name)` | ✅ | 一致。名称→数字 ID | -| `voxels.name(id)` | `voxels.name(id)` | ✅ | 一致。数字 ID→名称 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------- | ----------------- | ---- | ------------------ | +| `voxels.id(name)` | `voxels.id(name)` | ✅ | 一致。名称→数字 ID | +| `voxels.name(id)` | `voxels.name(id)` | ✅ | 一致。数字 ID→名称 | ### 4.3 写入 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `voxels.setVoxel(x, y, z, voxel)` | `voxels.setVoxel(x, y, z, voxel)` | ✅ | 一致 | -| `voxels.setVoxel(x, y, z, voxel, rotation)` | `voxels.setVoxel(x, y, z, voxel, rotation)` | ✅ | 一致。rotation 0-3 | -| `voxels.setVoxel(pos, voxel)` | `voxels.setVoxel(pos, voxel)` | ✅ | GameVector3 重载 | -| `voxels.setVoxel(pos, voxel, rotation)` | `voxels.setVoxel(pos, voxel, rotation)` | ✅ | GameVector3 重载 | -| `voxels.setVoxelId(x, y, z, voxel)` | `voxels.setVoxelId(x, y, z, voxel)` | ✅ | 一致。ID 包含旋转编码 | -| `voxels.setVoxelId(pos, voxel)` | `voxels.setVoxelId(pos, voxel)` | ✅ | GameVector3 重载 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------- | ------------------------------------------- | ---- | --------------------- | +| `voxels.setVoxel(x, y, z, voxel)` | `voxels.setVoxel(x, y, z, voxel)` | ✅ | 一致 | +| `voxels.setVoxel(x, y, z, voxel, rotation)` | `voxels.setVoxel(x, y, z, voxel, rotation)` | ✅ | 一致。rotation 0-3 | +| `voxels.setVoxel(pos, voxel)` | `voxels.setVoxel(pos, voxel)` | ✅ | GameVector3 重载 | +| `voxels.setVoxel(pos, voxel, rotation)` | `voxels.setVoxel(pos, voxel, rotation)` | ✅ | GameVector3 重载 | +| `voxels.setVoxelId(x, y, z, voxel)` | `voxels.setVoxelId(x, y, z, voxel)` | ✅ | 一致。ID 包含旋转编码 | +| `voxels.setVoxelId(pos, voxel)` | `voxels.setVoxelId(pos, voxel)` | ✅ | GameVector3 重载 | **旋转编码方案一致**: `finalId = rotation * 16384 + baseId`,rotation 0=南(0°), 1=西(90°), 2=北(180°), 3=东(270°)。 ### 4.4 读取 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `voxels.getVoxel(x, y, z)` | `voxels.getVoxel(x, y, z)` | ✅ | 一致。返回基础 ID(无旋转) | -| `voxels.getVoxel(pos)` | `voxels.getVoxel(pos)` | ✅ | GameVector3 重载 | -| `voxels.getVoxelId(x, y, z)` | `voxels.getVoxelId(x, y, z)` | ✅ | 一致。返回含旋转的完整 ID | -| `voxels.getVoxelId(pos)` | `voxels.getVoxelId(pos)` | ✅ | GameVector3 重载 | -| `voxels.getVoxelRotation(x, y, z)` | `voxels.getVoxelRotation(x, y, z)` | ✅ | 一致 | -| `voxels.getVoxelRotation(pos)` | `voxels.getVoxelRotation(pos)` | ✅ | GameVector3 重载 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ---------------------------------- | ---------------------------------- | ---- | --------------------------- | +| `voxels.getVoxel(x, y, z)` | `voxels.getVoxel(x, y, z)` | ✅ | 一致。返回基础 ID(无旋转) | +| `voxels.getVoxel(pos)` | `voxels.getVoxel(pos)` | ✅ | GameVector3 重载 | +| `voxels.getVoxelId(x, y, z)` | `voxels.getVoxelId(x, y, z)` | ✅ | 一致。返回含旋转的完整 ID | +| `voxels.getVoxelId(pos)` | `voxels.getVoxelId(pos)` | ✅ | GameVector3 重载 | +| `voxels.getVoxelRotation(x, y, z)` | `voxels.getVoxelRotation(x, y, z)` | ✅ | 一致 | +| `voxels.getVoxelRotation(pos)` | `voxels.getVoxelRotation(pos)` | ✅ | GameVector3 重载 | ### 4.5 MC 独有扩展 (Voxels) -| Box3JS API | 说明 | -|------------|------| -| `voxels.getVoxelName(x, y, z)` / `voxels.getVoxelName(pos)` | 返回方块的命名空间 ID(如 "minecraft:stone") | -| `voxels.fillVoxel(x1, y1, z1, x2, y2, z2, voxel)` / `voxels.fillVoxel(pos1, pos2, voxel)` | 批量填充方块区域 | -| `voxels.countVoxel(x1, y1, z1, x2, y2, z2, voxel)` / `voxels.countVoxel(pos1, pos2, voxel)` | 统计区域内匹配方块数量 | -| `voxels.setSpawner(x, y, z, entityType)` / `voxels.setSpawner(pos, entityType)` | 设置刷怪笼类型 | +| Box3JS API | 说明 | +| ------------------------------------------------------------------------------------------- | --------------------------------------------- | +| `voxels.getVoxelName(x, y, z)` / `voxels.getVoxelName(pos)` | 返回方块的命名空间 ID(如 "minecraft:stone") | +| `voxels.fillVoxel(x1, y1, z1, x2, y2, z2, voxel)` / `voxels.fillVoxel(pos1, pos2, voxel)` | 批量填充方块区域 | +| `voxels.countVoxel(x1, y1, z1, x2, y2, z2, voxel)` / `voxels.countVoxel(pos1, pos2, voxel)` | 统计区域内匹配方块数量 | +| `voxels.setSpawner(x, y, z, entityType)` / `voxels.setSpawner(pos, entityType)` | 设置刷怪笼类型 | --- @@ -810,25 +833,25 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 ### 5.1 存储空间管理 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `storage.key` (只读属性) | `storage.key` (只读属性) | ✅ | 一致。但 Box3JS 返回空字符串(根 storage) | -| `storage.getDataStorage(key)` | `storage.getDataStorage(key)` | ✅ | 一致 | -| `storage.getGroupStorage(key)` | `storage.getGroupStorage(key)` | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | ------------------------------ | ---- | ------------------------------------------ | +| `storage.key` (只读属性) | `storage.key` (只读属性) | ✅ | 一致。但 Box3JS 返回空字符串(根 storage) | +| `storage.getDataStorage(key)` | `storage.getDataStorage(key)` | ✅ | 一致 | +| `storage.getGroupStorage(key)` | `storage.getGroupStorage(key)` | ✅ | 一致 | ### 5.2 数据操作 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `ds.set(key, value)` | `ds.set(key, value)` | ⚠️ | **Box3 异步返回 Promise,Box3JS 同步执行** | -| `ds.get(key)` | `ds.get(key)` | ⚠️ | **Box3 异步返回 `Promise`,Box3JS 同步返回 value 或 null** | -| `ds.update(key, handler)` | `ds.update(key, handler)` | ⚠️ | **Box3 异步,Box3JS 同步** | -| `ds.remove(key)` | `ds.remove(key)` | ⚠️ | **Box3 异步,Box3JS 同步** | -| `ds.increment(key, value?)` | `ds.increment(key)` / `ds.increment(key, value)` | ⚠️ | **Box3 异步,Box3JS 同步** | -| `ds.list(options)` | `ds.list(options)` | ⚠️ | **Box3 异步,Box3JS 同步**。Box3JS 无 cursor 分页语义(cursor 仅作偏移量),不支持 constraintTarget 深层排序 | -| `ds.destroy()` | `ds.destroy()` | ⚠️ | **Box3 异步,Box3JS 同步** | -| — | `ds.keys()` | ⬆ | MC 扩展。返回所有 key 数组 | -| — | `ds.getKey()` | ⬆ | MC 扩展。返回存储空间名称 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| --------------------------- | ------------------------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------ | +| `ds.set(key, value)` | `ds.set(key, value)` | ⚠️ | **Box3 异步返回 Promise,Box3JS 同步执行** | +| `ds.get(key)` | `ds.get(key)` | ⚠️ | **Box3 异步返回 `Promise`,Box3JS 同步返回 value 或 null** | +| `ds.update(key, handler)` | `ds.update(key, handler)` | ⚠️ | **Box3 异步,Box3JS 同步** | +| `ds.remove(key)` | `ds.remove(key)` | ⚠️ | **Box3 异步,Box3JS 同步** | +| `ds.increment(key, value?)` | `ds.increment(key)` / `ds.increment(key, value)` | ⚠️ | **Box3 异步,Box3JS 同步** | +| `ds.list(options)` | `ds.list(options)` | ⚠️ | **Box3 异步,Box3JS 同步**。Box3JS 无 cursor 分页语义(cursor 仅作偏移量),不支持 constraintTarget 深层排序 | +| `ds.destroy()` | `ds.destroy()` | ⚠️ | **Box3 异步,Box3JS 同步** | +| — | `ds.keys()` | ⬆ | MC 扩展。返回所有 key 数组 | +| — | `ds.getKey()` | ⬆ | MC 扩展。返回存储空间名称 | ### 5.3 关键差异 @@ -844,120 +867,120 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 ### 6.1 GameVector3 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(x, y, z)` | ✅ | ✅ | | -| `x, y, z` 属性 | ✅ | ✅ | | -| `set(x, y, z)` | ✅ | ✅ | | -| `copy(v)` | ✅ | ✅ | | -| `clone()` | ✅ | ✅ | | -| `add(v)` | ✅ | ✅ | | -| `sub(v)` | ✅ | ✅ | | -| `mul(v)` | ✅ | ✅ | 逐分量乘法,零保护 | -| `div(v)` | ✅ | ✅ | 逐分量除法,零保护 | -| `addEq(v)` | ✅ | ✅ | 返回 this | -| `subEq(v)` | ✅ | ✅ | 返回 this | -| `mulEq(v)` | ✅ | ✅ | 返回 this | -| `divEq(v)` | ✅ | ✅ | 返回 this,零保护 | -| `dot(v)` | ✅ | ✅ | | -| `cross(v)` | ✅ | ✅ | 叉积 | -| `scale(n)` | ✅ | ✅ | | -| `lerp(v, n)` | ✅ | ✅ | | -| `towards(v)` | ✅ | ✅ | 返回归一化方向向量 | -| `mag()` | ✅ | ✅ | | -| `sqrMag()` | ✅ | ✅ | | -| `angle(v)` | ✅ | ✅ | 返回弧度 | -| `distance(v)` | ✅ | ✅ | | -| `equals(v)` | ✅ | ✅ | 使用 1e-6 容差,匹配 Box3 | -| `exactEquals(v)` | ✅ | ✅ | 精确比较 | -| `max(v)` | ✅ | ✅ | 逐分量取最大值 | -| `min(v)` | ✅ | ✅ | 逐分量取最小值 | -| `normalize()` | ✅ | ✅ | | -| `toString()` | ✅ | ✅ | | -| `fromPolar(mag, phi, theta)` (静态) | ✅ | ✅ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------------- | ----------- | ---- | ------------------------- | +| 构造函数 `(x, y, z)` | ✅ | ✅ | | +| `x, y, z` 属性 | ✅ | ✅ | | +| `set(x, y, z)` | ✅ | ✅ | | +| `copy(v)` | ✅ | ✅ | | +| `clone()` | ✅ | ✅ | | +| `add(v)` | ✅ | ✅ | | +| `sub(v)` | ✅ | ✅ | | +| `mul(v)` | ✅ | ✅ | 逐分量乘法,零保护 | +| `div(v)` | ✅ | ✅ | 逐分量除法,零保护 | +| `addEq(v)` | ✅ | ✅ | 返回 this | +| `subEq(v)` | ✅ | ✅ | 返回 this | +| `mulEq(v)` | ✅ | ✅ | 返回 this | +| `divEq(v)` | ✅ | ✅ | 返回 this,零保护 | +| `dot(v)` | ✅ | ✅ | | +| `cross(v)` | ✅ | ✅ | 叉积 | +| `scale(n)` | ✅ | ✅ | | +| `lerp(v, n)` | ✅ | ✅ | | +| `towards(v)` | ✅ | ✅ | 返回归一化方向向量 | +| `mag()` | ✅ | ✅ | | +| `sqrMag()` | ✅ | ✅ | | +| `angle(v)` | ✅ | ✅ | 返回弧度 | +| `distance(v)` | ✅ | ✅ | | +| `equals(v)` | ✅ | ✅ | 使用 1e-6 容差,匹配 Box3 | +| `exactEquals(v)` | ✅ | ✅ | 精确比较 | +| `max(v)` | ✅ | ✅ | 逐分量取最大值 | +| `min(v)` | ✅ | ✅ | 逐分量取最小值 | +| `normalize()` | ✅ | ✅ | | +| `toString()` | ✅ | ✅ | | +| `fromPolar(mag, phi, theta)` (静态) | ✅ | ✅ | | **GameVector3 完全实现**,所有 28 个 Box3 方法均已对齐。 ### 6.2 GameBounds3 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(lo, hi)` | ✅ | ✅ | | -| `lo, hi` 属性 | ✅ | ✅ | | -| `set(lox, loy, loz, hix, hiy, hiz)` | ✅ | ✅ | | -| `copy(b)` | ✅ | ✅ | | -| `intersect(b)` | ✅ | ✅ | 返回新对象或 null(无交集时) | -| `intersects(b)` | ✅ | ✅ | | -| `contains(v)` | ✅ | ✅ | | -| `containsBounds(b)` | ✅ | ✅ | | -| `toString()` | ✅ | ✅ | | -| `fromPoints(...points)` (静态) | ✅ | ✅ | 接受 NativeArray | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------------- | ----------- | ---- | ----------------------------- | +| 构造函数 `(lo, hi)` | ✅ | ✅ | | +| `lo, hi` 属性 | ✅ | ✅ | | +| `set(lox, loy, loz, hix, hiy, hiz)` | ✅ | ✅ | | +| `copy(b)` | ✅ | ✅ | | +| `intersect(b)` | ✅ | ✅ | 返回新对象或 null(无交集时) | +| `intersects(b)` | ✅ | ✅ | | +| `contains(v)` | ✅ | ✅ | | +| `containsBounds(b)` | ✅ | ✅ | | +| `toString()` | ✅ | ✅ | | +| `fromPoints(...points)` (静态) | ✅ | ✅ | 接受 NativeArray | **GameBounds3 完全实现**。 ### 6.3 GameRGBColor -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(r, g, b)` | ✅ | ✅ | | -| `r, g, b` 属性 | ✅ | ✅ | | -| `set(r, g, b)` | ✅ | ✅ | | -| `copy(c)` | ✅ | ✅ | | -| `clone()` | ✅ | ✅ | | -| `add/sub/mul/div` | ✅ | ✅ | 除法有零保护 | -| `addEq/subEq/mulEq/divEq` | ✅ | ✅ | 返回 this,除法有零保护 | -| `lerp(rgb, n)` | ✅ | ✅ | | -| `equals(rgb)` | ✅ | ✅ | 使用 1e-6 容差 | -| `toRGBA()` | ✅ | ✅ | 返回 `"rgba(r,g,b,1.0)"` 字符串 | -| `toString()` | ✅ | ✅ | | -| `random()` (静态) | ✅ | ✅ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------- | ----------- | ---- | ------------------------------- | +| 构造函数 `(r, g, b)` | ✅ | ✅ | | +| `r, g, b` 属性 | ✅ | ✅ | | +| `set(r, g, b)` | ✅ | ✅ | | +| `copy(c)` | ✅ | ✅ | | +| `clone()` | ✅ | ✅ | | +| `add/sub/mul/div` | ✅ | ✅ | 除法有零保护 | +| `addEq/subEq/mulEq/divEq` | ✅ | ✅ | 返回 this,除法有零保护 | +| `lerp(rgb, n)` | ✅ | ✅ | | +| `equals(rgb)` | ✅ | ✅ | 使用 1e-6 容差 | +| `toRGBA()` | ✅ | ✅ | 返回 `"rgba(r,g,b,1.0)"` 字符串 | +| `toString()` | ✅ | ✅ | | +| `random()` (静态) | ✅ | ✅ | | **GameRGBColor 完全实现**,所有 16 个 Box3 方法均已对齐。 ### 6.4 GameRGBAColor -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(r, g, b, a)` | ✅ | ✅ | | -| `r, g, b, a` 属性 | ✅ | ✅ | | -| `set(r, g, b, a)` | ✅ | ✅ | | -| `copy(c)` | ✅ | ✅ | | -| `clone()` | ✅ | ✅ | | -| `add/sub/mul/div` | ✅ | ✅ | | -| `addEq/subEq/mulEq/divEq` | ✅ | ✅ | | -| `lerp(rgba, n)` | ✅ | ✅ | | -| `equals(rgba)` | ✅ | ✅ | | -| `blendEq(rgb)` | ✅ | ✅ | | -| `toString()` | ✅ | ✅ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------- | ----------- | ---- | -------- | +| 构造函数 `(r, g, b, a)` | ✅ | ✅ | | +| `r, g, b, a` 属性 | ✅ | ✅ | | +| `set(r, g, b, a)` | ✅ | ✅ | | +| `copy(c)` | ✅ | ✅ | | +| `clone()` | ✅ | ✅ | | +| `add/sub/mul/div` | ✅ | ✅ | | +| `addEq/subEq/mulEq/divEq` | ✅ | ✅ | | +| `lerp(rgba, n)` | ✅ | ✅ | | +| `equals(rgba)` | ✅ | ✅ | | +| `blendEq(rgb)` | ✅ | ✅ | | +| `toString()` | ✅ | ✅ | | **GameRGBAColor 是完全实现的**,所有 Box3 方法均有对应。 ### 6.5 GameQuaternion -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(w, x, y, z)` | ✅ | ✅ | | -| `w, x, y, z` 属性 | ✅ | ✅ | | -| `set(w, x, y, z)` | ✅ | ✅ | | -| `copy(v)` | ✅ | ✅ | | -| `clone()` | ✅ | ✅ | | -| `rotateX/Y/Z(rad)` | ✅ | ✅ | | -| `add/sub` | ✅ | ✅ | | -| `mul(q)` | ✅ | ✅ | Hamilton 积 | -| `inv()` | ✅ | ✅ | | -| `div(q)` | ✅ | ✅ | | -| `dot(q)` | ✅ | ✅ | | -| `slerp(q, n)` | ✅ | ✅ | | -| `angle(q)` | ✅ | ✅ | | -| `getAxisAngle()` | ✅ | ✅ | 返回 `{angle, axis}` | -| `mag()` | ✅ | ✅ | | -| `sqrMag()` | ✅ | ✅ | | -| `equals(v)` | ✅ | ✅ | | -| `normalize()` | ✅ | ✅ | | -| `toString()` | ✅ | ✅ | | -| `fromAxisAngle(axis, rad)` (静态) | ✅ | ✅ | | -| `fromEuler(x, y, z)` (静态) | ✅ | ✅ | YZX 顺序 | -| `rotationBetween(a, b)` (静态) | ✅ | ✅ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| --------------------------------- | ----------- | ---- | -------------------- | +| 构造函数 `(w, x, y, z)` | ✅ | ✅ | | +| `w, x, y, z` 属性 | ✅ | ✅ | | +| `set(w, x, y, z)` | ✅ | ✅ | | +| `copy(v)` | ✅ | ✅ | | +| `clone()` | ✅ | ✅ | | +| `rotateX/Y/Z(rad)` | ✅ | ✅ | | +| `add/sub` | ✅ | ✅ | | +| `mul(q)` | ✅ | ✅ | Hamilton 积 | +| `inv()` | ✅ | ✅ | | +| `div(q)` | ✅ | ✅ | | +| `dot(q)` | ✅ | ✅ | | +| `slerp(q, n)` | ✅ | ✅ | | +| `angle(q)` | ✅ | ✅ | | +| `getAxisAngle()` | ✅ | ✅ | 返回 `{angle, axis}` | +| `mag()` | ✅ | ✅ | | +| `sqrMag()` | ✅ | ✅ | | +| `equals(v)` | ✅ | ✅ | | +| `normalize()` | ✅ | ✅ | | +| `toString()` | ✅ | ✅ | | +| `fromAxisAngle(axis, rad)` (静态) | ✅ | ✅ | | +| `fromEuler(x, y, z)` (静态) | ✅ | ✅ | YZX 顺序 | +| `rotationBetween(a, b)` (静态) | ✅ | ✅ | | **GameQuaternion 是完全实现的**。 @@ -971,12 +994,12 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 Box3 有完整的关键帧动画系统,支持 World/Entity/Player 级别的属性动画(位置、颜色、缩放、天气参数等)。Box3JS 无动画系统。 -| Box3 API | 状态 | -|----------|------| -| `world.animate(keyframes, playback?)` | ❌ | -| `entity.animate(keyframes, playback?)` | ❌ | -| `player.animate(keyframes, playback?)` | ❌ | -| `GameAnimation` 对象 (play/cancel/currentTime/playState/onFinish/...) | ❌ | +| Box3 API | 状态 | +| --------------------------------------------------------------------- | ---- | +| `world.animate(keyframes, playback?)` | ❌ | +| `entity.animate(keyframes, playback?)` | ❌ | +| `player.animate(keyframes, playback?)` | ❌ | +| `GameAnimation` 对象 (play/cancel/currentTime/playState/onFinish/...) | ❌ | ### 7.2 GameMotionController @@ -984,13 +1007,13 @@ Box3 有完整的关键帧动画系统,支持 World/Entity/Player 级别的属 Box3 的 Voxa 模型动画系统,用于控制自定义模型的骨骼动画。 -| Box3 API | 状态 | -|----------|------| -| `entity.motion` | ❌ | -| `motion.loadByName(configs)` | ❌ | -| `motion.pause()` / `motion.resume()` | ❌ | -| `motion.setDefaultMotionByName(name)` | ❌ | -| `GameMotionHandler` (play/cancel/onFinish) | ❌ | +| Box3 API | 状态 | +| ------------------------------------------ | ---- | +| `entity.motion` | ❌ | +| `motion.loadByName(configs)` | ❌ | +| `motion.pause()` / `motion.resume()` | ❌ | +| `motion.setDefaultMotionByName(name)` | ❌ | +| `GameMotionHandler` (play/cancel/onFinish) | ❌ | ### 7.3 Sound @@ -1000,19 +1023,23 @@ Box3 的 `sound()` 方法返回 Sound 对象,支持 `setCurrentTime`/`resume`/ ### 7.4 RemoteChannel (跨端通信) -**状态**: ❌ 完全未实现 +**状态**: ✅ 已实现(双向通信) + +Box3JS 通过自定义网络数据包实现了完整的服务端↔客户端双向事件通道,API 命名与 Box3 一致。 + +**服务端** (Box3JSWorld / Box3ScriptEngine): -Box3 的 `remoteChannel` 用于服务端↔客户端双向通信。MC 模组运行在服务端,无客户端代码,无法实现。 +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------------------------------- | ----------------------------------------------------- | ---- | ----------------------------------------------------------- | +| `remoteChannel.sendClientEvent(entities, event)` | `remoteChannel.sendClientEvent(entities, event)` | ✅ | 一致。entities 为单个 GamePlayerEntity 或数组 | +| `remoteChannel.broadcastClientEvent(event)` | `remoteChannel.broadcastClientEvent(event)` | ✅ | 一致。向所有在线玩家广播 | +| `remoteChannel.onServerEvent(handler)` | `remoteChannel.onServerEvent(handler)` | ✅ | 一致。回调接收 `{ tick, entity, args }`,返回 GameEventHandlerToken | +| `remoteChannel.sendServerEvent(event)` (客户端) | `remoteChannel.sendServerEvent(event)` | ✅ | 一致。客户端发送事件到服务端 | +| `remoteChannel.onClientEvent(handler)` (客户端) | `remoteChannel.onClientEvent(handler)` | ✅ | 一致。回调接收 `{ tick, args }`,返回 GameEventHandlerToken | -| Box3 API | 状态 | -|----------|------| -| `remoteChannel.sendClientEvent(entities, event)` (服务端) | ❌ | -| `remoteChannel.broadcastClientEvent(event)` (服务端) | ❌ | -| `remoteChannel.onServerEvent(handler)` (服务端) | ❌ | -| `remoteChannel.sendServerEvent(event)` (客户端) | ❌ | -| `remoteChannel.onClientEvent(handler)` (客户端) | ❌ | +**客户端事件数据自动 JSON 序列化/反序列化传输**,支持任意可序列化的 JS 值。 -**替代方案**: Box3JS 提供了 `world.sendMessage(target, data)` 和 `world.onMessage(fn)` 用于**脚本间**通信(同一服务端不同脚本项目),但这与 Box3 的跨端通信不同。 +> **注意**: `world.sendMessage(target, data)` 和 `world.onMessage(fn)` 是**额外的**跨脚本通信机制(同一服务端不同项目间通信),与 remoteChannel 的跨端通信互补。 ### 7.5 GameRTC (实时语音) @@ -1026,28 +1053,28 @@ MC 无内置语音通信,无法实现。 Box3 的 `http.fetch(url, options?)` 用于服务端发起 HTTP 请求。 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `http.fetch(url, options?)` → `Promise` | `http.fetch(url, options?)` → `Response` | ⚠️ | Box3 异步返回 Promise;Box3JS 同步阻塞调用 | -| `options.method` | ✅ | ✅ | GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS | -| `options.headers` | ✅ | ✅ | 键值对 | -| `options.body` (string) | ✅ | ✅ | 文本请求体 | -| `options.body` (ArrayBuffer) | ✅ | ✅ | 二进制请求体 | -| `options.timeout` | ✅ | ✅ | 超时毫秒 | -| — | `options.responseType` | ⬆ | 自动解析:`"json"` / `"text"` / `"arrayBuffer"`,结果见 `resp.data` | -| — | `options.maxBodySize` | ⬆ | 响应体最大字节数,超出截断并标记 `resp.truncated` | -| `Response.ok` | ✅ | ✅ | 状态码 200-299 | -| `Response.status` | ✅ | ✅ | HTTP 状态码 | -| `Response.statusText` | ✅ | ✅ | 状态描述 | -| `Response.headers` | ✅ | ✅ | 响应头键值对 | -| `Response.json()` → `Promise` | `Response.json()` → `any` | ⚠️ | Box3 异步;Box3JS 同步返回,解析失败返回 `null` | -| `Response.text()` → `Promise` | `Response.text()` → `string` | ⚠️ | Box3 异步;Box3JS 同步返回 | -| `Response.arrayBuffer()` → `Promise` | `Response.arrayBuffer()` → `ArrayBuffer` | ⚠️ | Box3 异步;Box3JS 同步返回 | -| — | `Response.getHeader(name)` | ⬆ | 获取单个响应头值 | -| — | `Response.errorMessage` | ⬆ | 请求失败时的错误信息 | -| — | `Response.truncated` | ⬆ | 响应体是否因 maxBodySize 被截断 | -| — | `Response.data` | ⬆ | responseType 自动解析的结果 | -| `Response.close()` | ✅ | ✅ | 关闭连接(Box3JS 为空操作) | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------------- | ---------------------------------------- | ---- | ------------------------------------------------------------------- | +| `http.fetch(url, options?)` → `Promise` | `http.fetch(url, options?)` → `Response` | ⚠️ | Box3 异步返回 Promise;Box3JS 同步阻塞调用 | +| `options.method` | ✅ | ✅ | GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS | +| `options.headers` | ✅ | ✅ | 键值对 | +| `options.body` (string) | ✅ | ✅ | 文本请求体 | +| `options.body` (ArrayBuffer) | ✅ | ✅ | 二进制请求体 | +| `options.timeout` | ✅ | ✅ | 超时毫秒 | +| — | `options.responseType` | ⬆ | 自动解析:`"json"` / `"text"` / `"arrayBuffer"`,结果见 `resp.data` | +| — | `options.maxBodySize` | ⬆ | 响应体最大字节数,超出截断并标记 `resp.truncated` | +| `Response.ok` | ✅ | ✅ | 状态码 200-299 | +| `Response.status` | ✅ | ✅ | HTTP 状态码 | +| `Response.statusText` | ✅ | ✅ | 状态描述 | +| `Response.headers` | ✅ | ✅ | 响应头键值对 | +| `Response.json()` → `Promise` | `Response.json()` → `any` | ⚠️ | Box3 异步;Box3JS 同步返回,解析失败返回 `null` | +| `Response.text()` → `Promise` | `Response.text()` → `string` | ⚠️ | Box3 异步;Box3JS 同步返回 | +| `Response.arrayBuffer()` → `Promise` | `Response.arrayBuffer()` → `ArrayBuffer` | ⚠️ | Box3 异步;Box3JS 同步返回 | +| — | `Response.getHeader(name)` | ⬆ | 获取单个响应头值 | +| — | `Response.errorMessage` | ⬆ | 请求失败时的错误信息 | +| — | `Response.truncated` | ⬆ | 响应体是否因 maxBodySize 被截断 | +| — | `Response.data` | ⬆ | responseType 自动解析的结果 | +| `Response.close()` | ✅ | ✅ | 关闭连接(Box3JS 为空操作) | > **⚠️ 重要差异:** Box3JS 的 `http.fetch()` 是**同步阻塞**调用(Rhino 引擎限制),会阻塞服务器 tick。Box3 原版是异步 Promise。请避免在高频回调(`world.onTick()` 等)中使用。 @@ -1071,19 +1098,158 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` --- -## 8. 客户端 API(不适用) +## 8. 客户端 API + +Box3JS 现已支持**客户端脚本引擎**(`src/client/app.ts`),在玩家客户端上运行,提供与 Box3 客户端 API 对应的能力。客户端脚本通过 `remoteChannel` 与服务端双向通信。 + +### 8.1 客户端生命周期 (client) + +| Box3JS 客户端 API | 说明 | +| ---------------------- | ---------------------------------------------------------------------------------------- | +| `client.onTick(fn)` | 注册每帧回调(每秒 20 次),返回 GameEventHandlerToken | +| `client.getFPS()` | 获取当前帧率 | +| `client.getPlayer()` | 获取本地玩家信息 `{ name, uuid, health, maxHealth, food, saturation, xp, dimension, position }` | +| `client.getLookingAt()` | 获取准星目标,返回 `{ type, position, entity?, blockPos?, direction? }` 或 null | +| `client.getServerInfo()` | 获取服务器信息 `{ ip, name, isLocal, playerCount, maxPlayers }` | + +### 8.2 客户端 UI (ui) + +与 Box3 的 ClientUI 对应,Box3JS 提供 HUD 绘制和屏幕消息能力: + +| Box3JS 客户端 API | 说明 | +| ------------------------------------------------ | -------------------------------------------------------- | +| `ui.showOverlay(text)` | 显示屏幕中央覆盖文字(2 秒自动消失) | +| `ui.showTitle(title, subtitle, fadeIn, stay, fadeOut)` | 显示标题/副标题 | +| `ui.showActionBar(text)` | 显示 ActionBar 消息 | +| `ui.getScreenSize()` | 获取屏幕尺寸 `{ scaledWidth, scaledHeight, guiScale }` | +| `ui.drawText(id, x, y, text)` | 在屏幕指定位置绘制文字(每帧调用维持显示) | +| `ui.removeDrawText(id)` | 移除指定绘制文字 | +| `ui.clearDrawTexts()` | 移除所有绘制文字 | + +**与 Box3 ClientUI 差异**: Box3 有完整的 2D UI 框架(盒子、文本、图片、输入框、滚动框等),Box3JS 目前提供轻量级的屏幕文字叠加层,不支持复杂的交互式 UI 控件。复杂 UI 建议通过 `gui.openGUI()` 使用容器 GUI。 + +### 8.3 客户端输入 (input) + +与 Box3 的 `player.onKeyDown/onKeyUp` 对应: + +| Box3JS 客户端 API | 说明 | +| ------------------------------ | --------------------------------------------------------- | +| `input.onKeyPress(key, fn)` | 注册按键按下回调,key 如 `"f5"`、`"c"`、`"space"` | +| `input.isKeyDown(key)` | 检查按键是否正在按住 | +| `input.onMouseClick(fn)` | 鼠标点击回调 `(button: 0=LMB/1=RMB/2=MMB, action, x, y)` | +| `input.getMouseX()` | 获取鼠标屏幕 X 坐标 | +| `input.getMouseY()` | 获取鼠标屏幕 Y 坐标 | + +**与 Box3 差异**: Box3 的 `player.onPress` 检测游戏内交互按钮(ACTION0/ACTION1/JUMP/WALK),Box3JS 的 `input.onKeyPress` 检测键盘按键。Box3 无鼠标事件,`input.onMouseClick` 是 Box3JS 独有扩展。 + +### 8.4 客户端聊天 (chat) + +与 Box3 的客户端聊天系统对应: + +| Box3JS 客户端 API | 说明 | +| ----------------------------------------- | --------------------------------------------------- | +| `chat.sendMessage(text)` | 发送聊天消息 | +| `chat.sendCommand(cmd)` | 执行客户端命令(以 `/` 开头) | +| `chat.onMessage(fn)` | 监听聊天消息 `(message, sender, isSystem)`,返回 false 可拦截 | + +### 8.5 客户端 GUI (gui) + +Box3JS 的容器 GUI 系统,与 Box3 的 UI 面板理念不同(Box3 是 2D UI 控件,Box3JS 是 MC 原版容器): + +| Box3JS 客户端 API | 说明 | +| ---------------------------------- | ---------------------------------------------------------------- | +| `gui.openGUI(config)` | 打开自定义容器 GUI,config: `{ title, rows(1-6), slots? }` | +| `controller.setItem(slot, id, n)` | 设置槽位物品 | +| `controller.getItem(slot)` | 获取槽位物品 `{ id, count }` | +| `controller.onSlotClick(fn)` | 槽位点击回调 `(slot)` | +| `controller.onClose(fn)` | GUI 关闭回调 | +| `controller.close()` | 主动关闭 GUI | + +**与 Box3 差异**: Box3 的 UI 系统是基于 2D 画布的布局控件(盒子/文本/图片/输入框),Box3JS 使用 MC 原版容器 GUI(类似箱子界面),更适合物品交互场景。Box3 的 2D UI 控件体系暂无对应实现。 + +### 8.6 客户端音频 (audio) + +与 Box3 的 ClientAudio 对应: + +| Box3JS 客户端 API | 说明 | +| -------------------------------------- | ------------------------------------------- | +| `audio.playSound(path, volume, pitch)` | 播放音效(支持原版和自定义音效) | +| `audio.playMusic(path, volume)` | 播放背景音乐(MUSIC 类别) | +| `audio.stopAll()` | 停止所有正在播放的音效和音乐 | +| `audio.getVolume(category)` | 获取指定音频类别的音量 | +| `audio.setVolume(category, value)` | 设置指定音频类别的音量(0.0–1.0) | -以下 Box3 API 全部运行在**客户端**(玩家浏览器),Box3JS 作为纯服务端模组**完全无法实现**: +**与 Box3 差异**: Box3 的 `Sound` 对象支持 `pause/resume/stop/setCurrentTime` 精细控制。Box3JS 的 `playSound/playMusic` 是 fire-and-forget 模式,无法暂停/恢复播放中的声音。`stopAll()` 可批量停止。 -| 类别 | 全局对象 | 说明 | -|------|---------|------| -| ClientUI | `ui`, `input`, `screenWidth`, `screenHeight` | 2D UI 系统(盒子、文本、图片、输入框、滚动框等) | -| ClientAudio | `Audio` | 客户端音频播放 | -| ClientMedia | `media` | 录音/播放 | -| ClientNavigator | `navigator` | 设备信息、语言 | -| ClientScreen | `screen` | 屏幕尺寸事件 | -| ClientHttp | `http` (客户端) | 客户端 HTTP 请求 | -| ClientWorld | `world` (客户端) | 3D 渲染开关 | +### 8.7 客户端存储 (storage) + +客户端独立的本地持久化存储,与服务端 storage 完全隔离: + +| Box3JS 客户端 API | 说明 | +| ---------------------------- | ---------------------------------------------------------- | +| `storage.getDataStorage(key)` | 获取具类型的数据存储空间(客户端本地),返回 `GameDataStorage` | +| `ds.set(key, value)` | 同步写入键值 | +| `ds.get(key)` | 同步读取,返回 value 或 null | +| `ds.update(key, fn)` | 原子更新 | +| `ds.increment(key, delta?)` | 原子增减(数值类型) | +| `ds.list(options)` | 分页列出 `{ pageSize?, ascending? }` | +| `ds.remove(key)` | 删除键 | +| `ds.keys()` | 返回所有键数组 | + +客户端存储位置: `config/box3/storage/-client/`,与服务端存储文件分离。 + +### 8.8 客户端数据库 (db) + +客户端本地 SQLite 数据库(需安装 `minecraft-sqlite-jdbc` 模组): + +| Box3JS 客户端 API | 说明 | +| ------------------------------- | --------------------------------------------------------- | +| `db.isAvailable()` | 检查 SQLite 驱动是否可用 | +| `db.sql(sql, ...params)` | 执行 SQL 查询/更新,返回 `GameQueryResult` | +| `db.sql\`SELECT ... WHERE id = ${id}\`` | Tagged template 语法,自动参数化防注入 | + +**GameQueryResult**: `rows` (数组), `firstRow`, `columnNames`, `columnCount`, `rowCount`, `affectedRows`, `isQuery` + +每个项目独立客户端数据库文件 `config/box3/data/-client.db`。 + +### 8.9 客户端 HTTP (http) + +客户端 HTTP 请求,支持同步和异步模式: + +| Box3JS 客户端 API | 说明 | +| --------------------------------------- | ------------------------------------------------------------ | +| `http.fetch(url, options?)` | 发起 HTTP 请求,options 同服务端,额外支持 `async: true` + `onResponse/onError` 回调 | +| `Response` 对象 | `ok`, `status`, `headers`, `data`, `json()`, `text()`, `arrayBuffer()` | + +> **客户端特有**: 支持 `async: true` 异步模式,通过 `onResponse`/`onError` 回调接收结果,不阻塞渲染帧。 + +### 8.10 RemoteChannel (客户端) + +客户端侧的跨端通信已在 [§7.4 RemoteChannel](#74-remotechannel-跨端通信) 详述。客户端通过 `remoteChannel.sendServerEvent()` 向服务端发送事件,通过 `remoteChannel.onClientEvent()` 接收服务端事件。 + +### 8.11 客户端控制台 (console) + +| Box3JS API | 说明 | +| -------------------------- | ---------------------------- | +| `console.log(...args)` | 输出到客户端日志 | +| `console.warn(...args)` | 输出警告 | +| `console.error(...args)` | 输出错误 | +| `console.debug(...args)` | 输出调试信息 | +| `console.clear()` | 清空控制台 | +| `console.assert(cond, ...)` | 条件断言输出 | + +### 8.12 与 Box3 客户端 API 差距 + +以下 Box3 客户端能力 Box3JS **暂未实现**: + +| Box3 客户端功能 | 状态 | 说明 | +| ------------------------- | ---- | --------------------------------------------------------------------------------------------- | +| 2D UI 控件体系 | ❌ | Box3 的盒子/文本/图片/输入框/滚动框等完整 UI 框架。Box3JS 仅有 drawText 叠加层 + 容器 GUI | +| `media` 录音/播放 | ❌ | MC 无内置录音 API | +| `navigator` 设备信息 | ❌ | MC 无对应 API | +| 3D 渲染开关 | ❌ | MC 渲染始终开启 | +| 客户端世界 API | ❌ | Box3 客户端可控制 3D 渲染开关等,Box3JS 无对应 | +| 自定义模型渲染 | ❌ | Box3 的 `entity.mesh` / `meshColor` / `meshScale` 等 | --- @@ -1092,6 +1258,7 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` 这些 API 是 Box3JS 利用 Minecraft 原生能力提供的,在 Box3 平台**不存在**: ### 9.1 世界管理 + - `world.thunderDensity` — 雷暴强度 - `world.clearWeather()` — 清除天气 - `world.getGameRule/setGameRule` — 游戏规则 @@ -1110,6 +1277,7 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` - `world.onButtonPressed` — 按钮点击事件(石质/木质按钮) ### 9.2 实体管理 + - `entity.nameTag` — 名牌 - `entity.glowing` — 发光 - `entity.invulnerable` — 无敌 @@ -1128,6 +1296,7 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` - `entity.setText/setTextColor/setTextBackgroundColor` — 文本显示实体控制 ### 9.3 玩家管理 + - `player.opLevel` — OP 权限 - `player.gameMode` — 游戏模式 - `player.dimension` — 维度切换 @@ -1149,6 +1318,7 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` - `player.grantAdvancement/revokeAdvancement` — 成就授予/撤销 ### 9.4 系统 + - `world.addScoreboard/removeScoreboard/setScore/getScore/showScoreboard/hideScoreboard/listScores` — 记分板 - `world.showBossbar/removeBossbar` — Boss 血条 - `world.createTeam/removeTeam/joinTeam/leaveTeam/getTeamOf` — 队伍管理 @@ -1157,56 +1327,94 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` - `voxels.fillVoxel/countVoxel/getVoxelName/setSpawner` — 方块批量操作 ### 9.5 额外事件 + - `world.onEntityDamage` — 实体受伤(Pre 阶段) - `world.onMessage` — 跨脚本消息 - `world.onButtonPressed` — 按钮点击事件(支持石质/木质按钮长按检测) ### 9.6 数据库 -- `db.sql` — SQLite 数据库操作(支持 tagged template 和参数化查询) + +- `db.sql` — SQLite 数据库操作 - `GameQueryResult` — 查询结果(rows, firstRow, columnNames, rowCount, affectedRows, isQuery) - 每个项目独立数据库文件 `config/box3/data/.db` ### 9.7 HTTP 请求 + - `http.fetch(url, options?)` — 同步 HTTP 请求,支持 GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS - `options.responseType` — 自动解析响应体(`"json"` / `"text"` / `"arrayBuffer"`),结果见 `resp.data` - `options.maxBodySize` — 响应体大小限制,超出截断并标记 `resp.truncated` - `Response.getHeader(name)` — 获取单个响应头值 - `Response.errorMessage` — 请求失败时的错误信息 +### 9.8 客户端扩展 + +Box3JS 客户端引擎提供的 Box3 平台**不存在**的能力: + +- `client.getFPS()` — 实时帧率监控 +- `client.getLookingAt()` — 准星目标检测(实体/方块 + 方向) +- `client.getPlayer()` — 本地玩家完整状态 +- `client.getServerInfo()` — 服务器连接信息 +- `ui.drawText(id, x, y, text)` — 屏幕坐标精确 HUD 绘制 +- `ui.removeDrawText(id)` / `ui.clearDrawTexts()` — HUD 文字管理 +- `ui.getScreenSize()` — 屏幕尺寸和 GUI 缩放 +- `input.onKeyPress(key, fn)` — 键盘快捷键注册(F1–F12 等) +- `input.onMouseClick(fn)` — 鼠标点击事件 +- `input.isKeyDown(key)` — 按键状态实时检测 +- `chat.onMessage(fn)` — 聊天消息拦截(可阻止广播) +- `gui.openGUI(config)` — 自定义容器 GUI(3–6 行,物品交互) +- `audio.playSound/playMusic/stopAll` — 客户端音效/音乐播放 +- `audio.getVolume/setVolume` — 音量类别精细控制 +- Client-side `storage` — 客户端独立持久化存储 +- Client-side `db` — 客户端本地 SQLite 数据库 +- Client-side `http.fetch(async: true)` — 非阻塞异步 HTTP 请求 + --- ## 10. 总结 ### 10.1 实现统计 -| 类别 | Box3 API 总数 | 已实现 | 部分实现 | 未实现 | MC 独有扩展 | -|------|--------------|--------|---------|--------|-------------| -| GameWorld | ~80 | ~30 | ~6 | ~44 | 33 | -| GameEntity | ~65 | ~21 | ~1 | ~43 | 21 | -| GamePlayerEntity | ~72 | ~27 | ~4 | ~41 | 22 | -| GameVoxels | 14 | 14 | 0 | 0 | 4 | -| GameDataStorage | 8 | 8 | 7 (同步化) | 0 | 2 | -| Math 类型 | ~100 | ~100 | 0 | 0 | 0 | -| 数据库 (db) | N/A | — | — | — | 1 | -| 其他服务端 | ~30 | 0 | 0 | ~30 | 0 | -| **总计** | **~369** | **~200** | **~18** | **~158** | **~83** | - -> **2026-05 更新**: 本阶段实现约 54 个新 Box3 API(属性对齐 + math 补全 + 物理属性 + token + 回调签名 + World API 补全),Math 类型现已完全对齐。 +| 类别 | Box3 API 总数 | 已实现 | 部分实现 | 未实现 | MC 独有扩展 | +| ---------------- | ------------- | -------- | ---------- | -------- | ----------- | +| GameWorld | ~80 | ~30 | ~6 | ~44 | 33 | +| GameEntity | ~65 | ~21 | ~1 | ~43 | 21 | +| GamePlayerEntity | ~72 | ~27 | ~6 | ~39 | 22 | +| GameVoxels | 14 | 14 | 0 | 0 | 4 | +| GameDataStorage | 8 | 8 | 7 (同步化) | 0 | 2 | +| Math 类型 | ~100 | ~100 | 0 | 0 | 0 | +| RemoteChannel | 5 | 5 | 0 | 0 | 0 | +| HTTP | 11 | 11 | 3 (同步化) | 0 | 4 | +| 数据库 (db) | N/A | — | — | — | 1 | +| **服务端总计** | **~355** | **~216** | **~23** | **~126** | **~87** | +| 客户端 ui | ~15 | 7 | 0 | ~8 | 3 | +| 客户端 input | ~8 | 5 | 0 | ~3 | 3 | +| 客户端 audio | ~6 | 5 | 0 | ~1 | 0 | +| 客户端 chat | 3 | 3 | 0 | 0 | 1 | +| 客户端 gui | ~12 | 6 | 0 | ~6 | 0 | +| 客户端 client | 5 | 5 | 0 | 0 | 5 | +| 客户端 storage | 8 | 8 | 7 (同步化) | 0 | 0 | +| 客户端 db | N/A | — | — | — | 1 | +| 客户端 http | 11 | 11 | 0 | 0 | 2 | +| **客户端总计** | **~68** | **~50** | **~7** | **~18** | **~15** | + +> **2026-05 更新**: 本阶段新增客户端脚本引擎,实现了 ~50 个 Box3 客户端 API(ui/input/audio/chat/gui/client/storage/db/http),并通过 remoteChannel 实现完整的服务端↔客户端双向通信。 ### 10.2 核心差异模式 1. **事件回调签名**: Box3 传事件对象 → Box3JS 传展开参数;已为 onTick/onPlayerJoin/Leave/Respawn 添加 tick 参数 -2. **异步存储**: Box3 的 Promise 存储 → Box3JS 的同步本地文件存储 -3. **视觉/渲染 API**: Box3 有独立渲染引擎 → Box3JS 依赖 MC 原版渲染,无法控制雾/光照/雪花/粒子系统参数 +2. **异步存储**: Box3 的 Promise 存储 → Box3JS 的同步本地文件存储(服务端 + 客户端一致) +3. **视觉/渲染 API**: Box3 有独立渲染引擎 → Box3JS 依赖 MC 原版渲染,暂无法控制雾/光照/雪花参数(客户端引擎已可用,未来可扩展) 4. **物理 API**: 基本物理属性(collides/fixed/gravity/friction/mass/restitution)已对齐,高级物理(接触力/OBB/碰撞过滤)不支持 -5. **客户端 API**: Box3 有完整 UI/音频/媒体客户端 API → Box3JS 纯服务端,全部不可用 +5. **客户端 API**: Box3 有完整 2D UI 框架 → Box3JS 客户端提供 HUD 叠加层 + 容器 GUI,暂无交互式 UI 控件 6. **动画系统**: Box3 有关键帧动画 → Box3JS 无 -7. **事件令牌**: 已实现 GameEventHandlerToken,所有 world.onXxx() 返回 token,支持 cancel()/active() +7. **事件令牌**: 已实现 GameEventHandlerToken,所有 onXxx() 返回 token,支持 cancel()/active() +8. **双端架构**: Box3JS 服务端 + 客户端分离,通过 remoteChannel 双向通信,与 Box3 架构理念一致 ### 10.3 Box3JS 的独特优势 -Box3JS 虽然缺失大量 Box3 视觉/渲染/客户端 API,但提供了 Box3 平台**完全不具备**的 MC 原生能力: +Box3JS 虽然缺失部分 Box3 视觉/渲染/2D UI API,但提供了 Box3 平台**完全不具备**的能力: +**服务端独有能力**: - **完整的原版方块系统** — 数百种方块类型、红石、容器、刷怪笼 - **生物 AI/寻路/战斗** — 全套 MC 生物行为控制 - **原版物品/装备/附魔** — 完整的物品系统 @@ -1216,14 +1424,30 @@ Box3JS 虽然缺失大量 Box3 视觉/渲染/客户端 API,但提供了 Box3 - **维度切换** — 下界/末地传送 - **游戏模式/OP 权限** — 权限管理 - **跨脚本通信** — 模块化脚本协作 +- **合成管理** — 配方禁用/恢复 +- **结构放置** — 数据包结构模板 + +**客户端独有能力**: +- **HUD 精确绘制** — `ui.drawText` 屏幕坐标文字,FPS 监控 +- **键盘快捷键** — `input.onKeyPress` 注册自定义热键 +- **鼠标事件** — `input.onMouseClick` 检测点击类型和坐标 +- **聊天拦截** — `chat.onMessage` 拦截/过滤/扩展聊天消息 +- **容器 GUI** — `gui.openGUI` 自定义物品交互界面 +- **客户端音效** — `audio.playSound/playMusic` 双类别音频控制 +- **客户端存储** — 玩家本地的持久化键值存储(独立于服务端) +- **客户端数据库** — 本地 SQLite 缓存/查询 +- **异步 HTTP** — `http.fetch(async: true)` 非阻塞网络请求 +- **准星检测** — `client.getLookingAt()` 实时方块/实体目标 +- **RemoteChannel** — 服务端↔客户端双向事件通信 ### 10.4 迁移建议 从 Box3 平台迁移脚本到 Box3JS 时: -1. **视觉相关代码需重写** — 雾/光照/天气效果无法直接迁移,需使用 MC 原版机制代替 +1. **视觉相关代码需重写** — 雾/光照/天气效果暂无法直接迁移,需使用 MC 原版机制代替(客户端引擎已可用,未来可扩展) 2. **回调参数需调整** — 将事件对象访问改为参数列表访问 -3. **异步存储改为同步** — 移除 `await`,直接调用存储方法 +3. **异步存储改为同步** — 移除 `await`,直接调用存储方法(服务端和客户端均同步) 4. **实体外观/物理不可用** — mesh/粒子/物理属性需删除或用 MC 代替 -5. **客户端代码全废弃** — UI/音频/媒体代码无对应 -6. **可充分利用 MC 扩展** — 记分板/Bossbar/生物 AI/物品系统/药水效果等是 Box3 没有的强大功能 +5. **客户端代码需重写** — Box3 的 2D UI 控件体系(盒子/文本/图片/输入框)暂无可直接对应的 API,可用 `ui.drawText` HUD + `gui.openGUI` 容器代替;音频代码可迁移至 `audio` 全局对象;键盘事件可迁移至 `input` 全局对象 +6. **充分利用双端架构** — 将客户端逻辑放入 `src/client/app.ts`,通过 `remoteChannel` 与服务端通信,与 Box3 的客户端/服务端分离理念一致 +7. **可充分利用 MC 扩展** — 记分板/Bossbar/生物 AI/物品系统/药水效果/合成管理等是 Box3 没有的强大功能 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README.md b/Box3JS-NeoForge-1.21.1/docs/api/README.md index 986563e..36512ae 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -167,6 +167,9 @@ Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服 | 发送服务端事件 | `remoteChannel.sendServerEvent({ ... })` | | 接收服务端事件 | `remoteChannel.onClientEvent((event) => { ... })` | | 客户端本地存储 | `storage.getDataStorage("key")` | +| 设置雾颜色 | `client.setFogColor(255, 100, 50)` | +| 设置雾距离 | `client.setFogStartDistance(10)` / `client.setFogEndDistance(50)` | +| 重置雾效果 | `client.resetFog()` | ### 视觉效果 @@ -200,8 +203,8 @@ Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服 | 右键方块时 | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | | 按钮按下时 | `world.onButtonPressed((entity, button, tick) => { ... })` | | 玩家重生时 | `world.onPlayerRespawn((entity, tick) => { ... })` | -| 定时执行一次 | `world.setTimeout(() => { ... }, ticks)` | -| 定时循环执行 | `world.setInterval(() => { ... }, ticks)` | +| 定时执行一次 | `setTimeout(() => { ... }, ticks)` | +| 定时循环执行 | `setInterval(() => { ... }, ticks)` | | 取消事件监听 | `token.cancel()` | | 检查事件是否活跃 | `token.active()` | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md index 901fa40..da87597 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md @@ -167,6 +167,9 @@ Find APIs by what you want to do, not by which global object they live on. | Send event to server | `remoteChannel.sendServerEvent({ ... })` | | Receive event from server | `remoteChannel.onClientEvent((event) => { ... })` | | Client-side local storage | `storage.getDataStorage("key")` | +| Set fog colour | `client.setFogColor(255, 100, 50)` | +| Set fog distance | `client.setFogStartDistance(10)` / `client.setFogEndDistance(50)` | +| Reset fog | `client.resetFog()` | ### Visual Effects @@ -200,8 +203,8 @@ Find APIs by what you want to do, not by which global object they live on. | On right-click block | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | | On button pressed | `world.onButtonPressed((entity, button, tick) => { ... })` | | On player respawn | `world.onPlayerRespawn((entity, tick) => { ... })` | -| Run once after delay | `world.setTimeout(() => { ... }, ticks)` | -| Run on interval | `world.setInterval(() => { ... }, ticks)` | +| Run once after delay | `setTimeout(() => { ... }, ticks)` | +| Run on interval | `setInterval(() => { ... }, ticks)` | | Cancel event listener | `token.cancel()` | | Check if active | `token.active()` | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client.md b/Box3JS-NeoForge-1.21.1/docs/api/client.md index 2fa08f0..8eb5a28 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client.md @@ -164,6 +164,70 @@ if (!info.isLocal) { } ``` +### 雾效控制 (Fog Control) + +Box3JS 客户端可以覆盖 Minecraft 的雾颜色和距离,实现类似 Box3 `world.fogColor` / `world.maxFog` 的效果。 + +### client.getFogColor() + +获取当前自定义雾颜色。未设置时返回 `null`。 + +```js +var color = client.getFogColor(); +if (color) { + console.log("Fog color: " + color.r + ", " + color.g + ", " + color.b); +} +``` + +### client.setFogColor(r, g, b) + +设置雾颜色(RGB 0-255)。 + +| 参数 | 类型 | 说明 | +|------|--------|------------| +| `r` | number | 红色 (0-255) | +| `g` | number | 绿色 (0-255) | +| `b` | number | 蓝色 (0-255) | + +```js +// 红色迷雾效果 +client.setFogColor(255, 50, 50); +``` + +### client.setFogStartDistance(distance) + +设置雾起始距离(方块)。低于此距离完全透明。 + +| 参数 | 类型 | 说明 | +|------------|--------|--------------------| +| `distance` | number | 雾起始距离(方块) | + +```js +// 雾从 10 个方块距离外开始 +client.setFogStartDistance(10); +``` + +### client.setFogEndDistance(distance) + +设置雾结束距离(方块),对应 Box3 的 `maxFog`。超过此距离完全被雾遮挡。 + +| 参数 | 类型 | 说明 | +|------------|--------|--------------------| +| `distance` | number | 雾结束距离(方块) | + +```js +// 50 格以外完全被雾遮挡 +client.setFogEndDistance(50); +``` + +### client.resetFog() + +重置雾效果为 Minecraft 默认值。 + +```js +client.resetFog(); +``` + ## input — 键盘输入 ### input.isKeyDown(key) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md b/Box3JS-NeoForge-1.21.1/docs/api/client_en.md index b01a224..d9d8acd 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client_en.md @@ -164,6 +164,70 @@ if (!info.isLocal) { } ``` +### Fog Control + +The Box3JS client can override Minecraft's fog colour and distance, providing effects similar to Box3's `world.fogColor` / `world.maxFog`. + +### client.getFogColor() + +Gets the current custom fog colour. Returns `null` if not set. + +```js +var color = client.getFogColor(); +if (color) { + console.log("Fog color: " + color.r + ", " + color.g + ", " + color.b); +} +``` + +### client.setFogColor(r, g, b) + +Sets the fog colour (RGB 0-255). + +| Parameter | Type | Description | +|-----------|--------|----------------| +| `r` | number | Red (0-255) | +| `g` | number | Green (0-255) | +| `b` | number | Blue (0-255) | + +```js +// Red fog effect +client.setFogColor(255, 50, 50); +``` + +### client.setFogStartDistance(distance) + +Sets the distance (in blocks) where fog begins. Fully transparent below this distance. + +| Parameter | Type | Description | +|------------|--------|---------------------------| +| `distance` | number | Fog start distance (blocks) | + +```js +// Fog starts 10 blocks away +client.setFogStartDistance(10); +``` + +### client.setFogEndDistance(distance) + +Sets the distance (in blocks) where fog becomes fully opaque, equivalent to Box3's `maxFog`. + +| Parameter | Type | Description | +|------------|--------|-------------------------| +| `distance` | number | Fog end distance (blocks) | + +```js +// Fully obscured by fog beyond 50 blocks +client.setFogEndDistance(50); +``` + +### client.resetFog() + +Resets fog to Minecraft's default behaviour. + +```js +client.resetFog(); +``` + ## input — Keyboard Input ### input.isKeyDown(key) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server.md b/Box3JS-NeoForge-1.21.1/docs/api/server.md index 84bad67..e8a65d0 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/server.md @@ -60,7 +60,7 @@ remoteChannel.sendClientEvent(entity, { | 实体交互 | `world.onInteract(handler)` | | 方块交互 | `world.onBlockActivate(handler)` | | 方块破坏/放置 | `world.onVoxelDestroy(handler)` / `world.onBlockPlace(handler)` | -| 定时器 | `world.setTimeout(fn, ticks)` / `world.setInterval(fn, ticks)` | +| 定时器 | `setTimeout(fn, ticks)` / `setInterval(fn, ticks)` | ```ts world.onChat((entity, message) => { diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md b/Box3JS-NeoForge-1.21.1/docs/api/server_en.md index be69033..ea0d25c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/server_en.md @@ -60,7 +60,7 @@ remoteChannel.sendClientEvent(entity, { | Entity interaction | `world.onInteract(handler)` | | Block interaction | `world.onBlockActivate(handler)` | | Block break/place | `world.onVoxelDestroy(handler)` / `world.onBlockPlace(handler)` | -| Timers | `world.setTimeout(fn, ticks)` / `world.setInterval(fn, ticks)` | +| Timers | `setTimeout(fn, ticks)` / `setInterval(fn, ticks)` | ```ts world.onChat((entity, message) => { diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md index 8eee60e..95149e7 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -367,34 +367,28 @@ world.say("§6[公告] §f比赛即将开始!"); ## 计时器 -### world.setTimeout(handler, ticks) +`setTimeout` 和 `setInterval` 是全局函数(不在 `world` 上)。Rhino 引擎不提供浏览器内置的定时器,由 Box3JS 提供。返回 `GameEventHandlerToken`,调用 `.cancel()` 取消。 -⬆ MC 扩展 | 延迟 `ticks` 后执行一次,返回 timer ID。 +### setTimeout(handler, ticks) -### world.setInterval(handler, ticks) +延迟 `ticks` 后执行一次。 -⬆ MC 扩展 | 每 `ticks` 重复执行,返回 timer ID。 +### setInterval(handler, ticks) -### world.clearTimeout(id) - -⬆ MC 扩展 | 取消 timeout。 - -### world.clearInterval(id) - -⬆ MC 扩展 | 取消 interval。 +每 `ticks` 重复执行。 ```js -var tid = world.setTimeout(() => { +var token = setTimeout(() => { world.say("3 秒后执行"); }, 60); // 60 ticks = 3 秒 -var iid = world.setInterval(() => { +var interval = setInterval(() => { world.say("每 10 秒执行一次"); }, 200); // 200 ticks = 10 秒 // 取消 -world.clearTimeout(tid); -world.clearInterval(iid); +token.cancel(); +interval.cancel(); ``` ## 记分板 @@ -467,12 +461,12 @@ world.removeScoreboard("kills"); ```js // 创建一个 3 分钟倒计时血条 var totalTicks = 3600; -var iid = world.setInterval(() => { +var iid = setInterval(() => { totalTicks -= 20; var remain = totalTicks / 3600; if (remain <= 0) { world.removeBossbar("timer"); - world.clearInterval(iid); + iid.cancel(); } else { world.showBossbar( "timer", @@ -550,7 +544,7 @@ world.borderSize = 500; world.setBorderDamage(2); world.setBorderWarning(10); -world.setTimeout(() => { +setTimeout(() => { world.shrinkBorder(100, 120); // 2 分钟缩到 100 }, 600); // 30 秒后开始 ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md index 979715d..b9974fe 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md @@ -369,34 +369,28 @@ world.say("§6[Announcement] §fThe match is about to begin!"); ## Timers -### world.setTimeout(handler, ticks) +`setTimeout` and `setInterval` are global functions (not on `world`). Rhino does not provide the browser built-in timers; Box3JS supplies them. They return `GameEventHandlerToken` — call `.cancel()` to stop. -⬆ MC Extension | Execute once after `ticks` delay, returns timer ID. +### setTimeout(handler, ticks) -### world.setInterval(handler, ticks) +Execute once after `ticks` delay. -⬆ MC Extension | Execute repeatedly every `ticks`, returns timer ID. +### setInterval(handler, ticks) -### world.clearTimeout(id) - -⬆ MC Extension | Cancel a timeout. - -### world.clearInterval(id) - -⬆ MC Extension | Cancel an interval. +Execute repeatedly every `ticks`. ```js -var tid = world.setTimeout(() => { +var token = setTimeout(() => { world.say("Executed after 3 seconds"); }, 60); // 60 ticks = 3 seconds -var iid = world.setInterval(() => { +var interval = setInterval(() => { world.say("Executed every 10 seconds"); }, 200); // 200 ticks = 10 seconds // Cancel -world.clearTimeout(tid); -world.clearInterval(iid); +token.cancel(); +interval.cancel(); ``` ## Scoreboard @@ -469,12 +463,12 @@ Remove a boss bar. ```js // Create a 3-minute countdown boss bar var totalTicks = 3600; -var iid = world.setInterval(() => { +var iid = setInterval(() => { totalTicks -= 20; var remain = totalTicks / 3600; if (remain <= 0) { world.removeBossbar("timer"); - world.clearInterval(iid); + iid.cancel(); } else { world.showBossbar( "timer", @@ -552,7 +546,7 @@ world.borderSize = 500; world.setBorderDamage(2); world.setBorderWarning(10); -world.setTimeout(() => { +setTimeout(() => { world.shrinkBorder(100, 120); // shrink to 100 over 2 minutes }, 600); // start after 30 seconds ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/README.md b/Box3JS-NeoForge-1.21.1/docs/guide/README.md index 4647174..fe4b34a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/README.md @@ -6,6 +6,7 @@ | 我想... | 读这个 | |---------|--------| +| 了解 Box3JS 的起源和为什么要做 | [Box3JS 与神奇代码岛](about-box3js.md) | | 10 分钟写出第一个脚本 | [快速开始](getting-started.md) | | 看现成模板改一改就实现功能 | [常用配方](recipes.md) | | 理解 Box3JS 内部怎么运作 | [运行原理](architecture.md) | @@ -15,16 +16,16 @@ ## 学习路径 ``` -快速开始 运行原理 JS vs Java - │ │ │ - │ 环境搭建 │ Rhino 引擎 │ 优劣势对比 - │ 第一个脚本 │ 作用域隔离 │ 适用场景 - │ 开发循环 │ 事件回调机制 │ 决策树 - │ 调试部署 │ 构建管线 │ 混合方案 - │ │ 网络通信 │ - │ │ 沙盒 + 热重载 │ - └───────────┬───────────┴──────────────────────┘ - │ - ▼ - API 参考 + 教程 +Box3JS 与神奇代码岛 → 快速开始 运行原理 JS vs Java + │ │ │ + │ 环境搭建 │ Rhino 引擎 │ 优劣势对比 + │ 第一个脚本 │ 作用域隔离 │ 适用场景 + │ 开发循环 │ 事件回调机制 │ 决策树 + │ 调试部署 │ 构建管线 │ 混合方案 + │ │ 网络通信 │ + │ │ 沙盒 + 热重载 │ + └───────────┬───────────┴──────────────────────┘ + │ + ▼ + API 参考 + 教程 ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/README_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/README_en.md index 8349aff..1d9c709 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/README_en.md @@ -6,6 +6,7 @@ Start here whether you want to get your hands dirty quickly or dive deep into in | I want to... | Read this | |-------------|----------| +| Understand where Box3JS comes from and why | [Box3JS & Box3](about-box3js_en.md) | | Write my first script in 10 minutes | [Quick Start](getting-started_en.md) | | Use ready-made templates to implement features | [Common Recipes](recipes_en.md) | | Understand how Box3JS works internally | [Architecture](architecture_en.md) | @@ -15,16 +16,16 @@ Start here whether you want to get your hands dirty quickly or dive deep into in ## Learning Path ``` -Quick Start Architecture JS vs Java - │ │ │ - │ Setup │ Rhino engine │ Pros & cons - │ First script │ Scope isolation │ Use case decision tree - │ Dev cycle │ Event callbacks │ Hybrid approach - │ Debug & deploy │ Build pipeline │ - │ │ Network comms │ - │ │ Sandbox + hot-reload │ - └──────────┬───────────────┴───────────────────────┘ - │ - ▼ - API Reference + Tutorials +Box3JS & Box3 → Quick Start Architecture JS vs Java + │ │ │ + │ Setup │ Rhino engine │ Pros & cons + │ First script │ Scope isolation │ Use case decision tree + │ Dev cycle │ Event callbacks │ Hybrid approach + │ Debug & deploy │ Build pipeline │ + │ │ Network comms │ + │ │ Sandbox + hot-reload │ + └──────────┬───────────────┴───────────────────────┘ + │ + ▼ + API Reference + Tutorials ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md b/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md new file mode 100644 index 0000000..77b551a --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md @@ -0,0 +1,137 @@ +# Box3JS 与神奇代码岛 + +本文介绍 Box3JS 的起源:它从哪来、为什么做、以及相比其他方案有什么独特优势。 + +## 神奇代码岛是什么 + +[神奇代码岛](https://dao3.fun)(Box3)是一款**多人联机 3D 游戏创作平台**。在这里,用户不需要学游戏引擎,只要会 JavaScript,就能在浏览器里创建竞速、对战、RPG、FPS 甚至 MOBA 等各类 3D 游戏。 + +核心特点: + +- **JavaScript 编程** — 用 JS/TypeScript 写游戏逻辑,零游戏引擎基础 +- **多人实时联机** — 平台自带服务器、房间系统、网络同步 +- **跨端即玩** — 电脑、手机、平板均可,无需下载 +- **社区 UGC** — 所有游戏均由玩家创作。 + +神奇代码岛为成千上万的年轻创作者提供了第一次"写游戏"的体验。它的 API 设计经过了大量创作者长期验证,形成了一套简洁、直觉、高效的编程模型。 + +## Box3JS 是什么 + +Box3JS 是一个**社区驱动的 Minecraft 模组**,它在 Minecraft 服务端内部嵌入了一个完整的 JavaScript 引擎(Mozilla Rhino),让开发者用 TypeScript 编写服务端游戏逻辑,并可选择下发客户端脚本实现按键监听、屏幕 UI 等本地交互。 + +**Box3JS 不是神奇代码岛的官方产品**,而是由熟悉 Box3 生态的社区开发者创建,延续了神奇代码岛 API 的设计哲学和命名风格。 + +## 为什么要做 Box3JS MC 版 + +### 出发点 + +神奇代码岛的 API 经过了长期打磨,设计得非常出色: + +- 全局对象注入,不需要 `import`/`require` +- Tick 制定时器,与游戏世界完美同步 +- 统一的事件取消模式(`GameEventHandlerToken`) +- 项目作用域隔离,多脚本互不干扰 + +但神奇代码岛跑在自己的封闭平台里,创作者无法接触到 Minecraft 生态——那里有更大的玩家社区、更丰富的方块和机制、以及成熟的模组分发体系。 + +**Box3JS 的核心目标:把神奇代码岛级别的开发体验带进 Minecraft。** + +### 谁适合用 Box3JS + +| 用户画像 | 为什么适合 | +|----------|-----------| +| 神奇代码岛开发者 | API 风格一致,已有技能直接复用,零学习成本 | +| 想给 MC 服务器写玩法的服主 | 不需要学 Java、Gradle、Mixin,写 JS 就行 | +| 编程教育场景 | TypeScript + 热重载 + 沙盒回滚 = 理想的编程教学环境 | +| 不想写 Java 模组的开发者 | 开箱即用的 API 覆盖常用功能,无编译管线负担 | + +## Box3JS 的独特优势 + +### 1. 延续 Box3 的 API 设计 + +如果你写过神奇代码岛的脚本,Box3JS 的代码你几乎能直接读懂: + +```js +// 这段代码在神奇代码岛和 Box3JS 中几乎一模一样 +world.onPlayerJoin((entity) => { + entity.player.directMessage(`§a欢迎 ${entity.player.name}!`); +}); + +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage("你好!"); + return false; + } + return true; +}); +``` + +API 对比详见 [Box3 API vs Box3JS 对比](../BOX3_API_COMPARISON.md)。 + +### 2. 真正的 Minecraft 世界 + +Box3JS 直接操作 Minecraft 的世界——真实的方块、原版实体、完整的游戏机制。你可以: + +- 操作真实 MC 方块(`voxels.setVoxel`、`voxels.fillVoxel`) +- 生成原版实体并设置 AI、装备、药水效果 +- 使用 Minecraft 的计分板、BossBar、队伍系统 +- 执行原版命令(`player.runCommand`) +- 控制天气、时间、世界边界、游戏规则 + +### 3. 热重载 + 沙盒 + +- **保存即生效** — 改代码 → `npm run build` → `/box3script reload`,不需要重启服务器 +- **沙盒保护** — 测试破坏性操作时开启沙盒,关闭时一键回滚所有修改 +- **自动热重载** — 开启 `watch` 后,保存构建产物自动触发 reload + +### 4. 独立分发 + +开发完成后,一键编译为独立 JAR 模组: + +``` +/box3script compile mygame +``` + +生成 `mygame-1.0.0.jar`,放入任意 NeoForge 1.21.1 服务端的 `mods/` 目录即可运行,无需源码,无需构建工具。可上传 CurseForge、Modrinth 分发。 + +### 5. 双端架构 + +``` +服务端(权威) 客户端(表现) +world.* / voxels.* client.* / input.* +entity.* / player.* ←→ ui.* / audio.* / gui.* +storage / db / http storage / db / http + └── remoteChannel ──────┘ +``` + +服务端控制游戏权威逻辑,客户端负责本地表现(UI、音效、按键监听),通过 `remoteChannel` 双向通信。 + +### 6. TypeScript 完整体验 + +- 完整的 `.d.ts` 类型声明,VS Code 智能提示 +- esbuild + Babel 构建管线,支持现代语法(`const`、箭头函数、模板字符串、`async/await`) +- ESLint 代码检查 +- `tsconfig.server.json` / `tsconfig.client.json` 互斥,防止 API 混用 + +## 与神奇代码岛的差异 + +Box3JS 并非 1:1 复制 Box3 的 API。差异源于两个平台的根本不同: + +| 差异领域 | 神奇代码岛 | Box3JS (MC) | +|----------|-----------|-------------| +| 渲染引擎 | 自研 3D 引擎 | Minecraft 原版渲染 | +| 物理引擎 | 自研物理 | Minecraft 原版物理 | +| 天气系统 | 独立的雨/雪/雾系统(丰富的参数控制) | 复用 MC 原版天气(rainDensity/thunderDensity) | +| 光照系统 | 手动/自然光照模式(lightMode/sunFrequency) | 复用 MC 原版光照 | +| 自定义模型 | 内置编辑器 | 需 Resource Pack(MC 机制) | +| 数据库 | 内置 KV 存储 | 内置 KV 存储 + SQLite | + +**设计原则:** 尽量保持 API 命名和语义一致,但对于 MC 无法支持或与 MC 机制冲突的功能,不强行模拟。详细的 API 对照见 [Box3 API vs Box3JS 对比](../BOX3_API_COMPARISON.md)。 + +--- + +## 下一步 + +- **从零开始**: [快速开始指南](getting-started.md) — 10 分钟写出第一个 MC 脚本 +- **理解原理**: [运行原理](architecture.md) — Rhino 引擎、作用域隔离、构建管线 +- **API 速查**: [API 功能速查](../api/README.md) — 按"我想做什么"查找 API diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js_en.md new file mode 100644 index 0000000..2386259 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js_en.md @@ -0,0 +1,139 @@ +# Box3JS and Box3 (Shenqi Code Island) + +This page explains where Box3JS comes from, why it exists, and what unique advantages it offers. + +## What is Box3 (神奇代码岛) + +[Box3](https://box3.fun) (also known as 神奇代码岛 or "Code Island 3.0") is a **multiplayer 3D game creation platform** developed by Shenzhen Qimengdao Technology Co., Ltd. (深圳奇梦岛科技有限公司), a brand under Codemao (编程猫). Users create racing games, PvP arenas, RPGs, FPS shooters, and even MOBAs — all in a browser, using nothing but JavaScript. + +Key features: + +- **JavaScript programming** — Write game logic in JS/TypeScript with zero game engine experience +- **Real-time multiplayer** — Platform provides servers, rooms, and network sync out of the box +- **Cross-platform** — Works on PC, mobile, and tablet with no downloads +- **Community UGC** — All games are created by users; the platform itself ships no preset content + +Box3 has given thousands of young creators their first experience of "making a game." Its API design has been battle-tested by a large community over years, resulting in a clean, intuitive, and efficient programming model. + +## What is Box3JS + +Box3JS is a **community-driven Minecraft mod** (NeoForge 1.21.1) that embeds a JavaScript engine (Mozilla Rhino) inside the Minecraft server, enabling developers to write server-side gameplay logic in TypeScript, with optional client-side scripts for key listeners, screen UI, and local audio. + +**Box3JS is not an official product of Box3.** It was created by community developers familiar with the Box3 ecosystem, continuing Box3's API design philosophy and naming conventions. + +## Why Box3JS for Minecraft Exists + +### The Idea + +Box3's API design is genuinely excellent — refined through years of community use: + +- Global object injection — no `import`/`require` needed +- Tick-based timers — perfectly synced with the game world +- Universal cancellation pattern (`GameEventHandlerToken`) +- Per-project scope isolation — multiple scripts never interfere + +But Box3 runs on its own closed platform. Creators can't reach the Minecraft ecosystem — which has a larger player community, richer block mechanics, and a mature mod distribution system. + +**Box3JS's core mission: bring the Box3-level developer experience into Minecraft.** + +### Who Box3JS is For + +| User | Why | +|------|-----| +| Box3 platform developers | Same API style — reuse existing skills with zero learning curve | +| MC server owners wanting custom gameplay | No Java, Gradle, or Mixin needed — just write JS | +| Programming education | TypeScript + hot reload + sandbox rollback = ideal teaching environment | +| Developers who don't want to write Java mods | Ready-to-use APIs cover common needs with no build pipeline burden | + +## Unique Advantages of Box3JS + +### 1. Box3-Compatible API Design + +If you've written scripts for Box3, Box3JS code looks immediately familiar: + +```js +// This code looks almost identical on Box3 and Box3JS +world.onPlayerJoin((entity) => { + entity.player.directMessage(`§aWelcome ${entity.player.name}!`); +}); + +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage("Hello!"); + return false; + } + return true; +}); +``` + +For a detailed API comparison, see [Box3 API vs Box3JS](../BOX3_API_COMPARISON.md). + +### 2. Real Minecraft World + +Box3JS directly operates the Minecraft world — real blocks, vanilla entities, complete game mechanics. You can: + +- Manipulate real MC blocks (`voxels.setVoxel`, `voxels.fillVoxel`) +- Spawn vanilla entities with AI, equipment, and potion effects +- Use Minecraft's scoreboards, BossBars, and team system +- Execute vanilla commands (`player.runCommand`) +- Control weather, time, world borders, and game rules + +### 3. Hot Reload + Sandbox + +- **Save-and-reload** — Edit → `npm run build` → `/box3script reload` — no server restart +- **Sandbox protection** — Enable sandbox to track all world changes; disable to roll back everything +- **Auto hot-reload** — Enable `watch` to auto-reload on build output changes + +### 4. Standalone Distribution + +When development is done, compile into a standalone JAR with one command: + +``` +/box3script compile mygame +``` + +Generates `mygame-1.0.0.jar` — drop it into any NeoForge 1.21.1 server's `mods/` directory. No source code needed, no build tools required. Ready for CurseForge and Modrinth distribution. + +### 5. Dual-Side Architecture + +``` +Server (authoritative) Client (presentation) +world.* / voxels.* client.* / input.* +entity.* / player.* ←→ ui.* / audio.* / gui.* +storage / db / http storage / db / http + └── remoteChannel ──────┘ +``` + +Server handles authoritative game logic; client handles local presentation (UI, audio, input). Bidirectional communication via `remoteChannel`. + +### 6. Full TypeScript Experience + +- Complete `.d.ts` type declarations for VS Code IntelliSense +- esbuild + Babel build pipeline with modern syntax support (`const`, arrow functions, template literals, `async/await`) +- ESLint code checking +- Mutually exclusive `tsconfig.server.json` / `tsconfig.client.json` to prevent API misuse + +## Differences from Box3 Platform + +Box3JS is not a 1:1 copy of Box3's API. Differences stem from the fundamental differences between the two platforms: + +| Area | Box3 Platform | Box3JS (MC) | +|------|--------------|-------------| +| Rendering | Custom 3D engine | Minecraft vanilla renderer | +| Physics | Custom physics engine | Minecraft vanilla physics | +| Weather | Independent rain/snow/fog (rich parameter control) | MC vanilla weather (rainDensity/thunderDensity) | +| Lighting | Manual/natural light modes (lightMode/sunFrequency) | MC vanilla lighting | +| Custom models | Built-in editor | Requires Resource Pack (MC mechanism) | +| Custom blocks/items | Runtime registration | Requires JAR compilation (`registries` + `/box3script compile`) | +| Database | Built-in KV storage | JSON storage + SQLite (requires sqlite-jdbc mod) | +| Networking | Platform-managed | `remoteChannel` custom payloads | + +**Design principle:** Keep API naming and semantics consistent where possible, but don't force-fit MC-incompatible features. For a detailed comparison, see [Box3 API vs Box3JS](../BOX3_API_COMPARISON.md). + +--- + +## Next Steps + +- **Get started**: [Quick Start Guide](getting-started_en.md) — write your first MC script in 10 minutes +- **How it works**: [Architecture](architecture_en.md) — Rhino engine, scope isolation, build pipeline +- **API reference**: [API by Task](../api/README_en.md) — find APIs by "I want to..." diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md index 0e537ee..7485cb1 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md @@ -1,62 +1,72 @@ # 快速开始:从零到第一个 Box3JS 脚本 -本指南面向**零模组开发经验**的读者。你只需要会 JavaScript,就能在 10 分钟内写出第一个 Minecraft 服务端脚本。 +本指南面向**零模组开发经验**的读者。你只需要会 JavaScript,就能在 10 分钟内写出第一个 Minecraft 服务端脚本,并在读完本指南后理解 Box3JS 的核心设计理念。 ## 目录 1. [Box3JS 是什么](#box3js-是什么) 2. [环境搭建](#环境搭建) 3. [创建项目](#创建项目) -4. [第一个脚本](#第一个脚本) -5. [开发循环](#开发循环) -6. [调试技巧](#调试技巧) -7. [发布部署](#发布部署) -8. [下一步](#下一步) +4. [理解项目结构](#理解项目结构) +5. [第一个脚本:逐行详解](#第一个脚本逐行详解) +6. [核心设计理念:为什么这样设计 API](#核心设计理念为什么这样设计-api) +7. [API 实战速览](#api-实战速览) +8. [开发循环](#开发循环) +9. [调试技巧](#调试技巧) +10. [发布部署](#发布部署) +11. [下一步](#下一步) --- ## Box3JS 是什么 -Box3JS 是一个**服务端脚本引擎模组**(NeoForge 1.21.1)。它在 Minecraft 服务器内嵌入了一个 JavaScript 运行时(Mozilla Rhino),让你用 JS/TypeScript 编写游戏玩法逻辑。 +Box3JS 是一个 **Minecraft 模组**,它在 Minecraft 服务器内部嵌入了一个完整的 JavaScript 运行时,让你用 JS/TypeScript 编写游戏玩法逻辑。同时,可选下发客户端脚本,实现按键监听、屏幕 UI、客户端音效等本地交互。 -### 能做什么 - -| 类别 | 示例 | -|------|------| -| 聊天命令 | `!heal`、`!home`、`!shop` | -| 事件响应 | 玩家进服欢迎、死亡惩罚、方块破坏记录 | -| 实体控制 | 生成怪物、设置 AI、自定义 Boss | -| 小游戏 | PvP 竞技场、跑酷、波次刷怪 | -| 世界操作 | 放置/替换方块、填充区域、修改天气时间 | -| 数据持久化 | JSON 存储、SQLite 数据库 | -| 游戏系统 | 计分板、BossBar、队伍、世界边界 | -| HTTP 请求 | 查询 Web API、Webhook 通知 | -| 客户端脚本 | 按键监听、屏幕 UI、客户端音效 | +> 了解 Box3JS 的起源和它与神奇代码岛的关系?→ [Box3JS 与神奇代码岛](about-box3js.md) -### 不能做什么 +### 一句话概括 -- **渲染自定义模型/粒子** — 需要客户端资源包或 Java 模组 -- **添加新方块/物品(实时)** — 需要编译为 JAR 模组(`/box3script compile`) -- **修改原版机制** — 如修改合成表、生物行为,这些需要 Mixin +> Box3JS = Minecraft 服务端里的 Node.js,但不需要你懂 Java 或模组开发。 -### 核心设计理念 +### 核心架构一览 ``` -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ -│ TypeScript │ ───→ │ Babel ES5 │ ───→ │ Rhino 引擎 │ -│ 源码 │ │ 编译 │ │ (JVM 内嵌) │ -└──────────────┘ └──────────────┘ └──────────────┘ - │ - ┌───────┴───────┐ - │ Minecraft │ - │ NeoForge API │ - └───────────────┘ +你在 VS Code 里写 构建工具帮你 Minecraft 帮你跑 + TypeScript ───→ 编译成 ES5 JS ───→ Rhino 引擎执行 + │ + ┌──────┴──────┐ + │ NeoForge │ + │ Minecraft │ + │ API 层 │ + └─────────────┘ ``` -- **运行在服务端 JVM 内**,直接调用 Minecraft 和 NeoForge API -- **TypeScript 源码**通过 Babel 编译为 ES5,适配 Rhino 引擎 -- **热重载** — 修改代码后不需要重启服务器 -- **沙盒隔离** — 每个项目独立作用域,互不影响 +- **你写 TypeScript**,享受类型提示和现代语法 +- **Babel 编译**为 ES5(因为 Rhino 引擎只支持 ES5) +- **esbuild 打包**为单个 JS 文件 +- **Rhino 在 JVM 内执行**,直接调用 Minecraft API +- **服务端 + 客户端双端运行**,通过 `remoteChannel` 通信 + +### 能做什么 + +| 类别 | 示例 | +| ---------- | ----------------------------------------- | +| 聊天命令 | `!heal`、`!home`、`!shop` | +| 事件响应 | 玩家进服欢迎、死亡惩罚、方块破坏记录 | +| 实体控制 | 生成怪物、设置 AI、自定义 Boss | +| 小游戏 | PvP 竞技场、跑酷、波次刷怪 | +| 世界操作 | 放置/替换方块、填充区域、修改天气时间 | +| 数据持久化 | JSON 存储、SQLite 数据库 | +| 游戏系统 | 计分板、BossBar、队伍、世界边界 | +| HTTP 请求 | 查询 Web API、Webhook 通知 | +| 客户端脚本 | 按键监听、屏幕 UI、客户端音效、自定义 GUI | + +### 不能做什么 + +- **渲染自定义模型/粒子** — 需要客户端资源包或 Java 模组 +- **添加新方块/物品(运行时)** — 需要编译为 JAR 模组(`/box3script compile`) +- **修改原版机制** — 如修改合成表、生物 AI 行为,这些需要 Mixin +- **使用现代 JS 语法在运行时** — Rhino 只支持 ES5,但源码中可以用 TypeScript 现代语法,构建时会转换 --- @@ -66,7 +76,7 @@ Box3JS 是一个**服务端脚本引擎模组**(NeoForge 1.21.1)。它在 Mi 1. **Minecraft 服务端** 安装了 Box3JS + NeoForge 1.21.1 2. **Node.js** 18+ (仅用于本地构建,服务端不需要) -3. 一个文本编辑器(VS Code 推荐) +3. 一个文本编辑器(VS Code 推荐,有完整的 TypeScript 智能提示) ### 验证安装 @@ -88,39 +98,43 @@ Box3JS 是一个**服务端脚本引擎模组**(NeoForge 1.21.1)。它在 Mi ## 创建项目 +### 一键创建 + 在游戏内执行: ``` /box3script create mygame ``` -这会在 `config/box3/script/mygame/` 生成完整的 TypeScript 项目: +这会在 `config/box3/script/mygame/` 生成完整的 TypeScript 项目。 + +### 理解项目结构 ``` config/box3/script/mygame/ ├── package.json ← 项目配置(名称、版本、构建依赖) ├── tsconfig.base.json ← TypeScript 公共编译选项 -├── tsconfig.server.json ← 服务端 TS 配置 -├── tsconfig.client.json ← 客户端 TS 配置 +├── tsconfig.server.json ← 服务端 TS 配置(引用 server/ 类型) +├── tsconfig.client.json ← 客户端 TS 配置(引用 client/ 类型) ├── build.mjs ← 构建脚本(esbuild + Babel) ├── eslint.config.mjs ← ESLint 规则 -├── types/ +├── types/ ← ★ 类型声明文件(API 的说明书) │ ├── shared.d.ts ← 服务端&客户端共享类型 │ ├── server/ │ │ ├── index.d.ts ← 服务端类型入口 -│ │ ├── server.d.ts -│ │ ├── entity.d.ts -│ │ ├── player.d.ts -│ │ ├── world.d.ts -│ │ └── voxels.d.ts +│ │ ├── server.d.ts ← world, remoteChannel, registries +│ │ ├── entity.d.ts ← GameEntity 接口 +│ │ ├── player.d.ts ← GamePlayer 接口 +│ │ ├── world.d.ts ← GameWorld 接口 +│ │ └── voxels.d.ts ← GameVoxels 接口 │ └── client/ │ ├── index.d.ts ← 客户端类型入口 -│ ├── client.d.ts -│ ├── audio.d.ts -│ ├── input.d.ts -│ ├── ui.d.ts -│ ├── chat.d.ts -│ └── gui.d.ts +│ ├── client.d.ts ← GameClient, RemoteChannel +│ ├── audio.d.ts ← GameAudio +│ ├── input.d.ts ← GameInput +│ ├── ui.d.ts ← GameUI +│ ├── chat.d.ts ← GameChat +│ └── gui.d.ts ← GameGUI, GuiController ├── src/ │ ├── server/ │ │ └── app.ts ← ★ 服务端入口(你写代码的地方) @@ -130,6 +144,12 @@ config/box3/script/mygame/ └── assets/lang/ ← 自定义内容本地化文本 ``` +**关键理解:** + +- `types/` 下的 `.d.ts` 文件是 API 的说明书 — VS Code 靠它们提供智能提示 +- `tsconfig.server.json` 和 `tsconfig.client.json` 是**互斥的** — 服务端代码中不会出现 `client`、`input` 等客户端全局对象 +- 你只需要在 `src/server/app.ts` 里写代码,构建工具处理剩下的一切 + ### 安装依赖 打开终端,进入项目目录: @@ -143,65 +163,675 @@ npm install --- -## 第一个脚本 +## 第一个脚本:逐行详解 + +打开 `src/server/app.ts`,清空已有内容,写入以下代码。我们来逐行理解。 -打开 `src/server/app.ts`,清空已有内容,写入: +### 1. 启动日志 ```js -// 1. 启动时输出日志 console.log("MyGame 脚本已启动!"); +``` + +**发生了什么:** `console` 是 Box3JS 注入的全局对象(不需要 `import`)。它背后是一个 Java 类 `Box3JSConsole`,通过 Rhino 桥接到 JS。`console` 支持 `log`、`warn`、`error`、`debug`、`clear`、`assert` 六个方法。 -// 2. 玩家加入时欢迎 +### 2. 玩家加入欢迎 + +```js world.onPlayerJoin((entity) => { const p = entity.player; - p.directMessage("§a欢迎 " + p.name + " 来到服务器!"); + + // 全服广播 + world.say(`§e${p.name} §7加入了服务器`); + + // 私密消息 + p.directMessage(`§a欢迎来到服务器,${p.name}!`); // 粒子欢迎特效 - const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + const { position: pos } = p; + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); +``` + +**逐行理解:** -// 3. 聊天命令 +- `world.onPlayerJoin(...)` — 注册一个"玩家加入"事件监听器。返回 `GameEventHandlerToken`(本例中没有保存,意味着这个监听器在脚本重载前一直有效)。 +- `entity.player` — `entity` 是回调参数,代表加入的实体。`.player` 获取该实体的玩家包装对象(如果实体不是玩家,`.player` 为 `undefined`)。 +- `p.directMessage(...)` — 发送私密消息,只有该玩家能看到。 +- `§a` 是 Minecraft 颜色代码(绿色)。`§e` = 黄色,`§6` = 金色,`§7` = 灰色,`§c` = 红色。 +- `p.position` — 返回 `GameVector3` 对象,包含 `.x`、`.y`、`.z` 属性。 +- `world.spawnParticleCircle(...)` — 在玩家位置生成一圈粒子效果。 +- `world.playSound(...)` — 在玩家位置播放音效。 + +### 3. 聊天命令 + +```js world.onChat((entity, message) => { const p = entity.player; if (message === "!hello") { - p.directMessage("§e你好," + p.name + "!"); + p.directMessage(`§e你好,${p.name}!`); return false; // 阻止消息显示在聊天栏 } if (message === "!pos") { - const pos = p.position; - p.directMessage("§e你的坐标: §f" + - Math.floor(pos.x) + ", " + - Math.floor(pos.y) + ", " + - Math.floor(pos.z)); + const { position: pos2 } = p; + p.directMessage( + `§e你的坐标: §f${Math.floor(pos2.x)}, ${Math.floor(pos2.y)}, ${Math.floor(pos2.z)}`, + ); + return false; + } + + if (message === "!help") { + p.directMessage("§e可用命令: !help, !hello, !pos, !home, !shop"); return false; } return true; // 不是命令的消息正常发送 }); +``` -// 4. 定时公告 -world.setInterval(() => { +**关键概念:** + +- `return false` — 阻止事件继续(消息不会广播到聊天栏)。这是 Box3JS 事件的通用约定:**返回 false 阻断默认行为**。 +- `return true` — 放行,消息正常广播。 +- `Math.floor()` — 常规 JS,坐标向下取整,更易读。 + +### 4. 定时公告 + +```js +setInterval(() => { const count = world.querySelectorAll("*").length; - world.say("§7[公告] §f当前在线: " + count + " 人"); + world.say(`§7[公告] §f当前在线: ${String(count)} 人`); }, 6000); // 6000 ticks = 5 分钟 ``` -### 关键概念 +**关键理解:** + +- `setInterval` 是**全局函数**(不是 `world.setInterval`),和浏览器/Node.js 一致。 +- 第二个参数是 **ticks**(Minecraft 时间单位),不是毫秒。1 秒 = 20 ticks。 +- `setInterval` 返回 `GameEventHandlerToken`,可以调用 `.cancel()` 取消。 +- `world.querySelectorAll("*")` 返回所有在线实体列表。 +- `world.say(...)` 向全服广播消息。 + +### 完整的 Tick 换算表 + +| 时长 | Ticks | +| ------- | ------ | +| 1 秒 | 20 | +| 5 秒 | 100 | +| 30 秒 | 600 | +| 1 分钟 | 1,200 | +| 5 分钟 | 6,000 | +| 10 分钟 | 12,000 | +| 30 分钟 | 36,000 | + +--- + +## 核心设计理念:为什么这样设计 API + +理解 Box3JS API 的设计理念,能让你写出更高效、更安全的脚本。以下是最重要的几个设计决策及其原因。 + +### 设计 1:全局对象注入,不需要 import + +```js +// Box3JS 中直接使用,不需要 import +world.onTick(() => { ... }); +console.log("hello"); +storage.getDataStorage("coins"); + +// 对比:如果在 Node.js 中 +// const { world } = require("box3js"); ← 不需要! +``` + +**为什么?** Rhino 是一个裸的 ECMAScript 引擎,不支持 CommonJS `require()` 或 ES Module `import`。Box3JS 通过 Java 层在 Rhino 作用域初始化时,直接把所有 API 对象注入为全局变量。TypeScript 的 `.d.ts` 文件用 `declare` 声明这些全局对象,让你在 VS Code 中获得完整的类型提示。 + +### 设计 2:Tick 制时间,不是毫秒 + +```js +// Box3JS 的时间单位是 tick(1/20 秒) +setTimeout(() => { ... }, 100); // 100 ticks = 5 秒后 + +// 对比浏览器: +// setTimeout(() => { ... }, 5000); // 5000 毫秒 = 5 秒后 +``` + +**为什么?** Box3JS 的定时器是**在主线程的游戏 Tick 循环中**执行的,不创建任何 Java 线程。每次游戏 Tick(1/20 秒),引擎检查所有定时器,递减剩余 tick 数,到 0 时触发回调。这样做的好处是: + +- **线程安全** — 回调总是在主线程执行,你可以安全地调用任何 Minecraft API +- **精确同步** — 定时器与游戏世界完全同步,不会出现"服务器卡了但定时器还在走"的情况 +- **零开销** — 不创建额外的线程或线程池 + +### 设计 3:GameEventHandlerToken — 统一的取消模式 + +```js +// 所有 onXxx() 和 setTimeout/setInterval 都返回 GameEventHandlerToken +const token = world.onTick(() => { + // 每 tick 执行 +}); + +// 取消监听(两种方式等效) +token.cancel(); + +// 检查是否仍活跃 +if (token.active()) { + // ... +} +``` + +**为什么?** 早期设计为每种事件提供独立的取消方法(如 `removeTickListener`、`removeChatListener`),但这样会导致: + +1. 需要记住每种事件的取消 API 名称 +2. 无法统一管理(你想在脚本停止时批量取消怎么办?) + +统一返回 `GameEventHandlerToken` 后: + +- **一个模式适用所有** — 不管是 `onTick`、`onPlayerJoin`、`onChat` 还是 `setInterval`,都用 `.cancel()` +- **脚本重载自动清理** — 停止项目时,引擎遍历所有 token 批量取消,无遗漏 +- **链式管理** — 你可以把多个 token 放进数组,统一 `.cancel()` + +### 设计 4:项目作用域隔离 + +``` +服务端同时运行 3 个脚本项目,互不影响: + +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Scope A │ │ Scope B │ │ Scope C │ +│ "mygame" │ │ "lobby" │ │ "survival" │ +│ │ │ │ │ │ +│ var x = 1 │ │ var x = 2 │ │ var x = 3 │ +│ 自己的事件 │ │ 自己的事件 │ │ 自己的事件 │ +│ 自己的存储 │ │ 自己的存储 │ │ 自己的存储 │ +│ 自己的定时器 │ │ 自己的定时器 │ │ 自己的定时器 │ +└──────────────┘ └──────────────┘ └──────────────┘ +``` + +**为什么?** 一个服务器可能同时运行多个脚本(大厅系统、小游戏、经济系统……)。如果没有隔离: + +- 变量名冲突(两个脚本都定义了 `var playerCount`) +- 事件回调互相干扰(`/box3script reload lobby` 意外清除了 survival 的回调) +- 数据泄露(lobby 的脚本读到了 survival 的存储数据) + +Box3JS 给每个项目分配**独立的 Rhino 顶级作用域**,存储在独立的 `Box3JSEventBus` 命名空间中。停止或重载一个项目完全不影响其他项目。 + +### 设计 5:双端架构 + remoteChannel + +``` +┌──────────────────────┐ ┌──────────────────────┐ +│ 服务端 (Server) │ │ 客户端 (Client) │ +│ │ │ │ +│ world.* │ │ client.* │ +│ voxels.* │ JSON │ audio.* │ +│ entity.* │ ←─────→ │ input.* │ +│ player.* │ 事件 │ ui.* │ +│ storage/db/http │ │ chat.* / gui.* │ +│ remoteChannel ──────┼─────────┼── remoteChannel │ +│ │ │ storage/db/http │ +└──────────────────────┘ └──────────────────────┘ +``` + +**为什么分开?** + +- **安全性** — 服务端是世界权威(方块、实体、数据),客户端只能操作本地表现(UI、音效、输入) +- **性能** — 客户端脚本运行在玩家自己的电脑上,不消耗服务器资源 +- **灵活性** — 你可以只写服务端脚本(大多数场景),或增加客户端脚本来提升体验 + +**remoteChannel 通信规则:** + +```js +// 服务端 → 单个客户端 +remoteChannel.sendClientEvent(player, { type: "welcome", msg: "hi" }); + +// 服务端 → 所有客户端 +remoteChannel.broadcastClientEvent({ type: "game_start" }); + +// 客户端 → 服务端 +remoteChannel.sendServerEvent({ key: "space", pressed: true }); +``` + +**重要限制:** 跨网络传输的数据必须是 **JSON 可序列化**的。不能传函数、`GameVector3` 实例、Java 对象。如果需要传坐标,用 `{ x: 1, y: 2, z: 3 }` 而不是 `new GameVector3(1, 2, 3)`。 + +### 设计 6:TypeScript 源码 + Babel 编译为 ES5 + +``` +src/server/app.ts Babel esbuild dist/server.js +(TypeScript, ES2020) ───→ ES5 JavaScript ───→ bundle ───→ (一个文件) +``` + +**为什么需要构建步骤?** + +- **Rhino 1.9.1 只支持 ES5** — `let`、`const`、箭头函数、模板字符串、`class` 都是 ES6+ 语法,Rhino 不认识 +- **Babel 负责降级** — 把现代语法转成 `var`、`function`、字符串拼接等 ES5 写法 +- **esbuild 负责打包** — 虽然你可以写多个 `.ts` 文件,但 Rhino 没有模块系统。esbuild 把所有文件合并为一个 IIFE + +**你可以放心在源码中用:** + +- `const` / `let`(转为 `var`) +- 箭头函数 `() => {}`(转为 `function(){}`) +- 模板字符串 `` `hello ${name}` ``(转为 `"hello " + name`) +- `class`(转为 `function` + prototype) +- `async/await`(通过 regenerator 转换) + +### 设计 7:事件回调返回 false 阻断 + +```js +world.onChat((entity, message) => { + if (message.startsWith("!")) { + // 这是命令,不要广播 + return false; + } + return true; // 正常消息放行 +}); +``` + +**为什么?** 借鉴了浏览器 DOM 事件的 `preventDefault` 模式。Minecraft 事件通常有"默认行为"(如聊天消息广播给所有人)。`return false` 告诉引擎:"我已经处理了这个事件,不要执行默认行为"。 + +### 设计 8:沙盒模式 — 安全测试 + +``` +/box3script sandbox mygame # 开启沙盒 +# ... 测试脚本(生成实体、修改方块、爆炸)... +/box3script sandbox mygame # 关闭 → 自动回滚所有修改 +``` + +**为什么?** 一旦脚本修改了世界,这些修改是永久性的(方块被替换、实体被生成)。沙盒模式追踪脚本对世界的所有修改,关闭时自动回滚。这让开发者可以大胆测试破坏性操作,不用担心搞坏正式服。 + +--- + +## API 实战速览 + +以下按"我想做什么"组织,覆盖最常用的 API。完整的 API 参考见 [API 文档](../api/README.md)。 + +### 消息与聊天 + +```js +// 全服广播 +world.say("§6服务器将在 5 分钟后重启"); + +// 私密消息(只有目标玩家看到) +player.directMessage("§a你的余额: 100 金币"); + +// 快捷栏上方文字 +player.actionBar("§e按 F 键打开菜单"); + +// 屏幕中央大标题 +player.title("§6BOSS 战", "§c远古巨龙 已苏醒"); + +// 拦截聊天(做命令系统) +world.onChat((entity, message) => { + if (message === "!help") { + entity.player.directMessage("§e可用命令: !help, !home, !shop"); + return false; + } + return true; +}); +``` + +### 玩家属性与控制 + +```js +// 获取玩家信息 +const name = player.name; +const pos = player.position; // GameVector3 { x, y, z } +const hp = player.hp; +const mode = player.gameMode; // "survival", "creative", "adventure", "spectator" + +// 修改玩家状态 +player.hp = 20; // 回满血 +player.maxHp = 40; // 增加最大生命值 +player.food = 20; // 回满饱食度 +player.gameMode = "creative"; // 切换创造模式 +player.canFly = true; // 允许飞行 +player.flying = true; // 开始飞行 + +// 传送 +player.teleport(new GameVector3(100, 64, 100)); + +// 踢出 +player.kick("你已被管理员踢出"); + +// 以玩家身份执行原版命令 +player.runCommand("effect give @s minecraft:speed 30 1"); +``` + +### 物品与背包 + +```js +// 给物品 +player.giveItem("minecraft:diamond", 64); +player.giveItem("minecraft:diamond_sword", 1); + +// 给带名称的物品(第 4 个参数为描述文字数组) +player.giveNamedItem("minecraft:stick", 1, "§6魔法棒", ["右键使用"]); + +// 给带附魔的物品(附魔为 { 附魔ID: 等级 } 对象) +player.giveEnchantedItem("minecraft:diamond_sword", 1, { + "minecraft:sharpness": 5, + "minecraft:unbreaking": 3, +}); + +// 查询手持物品 +const held = player.getHeldItem(); + +// 清空背包 +player.clearInventory(); +``` + +### 事件系统 + +```js +// 每 tick(谨慎使用!每 tick 中的重操作会拖慢服务器) +const tickToken = world.onTick(() => { + // 每 tick 执行 +}); + +// 玩家事件 +world.onPlayerJoin((entity) => { + entity.player.directMessage("欢迎!"); +}); + +world.onPlayerLeave((entity, _tick) => { + world.say(`${entity.player.name} 离开了服务器`); +}); + +world.onPlayerRespawn((entity, _tick) => { + entity.player.teleport(new GameVector3(0, 100, 0)); + entity.player.directMessage("你重生了!"); +}); + +// 实体事件 +world.onEntityDeath((entity, _killer, _tick) => { + if (entity.isPlayer()) { + world.say(`${entity.player.name} 死了`); + } +}); + +world.onEntityDamage((entity, amount, source, _attacker, _tick) => { + if (amount > 10) { + console.log(`高额伤害: ${String(amount)} 来源: ${source}`); + } +}); + +// 交互事件 +world.onInteract((entity, target, _tick) => { + // 玩家右键实体 + if (target.hasTag("npc")) { + entity.player.directMessage("你好!"); + } +}); + +world.onBlockActivate((entity, x, y, z, voxel, _tick) => { + // 玩家右键方块 + if (voxel === "minecraft:chest") { + entity.player.directMessage( + `你点击了位于 ${String(x)}, ${String(y)}, ${String(z)} 的箱子`, + ); + } +}); + +world.onBlockPlace((entity, x, y, z, voxel, _voxelId, _tick) => { + // 玩家放置方块 + console.log( + `${entity.player.name} 在 ${String(x)}, ${String(y)}, ${String(z)} 放置了 ${voxel}`, + ); +}); + +world.onVoxelDestroy((entity, _x, _y, _z, voxel, _tick) => { + // 玩家破坏方块 + if (voxel === "minecraft:diamond_block") { + entity.player.directMessage("§c不能破坏钻石块!"); + return false; // 阻止破坏 + } +}); + +// 定时器(全局函数) +const timer = setTimeout(() => { + world.say("30 秒到了!"); +}, 600); // 600 ticks = 30 秒 + +const interval = setInterval(() => { + world.say("每分钟公告"); +}, 1200); // 1200 ticks = 1 分钟 + +// 取消定时器 +timer.cancel(); +interval.cancel(); +``` + +### 实体操控 + +```js +// 生成实体(返回 GameEntity | null) +const zombie = world.spawnEntity( + "minecraft:zombie", + new GameVector3(100, 64, 100), +); +if (zombie) { + // 使用 zombie ... +} + +// 带完整配置创建实体(nameTag/glowing/equipment 需创建后设置) +const boss = world.createEntity({ + type: "minecraft:zombie", + position: new GameVector3(100, 64, 100), + hp: 200, + maxHp: 200, + tags: ["boss"], +}); +if (boss) { + boss.setNameTag("§c远古僵尸王"); + boss.glowing = true; + boss.setEquipment("head", "minecraft:diamond_helmet"); + boss.setEquipment("chest", "minecraft:diamond_chestplate"); + + // 操控实体 + boss.setAI(false); // 关闭 AI(原地不动) + boss.invulnerable = true; // 无敌 + boss.navigateTo(110, 64, 100, 1.5); // 导航到目标位置 + + // 药水效果 + boss.addEffect("minecraft:strength", 600, 2, false); + boss.addEffect("minecraft:speed", 600, 1, true); + boss.clearEffects(); + + // 装备 + boss.setEquipment("head", "minecraft:iron_helmet"); + boss.setEquipment("mainhand", "minecraft:iron_sword"); + + // 标签(用于标记和查询) + boss.addTag("boss"); + boss.addTag("stage_1"); + boss.hasTag("boss"); // → true +} + +// 查询实体 +const nearby = world.entitiesInRadius(pos, 10); // 半径 10 格内 +const all = world.querySelectorAll("*"); // 所有实体 +const players = world.querySelectorAll("player"); // 所有玩家 +const monsters = world.querySelectorAll("monster"); // 所有怪物 +``` + +### 方块操作 + +```js +// 读取方块 +const block = voxels.getVoxel(100, 64, 100); + +// 放置方块 +voxels.setVoxel(100, 64, 100, "minecraft:stone"); +voxels.setVoxel(100, 65, 100, "minecraft:torch"); -- **全局对象不需要 import** — `world`、`console`、`player` 等由 Box3JS 注入 -- **事件回调返回 false 阻止默认行为** — `onChat` 返回 false 阻止消息广播 -- **Tick 是 MC 的时间单位** — 1 秒 = 20 ticks,`setInterval` 参数是 ticks -- **§ 是 MC 颜色代码** — `§a` = 绿色, `§e` = 黄色, `§6` = 金色, `§7` = 灰色 +// 区域填充 +voxels.fillVoxel(0, 64, 0, 10, 70, 10, "minecraft:glass"); + +// 替换方块(只替换指定类型) +voxels.fillVoxel(0, 64, 0, 10, 70, 10, "minecraft:air", "minecraft:stone"); +``` + +### 数据持久化 + +```js +// JSON 存储(每个项目独立命名空间) +const store = storage.getDataStorage("coins"); +store.set("player1", 100); +const coins = store.get("player1"); // → 100 +store.delete("player1"); +const keys = store.keys(); // → 所有 key 的数组 + +// SQLite 数据库 +db.sql("CREATE TABLE IF NOT EXISTS players (name TEXT, score INT)"); +db.sql("INSERT INTO players VALUES ('Steve', 100)"); + +const result = db.sql("SELECT * FROM players WHERE score > 50"); +// result.rows[0] → { name: "Steve", score: 100 } +// result.firstRow → { name: "Steve", score: 100 } +// result.rowCount → 1 +// result.columnNames → ["name", "score"] +``` + +### 游戏系统 + +```js +// 计分板 +world.addScoreboard("kills"); +world.setScore("Steve", "kills", 42); +world.showScoreboard("sidebar", "kills"); + +// BossBar +world.showBossbar("boss1", "§c远古巨龙", 0.8, "red"); +world.setBossbar("boss1", "§c远古巨龙 §7[80%]", 0.5); + +// 队伍 +world.createTeam("red", "red"); +world.createTeam("blue", "blue"); +world.joinTeam(entity, "red"); + +// 世界边界 +world.borderSize = 500; // 设置边界大小 +world.shrinkBorder(100, 1200); // 在 1200 ticks 内缩到 100 + +// 天气和时间 +world.time = 6000; // 设置时间(0=日出, 6000=正午, 12000=日落, 18000=午夜) +world.rainDensity = 0; // 停雨 +world.clearWeather(); // 晴天 +world.thunderDensity = 1; // 雷暴 + +// 游戏规则 +world.setGameRule("keepInventory", true); +world.setGameRule("doDaylightCycle", false); +``` + +### 视觉效果 + +```js +const pos = new GameVector3(100, 64, 100); + +// 粒子 +world.spawnParticle("minecraft:flame", pos.x, pos.y, pos.z, 0, 0, 0, 1, 10); +world.spawnParticleCircle(pos.x, pos.y, pos.z, 2, "minecraft:heart", 30); + +// 烟花 +world.launchFirework(pos.x, pos.y, pos.z, "red", "large_ball"); +world.launchFirework(pos.x, pos.y, pos.z, "green", "star"); + +// 闪电和爆炸 +world.strikeLightning(pos.x, pos.y, pos.z); +world.explode(pos.x, pos.y, pos.z, 4); // 威力 4 的爆炸 + +// 音效 +world.playSound("minecraft:entity.ender_dragon.growl", pos, 1.0, 1.0); +player.playSound("minecraft:block.note_block.pling", 1.0, 2.0); // 只有该玩家听到 +``` + +### HTTP 请求 + +```js +// GET 请求 +const resp = http.fetch("https://api.example.com/data"); +if (resp.ok) { + const data: unknown = resp.json(); + console.log(data); +} + +// POST JSON +const resp2 = http.fetch("https://api.example.com/webhook", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "服务器重启了" }) +}); + +// 超时设置 +const resp3 = http.fetch("https://slow-api.com/data", { + timeout: 5000 // 5 秒超时 +}); +``` + +### 客户端脚本(需 Box3JS 客户端 Mod) + +```js +// 客户端入口: src/client/app.ts + +// 每帧执行 +client.onTick(() => { + // 客户端 tick 回调 +}); + +// 键盘输入 +if (input.isKeyDown("space")) { + // 空格键正在被按住 +} + +input.onKeyPress("f", () => { + // F 键被按下时触发 + remoteChannel.sendServerEvent({ action: "open_menu" }); +}); + +// 屏幕 UI +ui.showOverlay("§e按 F 打开菜单"); // 快捷栏上方 +ui.showTitle("§6BOSS 出现!", "§c准备战斗"); // 屏幕中央 + +// 客户端音效 +audio.playSound("minecraft:block.note_block.pling", 1.0, 1.0); +audio.playMusic("minecraft:music.game", 0.5, 1.0); +audio.stopAll(); + +// 雾效控制(客户端渲染) +client.setFogColor(255, 100, 50); // 红雾外观 +client.setFogStartDistance(10); // 雾从 10 格开始 +client.setFogEndDistance(50); // 50 格外完全遮挡 +client.resetFog(); // 恢复默认 + +// 聊天 +chat.sendMessage("大家好!"); +chat.onMessage((msg, _sender, _isSystem) => { + if (msg.includes("秘密")) { + // 处理包含"秘密"的消息 + } +}); + +// 接收服务端事件 +remoteChannel.onClientEvent((event) => { + if (event.args.type === "boss_spawned") { + audio.playSound("minecraft:entity.ender_dragon.growl", 1.0, 1.0); + } +}); +``` --- ## 开发循环 -每次修改代码后的标准流程: +### 标准流程 + +每次修改代码后: ``` 改代码 → npm run build → /box3script reload mygame → 测试 @@ -222,11 +852,11 @@ npm run build 构建做了什么: -1. **Babel** 将 TypeScript 编译为 ES5 JavaScript -2. **esbuild** 将所有模块打包为一个文件 +1. **Babel** 将 TypeScript 编译为 ES5 JavaScript(因为 Rhino 只支持 ES5) +2. **esbuild** 将所有模块打包为一个文件(因为 Rhino 没有 `require()`) 3. 输出到 `dist/server.js` 和 `dist/client.js` -### 加载 +### 加载与重载 在游戏内: @@ -235,6 +865,8 @@ npm run build /box3script reload mygame # 修改后重载(无需重启服务器) ``` +`reload` 是原子的:先停止旧脚本(清理所有事件回调、计时器、计分板),再加载新脚本。 + ### 自动热重载 开启文件监控后,保存代码 + build 会自动触发 reload: @@ -243,6 +875,18 @@ npm run build /box3script watch ``` +**注意**:`watch` 监控的是 `dist/` 下的编译产物(`.js`),不是 `src/` 下的源码。所以你需要先 `npm run build` 生成新的 `dist/` 文件,watch 才会检测到变化。配合 `npm run build -- --watch` 可以实现保存即热重载。 + +### 多项目管理 + +``` +/box3script start mygame lobby # 同时启动多个项目 +/box3script stop mygame # 停止单个 +/box3script stopall # 停止全部 +/box3script reload mygame # 重载单个 +/box3script # 查看所有项目状态 +``` + --- ## 调试技巧 @@ -255,17 +899,20 @@ npm run build 2. **看状态** — `/box3script` 检查项目是否是 `◉`(已加载) 3. **看构建** — `npm run build` 是否报错 4. **加日志** — 用 `console.log()` 在关键位置打印变量值 -5. **看行号** — Java 异常栈会包含 JS 文件名和行号 +5. **看行号** — Java 异常栈会包含 JS 文件名和行号(因为脚本被 Rhino 解释执行,行号对应编译后的 `dist/server.js`,不是 `.ts` 源码) ### 常见错误 -| 错误 | 原因 | 解决 | -|------|------|------| -| `console is not defined` | JS 引擎初始化失败 | 检查模组是否正确安装 | -| `world is not defined` | 作用域问题 | 确保代码在全局作用域,不在函数内 | -| `Cannot find name 'xxx'` | TypeScript 类型错误 | 检查拼写,或查看 `.d.ts` 中的正确 API 名 | -| `npm run build` 报错 | JS 语法错误 | 检查 ESLint 输出 | -| 脚本不执行 | 项目未启用 | `/box3script` 查看状态 | +| 错误 | 原因 | 解决 | +| ------------------------ | ------------------- | ------------------------------------------------------ | +| `console is not defined` | JS 引擎初始化失败 | 检查模组是否正确安装 | +| `world is not defined` | 作用域问题 | 确保代码在全局作用域,不在嵌套函数内定义后又引用 | +| `Cannot find name 'xxx'` | TypeScript 类型错误 | 检查拼写,或查看 `types/` 下的 `.d.ts` 中的正确 API 名 | +| `npm run build` 报错 | JS 语法错误 | 检查 ESLint 输出,或看终端错误行号 | +| 脚本不执行 | 项目未启用 | `/box3script` 查看状态 | +| 定时器不触发 | tick 数算错了 | 记住 1 秒 = 20 ticks,不是 1000 | +| 客户端脚本无效 | 玩家没装客户端 Mod | Box3JS 客户端 Mod 必须安装 | +| remoteChannel 没收到 | 数据不是 JSON | 确保传的是纯对象,不是 Java 对象或 `GameVector3` 实例 | ### 沙盒测试 @@ -273,14 +920,33 @@ npm run build ``` /box3script sandbox mygame # 开启沙盒 -# ... 测试脚本(生成实体、修改方块等)... +# ... 测试脚本(生成实体、修改方块、爆炸等)... /box3script sandbox mygame # 关闭 → 自动回滚所有修改 ``` +**适用场景:** + +- **新脚本首次测试** — 不确定脚本会做什么,先沙盒测试 +- **玩家试玩** — 让玩家试玩新功能,结束时回滚不影响正式服 +- **调试破坏性操作** — 测试 `fillVoxel`、`explode` 等操作 + +### 性能注意事项 + +Box3JS 脚本运行在服务器主线程上,不合理的代码会影响 TPS: + +1. **`onTick` 中避免大循环** — 遍历所有实体请在条件触发时做,不要每 tick 做 +2. **缓存查询结果** — 不要把 `querySelectorAll` 放在每 tick +3. **用 `setInterval` 代替 `onTick`** — 如果不需要 20 次/秒,用更长的间隔(比如 100 ticks = 5 秒) +4. **避免 JS ↔ Java 频繁跨越** — 批量操作比逐个操作快 + +一个跑酷脚本的性能消耗通常 < 0.5ms/tick,对服务器 TPS 几乎无影响。 + --- ## 发布部署 +### 开发模式 → 生产发布 + 开发完成后,将脚本编译为**独立 JAR 模组**: ``` @@ -290,9 +956,11 @@ npm run build 生成 `mygame-1.0.0.jar`(版本号从 `package.json` 读取),放入任意 NeoForge 服务端的 `mods/` 目录即可运行。 **注意:** + - 需要 Box3JS 作为依赖模组(提供 Rhino 运行时) - 如果使用了 `registries`(自定义方块/物品),客户端也需要安装 JAR - JAR 中包含编译后的 JS,无需原始源码 +- 编译后的 JAR 是一个独立的 NeoForge 模组,有自己的 `mods.toml` ### package.json 配置 @@ -309,14 +977,27 @@ npm run build } ``` -这些元数据会被写入 JAR 的 `mods.toml`。 +这些元数据会被写入 JAR 的 `mods.toml`,在游戏的模组列表中显示。 + +### 开发模式 vs 编译模式 + +| | 开发模式 (`/box3script start`) | 编译模式 (`/box3script compile`) | +| ------------ | ------------------------------ | -------------------------------- | +| 修改代码 | 热重载,无需重启 | 需重新编译 | +| `registries` | `undefined` | ✅ 可用 | +| 分发 | 需要源码 | 只需 JAR | +| 适用场景 | 开发、测试 | 发布、分发 | --- ## 下一步 -- **学 API**: 看 [API 功能速查](../api/README.md) — 按"我想做什么"查找对应 API -- **学事件**: 看 [教程三:事件系统与实体操控](../tutorial/03-events-entities.md) +现在你已经理解了 Box3JS 的核心设计理念和基本 API 用法。接下来: + +- **学 API 细节**: 看 [API 功能速查](../api/README.md) — 按"我想做什么"查找对应 API +- **学事件系统**: 看 [教程三:事件系统与实体操控](../tutorial/03-events-entities.md) - **学客户端**: 看 [客户端 API 文档](../api/client.md) — 按键监听、屏幕 UI、客户端音效 -- **懂原理**: 看 [运行原理](architecture.md) — Rhino 引擎、作用域、构建管线 +- **懂原理**: 看 [运行原理](architecture.md) — Rhino 引擎、作用域、构建管线、网络通信 - **选技术**: 看 [JS vs Java 对比](js-vs-java.md) — Box3JS 与原生模组怎么选 +- **常见问题**: 看 [FAQ](faq.md) +- **实战菜谱**: 看 [代码片段与菜谱](recipes.md) — 复制即用的常见功能实现 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md index 27320d4..957b598 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md @@ -1,23 +1,51 @@ # Quick Start: From Zero to Your First Box3JS Script -This guide is for readers with **zero modding experience**. If you know JavaScript, you can write your first Minecraft server script in 10 minutes. +This guide is for readers with **zero modding experience**. If you know JavaScript, you can write your first Minecraft server script in 10 minutes. By the end of this guide, you'll understand both how to use the APIs and why they're designed the way they are. ## Table of Contents 1. [What is Box3JS](#what-is-box3js) 2. [Setup](#setup) 3. [Create a Project](#create-a-project) -4. [Your First Script](#your-first-script) -5. [Dev Cycle](#dev-cycle) -6. [Debugging](#debugging) -7. [Deployment](#deployment) -8. [Next Steps](#next-steps) +4. [Understanding Project Structure](#understanding-project-structure) +5. [Your First Script: Line by Line](#your-first-script-line-by-line) +6. [Core Design Philosophy: Why the API Works This Way](#core-design-philosophy-why-the-api-works-this-way) +7. [API Quick Tour](#api-quick-tour) +8. [Dev Cycle](#dev-cycle) +9. [Debugging](#debugging) +10. [Deployment](#deployment) +11. [Next Steps](#next-steps) --- ## What is Box3JS -Box3JS is a **server-side scripting engine mod** for NeoForge 1.21.1. It embeds a JavaScript runtime (Mozilla Rhino) inside the Minecraft server, letting you write gameplay logic in JS/TypeScript. +Box3JS is a **Minecraft mod** (NeoForge 1.21.1) that embeds a complete JavaScript runtime inside the Minecraft server, letting you write gameplay logic in JS/TypeScript. It also optionally delivers client-side scripts for key listeners, screen UI, client audio, and other local interactions. + +> Curious where Box3JS comes from and how it relates to the Box3 platform? → [Box3JS & Box3](about-box3js_en.md) + +### In One Sentence + +> Box3JS is like Node.js inside your Minecraft server — but you don't need to know Java or modding. + +### Architecture at a Glance + +``` +You write Build tools Minecraft runs + TypeScript ───→ compile to ES5 ───→ Rhino engine executes + │ + ┌──────┴──────┐ + │ NeoForge │ + │ Minecraft │ + │ API layer │ + └─────────────┘ +``` + +- **You write TypeScript** with full type hints and modern syntax +- **Babel compiles** down to ES5 (Rhino only supports ES5) +- **esbuild bundles** into a single JS file +- **Rhino runs inside the JVM**, directly calling Minecraft APIs +- **Dual-side execution** (server + client), communicating via `remoteChannel` ### What You Can Do @@ -31,32 +59,14 @@ Box3JS is a **server-side scripting engine mod** for NeoForge 1.21.1. It embeds | Data Persistence | JSON storage, SQLite database | | Game Systems | Scoreboards, BossBars, teams, world borders | | HTTP Requests | Web API calls, webhook notifications | -| Client Scripts | Key listeners, screen UI, client audio | +| Client Scripts | Key listeners, screen UI, client audio, custom GUIs | ### What You Can't Do - **Render custom models/particles** — requires a client resource pack or Java mod - **Add new blocks/items at runtime** — requires compiling to a JAR (`/box3script compile`) -- **Modify vanilla mechanics** — changing recipes, mob behavior requires Mixin - -### Core Design - -``` -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ -│ TypeScript │ ───→ │ Babel ES5 │ ───→ │ Rhino Engine │ -│ Source │ │ Compile │ │ (in JVM) │ -└──────────────┘ └──────────────┘ └──────────────┘ - │ - ┌───────┴───────┐ - │ Minecraft │ - │ NeoForge API │ - └───────────────┘ -``` - -- **Runs inside the server JVM**, directly calling Minecraft/NeoForge APIs -- **TypeScript source** compiled to ES5 via Babel, targeting Rhino -- **Hot reload** — no server restart when you change code -- **Sandbox isolation** — each project has an independent scope +- **Modify vanilla mechanics** — changing recipes or mob AI behavior requires Mixin +- **Use modern JS syntax at runtime** — Rhino supports ES5 only, but you can use modern TypeScript in source (the build step handles transpilation) --- @@ -66,7 +76,7 @@ Box3JS is a **server-side scripting engine mod** for NeoForge 1.21.1. It embeds 1. **Minecraft server** with Box3JS + NeoForge 1.21.1 installed 2. **Node.js** 18+ (for local builds only — not needed on the server) -3. A text editor (VS Code recommended) +3. A text editor (VS Code recommended, with full TypeScript IntelliSense) ### Verify Installation @@ -78,43 +88,67 @@ In-game, run: If you see the project status panel, Box3JS is running. +``` +══ Box3JS Script Engine ══ + Watch: ○ Inactive Sandbox: ○ Inactive + Projects: 0 enabled | 0 loaded +``` + --- ## Create a Project +### One-Command Creation + In-game: ``` /box3script create mygame ``` -This generates a complete TypeScript project at `config/box3/script/mygame/`: +This generates a complete TypeScript project at `config/box3/script/mygame/`. + +### Understanding Project Structure ``` config/box3/script/mygame/ ├── package.json ← Project config (name, version, build deps) ├── tsconfig.base.json ← Shared TS compiler options -├── tsconfig.server.json ← Server TS config -├── tsconfig.client.json ← Client TS config +├── tsconfig.server.json ← Server TS config (references server/ types) +├── tsconfig.client.json ← Client TS config (references client/ types) ├── build.mjs ← Build script (esbuild + Babel) ├── eslint.config.mjs ← ESLint rules -├── types/ +├── types/ ← ★ Type declarations (API reference) │ ├── shared.d.ts ← Shared server & client types │ ├── server/ │ │ ├── index.d.ts ← Server type entry point -│ │ └── ... +│ │ ├── server.d.ts ← world, remoteChannel, registries +│ │ ├── entity.d.ts ← GameEntity interface +│ │ ├── player.d.ts ← GamePlayer interface +│ │ ├── world.d.ts ← GameWorld interface +│ │ └── voxels.d.ts ← GameVoxels interface │ └── client/ │ ├── index.d.ts ← Client type entry point -│ └── ... +│ ├── client.d.ts ← GameClient, RemoteChannel +│ ├── audio.d.ts ← GameAudio +│ ├── input.d.ts ← GameInput +│ ├── ui.d.ts ← GameUI +│ ├── chat.d.ts ← GameChat +│ └── gui.d.ts ← GameGUI, GuiController ├── src/ │ ├── server/ │ │ └── app.ts ← ★ Server entry point (where you write code) │ └── client/ │ └── app.ts ← Client entry point ├── registries/ ← Custom content (blocks/items/sounds JSON) -└── assets/lang/ ← Custom content localization text +└── assets/lang/ ← Custom content localization ``` +**Key insights:** +- The `.d.ts` files in `types/` are your API reference — VS Code uses them for IntelliSense +- `tsconfig.server.json` and `tsconfig.client.json` are **mutually exclusive** — server code never sees client globals like `client`, `input`, etc. +- You only need to write code in `src/server/app.ts` (and optionally `src/client/app.ts`); the build tooling handles everything else + ### Install Dependencies ```bash @@ -122,69 +156,665 @@ cd config/box3/script/mygame npm install ``` -`npm install` only needs to run once (installs esbuild, Babel, TypeScript build tooling). +Run `npm install` once (installs esbuild, Babel, TypeScript build tooling). --- -## Your First Script +## Your First Script: Line by Line + +Open `src/server/app.ts`, clear the contents, and write the code below. Let's understand each part. -Open `src/server/app.ts`, clear the contents, and write: +### 1. Startup Log ```js -// 1. Startup log console.log("MyGame script started!"); +``` + +**What happens:** `console` is a global object injected by Box3JS (no `import` needed). Behind it is a Java class `Box3JSConsole`, bridged to JS via Rhino. `console` supports six methods: `log`, `warn`, `error`, `debug`, `clear`, `assert`. -// 2. Welcome players on join +### 2. Welcome Players on Join + +```js world.onPlayerJoin((entity) => { const p = entity.player; - p.directMessage("Welcome, " + p.name + "!"); + + // Broadcast to all + world.say(`§e${p.name} §7joined the server`); + + // Private message + p.directMessage(`§aWelcome to the server, ${p.name}!`); // Particle welcome effect - const pos = p.position; + const { position: pos } = p; world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); +``` + +**Line by line:** -// 3. Chat commands +- `world.onPlayerJoin(...)` — Registers a "player join" event listener. Returns a `GameEventHandlerToken` (not saved in this example, so the listener stays active until script reload). +- `entity.player` — `entity` is the callback parameter, representing the joining entity. `.player` gets the player wrapper (if the entity isn't a player, `.player` is `undefined`). +- `p.directMessage(...)` — Sends a private message visible only to that player. +- `§a` is a Minecraft color code (green). `§e` = yellow, `§6` = gold, `§7` = gray, `§c` = red. +- `p.position` — Returns a `GameVector3` object with `.x`, `.y`, `.z` properties. +- `world.spawnParticleCircle(...)` — Spawns a ring of particles at the player's position. +- `world.playSound(...)` — Plays a sound at the player's position. + +### 3. Chat Commands + +```js world.onChat((entity, message) => { const p = entity.player; if (message === "!hello") { - p.directMessage("Hello, " + p.name + "!"); - return false; // suppress chat message + p.directMessage(`§eHello, ${p.name}!`); + return false; // suppress chat message from broadcasting } if (message === "!pos") { - const pos = p.position; - p.directMessage("Your position: " + - Math.floor(pos.x) + ", " + - Math.floor(pos.y) + ", " + - Math.floor(pos.z)); + const { position: pos2 } = p; + p.directMessage( + `§eYour position: §f${Math.floor(pos2.x)}, ${Math.floor(pos2.y)}, ${Math.floor(pos2.z)}` + ); return false; } - return true; // normal chat messages pass through + if (message === "!help") { + p.directMessage("§eAvailable commands: !help, !hello, !pos, !home, !shop"); + return false; + } + + return true; // normal messages pass through }); +``` -// 4. Periodic announcement -world.setInterval(() => { +**Key concepts:** + +- `return false` — Blocks the event from continuing (message won't appear in chat). This is the universal Box3JS event convention: **return false to cancel the default behavior**. +- `return true` — Let it through; message broadcasts normally. +- `Math.floor()` — Regular JS, rounds coordinates down for readability. + +### 4. Periodic Announcement + +```js +setInterval(() => { const count = world.querySelectorAll("*").length; - world.say("[Info] Players online: " + count); + world.say(`§7[Info] §fPlayers online: ${String(count)}`); }, 6000); // 6000 ticks = 5 minutes ``` -### Key Concepts +**Key understanding:** + +- `setInterval` is a **global function** (not `world.setInterval`), just like in browsers and Node.js. +- The second argument is **ticks** (Minecraft time unit), not milliseconds. 1 second = 20 ticks. +- `setInterval` returns a `GameEventHandlerToken` — call `.cancel()` to stop it. +- `world.querySelectorAll("*")` returns all online entities. +- `world.say(...)` broadcasts to the entire server. + +### Tick Conversion Table + +| Duration | Ticks | +|----------|-------| +| 1 second | 20 | +| 5 seconds | 100 | +| 30 seconds | 600 | +| 1 minute | 1,200 | +| 5 minutes | 6,000 | +| 10 minutes | 12,000 | +| 30 minutes | 36,000 | + +--- + +## Core Design Philosophy: Why the API Works This Way + +Understanding the design rationale behind Box3JS APIs helps you write more efficient and safer scripts. Here are the most important design decisions and their reasons. + +### Design 1: Global Object Injection — No Imports Needed + +```js +// In Box3JS, use directly — no import required +world.onTick(() => { ... }); +console.log("hello"); +storage.getDataStorage("coins"); + +// Compare: if this were Node.js +// const { world } = require("box3js"); ← Not needed! +``` + +**Why?** Rhino is a bare ECMAScript engine — it doesn't support CommonJS `require()` or ES Module `import`. Box3JS injects all API objects as global variables at Rhino scope initialization time, directly from Java. The `.d.ts` TypeScript files declare these globals with `declare`, giving you full IntelliSense in VS Code without any imports. + +### Design 2: Tick-Based Timing — Not Milliseconds + +```js +// Box3JS time unit is ticks (1/20 second) +setTimeout(() => { ... }, 100); // 100 ticks = 5 seconds later + +// Compare browser: +// setTimeout(() => { ... }, 5000); // 5000 milliseconds = 5 seconds +``` + +**Why?** Box3JS timers execute **on the main game tick loop**, not on separate Java threads. Each game tick (1/20 second), the engine checks all timers, decrements their remaining ticks, and fires callbacks whose countdown reaches 0. Benefits: + +- **Thread safety** — Callbacks always run on the main thread; you can safely call any Minecraft API +- **Precise synchronization** — Timers are perfectly synced with the game world; a lagging server slows timers too +- **Zero overhead** — No extra threads or thread pools + +### Design 3: GameEventHandlerToken — Universal Cancellation + +```js +// All onXxx() and setTimeout/setInterval return GameEventHandlerToken +const token = world.onTick(() => { + // runs every tick +}); + +// Cancel the listener (these are equivalent) +token.cancel(); + +// Check if still active +if (token.active()) { + // ... +} +``` + +**Why?** Early designs provided separate cancellation methods per event type (`removeTickListener`, `removeChatListener`...), which meant: + +1. Memorizing different cancellation API names for each event type +2. No unified way to manage them (how do you batch-cancel all listeners on script stop?) + +A unified `GameEventHandlerToken` pattern solves this: + +- **One pattern for everything** — `onTick`, `onPlayerJoin`, `onChat`, `setInterval` all use `.cancel()` +- **Auto-cleanup on reload** — When stopping a project, the engine iterates all tokens and cancels them in bulk +- **Chainable management** — Collect tokens in an array and cancel them all at once + +### Design 4: Per-Project Scope Isolation + +``` +Server runs 3 script projects simultaneously, completely independent: + +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Scope A │ │ Scope B │ │ Scope C │ +│ "mygame" │ │ "lobby" │ │ "survival" │ +│ │ │ │ │ │ +│ var x = 1 │ │ var x = 2 │ │ var x = 3 │ +│ own events │ │ own events │ │ own events │ +│ own storage │ │ own storage │ │ own storage │ +│ own timers │ │ own timers │ │ own timers │ +└──────────────┘ └──────────────┘ └──────────────┘ +``` + +**Why?** A server might run multiple scripts simultaneously (lobby system, mini-games, economy...). Without isolation: + +- Variable name collisions (`var playerCount` defined in two scripts) +- Event callback interference (`/box3script reload lobby` accidentally clearing survival's callbacks) +- Data leakage (lobby's script reading survival's storage) + +Box3JS gives each project an **independent Rhino top-level scope**, backed by separate `Box3JSEventBus` namespaces. Stopping or reloading one project never affects others. + +### Design 5: Dual-Side Architecture + remoteChannel + +``` +┌──────────────────────┐ ┌──────────────────────┐ +│ Server │ │ Client │ +│ │ │ │ +│ world.* │ │ client.* │ +│ voxels.* │ JSON │ audio.* │ +│ entity.* │ ←─────→ │ input.* │ +│ player.* │ events │ ui.* │ +│ storage/db/http │ │ chat.* / gui.* │ +│ remoteChannel ──────┼─────────┼── remoteChannel │ +│ │ │ storage/db/http │ +└──────────────────────┘ └──────────────────────┘ +``` + +**Why the split?** + +- **Security** — The server is the world authority (blocks, entities, data); clients only handle local presentation (UI, audio, input) +- **Performance** — Client scripts run on each player's own machine, consuming zero server resources +- **Flexibility** — Write server-only scripts for most cases, or add client scripts to enhance the experience + +**remoteChannel communication rules:** + +```js +// Server → single client +remoteChannel.sendClientEvent(player, { type: "welcome", msg: "hi" }); + +// Server → all clients +remoteChannel.broadcastClientEvent({ type: "game_start" }); + +// Client → server +remoteChannel.sendServerEvent({ key: "space", pressed: true }); +``` + +**Critical limitation:** Data crossing the network must be **JSON-serializable**. No functions, `GameVector3` instances, or Java objects. To send coordinates, use `{ x: 1, y: 2, z: 3 }` instead of `new GameVector3(1, 2, 3)`. + +### Design 6: TypeScript Source + Babel Compilation to ES5 + +``` +src/server/app.ts Babel esbuild dist/server.js +(TypeScript, ES2020) ───→ ES5 JavaScript ───→ bundle ───→ (single file) +``` + +**Why the build step?** + +- **Rhino 1.9.1 only supports ES5** — `let`, `const`, arrow functions, template literals, and `class` are all ES6+ features Rhino can't parse +- **Babel downlevels** — Transforms modern syntax into `var`, `function`, string concatenation, and other ES5 equivalents +- **esbuild bundles** — You can write multiple `.ts` files, but Rhino has no module system. esbuild merges everything into a single IIFE + +**You can safely use in source:** +- `const` / `let` (transpiled to `var`) +- Arrow functions `() => {}` (transpiled to `function(){}`) +- Template literals `` `hello ${name}` `` (transpiled to `"hello " + name`) +- `class` (transpiled to `function` + prototype) +- `async/await` (via regenerator transform) + +### Design 7: Event Callbacks Return false to Cancel + +```js +world.onChat((entity, message) => { + if (message.startsWith("!")) { + // This is a command — don't broadcast + return false; + } + return true; // normal messages pass through +}); +``` + +**Why?** Inspired by the browser DOM event `preventDefault` pattern. Minecraft events typically have "default behavior" (e.g., chat messages broadcast to everyone). `return false` tells the engine: "I've handled this event — skip the default behavior." -- **Globals need no import** — `world`, `console`, `player` are injected by Box3JS -- **Return false to block default behavior** — `onChat` returning false suppresses the message -- **Ticks are MC time units** — 1 second = 20 ticks, `setInterval` uses ticks -- **§ codes are MC color codes** — `§a` = green, `§e` = yellow, `§6` = gold, `§7` = gray +### Design 8: Sandbox Mode — Safe Testing + +``` +/box3script sandbox mygame # enable sandbox +# ... test script (spawn entities, modify blocks, explode)... +/box3script sandbox mygame # disable → auto-rollback all changes +``` + +**Why?** Once a script modifies the world, those changes are permanent (blocks replaced, entities spawned). Sandbox mode tracks all world modifications made by the script and auto-rolls them back when disabled. This lets developers fearlessly test destructive operations without damaging the live server. + +--- + +## API Quick Tour + +Organized by "what do I want to do?" — the most commonly used APIs. For the complete reference, see the [API docs](../api/README_en.md). + +### Messages & Chat + +```js +// Broadcast to all +world.say("§6Server will restart in 5 minutes"); + +// Private message (only target player sees it) +player.directMessage("§aYour balance: 100 coins"); + +// Action bar text (above hotbar) +player.actionBar("§ePress F to open menu"); + +// Screen title (large centered text) +player.title("§6BOSS FIGHT", "§cThe Ancient Dragon awakens"); + +// Chat interception (command system) +world.onChat((entity, message) => { + if (message === "!help") { + entity.player.directMessage("§eAvailable commands: !help, !home, !shop"); + return false; + } + return true; +}); +``` + +### Player Properties & Control + +```js +// Get player info +const name = player.name; +const pos = player.position; // GameVector3 { x, y, z } +const hp = player.hp; +const mode = player.gameMode; // "survival", "creative", "adventure", "spectator" + +// Modify player state +player.hp = 20; // full heal +player.maxHp = 40; // increase max health +player.food = 20; // full hunger +player.gameMode = "creative"; // switch to creative +player.canFly = true; // allow flight +player.flying = true; // start flying + +// Teleport +player.teleport(new GameVector3(100, 64, 100)); + +// Kick +player.kick("You have been kicked by an admin"); + +// Run vanilla commands as the player +player.runCommand("effect give @s minecraft:speed 30 1"); +``` + +### Items & Inventory + +```js +// Give items +player.giveItem("minecraft:diamond", 64); +player.giveItem("minecraft:diamond_sword", 1); + +// Give named items (4th param is the lore/description array) +player.giveNamedItem("minecraft:stick", 1, "§6Magic Wand", ["A magical wand"]); + +// Give enchanted items (enchantments as { enchantId: level } record) +player.giveEnchantedItem("minecraft:diamond_sword", 1, { + "minecraft:sharpness": 5, + "minecraft:unbreaking": 3, +}); + +// Check held item +const held = player.getHeldItem(); + +// Clear inventory +player.clearInventory(); +``` + +### Event System + +```js +// Every tick (use sparingly! Heavy work in onTick drags down TPS) +const tickToken = world.onTick(() => { + // runs every tick +}); + +// Player events +world.onPlayerJoin((entity) => { + entity.player.directMessage("Welcome!"); +}); + +world.onPlayerLeave((entity, _tick) => { + world.say(`${entity.player.name} left the server`); +}); + +world.onPlayerRespawn((entity, _tick) => { + entity.player.teleport(new GameVector3(0, 100, 0)); + entity.player.directMessage("You respawned!"); +}); + +// Entity events +world.onEntityDeath((entity, _killer, _tick) => { + if (entity.isPlayer()) { + world.say(`${entity.player.name} died`); + } +}); + +world.onEntityDamage((entity, amount, source, _attacker, _tick) => { + if (amount > 10) { + console.log(`High damage: ${String(amount)} from ${source}`); + } +}); + +// Interaction events +world.onInteract((entity, target, _tick) => { + // Player right-clicked an entity + if (target.hasTag("npc")) { + entity.player.directMessage("Hello!"); + } +}); + +world.onBlockActivate((entity, x, y, z, voxel, _tick) => { + // Player right-clicked a block + if (voxel === "minecraft:chest") { + entity.player.directMessage(`You clicked a chest at ${String(x)}, ${String(y)}, ${String(z)}`); + } +}); + +world.onBlockPlace((entity, x, y, z, voxel, _voxelId, _tick) => { + // Player placed a block + console.log(`${entity.player.name} placed ${voxel} at ${String(x)}, ${String(y)}, ${String(z)}`); +}); + +world.onVoxelDestroy((entity, _x, _y, _z, voxel, _tick) => { + // Player is about to break a block + if (voxel === "minecraft:diamond_block") { + entity.player.directMessage("§cYou can't break diamond blocks!"); + return false; // cancel the break + } +}); + +// Timers (global functions) +const timer = setTimeout(() => { + world.say("30 seconds have passed!"); +}, 600); // 600 ticks = 30 seconds + +const interval = setInterval(() => { + world.say("Minute announcement"); +}, 1200); // 1200 ticks = 1 minute + +// Cancel timers +timer.cancel(); +interval.cancel(); +``` + +### Entity Manipulation + +```js +// Spawn an entity (returns GameEntity | null) +const zombie = world.spawnEntity("minecraft:zombie", + new GameVector3(100, 64, 100)); +if (zombie) { + // use zombie ... +} + +// Create with full config (nameTag/glowing/equipment set after creation) +const boss = world.createEntity({ + type: "minecraft:zombie", + position: new GameVector3(100, 64, 100), + hp: 200, + maxHp: 200, + tags: ["boss"], +}); +if (boss) { + boss.setNameTag("§cAncient Zombie King"); + boss.glowing = true; + boss.setEquipment("head", "minecraft:diamond_helmet"); + boss.setEquipment("chest", "minecraft:diamond_chestplate"); + + // Control entities + boss.setAI(false); // disable AI (stands still) + boss.invulnerable = true; // invincible + boss.navigateTo(110, 64, 100, 1.5); // navigate to target + + // Potion effects + boss.addEffect("minecraft:strength", 600, 2, false); + boss.addEffect("minecraft:speed", 600, 1, true); + boss.clearEffects(); + + // Equipment + boss.setEquipment("head", "minecraft:iron_helmet"); + boss.setEquipment("mainhand", "minecraft:iron_sword"); + + // Tags (for marking and querying) + boss.addTag("boss"); + boss.addTag("stage_1"); + boss.hasTag("boss"); // → true +} + +// Query entities +const nearby = world.entitiesInRadius(pos, 10); // within 10 blocks +const all = world.querySelectorAll("*"); // all entities +const players = world.querySelectorAll("player"); // all players +const monsters = world.querySelectorAll("monster"); // all monsters +``` + +### Block Operations + +```js +// Read a block +const block = voxels.getVoxel(100, 64, 100); + +// Place a block +voxels.setVoxel(100, 64, 100, "minecraft:stone"); +voxels.setVoxel(100, 65, 100, "minecraft:torch"); + +// Fill a region +voxels.fillVoxel(0, 64, 0, 10, 70, 10, "minecraft:glass"); + +// Replace blocks (only matching type) +voxels.fillVoxel(0, 64, 0, 10, 70, 10, "minecraft:air", "minecraft:stone"); +``` + +### Data Persistence + +```js +// JSON storage (per-project namespace) +const store = storage.getDataStorage("coins"); +store.set("player1", 100); +const coins = store.get("player1"); // → 100 +store.delete("player1"); +const keys = store.keys(); // → array of all keys + +// SQLite database +db.sql("CREATE TABLE IF NOT EXISTS players (name TEXT, score INT)"); +db.sql("INSERT INTO players VALUES ('Steve', 100)"); + +const result = db.sql("SELECT * FROM players WHERE score > 50"); +// result.rows[0] → { name: "Steve", score: 100 } +// result.firstRow → { name: "Steve", score: 100 } +// result.rowCount → 1 +// result.columnNames → ["name", "score"] +``` + +### Game Systems + +```js +// Scoreboard +world.addScoreboard("kills"); +world.setScore("Steve", "kills", 42); +world.showScoreboard("sidebar", "kills"); + +// BossBar +world.showBossbar("boss1", "§cAncient Dragon", 0.8, "red"); +world.setBossbar("boss1", "§cAncient Dragon §7[80%]", 0.5); + +// Teams +world.createTeam("red", "red"); +world.createTeam("blue", "blue"); +world.joinTeam(entity, "red"); + +// World border +world.borderSize = 500; // set border size +world.shrinkBorder(100, 1200); // shrink to 100 over 1200 ticks + +// Weather & time +world.time = 6000; // set time (0=dawn, 6000=noon, 12000=dusk, 18000=midnight) +world.rainDensity = 0; // stop rain +world.clearWeather(); // clear skies +world.thunderDensity = 1; // thunderstorm + +// Game rules +world.setGameRule("keepInventory", true); +world.setGameRule("doDaylightCycle", false); +``` + +### Visual Effects + +```js +const pos = new GameVector3(100, 64, 100); + +// Particles +world.spawnParticle("minecraft:flame", pos.x, pos.y, pos.z, 0, 0, 0, 1, 10); +world.spawnParticleCircle(pos.x, pos.y, pos.z, 2, "minecraft:heart", 30); + +// Fireworks +world.launchFirework(pos.x, pos.y, pos.z, "red", "large_ball"); +world.launchFirework(pos.x, pos.y, pos.z, "green", "star"); + +// Lightning & explosion +world.strikeLightning(pos.x, pos.y, pos.z); +world.explode(pos.x, pos.y, pos.z, 4); // power 4 explosion + +// Sounds +world.playSound("minecraft:entity.ender_dragon.growl", pos, 1.0, 1.0); +player.playSound("minecraft:block.note_block.pling", 1.0, 2.0); // only that player hears it +``` + +### HTTP Requests + +```js +// GET request +const resp = http.fetch("https://api.example.com/data"); +if (resp.ok) { + const data: unknown = resp.json(); + console.log(data); +} + +// POST JSON +const resp2 = http.fetch("https://api.example.com/webhook", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "Server restarted" }) +}); + +// With timeout +const resp3 = http.fetch("https://slow-api.com/data", { + timeout: 5000 // 5 second timeout +}); +``` + +### Client Scripting (requires Box3JS client mod) + +```js +// Client entry: src/client/app.ts + +// Every frame +client.onTick(() => { + // client tick callback +}); + +// Keyboard input +if (input.isKeyDown("space")) { + // spacebar is being held down +} + +input.onKeyPress("f", () => { + // F key pressed — notify server + remoteChannel.sendServerEvent({ action: "open_menu" }); +}); + +// Screen UI +ui.showOverlay("§ePress F to open menu"); // above hotbar +ui.showTitle("§6BOSS SPAWNED!", "§cGet ready!"); // centered screen title + +// Client audio +audio.playSound("minecraft:block.note_block.pling", 1.0, 1.0); +audio.playMusic("minecraft:music.game", 0.5, 1.0); +audio.stopAll(); + +// Fog control (client-side rendering) +client.setFogColor(255, 100, 50); // reddish fog appearance +client.setFogStartDistance(10); // fog begins at 10 blocks +client.setFogEndDistance(50); // fully obscured at 50 blocks +client.resetFog(); // restore default + +// Chat +chat.sendMessage("Hello everyone!"); +chat.onMessage((msg, _sender, _isSystem) => { + if (msg.includes("secret")) { + // handle messages containing "secret" + } +}); + +// Receive server events +remoteChannel.onClientEvent((event) => { + if (event.args.type === "boss_spawned") { + audio.playSound("minecraft:entity.ender_dragon.growl", 1.0, 1.0); + } +}); +``` --- ## Dev Cycle -Standard flow after each code change: +### Standard Flow + +After each code change: ``` Edit code → npm run build → /box3script reload mygame → test @@ -205,11 +835,11 @@ Done in 240ms What the build does: -1. **Babel** compiles TypeScript to ES5 JavaScript -2. **esbuild** bundles all modules into a single file +1. **Babel** compiles TypeScript to ES5 JavaScript (Rhino only supports ES5) +2. **esbuild** bundles all modules into a single file (Rhino lacks `require()`) 3. Outputs to `dist/server.js` and `dist/client.js` -### Load +### Load & Reload In-game: @@ -218,14 +848,28 @@ In-game: /box3script reload mygame # reload after changes (no server restart) ``` +`reload` is atomic: stops the old script (cleans up all callbacks, timers, scoreboards), then loads the new one. + ### Auto Hot-Reload -Enable file watching so build + save auto-triggers reload: +Enable file watching so builds auto-trigger reload: ``` /box3script watch ``` +**Note:** `watch` monitors the `dist/` compiled output (`.js`), not `src/` source. So you need to run `npm run build` first to generate new `dist/` files. Combine with `npm run build -- --watch` for save-and-reload workflow. + +### Multi-Project Management + +``` +/box3script start mygame lobby # start multiple projects +/box3script stop mygame # stop one +/box3script stopall # stop all +/box3script reload mygame # reload one +/box3script # view all project statuses +``` + --- ## Debugging @@ -233,36 +877,57 @@ Enable file watching so build + save auto-triggers reload: ### Troubleshooting Order 1. **Check console** — server logs show errors with `[Box3JS] [projectName]` prefix -2. **Check status** — `/box3script` to see if project shows `◉` (loaded) +2. **Check status** — `/box3script` to see if the project shows `◉` (loaded) 3. **Check build** — `npm run build` should complete without errors 4. **Add logging** — use `console.log()` at key points to print variable values -5. **Read line numbers** — Java exception stacks include JS filenames and line numbers +5. **Read line numbers** — Java exception stacks include JS filenames and line numbers (line numbers correspond to compiled `dist/server.js`, not `.ts` source) ### Common Errors | Error | Cause | Fix | |-------|-------|-----| | `console is not defined` | Engine init failed | Check mod installation | -| `world is not defined` | Scope issue | Ensure code is at global scope, not inside a function | -| `Cannot find name 'xxx'` | TypeScript type error | Check spelling or look up the correct API name in `.d.ts` | -| `npm run build` fails | JS syntax error | Check ESLint output | +| `world is not defined` | Scope issue | Ensure code is at global scope, not inside a nested function | +| `Cannot find name 'xxx'` | TypeScript type error | Check spelling, or look up the correct API name in `types/` `.d.ts` | +| `npm run build` fails | JS syntax error | Check ESLint output or terminal error line numbers | | Script not executing | Project not enabled | Check `/box3script` status | +| Timer never fires | Tick count miscalculation | Remember: 1 sec = 20 ticks, not 1000 | +| Client script not working | Player lacks client mod | Box3JS client mod must be installed | +| remoteChannel not receiving | Data isn't JSON | Ensure you're sending plain objects, not Java objects or `GameVector3` instances | ### Sandbox Testing -Sandbox mode enables safe testing: all world modifications are tracked and rolled back on close. +Sandbox mode enables safe testing: all world modifications are tracked and rolled back when disabled. ``` /box3script sandbox mygame # enable sandbox -# ... test script (spawn entities, modify blocks, etc.)... +# ... test script (spawn entities, modify blocks, explode, etc.)... /box3script sandbox mygame # disable → auto-rollback all changes ``` +**Use cases:** +- **First test of a new script** — unsure what it does? Sandbox first +- **Player play-testing** — let players try new features, rollback after without affecting the live server +- **Debugging destructive operations** — test `fillVoxel`, `explode`, etc. + +### Performance Tips + +Box3JS scripts run on the server main thread — unreasonable code affects TPS: + +1. **Avoid large loops in `onTick`** — scan entities on condition triggers, not every tick +2. **Cache query results** — don't put `querySelectorAll` in onTick +3. **Use `setInterval` over `onTick`** — if you don't need 20/sec, use longer intervals (e.g., 100 ticks = 5 seconds) +4. **Minimize JS ↔ Java crossings** — batch operations are faster than individual calls + +A typical parkour script consumes < 0.5ms/tick, with virtually no impact on server TPS. + --- ## Deployment -Once development is done, compile your script into a **standalone JAR mod**: +### Dev Mode → Production Release + +When development is done, compile your script into a **standalone JAR mod**: ``` /box3script compile mygame @@ -274,6 +939,7 @@ Generates `mygame-1.0.0.jar` (version from `package.json`). Drop it into any Neo - Box3JS must also be installed as a dependency (provides the Rhino runtime) - If you use `registries` (custom blocks/items), clients must also install the JAR - The JAR contains compiled JS — no source code needed +- The compiled JAR is a standalone NeoForge mod with its own `mods.toml` ### package.json Config @@ -290,14 +956,27 @@ Generates `mygame-1.0.0.jar` (version from `package.json`). Drop it into any Neo } ``` -These metadata fields are written into the JAR's `mods.toml`. +These metadata fields are written into the JAR's `mods.toml` and shown in the game's mod list. + +### Dev Mode vs Compiled Mode + +| | Dev Mode (`/box3script start`) | Compiled Mode (`/box3script compile`) | +|---|---|---| +| Code changes | Hot reload, no restart | Must recompile | +| `registries` | `undefined` | ✅ Available | +| Distribution | Source code required | JAR only | +| Use case | Development, testing | Release, distribution | --- ## Next Steps -- **Learn APIs**: [API by Task](../api/README_en.md) — find APIs by "I want to..." -- **Learn Events**: [Tutorial 3: Events & Entities](../tutorial/03-events-entities.md) -- **Learn Client**: [Client API](../api/client_en.md) — key listeners, screen UI, client audio -- **Understand Internals**: [Architecture](architecture_en.md) — Rhino engine, scopes, build pipeline -- **Tech Decision**: [JS vs Java](js-vs-java_en.md) — Box3JS vs native modding +Now you understand Box3JS's core design philosophy and basic API usage. Next: + +- **API details**: [API by Task](../api/README_en.md) — find APIs by "I want to..." +- **Event system**: [Tutorial 3: Events & Entities](../tutorial/03-events-entities.md) +- **Client APIs**: [Client API docs](../api/client_en.md) — key listeners, screen UI, client audio +- **Internals**: [Architecture](architecture_en.md) — Rhino engine, scopes, build pipeline, networking +- **Tech choice**: [JS vs Java](js-vs-java_en.md) — Box3JS scripting vs native Java modding +- **FAQ**: [Frequently Asked Questions](faq_en.md) +- **Recipes**: [Code Snippets & Recipes](recipes_en.md) — copy-paste solutions for common tasks diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md b/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md index 44851f0..7ca0c83 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md @@ -403,7 +403,7 @@ function spawnWave(pos: GameVector3): void { world.say(`§c§l⚔ 第 ${wave} 波开始!§f ${count} 只怪物`); for (let i = 0; i < count; i++) { - world.setTimeout(() => { + setTimeout(() => { const x = pos.x + (Math.random() - 0.5) * 12; const z = pos.z + (Math.random() - 0.5) * 12; const type = types[Math.floor(Math.random() * types.length)]; @@ -445,10 +445,10 @@ function startShrinkPhase(centerX: number, centerZ: number, stages: { size: numb world.say(`§c边界缩小至 ${stage.size} 格!(${stage.duration} 秒)`); world.shrinkBorder(stage.size * 2, stage.duration); stageIndex++; - world.setTimeout(nextStage, stage.duration * 20); + setTimeout(nextStage, stage.duration * 20); } - world.setTimeout(nextStage, 100); // 5 秒后开始 + setTimeout(nextStage, 100); // 5 秒后开始 } // 用法:100→50→25→10,每段 60 秒 @@ -488,7 +488,7 @@ world.onEntityDeath((entity, killer) => { const SERVER_NAME = "My Server"; const WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_ID"; -world.setInterval(() => { +setInterval(() => { const playerCount = world.querySelectorAll("*").length; const tps = "20"; // 正常情况 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md index 6366ccc..1af00cc 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md @@ -403,7 +403,7 @@ function spawnWave(pos: GameVector3): void { world.say(`§c§l⚔ Wave ${wave} begins! §f${count} mobs`); for (let i = 0; i < count; i++) { - world.setTimeout(() => { + setTimeout(() => { const x = pos.x + (Math.random() - 0.5) * 12; const z = pos.z + (Math.random() - 0.5) * 12; const type = types[Math.floor(Math.random() * types.length)]; @@ -445,10 +445,10 @@ function startShrinkPhase(centerX: number, centerZ: number, stages: { size: numb world.say(`§cBorder shrinking to ${stage.size} blocks! (${stage.duration}s)`); world.shrinkBorder(stage.size * 2, stage.duration); stageIndex++; - world.setTimeout(nextStage, stage.duration * 20); + setTimeout(nextStage, stage.duration * 20); } - world.setTimeout(nextStage, 100); // Start after 5 seconds + setTimeout(nextStage, 100); // Start after 5 seconds } // Usage: 100→50→25→10, 60s per stage @@ -488,7 +488,7 @@ world.onEntityDeath((entity, killer) => { const SERVER_NAME = "My Server"; const WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_ID"; -world.setInterval(() => { +setInterval(() => { const playerCount = world.querySelectorAll("*").length; http.fetch(WEBHOOK_URL, { diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md index 10bc2eb..62e4c4f 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md @@ -57,7 +57,7 @@ world.onPlayerJoin((entity) => { 试着把欢迎消息改成: ```js -entity.player.directMessage("§6你好," + entity.player.name + "!"); +entity.player.directMessage(`§6你好,${entity.player.name}!`); ``` 保存后执行 `npm run build`,然后在游戏内: @@ -186,13 +186,13 @@ world.onPlayerJoin((entity) => { ```js // 每 5 分钟广播一次在线人数 -world.setInterval(() => { +setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7在线: §f${count} §7人`); }, 6000); // 6000 ticks = 5 分钟 // 30 秒后执行一次 -world.setTimeout(() => { +setTimeout(() => { world.say("§6服务器已运行 30 秒"); }, 600); // 600 ticks = 30 秒 ``` @@ -248,7 +248,7 @@ world.onPlayerJoin((entity) => { }); // ── 定时公告 ── -world.setInterval(() => { +setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7在线: §f${count} §7人`); }, 6000); diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md index 9faf13a..c2f46c4 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md @@ -57,7 +57,7 @@ Now when a player joins, they'll receive "§aWelcome to the server!" in green. T Try changing the welcome message to: ```js -entity.player.directMessage("§6Hello, " + entity.player.name + "!"); +entity.player.directMessage(`§6Hello, ${entity.player.name}!`); ``` Save, run `npm run build`, then in-game: @@ -186,13 +186,13 @@ Effect: when a player joins, they see a screen title, hear a bell chime, and gre ```js // Broadcast player count every 5 minutes -world.setInterval(() => { +setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7Online: §f${count} §7players`); }, 6000); // 6000 ticks = 5 minutes // Run once after 30 seconds -world.setTimeout(() => { +setTimeout(() => { world.say("§6Server has been running for 30 seconds"); }, 600); // 600 ticks = 30 seconds ``` @@ -248,7 +248,7 @@ world.onPlayerJoin((entity) => { }); // ── Periodic announcement ── -world.setInterval(() => { +setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7Online: §f${count} §7players`); }, 6000); diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md index 5c48e75..e684d59 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md @@ -26,7 +26,7 @@ ```js const token = world.onTick((info) => { - console.log("Tick: " + info.tick); + console.log(`Tick: ${info.tick}`); }); token.cancel(); // 取消监听 @@ -164,9 +164,10 @@ const entity = world.createEntity({ maxHp: 40, tags: ["elite", "undead"], }); +if (!entity) return; // createEntity 可能返回 null entity.setEquipment("mainhand", "minecraft:bow"); -entity.setTarget(somePlayerEntity); // 设置攻击目标 +// entity.setTarget(targetEntity); // 设置攻击目标(需要先获取实体引用) entity.clearTarget(); // 清除目标 entity.navigateTo(10, 100, 10, 0.5); // 导航到指定位置 entity.setPersistent(true); // 持久化(不会被卸载) @@ -199,9 +200,9 @@ function createPatrol( guard.setAI(true); let wpIndex = 0; - const tid = world.setInterval(() => { + const tid = setInterval(() => { if (guard.destroyed) { - world.clearInterval(tid); + tid.cancel(); return; } // 到达当前路点 → 下一个 @@ -251,13 +252,13 @@ if (entity.hasTag("boss")) { const tags = entity.tags(); // ["boss", "undead"] // 实体碰撞 -world.onEntityContact((entityA, entityB, tick) => { +world.onEntityContact((entityA, entityB, _tick) => { if (entityA.isPlayer() && entityB.hasTag("boss")) { entityA.player.actionBar("§c小心 Boss!"); } }); -world.onEntitySeparate((entityA, entityB, tick) => { +world.onEntitySeparate((entityA, entityB, _tick) => { // 两个实体分离 }); ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md index 1ef72fc..f943be2 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md @@ -26,7 +26,7 @@ All events are registered via `world.onXxx(handler)` and return a `GameEventHand ```js const token = world.onTick((info) => { - console.log("Tick: " + info.tick); + console.log(`Tick: ${info.tick}`); }); token.cancel(); // Unsubscribe @@ -164,9 +164,10 @@ const entity = world.createEntity({ maxHp: 40, tags: ["elite", "undead"], }); +if (!entity) return; // createEntity may return null entity.setEquipment("mainhand", "minecraft:bow"); -entity.setTarget(somePlayerEntity); // Set attack target +// entity.setTarget(targetEntity); // Set attack target (requires entity reference) entity.clearTarget(); // Clear target entity.navigateTo(10, 100, 10, 0.5); // Navigate to position entity.setPersistent(true); // Persistent (won't be unloaded) @@ -199,9 +200,9 @@ function createPatrol( guard.setAI(true); let wpIndex = 0; - const tid = world.setInterval(() => { + const tid = setInterval(() => { if (guard.destroyed) { - world.clearInterval(tid); + tid.cancel(); return; } // Reached current waypoint → move to next @@ -251,13 +252,13 @@ if (entity.hasTag("boss")) { const tags = entity.tags(); // ["boss", "undead"] // Entity collision -world.onEntityContact((entityA, entityB, tick) => { +world.onEntityContact((entityA, entityB, _tick) => { if (entityA.isPlayer() && entityB.hasTag("boss")) { entityA.player.actionBar("§cWatch out — Boss!"); } }); -world.onEntitySeparate((entityA, entityB, tick) => { +world.onEntitySeparate((entityA, entityB, _tick) => { // Two entities separated }); ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems.md index 2ac8de6..1005e31 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems.md @@ -38,7 +38,7 @@ world.addScoreboard("playtime", "dummy"); world.showScoreboard("sidebar", "playtime"); // 每分钟 +1 -world.setInterval(() => { +setInterval(() => { world.querySelectorAll("*").forEach((entity) => { if (!entity.isPlayer()) { return; } const p = entity.player; @@ -94,11 +94,11 @@ world.removeBossbar("my_bar"); let timeLeft = 30; world.showBossbar("demo_timer", "§e倒计时演示", 1.0, "green"); -const timerId = world.setInterval(() => { +const timerId = setInterval(() => { timeLeft--; if (timeLeft <= 0) { world.removeBossbar("demo_timer"); - world.clearInterval(timerId); + timerId.cancel(); world.say("§c⏰ 时间到!"); world.playSound("minecraft:block.note_block.pling", new GameVector3(0, 70, 0), 1.0, 0.5); return; @@ -201,7 +201,7 @@ world.borderSize = 200; world.setBorderDamage(1); world.setBorderWarning(10); -world.setTimeout(() => { +setTimeout(() => { world.say("§c边界缩小至 50 格!"); world.shrinkBorder(50, 60); world.playSound( diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md index b3f6efd..863a72f 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md @@ -38,7 +38,7 @@ world.addScoreboard("playtime", "dummy"); world.showScoreboard("sidebar", "playtime"); // +1 every minute -world.setInterval(() => { +setInterval(() => { world.querySelectorAll("*").forEach((entity) => { if (!entity.isPlayer()) { return; } const p = entity.player; @@ -94,11 +94,11 @@ Color options: `"blue"` `"green"` `"pink"` `"purple"` `"red"` `"white"` `"yellow let timeLeft = 30; world.showBossbar("demo_timer", "§eCountdown Demo", 1.0, "green"); -const timerId = world.setInterval(() => { +const timerId = setInterval(() => { timeLeft--; if (timeLeft <= 0) { world.removeBossbar("demo_timer"); - world.clearInterval(timerId); + timerId.cancel(); world.say("§c⏰ Time's up!"); world.playSound("minecraft:block.note_block.pling", new GameVector3(0, 70, 0), 1.0, 0.5); return; @@ -201,7 +201,7 @@ world.borderSize = 200; world.setBorderDamage(1); world.setBorderWarning(10); -world.setTimeout(() => { +setTimeout(() => { world.say("§cBorder shrinking to 50 blocks!"); world.shrinkBorder(50, 60); world.playSound( diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md index cc6c0de..b7874e0 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md @@ -40,7 +40,7 @@ world.spawnParticleCircle(0, 100, 0, 4.0, "minecraft:end_rod", 36); ```js function spiralEffect(pos: GameVector3): void { for (let i = 0; i < 40; i++) { - world.setTimeout(() => { + setTimeout(() => { const angle = (i / 40) * Math.PI * 4; const radius = 2.0; const px = pos.x + Math.cos(angle) * radius; @@ -74,7 +74,7 @@ const colors = ["red", "gold", "green", "blue", "purple", "white", "pink", "aqua const shapes = ["ball", "large_ball", "star", "creeper", "burst"]; for (let i = 0; i < 8; i++) { - world.setTimeout(() => { + setTimeout(() => { const c = colors[i % colors.length]; const s = shapes[i % shapes.length]; world.launchFirework( @@ -97,7 +97,7 @@ world.strikeLightning(0, 100, 0, 0); // 无伤害,纯视觉效果 // 在玩家周围召唤闪电 for (let i = 0; i < 3; i++) { - world.setTimeout(() => { + setTimeout(() => { const lx = pos.x + (Math.random() - 0.5) * 12; const lz = pos.z + (Math.random() - 0.5) * 12; world.strikeLightning(lx, pos.y, lz, 0); @@ -115,9 +115,9 @@ world.explode(0, 100, 0, 8, true); // 威力 8,引火 // 玩家引爆自身周围(3 秒倒计时) world.playSound("minecraft:block.note_block.bass", pos, 1.0, 0.5); -world.setTimeout(() => { +setTimeout(() => { world.spawnParticle("minecraft:explosion", pos.x, pos.y, pos.z, 1, 0, 0, 0, 0); - world.setTimeout(() => { + setTimeout(() => { world.explode(pos.x, pos.y, pos.z, 4, false); world.playSound("minecraft:entity.generic.explode", pos, 1.0, 1.0); }, 10); @@ -215,9 +215,9 @@ const state: PvPState = { blueScore: 0, }; -let pvpGameTimer: number | null = null; -let pvpAirdropTimer: number | null = null; -let pvpLobbyTimer: number | null = null; +let pvpGameTimer: GameEventHandlerToken | null = null; +let pvpAirdropTimer: GameEventHandlerToken | null = null; +let pvpLobbyTimer: GameEventHandlerToken | null = null; // ── 初始化 ── world.setGameRule("keepInventory", false); @@ -269,9 +269,9 @@ world.onChat((entity, message, _tick) => { function startLobby(): void { state.phase = "starting"; let cd = 30; - pvpLobbyTimer = world.setInterval(() => { + pvpLobbyTimer = setInterval(() => { cd--; - if (cd <= 0 && pvpLobbyTimer) { world.clearInterval(pvpLobbyTimer); beginPvPGame(); } + if (cd <= 0 && pvpLobbyTimer) { pvpLobbyTimer.cancel(); beginPvPGame(); } else if (cd <= 5) { world.say(`§e游戏将在 §c${cd} §e秒后开始!`); } else if (cd % 10 === 0) { world.say(`§7游戏将在 ${cd} 秒后开始...`); } }, 20); @@ -322,7 +322,7 @@ function beginPvPGame(): void { // 游戏倒计时 let remaining = DURATION; - pvpGameTimer = world.setInterval(() => { + pvpGameTimer = setInterval(() => { remaining--; const progress = remaining / DURATION; const mins = Math.floor(remaining / 60); @@ -343,20 +343,20 @@ function beginPvPGame(): void { if (remaining === 60) { world.say("§c最后一分钟!"); } if (remaining === 30) { world.strikeLightning(ARENA.x, ARENA.y, ARENA.z, 0); } if (remaining <= 0 && pvpGameTimer) { - world.clearInterval(pvpGameTimer); + pvpGameTimer.cancel(); endPvPGame(); } }, 20); // 空投 - pvpAirdropTimer = world.setInterval(() => { + pvpAirdropTimer = setInterval(() => { if (state.phase !== "playing") return; const angle = Math.random() * Math.PI * 2; const dist = Math.random() * ARENA_RADIUS * 0.6; const dx = ARENA.x + Math.cos(angle) * dist; const dz = ARENA.z + Math.sin(angle) * dist; world.strikeLightning(dx, ARENA.y + 30, dz, 0); - world.setTimeout(() => { + setTimeout(() => { world.dropItem(dx, ARENA.y + 1, dz, "minecraft:ender_pearl", 2); world.dropItem(dx, ARENA.y + 1, dz, "minecraft:golden_apple", 2); world.launchFirework(dx, ARENA.y + 3, dz, "yellow", "ball"); @@ -408,7 +408,7 @@ world.onPlayerRespawn((entity, _tick) => { function endPvPGame(): void { state.phase = "ending"; world.removeBossbar("pvp_timer"); - if (pvpAirdropTimer) { world.clearInterval(pvpAirdropTimer); } + if (pvpAirdropTimer) { pvpAirdropTimer.cancel(); } let winner = "平局!"; let color = "e"; @@ -427,7 +427,7 @@ function endPvPGame(): void { // 烟花庆祝 for (let i = 0; i < 8; i++) { - world.setTimeout(() => { + setTimeout(() => { const cs = ["red", "gold", "green", "blue", "purple"]; const ss = ["ball", "large_ball", "star", "burst"]; world.launchFirework( @@ -441,7 +441,7 @@ function endPvPGame(): void { } // 30 秒后重置 - world.setTimeout(() => { + setTimeout(() => { state.phase = "waiting"; state.playersReady = 0; state.redScore = 0; state.blueScore = 0; world.hideScoreboard("sidebar"); @@ -546,7 +546,7 @@ function startWave(pos: GameVector3): void { world.say(`§c§l⚔ 第 ${wave} 波开始!§f生成 ${count} 只僵尸`); for (let i = 0; i < count; i++) { - world.setTimeout(() => { + setTimeout(() => { const x = pos.x + (Math.random() - 0.5) * 10; const z = pos.z + (Math.random() - 0.5) * 10; const zombie = world.spawnEntity("minecraft:zombie", new GameVector3(x, pos.y, z)); @@ -565,7 +565,7 @@ world.onEntityDeath((entity, killer, _tick) => { mobsAlive--; if (mobsAlive <= 0) { world.say(`§a§l✔ 第 ${wave} 波清除!`); - world.setTimeout(() => startWave(entity.position), 200); + setTimeout(() => startWave(entity.position), 200); } }); ``` @@ -580,7 +580,7 @@ world.onChat((entity, message, _tick) => { if (message === "!sounds") { const notes = [1.0, 1.2, 1.5, 2.0]; notes.forEach((pitch, i) => { - world.setTimeout(() => { + setTimeout(() => { p.playSound("minecraft:block.note_block.pling", 1.0, pitch); }, i * 100); }); diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md index 802b761..27f9d1e 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md @@ -40,7 +40,7 @@ Common particles: ```js function spiralEffect(pos: GameVector3): void { for (let i = 0; i < 40; i++) { - world.setTimeout(() => { + setTimeout(() => { const angle = (i / 40) * Math.PI * 4; const radius = 2.0; const px = pos.x + Math.cos(angle) * radius; @@ -74,7 +74,7 @@ const colors = ["red", "gold", "green", "blue", "purple", "white", "pink", "aqua const shapes = ["ball", "large_ball", "star", "creeper", "burst"]; for (let i = 0; i < 8; i++) { - world.setTimeout(() => { + setTimeout(() => { const c = colors[i % colors.length]; const s = shapes[i % shapes.length]; world.launchFirework( @@ -97,7 +97,7 @@ world.strikeLightning(0, 100, 0, 0); // No damage, visual only // Summon lightning around a player for (let i = 0; i < 3; i++) { - world.setTimeout(() => { + setTimeout(() => { const lx = pos.x + (Math.random() - 0.5) * 12; const lz = pos.z + (Math.random() - 0.5) * 12; world.strikeLightning(lx, pos.y, lz, 0); @@ -115,9 +115,9 @@ world.explode(0, 100, 0, 8, true); // Power 8, causes fire // Player-triggered self-destruct (3-second countdown) world.playSound("minecraft:block.note_block.bass", pos, 1.0, 0.5); -world.setTimeout(() => { +setTimeout(() => { world.spawnParticle("minecraft:explosion", pos.x, pos.y, pos.z, 1, 0, 0, 0, 0); - world.setTimeout(() => { + setTimeout(() => { world.explode(pos.x, pos.y, pos.z, 4, false); world.playSound("minecraft:entity.generic.explode", pos, 1.0, 1.0); }, 10); @@ -215,9 +215,9 @@ const state: PvPState = { blueScore: 0, }; -let pvpGameTimer: number | null = null; -let pvpAirdropTimer: number | null = null; -let pvpLobbyTimer: number | null = null; +let pvpGameTimer: GameEventHandlerToken | null = null; +let pvpAirdropTimer: GameEventHandlerToken | null = null; +let pvpLobbyTimer: GameEventHandlerToken | null = null; // ── Initialization ── world.setGameRule("keepInventory", false); @@ -269,9 +269,9 @@ world.onChat((entity, message, _tick) => { function startLobby(): void { state.phase = "starting"; let cd = 30; - pvpLobbyTimer = world.setInterval(() => { + pvpLobbyTimer = setInterval(() => { cd--; - if (cd <= 0 && pvpLobbyTimer) { world.clearInterval(pvpLobbyTimer); beginPvPGame(); } + if (cd <= 0 && pvpLobbyTimer) { pvpLobbyTimer.cancel(); beginPvPGame(); } else if (cd <= 5) { world.say(`§eGame starts in §c${cd} §eseconds!`); } else if (cd % 10 === 0) { world.say(`§7Game starts in ${cd} seconds...`); } }, 20); @@ -322,7 +322,7 @@ function beginPvPGame(): void { // Game countdown let remaining = DURATION; - pvpGameTimer = world.setInterval(() => { + pvpGameTimer = setInterval(() => { remaining--; const progress = remaining / DURATION; const mins = Math.floor(remaining / 60); @@ -343,20 +343,20 @@ function beginPvPGame(): void { if (remaining === 60) { world.say("§cFinal minute!"); } if (remaining === 30) { world.strikeLightning(ARENA.x, ARENA.y, ARENA.z, 0); } if (remaining <= 0 && pvpGameTimer) { - world.clearInterval(pvpGameTimer); + pvpGameTimer.cancel(); endPvPGame(); } }, 20); // Airdrops - pvpAirdropTimer = world.setInterval(() => { + pvpAirdropTimer = setInterval(() => { if (state.phase !== "playing") return; const angle = Math.random() * Math.PI * 2; const dist = Math.random() * ARENA_RADIUS * 0.6; const dx = ARENA.x + Math.cos(angle) * dist; const dz = ARENA.z + Math.sin(angle) * dist; world.strikeLightning(dx, ARENA.y + 30, dz, 0); - world.setTimeout(() => { + setTimeout(() => { world.dropItem(dx, ARENA.y + 1, dz, "minecraft:ender_pearl", 2); world.dropItem(dx, ARENA.y + 1, dz, "minecraft:golden_apple", 2); world.launchFirework(dx, ARENA.y + 3, dz, "yellow", "ball"); @@ -408,7 +408,7 @@ world.onPlayerRespawn((entity, _tick) => { function endPvPGame(): void { state.phase = "ending"; world.removeBossbar("pvp_timer"); - if (pvpAirdropTimer) { world.clearInterval(pvpAirdropTimer); } + if (pvpAirdropTimer) { pvpAirdropTimer.cancel(); } let winner = "Draw!"; let color = "e"; @@ -427,7 +427,7 @@ function endPvPGame(): void { // Victory fireworks for (let i = 0; i < 8; i++) { - world.setTimeout(() => { + setTimeout(() => { const cs = ["red", "gold", "green", "blue", "purple"]; const ss = ["ball", "large_ball", "star", "burst"]; world.launchFirework( @@ -441,7 +441,7 @@ function endPvPGame(): void { } // Reset after 30 seconds - world.setTimeout(() => { + setTimeout(() => { state.phase = "waiting"; state.playersReady = 0; state.redScore = 0; state.blueScore = 0; world.hideScoreboard("sidebar"); @@ -546,7 +546,7 @@ function startWave(pos: GameVector3): void { world.say(`§c§l⚔ Wave ${wave} begins!§f Spawning ${count} zombies`); for (let i = 0; i < count; i++) { - world.setTimeout(() => { + setTimeout(() => { const x = pos.x + (Math.random() - 0.5) * 10; const z = pos.z + (Math.random() - 0.5) * 10; const zombie = world.spawnEntity("minecraft:zombie", new GameVector3(x, pos.y, z)); @@ -565,7 +565,7 @@ world.onEntityDeath((entity, killer, _tick) => { mobsAlive--; if (mobsAlive <= 0) { world.say(`§a§l✔ Wave ${wave} cleared!`); - world.setTimeout(() => startWave(entity.position), 200); + setTimeout(() => startWave(entity.position), 200); } }); ``` @@ -580,7 +580,7 @@ world.onChat((entity, message, _tick) => { if (message === "!sounds") { const notes = [1.0, 1.2, 1.5, 2.0]; notes.forEach((pitch, i) => { - world.setTimeout(() => { + setTimeout(() => { p.playSound("minecraft:block.note_block.pling", 1.0, pitch); }, i * 100); }); diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md index 3461885..54ab070 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md @@ -34,6 +34,7 @@ colorzone 项目包含完整的客户端脚本示例(`src/client/app.ts`), | ui | 屏幕文字 | F6 显示设置 | | chat | 聊天命令 | `!fav` `!mob` | | audio | 自定义音效 | V 键 | +| fog | 雾颜色和距离控制 | — | ## 6.3 client — 生命周期 @@ -137,7 +138,28 @@ const musicVol = audio.getVolume("music"); // 读取当前音量 audio.playSound("colorzone:victory_fanfare", 1.0, 1.0); ``` -## 6.8 storage — 客户端本地存储 +## 6.8 fog — 雾效控制 + +覆盖 Minecraft 的雾颜色和渲染距离: + +```js +// 设置雾颜色(RGB 0-255) +client.setFogColor(255, 100, 50); + +// 设置雾距离(单位:方块) +client.setFogStartDistance(10); // 雾从 10 格外开始 +client.setFogEndDistance(50); // 50 格外完全被雾遮挡 + +// 读取当前雾颜色 +const color = client.getFogColor(); // 返回 GameRGBColor 或 null + +// 恢复 Minecraft 默认雾效果 +client.resetFog(); +``` + +> **注意**: 雾效修改在客户端本地生效。可通过 `remoteChannel` 让服务端指令触发客户端雾效变化,实现服务端控制的天气效果。 + +## 6.9 storage — 客户端本地存储 客户端的 `storage` 与服务端用法相同,但数据存储在玩家本地: @@ -194,7 +216,7 @@ const page = notes.list({ pageSize: 10, ascending: false }); const entries = page.getCurrentPage(); ``` -## 6.9 db — 客户端 SQLite +## 6.10 db — 客户端 SQLite 客户端也支持 SQLite(需要 `minecraft-sqlite-jdbc` 模组): @@ -244,7 +266,7 @@ function searchMobs(keyword: string): void { > 未安装 `minecraft-sqlite-jdbc` 时,`db.isAvailable()` 返回 `false`,所有 SQL 调用静默返回空结果。 -## 6.10 http — 客户端 HTTP 请求 +## 6.11 http — 客户端 HTTP 请求 ```js // 同步 GET @@ -286,7 +308,7 @@ http.fetch("https://httpbin.org/post", { }); ``` -## 6.11 remoteChannel — 两端通讯 +## 6.12 remoteChannel — 两端通讯 这是客户端脚本最强大的功能:服务端和客户端可以互相发送事件。 @@ -369,7 +391,7 @@ remoteChannel.onServerEvent((event) => { > **重要:** 跨网络传输的数据必须是 JSON 可序列化的类型(string、number、boolean、null、普通对象、数组)。不能传函数、Java 对象或 `GameVector3`。 -## 6.12 完整实战:客户端 HUD 状态栏 +## 6.13 完整实战:客户端 HUD 状态栏 综合运用 input、ui、remoteChannel 和 storage 创建一个自定义 HUD: @@ -437,7 +459,7 @@ ui.showTitle("§6自定义 HUD 已启动", "§7F6=坐标 F7=FPS", 10, 40, 10); console.log("[HUD] Client HUD demo loaded"); ``` -## 6.13 客户端脚本调试 +## 6.14 客户端脚本调试 客户端脚本的 `console.log` 输出到**客户端日志**(不是服务端)。在 Minecraft 启动器或日志目录中查看。 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md index 61b95ff..fe17f91 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md @@ -34,6 +34,7 @@ The colorzone project (`src/client/app.ts`) contains a complete client-side demo | ui | On-screen text | F6 to show settings | | chat | Chat commands | `!fav` `!mob` | | audio | Custom sounds | V key | +| fog | Fog colour and distance control | — | ## 6.3 client — Lifecycle @@ -137,7 +138,28 @@ const musicVol = audio.getVolume("music"); // Read current volume audio.playSound("colorzone:victory_fanfare", 1.0, 1.0); ``` -## 6.8 storage — Client-Side Local Storage +## 6.8 fog — Fog Control + +Override Minecraft's fog colour and render distance: + +```js +// Set fog colour (RGB 0-255) +client.setFogColor(255, 100, 50); + +// Set fog distance (in blocks) +client.setFogStartDistance(10); // fog begins beyond 10 blocks +client.setFogEndDistance(50); // fully obscured beyond 50 blocks + +// Read current fog colour +const color = client.getFogColor(); // returns GameRGBColor or null + +// Restore Minecraft default fog +client.resetFog(); +``` + +> **Note**: Fog changes take effect locally on each client. Use `remoteChannel` to let the server trigger fog changes on clients, enabling server-controlled weather effects. + +## 6.9 storage — Client-Side Local Storage Client-side `storage` uses the same API as the server but stores data locally on each player's machine: @@ -194,7 +216,7 @@ const page = notes.list({ pageSize: 10, ascending: false }); const entries = page.getCurrentPage(); ``` -## 6.9 db — Client-Side SQLite +## 6.10 db — Client-Side SQLite The client also supports SQLite (requires `minecraft-sqlite-jdbc` mod): @@ -244,7 +266,7 @@ function searchMobs(keyword: string): void { > When `minecraft-sqlite-jdbc` is not installed, `db.isAvailable()` returns `false` and all SQL calls silently return empty results. -## 6.10 http — Client HTTP Requests +## 6.11 http — Client HTTP Requests ```js // Synchronous GET @@ -286,7 +308,7 @@ http.fetch("https://httpbin.org/post", { }); ``` -## 6.11 remoteChannel — Bidirectional Communication +## 6.12 remoteChannel — Bidirectional Communication This is the most powerful client scripting feature: server and client can send events to each other. @@ -369,7 +391,7 @@ No manual detection is needed. `remoteChannel.sendClientEvent()` uses optional p > **Important:** Data sent across the network must be JSON-serializable (string, number, boolean, null, plain objects, arrays). You cannot send functions, Java objects, or `GameVector3`. -## 6.12 Practical Example: Custom HUD Status Bar +## 6.13 Practical Example: Custom HUD Status Bar Combining input, ui, remoteChannel, and storage to create a custom HUD: @@ -436,7 +458,7 @@ ui.showTitle("§6Custom HUD Active", "§7F6=Coords F7=FPS", 10, 40, 10); console.log("[HUD] Client HUD demo loaded"); ``` -## 6.13 Debugging Client Scripts +## 6.14 Debugging Client Scripts Client script `console.log` output goes to the **client log** (not the server log). Check your Minecraft launcher or logs directory. diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md index bc0085d..874d19d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md @@ -80,7 +80,7 @@ world.onChat((entity, message) => { return true; }); -world.setInterval(() => { +setInterval(() => { world.say("当前在线: " + world.querySelectorAll("*").length + " 人"); }, 6000); ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md index 038197c..bf5548a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md @@ -58,7 +58,7 @@ world.onChat((entity, message) => { return true; }); -world.setInterval(() => { +setInterval(() => { world.say("Players online: " + world.querySelectorAll("*").length); }, 6000); ``` diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java index 9cd85ce..56f9fbd 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java @@ -26,6 +26,7 @@ import org.mozilla.javascript.*; import org.slf4j.Logger; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -40,6 +41,7 @@ import net.minecraft.world.entity.Entity; import net.minecraft.client.multiplayer.ServerData; import net.neoforged.neoforge.client.event.RenderGuiEvent; +import net.neoforged.neoforge.client.event.ViewportEvent; /** * Singleton client-side Rhino engine. @@ -83,6 +85,20 @@ public class Box3JSClientEngine { private Box3JSGuiProxy activeGuiProxy; private volatile boolean dbWarningShown; + // ── Fog state (client-side rendering) ── + private volatile boolean fogColorEnabled; + private volatile float fogColorR = 1.0f; + private volatile float fogColorG = 1.0f; + private volatile float fogColorB = 1.0f; + private volatile boolean fogDistanceEnabled; + private volatile float fogStartDist = -1f; + private volatile float fogEndDist = -1f; + private volatile boolean fogRegistered; + + // ── Timers ── + private final List timers = new CopyOnWriteArrayList<>(); + private final AtomicInteger timerIdCounter = new AtomicInteger(0); + private static final Map KEY_MAP = new HashMap<>(); static { // Letters a-z @@ -156,6 +172,28 @@ private void init() { ScriptableObject.putProperty(scope, "Box3JSQueryResult", new NativeJavaClass(scope, Box3JSQueryResult.class)); + // -- setTimeout / setInterval global functions ------------------- + ScriptableObject.putProperty(scope, "setTimeout", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + Function handler = (Function) args[0]; + int ticks = ((Number) args[1]).intValue(); + int id = scheduleTimeout(handler, ticks); + return new GameEventHandlerToken(() -> clearTimer(id)); + } + }); + ScriptableObject.putProperty(scope, "setInterval", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + Function handler = (Function) args[0]; + int ticks = ((Number) args[1]).intValue(); + int id = scheduleInterval(handler, ticks); + return new GameEventHandlerToken(() -> clearTimer(id)); + } + }); + // -- client global (lifecycle + server-bound actions) ---------- ScriptableObject clientObj = (ScriptableObject) cx.newObject(scope); @@ -277,6 +315,70 @@ public Object call(Context cx, Scriptable scope, } }); + // client.getFogColor() -> GameRGBColor or null + ScriptableObject.putProperty(clientObj, "getFogColor", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (!fogColorEnabled) return null; + return new GameRGBColor(fogColorR, fogColorG, fogColorB); + } + }); + + // client.setFogColor(r, g, b) — r,g,b each 0-255 + ScriptableObject.putProperty(clientObj, "setFogColor", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 3) return Undefined.instance; + fogColorR = Math.max(0f, Math.min(1f, ((Number) args[0]).floatValue() / 255f)); + fogColorG = Math.max(0f, Math.min(1f, ((Number) args[1]).floatValue() / 255f)); + fogColorB = Math.max(0f, Math.min(1f, ((Number) args[2]).floatValue() / 255f)); + fogColorEnabled = true; + registerFogListener(); + return Undefined.instance; + } + }); + + // client.setFogStartDistance(distanceInBlocks) + ScriptableObject.putProperty(clientObj, "setFogStartDistance", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 1) return Undefined.instance; + fogStartDist = Math.max(0f, ((Number) args[0]).floatValue()); + fogDistanceEnabled = true; + registerFogListener(); + return Undefined.instance; + } + }); + + // client.setFogEndDistance(distanceInBlocks) — maps to Box3 maxFog + ScriptableObject.putProperty(clientObj, "setFogEndDistance", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 1) return Undefined.instance; + fogEndDist = Math.max(0f, ((Number) args[0]).floatValue()); + fogDistanceEnabled = true; + registerFogListener(); + return Undefined.instance; + } + }); + + // client.resetFog() — restore Minecraft default fog + ScriptableObject.putProperty(clientObj, "resetFog", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + fogColorEnabled = false; + fogDistanceEnabled = false; + fogStartDist = -1f; + fogEndDist = -1f; + return Undefined.instance; + } + }); + ScriptableObject.putProperty(scope, "client", clientObj); // -- audio global (sound playback) ------------------------------ @@ -826,6 +928,64 @@ private void fireTick() { LOGGER.error("Client tick callback error", e); } } + fireTimers(); + } + + // ── Timers ── + + private int scheduleTimeout(Function handler, int ticks) { + int id = timerIdCounter.incrementAndGet(); + timers.add(new TimerEntry(id, handler, ticks, 0)); + return id; + } + + private int scheduleInterval(Function handler, int ticks) { + int id = timerIdCounter.incrementAndGet(); + timers.add(new TimerEntry(id, handler, ticks, ticks)); + return id; + } + + private void clearTimer(int id) { + timers.removeIf(t -> t.id == id); + } + + private void fireTimers() { + var toFire = new ArrayList(); + var toRemove = new ArrayList(); + for (var t : timers) { + if (--t.remaining <= 0) { + toFire.add(t); + if (t.interval == 0) + toRemove.add(t); + else + t.remaining = t.interval; + } + } + timers.removeAll(toRemove); + for (var t : toFire) { + Context cx = Context.enter(); + try { + t.handler.call(cx, scope, scope, new Object[0]); + } catch (Exception e) { + LOGGER.error("Client timer error", e); + } finally { + Context.exit(); + } + } + } + + static class TimerEntry { + final int id; + final Function handler; + int remaining; + final int interval; + + TimerEntry(int id, Function handler, int remaining, int interval) { + this.id = id; + this.handler = handler; + this.remaining = remaining; + this.interval = interval; + } } private void registerKeyListener() { @@ -984,6 +1144,37 @@ private void registerMouseListener() { }); } + // ── Fog override listener ── + + private void registerFogListener() { + if (fogRegistered) return; + Minecraft.getInstance().execute(() -> { + if (fogRegistered) return; + // Fog color override + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeFogColor.class, event -> { + if (fogColorEnabled) { + event.setRed(fogColorR); + event.setGreen(fogColorG); + event.setBlue(fogColorB); + } + }); + // Fog distance override + NeoForge.EVENT_BUS.addListener(ViewportEvent.RenderFog.class, event -> { + if (fogDistanceEnabled) { + if (fogStartDist >= 0) { + event.setNearPlaneDistance(fogStartDist); + } + if (fogEndDist >= 0) { + event.setFarPlaneDistance(fogEndDist); + } + event.setCanceled(true); + } + }); + fogRegistered = true; + LOGGER.debug("Fog rendering listener registered"); + }); + } + // ── Draw text entry ── private record DrawTextEntry(int x, int y, String text, int color) {} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java index 25c2f6f..0de8a8e 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java @@ -358,13 +358,6 @@ public void say(String message) { server.getPlayerList().broadcastSystemMessage(net.minecraft.network.chat.Component.literal(message), false); } - // ---- Timers ---- - - public int setTimeout(Function handler, int ticks) { return engine.scheduleTimeout(handler, ticks); } - public int setInterval(Function handler, int ticks) { return engine.scheduleInterval(handler, ticks); } - public void clearTimeout(int id) { engine.clearTimer(id); } - public void clearInterval(int id) { engine.clearTimer(id); } - // ---- Command ---- public void runCommand(String cmd) { diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java index e98ec7e..73697c8 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java @@ -975,6 +975,26 @@ public Object call(Context cx, Scriptable scope, ScriptableObject.putProperty(scope, "db", dbObj); ScriptableObject.putProperty(scope, "http", Context.javaToJS(httpBinding, scope)); ScriptableObject.putProperty(scope, "remoteChannel", Context.javaToJS(remoteChannel, scope)); + ScriptableObject.putProperty(scope, "setTimeout", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + Function handler = (Function) args[0]; + int ticks = ((Number) args[1]).intValue(); + int id = scheduleTimeout(handler, ticks); + return new GameEventHandlerToken(() -> clearTimer(id)); + } + }); + ScriptableObject.putProperty(scope, "setInterval", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + Function handler = (Function) args[0]; + int ticks = ((Number) args[1]).intValue(); + int id = scheduleInterval(handler, ticks); + return new GameEventHandlerToken(() -> clearTimer(id)); + } + }); ScriptableObject.putProperty(scope, "_jConsole", Context.javaToJS(new Box3JSConsole(() -> currentProject), scope)); cx.evaluateString(scope, Box3ScriptUtils.CONSOLE_INIT_JS, "console-init", 1, null); diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts index ee5268e..200de8d 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts @@ -72,15 +72,14 @@ interface GameClient { * @returns @zh `{ type: "block"|"entity", position, entity?, blockPos?, direction? }` 或 null @en `{ type: "block"|"entity", position, entity?, blockPos?, direction? }` or null */ getLookingAt(): { - type: "block" | "entity"; + type: "entity"; position: { x: number; y: number; z: number }; - entity?: { - name: string; - uuid: string; - type: string; - }; - blockPos?: { x: number; y: number; z: number }; - direction?: string; + entity: { name: string; uuid: string; type: string }; + } | { + type: "block"; + position: { x: number; y: number; z: number }; + blockPos: { x: number; y: number; z: number }; + direction: string; } | null; /** @@ -95,6 +94,44 @@ interface GameClient { playerCount?: number; maxPlayers?: number; }; + + // ── Fog control ── + + /** + * @zh 获取当前自定义雾颜色。未设置时返回 null。 + * @en Gets the current custom fog colour. Returns null if not set. + * @returns @zh GameRGBColor 或 null @en GameRGBColor or null + */ + getFogColor(): GameRGBColor | null; + + /** + * @zh 设置雾颜色(RGB 0-255)。 + * @en Sets the fog colour (RGB 0-255). + * @param r - @zh 红色 (0-255) @en Red (0-255) + * @param g - @zh 绿色 (0-255) @en Green (0-255) + * @param b - @zh 蓝色 (0-255) @en Blue (0-255) + */ + setFogColor(r: number, g: number, b: number): void; + + /** + * @zh 设置雾起始距离(方块)。低于此距离完全透明。 + * @en Sets the distance (in blocks) where fog begins. Fully transparent below this distance. + * @param distance - @zh 雾起始距离(方块) @en Fog start distance (blocks) + */ + setFogStartDistance(distance: number): void; + + /** + * @zh 设置雾结束距离(方块),对应 Box3 的 maxFog。超过此距离完全被雾遮挡。 + * @en Sets the distance (in blocks) where fog is fully opaque, equivalent to Box3's maxFog. + * @param distance - @zh 雾结束距离(方块) @en Fog end distance (blocks) + */ + setFogEndDistance(distance: number): void; + + /** + * @zh 重置雾效果为 Minecraft 默认值。 + * @en Resets fog to Minecraft's default behaviour. + */ + resetFog(): void; } // ── §7 @zh 全局声明(客户端) @en Global Declarations (client) ── diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts index 674c11b..fa8a6d7 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts @@ -657,32 +657,6 @@ interface GameWorld { */ setBorderWarning(blocks: number): void; - // ── @zh 定时器 @en Timers ── - - /** - * @zh 设置一次性延时回调。 - * @en Schedules a one‑shot delayed callback. - * @param handler - 回调函数 - * @param ticks - 延迟 tick 数 - * @returns @zh 定时器 ID(可用于 clearTimeout) @en Timer ID (can be used with clearTimeout) - */ - setTimeout(handler: () => void, ticks: number): number; - - /** - * @zh 设置循环定时回调。 - * @en Schedules a recurring interval callback. - * @param handler - 回调函数 - * @param ticks - 间隔 tick 数 - * @returns @zh 定时器 ID(可用于 clearInterval) @en Timer ID (can be used with clearInterval) - */ - setInterval(handler: () => void, ticks: number): number; - - /** @zh 取消 setTimeout @en Clears a timeout by ID. */ - clearTimeout(id: number): void; - - /** @zh 取消 setInterval @en Clears an interval by ID. */ - clearInterval(id: number): void; - // ── @zh 项目间消息 @en Cross‑project Messaging ── /** diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts index 8cfe49c..5ccda11 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts @@ -1015,3 +1015,36 @@ declare const db: GameDatabase; /** @zh HTTP 请求 API @en HTTP request API */ declare const http: GameHttpAPI; + +// ── §8 @zh 定时器(全局函数) @en Timers (global functions) ── + +/** + * @zh 设置一次性延时回调。Rhino 引擎不提供浏览器内置的 setTimeout,由 Box3JS 提供。 + * @en Schedules a one‑shot delayed callback. Rhino does not provide the browser built‑in setTimeout; Box3JS supplies it. + * @param handler - @zh 回调函数 @en callback function + * @param ticks - @zh 延迟 tick 数(20 ticks = 1 秒) @en delay in ticks (20 ticks = 1 second) + * @returns @zh GameEventHandlerToken — 调用 .cancel() 取消 @en GameEventHandlerToken — call .cancel() to cancel + * + * @example + * const token = setTimeout(() => { + * world.say("3 seconds passed"); + * }, 60); + * // token.cancel(); // cancel before it fires + */ +declare function setTimeout(handler: () => void, ticks: number): GameEventHandlerToken; + +/** + * @zh 设置循环定时回调。Rhino 引擎不提供浏览器内置的 setInterval,由 Box3JS 提供。 + * @en Schedules a recurring interval callback. Rhino does not provide the browser built‑in setInterval; Box3JS supplies it. + * @param handler - @zh 回调函数 @en callback function + * @param ticks - @zh 间隔 tick 数(20 ticks = 1 秒) @en interval in ticks (20 ticks = 1 second) + * @returns @zh GameEventHandlerToken — 调用 .cancel() 取消 @en GameEventHandlerToken — call .cancel() to cancel + * + * @example + * const token = setInterval(() => { + * world.say("Every 5 seconds"); + * }, 100); + * // token.cancel(); // stop the interval + */ +declare function setInterval(handler: () => void, ticks: number): GameEventHandlerToken; + diff --git a/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json b/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json index dfe2a8d..cbbd474 100644 --- a/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json +++ b/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json @@ -7,6 +7,10 @@ { "side": "server", "name": "db", "type": "GameDatabase", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, { "side": "server", "name": "http", "type": "GameHttpAPI", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, { "side": "server", "name": "registries", "type": "GameRegistries | undefined", "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", "runtime": "src/main/java/com/box3lab/box3js/standalone/Box3StandaloneBootstrap.java" }, + { "side": "server", "name": "setTimeout", "type": "GlobalFunction", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "setInterval", "type": "GlobalFunction", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "client", "name": "setTimeout", "type": "GlobalFunction", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "setInterval", "type": "GlobalFunction", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, { "side": "client", "name": "audio", "type": "GameAudio", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, { "side": "client", "name": "client", "type": "GameClient", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, { "side": "client", "name": "input", "type": "GameInput", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, diff --git a/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs b/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs index b9a9545..eb69dbe 100644 --- a/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs +++ b/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs @@ -384,6 +384,11 @@ function extractJavaScriptableMembers(path, objectVariable) { function hasDtsConst(paths, name, type) { const files = Array.isArray(paths) ? paths : [paths]; + // GlobalFunction entries use `declare function` instead of `declare const` + if (type === "GlobalFunction") { + const funcRe = new RegExp(`declare\\s+function\\s+${name}\\s*\\(`); + return files.some((path) => funcRe.test(read(path))); + } const escapedType = type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s*"); const declarationRe = new RegExp(`declare\\s+const\\s+${name}\\s*:\\s*${escapedType}\\s*;`); return files.some((path) => declarationRe.test(read(path))); From f8622ca91e8e71ebb9698a1a76a03de16656ba39 Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Thu, 14 May 2026 17:49:50 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(script):=20=E4=BF=AE=E6=AD=A3=E7=8E=A9?= =?UTF-8?q?=E5=AE=B6=E6=98=BE=E7=A4=BA=E5=90=8D=E7=A7=B0=E7=9A=84=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Player 类反射访问中的字段名被错误地写成了 "displayName",实际应为 "displayname"。 feat(build): 为 Rhino 环境添加正则表达式插件支持 添加 rhinoRegexPlugin 插件,用于处理 Minecraft Rhino 环境中的正则表达式操作。由于类加载器隔离导致 NativeRegExp 无法加载,该插件将正则表达式字面量转换为对辅助函数的调用,支持 split、match、replace、test 和 exec 操作。 该插件将以下语法进行转换: - str.split(/pattern/flags) → __regexSplit(str, "pattern", "flags") - str.match(/pattern/flags) → __regexMatch(str, "pattern", "flags") - str.replace(/pattern/flags, r) → __regexReplace(str, "pattern", "flags", r) - /pattern/flags.test(str) → __regexTest("pattern", "flags", str) --- .../box3lab/box3js/script/Box3JSPlayer.java | 2 +- .../assets/box3js/template/build.mjs | 132 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java index 88ca22f..a43de4b 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java @@ -385,7 +385,7 @@ public void link(String href) { public void setPlayerListName(String name) { try { - java.lang.reflect.Field f = net.minecraft.world.entity.player.Player.class.getDeclaredField("displayName"); + java.lang.reflect.Field f = net.minecraft.world.entity.player.Player.class.getDeclaredField("displayname"); f.setAccessible(true); f.set(player, Component.literal(name)); server.getPlayerList().broadcastAll( diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs index 5e42a34..3c211fe 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs @@ -362,6 +362,137 @@ function rhinoArrayMethodsPlugin({ types: t }) { }; } +/** + * Regex literal → __regexSplit / __regexMatch / __regexReplace / __regexTest + * + * Rhino inside Minecraft cannot load NativeRegExp (classloader isolation). + * All regex operations are delegated to pure-JS helpers injected from Java. + * + * str.split(/pattern/flags) → __regexSplit(str, "pattern", "flags") + * str.match(/pattern/flags) → __regexMatch(str, "pattern", "flags") + * str.replace(/pattern/flags,r) → __regexReplace(str, "pattern", "flags", r) + * /pattern/flags.test(str) → __regexTest("pattern", "flags", str) + */ +function rhinoRegexPlugin({ types: t }) { + function patternString(node) { + return t.stringLiteral(node.pattern); + } + function flagsString(node) { + return t.stringLiteral(node.flags || ""); + } + + return { + visitor: { + CallExpression(path) { + const { callee, arguments: args } = path.node; + + // str.split(regex) → __regexSplit(str, pattern, flags) + if ( + t.isMemberExpression(callee) && + t.isIdentifier(callee.property, { name: "split" }) && + args.length >= 1 && + t.isRegExpLiteral(args[0]) + ) { + const regex = args[0]; + path.replaceWith( + t.callExpression(t.identifier("__regexSplit"), [ + callee.object, + patternString(regex), + flagsString(regex), + ]), + ); + return; + } + + // str.match(regex) → __regexMatch(str, pattern, flags) + if ( + t.isMemberExpression(callee) && + t.isIdentifier(callee.property, { name: "match" }) && + args.length >= 1 && + t.isRegExpLiteral(args[0]) + ) { + const regex = args[0]; + path.replaceWith( + t.callExpression(t.identifier("__regexMatch"), [ + callee.object, + patternString(regex), + flagsString(regex), + ]), + ); + return; + } + + // str.replace(regex, replacement) → __regexReplace(str, pattern, flags, replacement) + if ( + t.isMemberExpression(callee) && + t.isIdentifier(callee.property, { name: "replace" }) && + args.length >= 1 && + t.isRegExpLiteral(args[0]) + ) { + const regex = args[0]; + path.replaceWith( + t.callExpression(t.identifier("__regexReplace"), [ + callee.object, + patternString(regex), + flagsString(regex), + args[1] || t.nullLiteral(), + ]), + ); + return; + } + + // regex.test(str) → __regexTest(pattern, flags, str) + if ( + t.isMemberExpression(callee) && + t.isRegExpLiteral(callee.object) && + t.isIdentifier(callee.property, { name: "test" }) && + args.length >= 1 + ) { + const regex = callee.object; + path.replaceWith( + t.callExpression(t.identifier("__regexTest"), [ + patternString(regex), + flagsString(regex), + args[0], + ]), + ); + return; + } + + // regex.exec(str) → __regexExec(pattern, flags, str) + if ( + t.isMemberExpression(callee) && + t.isRegExpLiteral(callee.object) && + t.isIdentifier(callee.property, { name: "exec" }) && + args.length >= 1 + ) { + const regex = callee.object; + path.replaceWith( + t.callExpression(t.identifier("__regexExec"), [ + patternString(regex), + flagsString(regex), + args[0], + ]), + ); + } + }, + + // Standalone regex literals not in a call expression context + RegExpLiteral(path) { + if (t.isCallExpression(path.parent) && path.parent.arguments.includes(path.node)) { + return; + } + if (t.isMemberExpression(path.parent) && path.parent.object === path.node) { + return; + } + throw path.buildCodeFrameError( + "Regex literals must be used with .split(), .match(), .replace(), .test(), or .exec()", + ); + }, + }, + }; +} + // ═══════════════════════════════════════════════════════════════ // esbuild plugin // ═══════════════════════════════════════════════════════════════ @@ -390,6 +521,7 @@ const babelRhinoPlugin = { "@babel/preset-typescript", ], plugins: [ + rhinoRegexPlugin, rhinoArrayMethodsPlugin, rhinoForOfPlugin, rhinoTemplatePlugin, From be4cee5d4c819c9ebf05660d2649c57ecfcbc01a Mon Sep 17 00:00:00 2001 From: viyrs <2991883280@qq.com> Date: Fri, 15 May 2026 16:47:37 +0800 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20BOX3=5FAPI=5FC?= =?UTF-8?q?OMPARISON.md=20=E5=B9=B6=E7=A7=BB=E9=99=A4=E5=86=97=E4=BD=99?= =?UTF-8?q?=E5=AF=BC=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 .gitignore 中添加 VitePress 相关条目,包括 node_modules、docs/.vitepress/dist/ 及 docs/.vitepress/cache/ - 移除 BOX3_API_COMPARISON.md 中冗余的目录,因其与章节标题重复 - 优化天气 API 对比表格的列对齐,改进 Markdown 格式 - 移除 API 对比文档中各章节之间冗余的分隔线 - 在 Box3JS 独有能力章节中补充缺失的换行,提升可读性 - 移除主 README.md 文件,因文档结构已变更 --- Box3JS-NeoForge-1.21.1/.gitignore | 5 + .../docs/.vitepress/config.mjs | 233 ++ .../docs/BOX3_API_COMPARISON.md | 327 +-- Box3JS-NeoForge-1.21.1/docs/README_en.md | 70 - Box3JS-NeoForge-1.21.1/docs/api/README.md | 438 +-- Box3JS-NeoForge-1.21.1/docs/api/README_en.md | 408 --- Box3JS-NeoForge-1.21.1/docs/api/client.md | 14 +- Box3JS-NeoForge-1.21.1/docs/api/commands.md | 46 +- Box3JS-NeoForge-1.21.1/docs/api/database.md | 38 +- Box3JS-NeoForge-1.21.1/docs/api/entity.md | 6 +- Box3JS-NeoForge-1.21.1/docs/api/http.md | 14 +- Box3JS-NeoForge-1.21.1/docs/api/math.md | 312 +- Box3JS-NeoForge-1.21.1/docs/api/math_en.md | 413 --- Box3JS-NeoForge-1.21.1/docs/api/player.md | 2 +- Box3JS-NeoForge-1.21.1/docs/api/registries.md | 28 +- Box3JS-NeoForge-1.21.1/docs/api/server.md | 4 +- Box3JS-NeoForge-1.21.1/docs/api/storage.md | 12 +- Box3JS-NeoForge-1.21.1/docs/api/world.md | 2 +- Box3JS-NeoForge-1.21.1/docs/en/README.md | 77 + Box3JS-NeoForge-1.21.1/docs/en/api/README.md | 413 +++ .../{api/client_en.md => en/api/client.md} | 19 +- .../commands_en.md => en/api/commands.md} | 49 +- .../database_en.md => en/api/database.md} | 15 +- .../{api/entity_en.md => en/api/entity.md} | 9 +- .../docs/{api/http_en.md => en/api/http.md} | 17 +- Box3JS-NeoForge-1.21.1/docs/en/api/math.md | 408 +++ .../{api/player_en.md => en/api/player.md} | 5 +- .../registries_en.md => en/api/registries.md} | 31 +- .../{api/server_en.md => en/api/server.md} | 25 +- .../{api/storage_en.md => en/api/storage.md} | 15 +- .../{api/voxels_en.md => en/api/voxels.md} | 3 + .../docs/{api/world_en.md => en/api/world.md} | 5 +- .../README_en.md => en/guide/README.md} | 19 +- .../guide/about-box3js.md} | 49 +- .../guide/architecture.md} | 129 +- .../docs/{guide/faq_en.md => en/guide/faq.md} | 30 +- .../guide/getting-started.md} | 242 +- .../guide/js-vs-java.md} | 118 +- .../recipes_en.md => en/guide/recipes.md} | 73 +- Box3JS-NeoForge-1.21.1/docs/en/index.md | 78 + .../tutorial/01-basics.md} | 104 +- .../tutorial/02-player-items.md} | 3 + .../tutorial/03-events-entities.md} | 5 +- .../tutorial/04-advanced-systems.md} | 3 + .../tutorial/05-examples.md} | 126 +- .../tutorial/06-client-scripting.md} | 17 +- .../README_en.md => en/tutorial/README.md} | 59 +- Box3JS-NeoForge-1.21.1/docs/guide/README.md | 4 +- .../docs/guide/about-box3js.md | 36 +- .../docs/guide/architecture.md | 63 +- Box3JS-NeoForge-1.21.1/docs/guide/faq.md | 25 +- .../docs/guide/getting-started.md | 107 +- .../docs/guide/js-vs-java.md | 111 +- Box3JS-NeoForge-1.21.1/docs/guide/recipes.md | 66 +- Box3JS-NeoForge-1.21.1/docs/index.md | 78 + .../docs/{README.md => overview.md} | 0 .../docs/tutorial/01-basics.md | 91 +- .../docs/tutorial/03-events-entities.md | 2 +- .../docs/tutorial/05-examples.md | 121 +- .../docs/tutorial/06-client-scripting.md | 12 +- .../docs/tutorial/README.md | 6 +- Box3JS-NeoForge-1.21.1/package-lock.json | 2513 +++++++++++++++++ Box3JS-NeoForge-1.21.1/package.json | 15 + 63 files changed, 5353 insertions(+), 2415 deletions(-) create mode 100644 Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs delete mode 100644 Box3JS-NeoForge-1.21.1/docs/README_en.md delete mode 100644 Box3JS-NeoForge-1.21.1/docs/api/README_en.md delete mode 100644 Box3JS-NeoForge-1.21.1/docs/api/math_en.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/en/README.md create mode 100644 Box3JS-NeoForge-1.21.1/docs/en/api/README.md rename Box3JS-NeoForge-1.21.1/docs/{api/client_en.md => en/api/client.md} (95%) rename Box3JS-NeoForge-1.21.1/docs/{api/commands_en.md => en/api/commands.md} (84%) rename Box3JS-NeoForge-1.21.1/docs/{api/database_en.md => en/api/database.md} (94%) rename Box3JS-NeoForge-1.21.1/docs/{api/entity_en.md => en/api/entity.md} (96%) rename Box3JS-NeoForge-1.21.1/docs/{api/http_en.md => en/api/http.md} (84%) create mode 100644 Box3JS-NeoForge-1.21.1/docs/en/api/math.md rename Box3JS-NeoForge-1.21.1/docs/{api/player_en.md => en/api/player.md} (98%) rename Box3JS-NeoForge-1.21.1/docs/{api/registries_en.md => en/api/registries.md} (92%) rename Box3JS-NeoForge-1.21.1/docs/{api/server_en.md => en/api/server.md} (90%) rename Box3JS-NeoForge-1.21.1/docs/{api/storage_en.md => en/api/storage.md} (90%) rename Box3JS-NeoForge-1.21.1/docs/{api/voxels_en.md => en/api/voxels.md} (99%) rename Box3JS-NeoForge-1.21.1/docs/{api/world_en.md => en/api/world.md} (99%) rename Box3JS-NeoForge-1.21.1/docs/{guide/README_en.md => en/guide/README.md} (86%) rename Box3JS-NeoForge-1.21.1/docs/{guide/about-box3js_en.md => en/guide/about-box3js.md} (67%) rename Box3JS-NeoForge-1.21.1/docs/{guide/architecture_en.md => en/guide/architecture.md} (90%) rename Box3JS-NeoForge-1.21.1/docs/{guide/faq_en.md => en/guide/faq.md} (93%) rename Box3JS-NeoForge-1.21.1/docs/{guide/getting-started_en.md => en/guide/getting-started.md} (82%) rename Box3JS-NeoForge-1.21.1/docs/{guide/js-vs-java_en.md => en/guide/js-vs-java.md} (59%) rename Box3JS-NeoForge-1.21.1/docs/{guide/recipes_en.md => en/guide/recipes.md} (90%) create mode 100644 Box3JS-NeoForge-1.21.1/docs/en/index.md rename Box3JS-NeoForge-1.21.1/docs/{tutorial/01-basics_en.md => en/tutorial/01-basics.md} (79%) rename Box3JS-NeoForge-1.21.1/docs/{tutorial/02-player-items_en.md => en/tutorial/02-player-items.md} (99%) rename Box3JS-NeoForge-1.21.1/docs/{tutorial/03-events-entities_en.md => en/tutorial/03-events-entities.md} (99%) rename Box3JS-NeoForge-1.21.1/docs/{tutorial/04-advanced-systems_en.md => en/tutorial/04-advanced-systems.md} (99%) rename Box3JS-NeoForge-1.21.1/docs/{tutorial/05-examples_en.md => en/tutorial/05-examples.md} (85%) rename Box3JS-NeoForge-1.21.1/docs/{tutorial/06-client-scripting_en.md => en/tutorial/06-client-scripting.md} (95%) rename Box3JS-NeoForge-1.21.1/docs/{tutorial/README_en.md => en/tutorial/README.md} (57%) create mode 100644 Box3JS-NeoForge-1.21.1/docs/index.md rename Box3JS-NeoForge-1.21.1/docs/{README.md => overview.md} (100%) create mode 100644 Box3JS-NeoForge-1.21.1/package-lock.json create mode 100644 Box3JS-NeoForge-1.21.1/package.json diff --git a/Box3JS-NeoForge-1.21.1/.gitignore b/Box3JS-NeoForge-1.21.1/.gitignore index fee2f7b..89203a2 100644 --- a/Box3JS-NeoForge-1.21.1/.gitignore +++ b/Box3JS-NeoForge-1.21.1/.gitignore @@ -32,6 +32,11 @@ bin/ ### Mac OS ### .DS_Store +### VitePress ### +node_modules/ +docs/.vitepress/dist/ +docs/.vitepress/cache/ + ### Minecraft Modding ### run/ !**/src/**/run/ diff --git a/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs b/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs new file mode 100644 index 0000000..849e398 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs @@ -0,0 +1,233 @@ +import { defineConfig } from 'vitepress' + +const cnNav = [ + { text: '首页', link: '/' }, + { text: '指南', link: '/guide/README' }, + { text: '教程', link: '/tutorial/README' }, + { text: 'API', link: '/api/README' }, +] + +const enNav = [ + { text: 'Home', link: '/en/' }, + { text: 'Guide', link: '/en/guide/README' }, + { text: 'Tutorials', link: '/en/tutorial/README' }, + { text: 'API', link: '/en/api/README' }, +] + +const cnSidebar = [ + { + text: '快速开始', + collapsed: false, + items: [ + { text: '文档索引', link: '/overview' }, + { text: '总览', link: '/guide/README' }, + { text: '快速开始', link: '/guide/getting-started' }, + { text: '常用配方', link: '/guide/recipes' }, + { text: '常见问题', link: '/guide/faq' }, + { text: '运行原理', link: '/guide/architecture' }, + { text: 'JS vs Java', link: '/guide/js-vs-java' }, + { text: '关于 Box3JS', link: '/guide/about-box3js' }, + ], + }, + { + text: '教程', + collapsed: false, + items: [ + { text: '总览', link: '/tutorial/README' }, + { text: '01 — 基础入门', link: '/tutorial/01-basics' }, + { text: '02 — 玩家与物品', link: '/tutorial/02-player-items' }, + { text: '03 — 事件与实体', link: '/tutorial/03-events-entities' }, + { text: '04 — 进阶系统', link: '/tutorial/04-advanced-systems' }, + { text: '05 — 实战案例', link: '/tutorial/05-examples' }, + { text: '06 — 客户端脚本', link: '/tutorial/06-client-scripting' }, + ], + }, + { + text: 'API 参考', + collapsed: false, + items: [ + { text: '总览', link: '/api/README' }, + { + text: '服务端 API', + collapsed: false, + items: [ + { text: 'Server', link: '/api/server' }, + { text: 'World', link: '/api/world' }, + { text: 'Entity', link: '/api/entity' }, + { text: 'Player', link: '/api/player' }, + { text: 'Voxels', link: '/api/voxels' }, + { text: 'Registries', link: '/api/registries' }, + ], + }, + { + text: '客户端 API', + collapsed: false, + items: [ + { text: 'Client', link: '/api/client' }, + ], + }, + { + text: '共用 API', + collapsed: false, + items: [ + { text: 'Storage', link: '/api/storage' }, + { text: 'Database', link: '/api/database' }, + { text: 'HTTP', link: '/api/http' }, + { text: 'Math', link: '/api/math' }, + ], + }, + { + text: '命令参考', + collapsed: false, + items: [ + { text: '/box3script', link: '/api/commands' }, + ], + }, + ], + }, +] + +const enSidebar = [ + { + text: 'Get Started', + collapsed: false, + items: [ + { text: 'Overview', link: '/en/guide/README' }, + { text: 'Getting Started', link: '/en/guide/getting-started' }, + { text: 'Recipes', link: '/en/guide/recipes' }, + { text: 'FAQ', link: '/en/guide/faq' }, + { text: 'Architecture', link: '/en/guide/architecture' }, + { text: 'JS vs Java', link: '/en/guide/js-vs-java' }, + { text: 'About Box3JS', link: '/en/guide/about-box3js' }, + ], + }, + { + text: 'Tutorials', + collapsed: false, + items: [ + { text: 'Overview', link: '/en/tutorial/README' }, + { text: '01 — Basics', link: '/en/tutorial/01-basics' }, + { text: '02 — Player & Items', link: '/en/tutorial/02-player-items' }, + { text: '03 — Events & Entities', link: '/en/tutorial/03-events-entities' }, + { text: '04 — Advanced Systems', link: '/en/tutorial/04-advanced-systems' }, + { text: '05 — Examples', link: '/en/tutorial/05-examples' }, + { text: '06 — Client Scripting', link: '/en/tutorial/06-client-scripting' }, + ], + }, + { + text: 'API Reference', + collapsed: false, + items: [ + { text: 'Overview', link: '/en/api/README' }, + { + text: 'Server-side API', + collapsed: false, + items: [ + { text: 'Server', link: '/en/api/server' }, + { text: 'World', link: '/en/api/world' }, + { text: 'Entity', link: '/en/api/entity' }, + { text: 'Player', link: '/en/api/player' }, + { text: 'Voxels', link: '/en/api/voxels' }, + { text: 'Registries', link: '/en/api/registries' }, + ], + }, + { + text: 'Client-side API', + collapsed: false, + items: [ + { text: 'Client', link: '/en/api/client' }, + ], + }, + { + text: 'Shared API', + collapsed: false, + items: [ + { text: 'Storage', link: '/en/api/storage' }, + { text: 'Database', link: '/en/api/database' }, + { text: 'HTTP', link: '/en/api/http' }, + { text: 'Math', link: '/en/api/math' }, + ], + }, + { + text: 'CLI Reference', + collapsed: false, + items: [ + { text: '/box3script', link: '/en/api/commands' }, + ], + }, + ], + }, +] + +export default defineConfig({ + title: 'Box3JS', + description: 'Minecraft NeoForge 1.21.1 的 JavaScript/TypeScript 脚本引擎', + lastUpdated: true, + cleanUrls: true, + ignoreDeadLinks: true, + + head: [ + ['link', { rel: 'icon', href: '/favicon.ico' }], + ], + + locales: { + root: { + label: '简体中文', + lang: 'zh-CN', + title: 'Box3JS', + description: '基于 Mozilla Rhino 引擎,为 Minecraft NeoForge 1.21.1 提供 JS/TS 双端脚本能力 — 神奇代码岛同款编程体验', + themeConfig: { + nav: cnNav, + sidebar: cnSidebar, + editLink: { + pattern: 'https://github.com/box3lab/Box3JS/edit/main/Box3JS-NeoForge-1.21.1/docs/:path', + }, + lastUpdated: { + text: '最后更新', + }, + docFooter: { + prev: '上一页', + next: '下一页', + }, + footer: { + message: '基于 MIT 许可证发布', + }, + }, + }, + en: { + label: 'English', + lang: 'en-US', + title: 'Box3JS', + description: 'JavaScript/TypeScript dual-side scripting engine for Minecraft NeoForge 1.21.1 — same programming experience as Box3', + themeConfig: { + nav: enNav, + sidebar: enSidebar, + editLink: { + pattern: 'https://github.com/box3lab/Box3JS/edit/main/Box3JS-NeoForge-1.21.1/docs/en/:path', + }, + lastUpdated: { + text: 'Last updated', + }, + docFooter: { + prev: 'Previous page', + next: 'Next page', + }, + footer: { + message: 'Released under the MIT License.', + }, + }, + }, + }, + + themeConfig: { + logo: false, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/box3lab/Box3JS' }, + ], + + search: { + provider: 'local', + }, + }, +}) diff --git a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md index 73b834d..d7f6338 100644 --- a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md +++ b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md @@ -4,23 +4,6 @@ > **图例**: ✅ 已实现 | ⚠️ 部分实现 | ❌ 未实现 | ⬆ 独有扩展 ---- - -## 目录 - -1. [GameWorld (world)](#1-gameworld-world) -2. [GameEntity (entity)](#2-gameentity-entity) -3. [GamePlayerEntity (entity.player)](#3-gameplayerentity-entityplayer) -4. [GameVoxels (voxels)](#4-gamevoxels-voxels) -5. [GameDataStorage (storage)](#5-gamedatastorage-storage) -6. [Math 类型](#6-math-类型) -7. [其他服务端 API](#7-其他服务端-api) -8. [客户端 API](#8-客户端-api) -9. [Box3JS 独有 MC 扩展](#9-box3js-独有-mc-扩展) -10. [总结](#10-总结) - ---- - ## 1. GameWorld (world) ### 1.1 基础属性 @@ -34,32 +17,32 @@ ### 1.2 天气 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -| ------------------------------------------ | ----------------------------- | ---- | ------------------------------------------ | -| `world.rainDensity` (属性 0-1) | `world.rainDensity` (属性) | ✅ | 一致 | -| — | `world.thunderDensity` (属性) | ⬆ | MC 扩展。Box3 无打雷概念 | -| — | `world.clearWeather()` | ⬆ | MC 扩展。清除雨和雷 | -| `world.maxFog` | `client.setFogEndDistance(d)` | ✅ | 客户端 API。设置雾完全遮挡距离(方块) | -| `world.fogColor` | `client.setFogColor(r, g, b)` | ✅ | 客户端 API。RGB 0-255 | -| `world.fogStartDistance` | `client.setFogStartDistance(d)` | ✅ | 客户端 API。雾开始距离(方块) | -| `world.fogHeightOffset` | — | ❌ | | -| `world.fogUniformDensity` | — | ❌ | | -| `world.fogHeightFalloff` | — | ❌ | | -| `world.rainSpeed` | — | ❌ | | -| `world.rainColor` | — | ❌ | | -| `world.rainDirection` | — | ❌ | | -| `world.rainInterference` | — | ❌ | | -| `world.rainSizeLo/Hi` | — | ❌ | | -| `world.snowColor` | — | ❌ | Box3 有独立雪花系统,MC 降雪依附于原版天气 | -| `world.snowTexture` | — | ❌ | | -| `world.snowDensity` | — | ❌ | | -| `world.snowFallSpeed` | — | ❌ | | -| `world.snowSpinSpeed` | — | ❌ | | -| `world.snowSizeLo/Hi` | — | ❌ | | -| `world.lightMode` | — | ❌ | Box3 的手动/自然光照模式 | -| `world.sunFrequency/Phase/Direction/Light` | — | ❌ | | -| `world.skyLeftLight/RightLight/...` | — | ❌ | Box3 六个方向环境光 | -| `world.lunarPhase` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------ | ------------------------------- | ---- | ------------------------------------------ | +| `world.rainDensity` (属性 0-1) | `world.rainDensity` (属性) | ✅ | 一致 | +| — | `world.thunderDensity` (属性) | ⬆ | MC 扩展。Box3 无打雷概念 | +| — | `world.clearWeather()` | ⬆ | MC 扩展。清除雨和雷 | +| `world.maxFog` | `client.setFogEndDistance(d)` | ✅ | 客户端 API。设置雾完全遮挡距离(方块) | +| `world.fogColor` | `client.setFogColor(r, g, b)` | ✅ | 客户端 API。RGB 0-255 | +| `world.fogStartDistance` | `client.setFogStartDistance(d)` | ✅ | 客户端 API。雾开始距离(方块) | +| `world.fogHeightOffset` | — | ❌ | | +| `world.fogUniformDensity` | — | ❌ | | +| `world.fogHeightFalloff` | — | ❌ | | +| `world.rainSpeed` | — | ❌ | | +| `world.rainColor` | — | ❌ | | +| `world.rainDirection` | — | ❌ | | +| `world.rainInterference` | — | ❌ | | +| `world.rainSizeLo/Hi` | — | ❌ | | +| `world.snowColor` | — | ❌ | Box3 有独立雪花系统,MC 降雪依附于原版天气 | +| `world.snowTexture` | — | ❌ | | +| `world.snowDensity` | — | ❌ | | +| `world.snowFallSpeed` | — | ❌ | | +| `world.snowSpinSpeed` | — | ❌ | | +| `world.snowSizeLo/Hi` | — | ❌ | | +| `world.lightMode` | — | ❌ | Box3 的手动/自然光照模式 | +| `world.sunFrequency/Phase/Direction/Light` | — | ❌ | | +| `world.skyLeftLight/RightLight/...` | — | ❌ | Box3 六个方向环境光 | +| `world.lunarPhase` | — | ❌ | | **原因**: Box3 拥有独立的天气/光照渲染引擎,可以精细控制雾、雨、雪、光照参数。MC 的天气和光照系统由原版引擎控制,这些视觉效果需要客户端侧渲染 hook。Box3JS 客户端脚本引擎**已暴露雾颜色和距离 API**(`client.setFogColor` / `client.setFogStartDistance` / `client.setFogEndDistance` / `client.resetFog`)。雪花/光照等高级渲染参数 API 未来可继续扩展。 @@ -360,8 +343,6 @@ Box3 平台无独立数据库 API。Box3JS 通过 `db` 全局对象提供 SQLite 每个脚本项目拥有独立的数据库文件 `config/box3/data/.db`。 ---- - ## 2. GameEntity (entity) ### 2.1 身份标识 @@ -540,8 +521,6 @@ Box3 的实体级事件非常丰富: | `entity.setAttribute(attributeId, value)` | 设置属性值 | | `entity.lookAt(x, y, z)` / `entity.lookAt(pos)` | 使实体看向某位置 | ---- - ## 3. GamePlayerEntity (entity.player) ### 3.1 基础信息 @@ -679,53 +658,53 @@ Box3 的实体级事件非常丰富: Box3 的 `player.onPress/onRelease/onKeyDown/onKeyUp` 是客户端事件。Box3JS 通过客户端脚本引擎提供独立的 `input` 全局对象: -| Box3 API | Box3JS 客户端实现 | 状态 | 差异说明 | -| --------------------------- | -------------------------------- | ---- | ------------------------------------------ | -| `player.onPress(handler)` | `world.onButtonPressed(handler)` | ⚠️ | 服务端可检测按钮点击;客户端用 `input` API | -| `player.onRelease(handler)` | — | ❌ | | +| Box3 API | Box3JS 客户端实现 | 状态 | 差异说明 | +| --------------------------- | --------------------------------- | ---- | ------------------------------------------ | +| `player.onPress(handler)` | `world.onButtonPressed(handler)` | ⚠️ | 服务端可检测按钮点击;客户端用 `input` API | +| `player.onRelease(handler)` | — | ❌ | | | `player.onKeyDown(handler)` | `input.onKeyPress(key, callback)` | ⚠️ | 客户端 `input` API,按键名如 `"f5"`、`"c"` | -| `player.onKeyUp(handler)` | — | ❌ | | +| `player.onKeyUp(handler)` | — | ❌ | | **客户端 `input` 全局对象补充能力**: -| Box3JS 客户端 API | 说明 | -| ------------------------------ | ----------------------------------- | -| `input.onKeyPress(key, fn)` | 注册按键按下回调(如 `"f5"`) | -| `input.isKeyDown(key)` | 检查按键是否正在按住 | -| `input.onMouseClick(fn)` | 鼠标点击回调 `(button, action, x, y)` | -| `input.getMouseX()` / `getMouseY()` | 获取鼠标屏幕坐标 | +| Box3JS 客户端 API | 说明 | +| ----------------------------------- | ------------------------------------- | +| `input.onKeyPress(key, fn)` | 注册按键按下回调(如 `"f5"`) | +| `input.isKeyDown(key)` | 检查按键是否正在按住 | +| `input.onMouseClick(fn)` | 鼠标点击回调 `(button, action, x, y)` | +| `input.getMouseX()` / `getMouseY()` | 获取鼠标屏幕坐标 | ### 3.10 音效 Box3 玩家有 14 种音效属性(`music`/`jumpSound`/`landSound` 等),这些是客户端音效控制。Box3JS 通过客户端脚本引擎提供 `audio` 全局对象: -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -| ------------------------ | --------------------------------------- | ---- | --------------------------------------------------------------------------------- | -| `player.music` | `audio.playMusic(path, volume)` | ⚠️ | 客户端 `audio` API,通过背景音乐类别播放 | -| `player.action0Sound` | — | ❌ | | -| `player.action1Sound` | — | ❌ | | -| `player.crouchSound` | — | ❌ | | -| `player.jumpSound` | — | ❌ | | -| `player.doubleJumpSound` | — | ❌ | | -| `player.landSound` | — | ❌ | | -| `player.enterWaterSound` | — | ❌ | | -| `player.leaveWaterSound` | — | ❌ | | -| `player.swimSound` | — | ❌ | | -| `player.spawnSound` | — | ❌ | | -| `player.stepSound` | — | ❌ | | -| `player.startFlySound` | — | ❌ | | -| `player.stopFlySound` | — | ❌ | | -| `player.sound(config)` | `player.playSound(path, volume, pitch)` | ⚠️ | Box3 接受完整 Sound 对象或路径;Box3JS 展开参数 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------ | --------------------------------------- | ---- | ----------------------------------------------- | +| `player.music` | `audio.playMusic(path, volume)` | ⚠️ | 客户端 `audio` API,通过背景音乐类别播放 | +| `player.action0Sound` | — | ❌ | | +| `player.action1Sound` | — | ❌ | | +| `player.crouchSound` | — | ❌ | | +| `player.jumpSound` | — | ❌ | | +| `player.doubleJumpSound` | — | ❌ | | +| `player.landSound` | — | ❌ | | +| `player.enterWaterSound` | — | ❌ | | +| `player.leaveWaterSound` | — | ❌ | | +| `player.swimSound` | — | ❌ | | +| `player.spawnSound` | — | ❌ | | +| `player.stepSound` | — | ❌ | | +| `player.startFlySound` | — | ❌ | | +| `player.stopFlySound` | — | ❌ | | +| `player.sound(config)` | `player.playSound(path, volume, pitch)` | ⚠️ | Box3 接受完整 Sound 对象或路径;Box3JS 展开参数 | **客户端 `audio` 全局对象补充能力**: -| Box3JS 客户端 API | 说明 | -| -------------------------------------- | ------------------------------------------- | -| `audio.playSound(path, volume, pitch)` | 播放音效(支持原版和自定义音效) | -| `audio.playMusic(path, volume)` | 播放背景音乐(MUSIC 类别) | -| `audio.stopAll()` | 停止所有正在播放的音效和音乐 | -| `audio.getVolume(category)` | 获取指定音频类别的音量 | -| `audio.setVolume(category, value)` | 设置指定音频类别的音量(0.0–1.0) | +| Box3JS 客户端 API | 说明 | +| -------------------------------------- | --------------------------------- | +| `audio.playSound(path, volume, pitch)` | 播放音效(支持原版和自定义音效) | +| `audio.playMusic(path, volume)` | 播放背景音乐(MUSIC 类别) | +| `audio.stopAll()` | 停止所有正在播放的音效和音乐 | +| `audio.getVolume(category)` | 获取指定音频类别的音量 | +| `audio.setVolume(category, value)` | 设置指定音频类别的音量(0.0–1.0) | > **注意**: Box3 的逐事件音效属性(如 `player.jumpSound`)需要 hook 每个游戏事件,Box3JS 目前提供的是主动播放 API。如需自动触发,可在 `client.onTick()` 中检测玩家状态变化后调用 `audio.playSound()`。 @@ -774,8 +753,6 @@ Box3 玩家有 14 种音效属性(`music`/`jumpSound`/`landSound` 等),这 | `player.lookAt(x, y, z)` / `player.lookAt(pos)` | 使玩家看向某位置 | | `player.setPlayerListName(name)` | 设置 TAB 列表显示名称 | ---- - ## 4. GameVoxels (voxels) Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 @@ -827,8 +804,6 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 | `voxels.countVoxel(x1, y1, z1, x2, y2, z2, voxel)` / `voxels.countVoxel(pos1, pos2, voxel)` | 统计区域内匹配方块数量 | | `voxels.setSpawner(x, y, z, entityType)` / `voxels.setSpawner(pos, entityType)` | 设置刷怪笼类型 | ---- - ## 5. GameDataStorage (storage) ### 5.1 存储空间管理 @@ -861,8 +836,6 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 4. **速率限制**: Box3 有严格的读写速率限制,Box3JS 无限制。 5. **错误处理**: Box3 有详细的错误码(400/429/500),Box3JS 静默失败。 ---- - ## 6. Math 类型 ### 6.1 GameVector3 @@ -984,8 +957,6 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 **GameQuaternion 是完全实现的**。 ---- - ## 7. 其他服务端 API ### 7.1 GameAnimation @@ -1029,13 +1000,13 @@ Box3JS 通过自定义网络数据包实现了完整的服务端↔客户端双 **服务端** (Box3JSWorld / Box3ScriptEngine): -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -| ----------------------------------------------------- | ----------------------------------------------------- | ---- | ----------------------------------------------------------- | -| `remoteChannel.sendClientEvent(entities, event)` | `remoteChannel.sendClientEvent(entities, event)` | ✅ | 一致。entities 为单个 GamePlayerEntity 或数组 | -| `remoteChannel.broadcastClientEvent(event)` | `remoteChannel.broadcastClientEvent(event)` | ✅ | 一致。向所有在线玩家广播 | -| `remoteChannel.onServerEvent(handler)` | `remoteChannel.onServerEvent(handler)` | ✅ | 一致。回调接收 `{ tick, entity, args }`,返回 GameEventHandlerToken | -| `remoteChannel.sendServerEvent(event)` (客户端) | `remoteChannel.sendServerEvent(event)` | ✅ | 一致。客户端发送事件到服务端 | -| `remoteChannel.onClientEvent(handler)` (客户端) | `remoteChannel.onClientEvent(handler)` | ✅ | 一致。回调接收 `{ tick, args }`,返回 GameEventHandlerToken | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------------ | ------------------------------------------------ | ---- | ------------------------------------------------------------------- | +| `remoteChannel.sendClientEvent(entities, event)` | `remoteChannel.sendClientEvent(entities, event)` | ✅ | 一致。entities 为单个 GamePlayerEntity 或数组 | +| `remoteChannel.broadcastClientEvent(event)` | `remoteChannel.broadcastClientEvent(event)` | ✅ | 一致。向所有在线玩家广播 | +| `remoteChannel.onServerEvent(handler)` | `remoteChannel.onServerEvent(handler)` | ✅ | 一致。回调接收 `{ tick, entity, args }`,返回 GameEventHandlerToken | +| `remoteChannel.sendServerEvent(event)` (客户端) | `remoteChannel.sendServerEvent(event)` | ✅ | 一致。客户端发送事件到服务端 | +| `remoteChannel.onClientEvent(handler)` (客户端) | `remoteChannel.onClientEvent(handler)` | ✅ | 一致。回调接收 `{ tick, args }`,返回 GameEventHandlerToken | **客户端事件数据自动 JSON 序列化/反序列化传输**,支持任意可序列化的 JS 值。 @@ -1096,35 +1067,33 @@ Box3 的 `resources.ls(type?)` 浏览资源文件。MC 无对应资源管理 API Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` / `.active()`。Box3JS 的所有 `world.onXxx()` 方法均返回 `GameEventHandlerToken`,支持 `.cancel()` 取消注册和 `.active()` 检查状态。`.resume()` 抛出 UnsupportedOperationException(需重新注册)。 ---- - ## 8. 客户端 API Box3JS 现已支持**客户端脚本引擎**(`src/client/app.ts`),在玩家客户端上运行,提供与 Box3 客户端 API 对应的能力。客户端脚本通过 `remoteChannel` 与服务端双向通信。 ### 8.1 客户端生命周期 (client) -| Box3JS 客户端 API | 说明 | -| ---------------------- | ---------------------------------------------------------------------------------------- | -| `client.onTick(fn)` | 注册每帧回调(每秒 20 次),返回 GameEventHandlerToken | -| `client.getFPS()` | 获取当前帧率 | -| `client.getPlayer()` | 获取本地玩家信息 `{ name, uuid, health, maxHealth, food, saturation, xp, dimension, position }` | -| `client.getLookingAt()` | 获取准星目标,返回 `{ type, position, entity?, blockPos?, direction? }` 或 null | -| `client.getServerInfo()` | 获取服务器信息 `{ ip, name, isLocal, playerCount, maxPlayers }` | +| Box3JS 客户端 API | 说明 | +| ------------------------ | ----------------------------------------------------------------------------------------------- | +| `client.onTick(fn)` | 注册每帧回调(每秒 20 次),返回 GameEventHandlerToken | +| `client.getFPS()` | 获取当前帧率 | +| `client.getPlayer()` | 获取本地玩家信息 `{ name, uuid, health, maxHealth, food, saturation, xp, dimension, position }` | +| `client.getLookingAt()` | 获取准星目标,返回 `{ type, position, entity?, blockPos?, direction? }` 或 null | +| `client.getServerInfo()` | 获取服务器信息 `{ ip, name, isLocal, playerCount, maxPlayers }` | ### 8.2 客户端 UI (ui) 与 Box3 的 ClientUI 对应,Box3JS 提供 HUD 绘制和屏幕消息能力: -| Box3JS 客户端 API | 说明 | -| ------------------------------------------------ | -------------------------------------------------------- | -| `ui.showOverlay(text)` | 显示屏幕中央覆盖文字(2 秒自动消失) | -| `ui.showTitle(title, subtitle, fadeIn, stay, fadeOut)` | 显示标题/副标题 | -| `ui.showActionBar(text)` | 显示 ActionBar 消息 | -| `ui.getScreenSize()` | 获取屏幕尺寸 `{ scaledWidth, scaledHeight, guiScale }` | -| `ui.drawText(id, x, y, text)` | 在屏幕指定位置绘制文字(每帧调用维持显示) | -| `ui.removeDrawText(id)` | 移除指定绘制文字 | -| `ui.clearDrawTexts()` | 移除所有绘制文字 | +| Box3JS 客户端 API | 说明 | +| ------------------------------------------------------ | ------------------------------------------------------ | +| `ui.showOverlay(text)` | 显示屏幕中央覆盖文字(2 秒自动消失) | +| `ui.showTitle(title, subtitle, fadeIn, stay, fadeOut)` | 显示标题/副标题 | +| `ui.showActionBar(text)` | 显示 ActionBar 消息 | +| `ui.getScreenSize()` | 获取屏幕尺寸 `{ scaledWidth, scaledHeight, guiScale }` | +| `ui.drawText(id, x, y, text)` | 在屏幕指定位置绘制文字(每帧调用维持显示) | +| `ui.removeDrawText(id)` | 移除指定绘制文字 | +| `ui.clearDrawTexts()` | 移除所有绘制文字 | **与 Box3 ClientUI 差异**: Box3 有完整的 2D UI 框架(盒子、文本、图片、输入框、滚动框等),Box3JS 目前提供轻量级的屏幕文字叠加层,不支持复杂的交互式 UI 控件。复杂 UI 建议通过 `gui.openGUI()` 使用容器 GUI。 @@ -1132,13 +1101,13 @@ Box3JS 现已支持**客户端脚本引擎**(`src/client/app.ts`),在玩 与 Box3 的 `player.onKeyDown/onKeyUp` 对应: -| Box3JS 客户端 API | 说明 | -| ------------------------------ | --------------------------------------------------------- | -| `input.onKeyPress(key, fn)` | 注册按键按下回调,key 如 `"f5"`、`"c"`、`"space"` | -| `input.isKeyDown(key)` | 检查按键是否正在按住 | -| `input.onMouseClick(fn)` | 鼠标点击回调 `(button: 0=LMB/1=RMB/2=MMB, action, x, y)` | -| `input.getMouseX()` | 获取鼠标屏幕 X 坐标 | -| `input.getMouseY()` | 获取鼠标屏幕 Y 坐标 | +| Box3JS 客户端 API | 说明 | +| --------------------------- | -------------------------------------------------------- | +| `input.onKeyPress(key, fn)` | 注册按键按下回调,key 如 `"f5"`、`"c"`、`"space"` | +| `input.isKeyDown(key)` | 检查按键是否正在按住 | +| `input.onMouseClick(fn)` | 鼠标点击回调 `(button: 0=LMB/1=RMB/2=MMB, action, x, y)` | +| `input.getMouseX()` | 获取鼠标屏幕 X 坐标 | +| `input.getMouseY()` | 获取鼠标屏幕 Y 坐标 | **与 Box3 差异**: Box3 的 `player.onPress` 检测游戏内交互按钮(ACTION0/ACTION1/JUMP/WALK),Box3JS 的 `input.onKeyPress` 检测键盘按键。Box3 无鼠标事件,`input.onMouseClick` 是 Box3JS 独有扩展。 @@ -1146,24 +1115,24 @@ Box3JS 现已支持**客户端脚本引擎**(`src/client/app.ts`),在玩 与 Box3 的客户端聊天系统对应: -| Box3JS 客户端 API | 说明 | -| ----------------------------------------- | --------------------------------------------------- | -| `chat.sendMessage(text)` | 发送聊天消息 | -| `chat.sendCommand(cmd)` | 执行客户端命令(以 `/` 开头) | -| `chat.onMessage(fn)` | 监听聊天消息 `(message, sender, isSystem)`,返回 false 可拦截 | +| Box3JS 客户端 API | 说明 | +| ------------------------ | ------------------------------------------------------------- | +| `chat.sendMessage(text)` | 发送聊天消息 | +| `chat.sendCommand(cmd)` | 执行客户端命令(以 `/` 开头) | +| `chat.onMessage(fn)` | 监听聊天消息 `(message, sender, isSystem)`,返回 false 可拦截 | ### 8.5 客户端 GUI (gui) Box3JS 的容器 GUI 系统,与 Box3 的 UI 面板理念不同(Box3 是 2D UI 控件,Box3JS 是 MC 原版容器): -| Box3JS 客户端 API | 说明 | -| ---------------------------------- | ---------------------------------------------------------------- | -| `gui.openGUI(config)` | 打开自定义容器 GUI,config: `{ title, rows(1-6), slots? }` | -| `controller.setItem(slot, id, n)` | 设置槽位物品 | -| `controller.getItem(slot)` | 获取槽位物品 `{ id, count }` | -| `controller.onSlotClick(fn)` | 槽位点击回调 `(slot)` | -| `controller.onClose(fn)` | GUI 关闭回调 | -| `controller.close()` | 主动关闭 GUI | +| Box3JS 客户端 API | 说明 | +| --------------------------------- | ---------------------------------------------------------- | +| `gui.openGUI(config)` | 打开自定义容器 GUI,config: `{ title, rows(1-6), slots? }` | +| `controller.setItem(slot, id, n)` | 设置槽位物品 | +| `controller.getItem(slot)` | 获取槽位物品 `{ id, count }` | +| `controller.onSlotClick(fn)` | 槽位点击回调 `(slot)` | +| `controller.onClose(fn)` | GUI 关闭回调 | +| `controller.close()` | 主动关闭 GUI | **与 Box3 差异**: Box3 的 UI 系统是基于 2D 画布的布局控件(盒子/文本/图片/输入框),Box3JS 使用 MC 原版容器 GUI(类似箱子界面),更适合物品交互场景。Box3 的 2D UI 控件体系暂无对应实现。 @@ -1171,13 +1140,13 @@ Box3JS 的容器 GUI 系统,与 Box3 的 UI 面板理念不同(Box3 是 2D U 与 Box3 的 ClientAudio 对应: -| Box3JS 客户端 API | 说明 | -| -------------------------------------- | ------------------------------------------- | -| `audio.playSound(path, volume, pitch)` | 播放音效(支持原版和自定义音效) | -| `audio.playMusic(path, volume)` | 播放背景音乐(MUSIC 类别) | -| `audio.stopAll()` | 停止所有正在播放的音效和音乐 | -| `audio.getVolume(category)` | 获取指定音频类别的音量 | -| `audio.setVolume(category, value)` | 设置指定音频类别的音量(0.0–1.0) | +| Box3JS 客户端 API | 说明 | +| -------------------------------------- | --------------------------------- | +| `audio.playSound(path, volume, pitch)` | 播放音效(支持原版和自定义音效) | +| `audio.playMusic(path, volume)` | 播放背景音乐(MUSIC 类别) | +| `audio.stopAll()` | 停止所有正在播放的音效和音乐 | +| `audio.getVolume(category)` | 获取指定音频类别的音量 | +| `audio.setVolume(category, value)` | 设置指定音频类别的音量(0.0–1.0) | **与 Box3 差异**: Box3 的 `Sound` 对象支持 `pause/resume/stop/setCurrentTime` 精细控制。Box3JS 的 `playSound/playMusic` 是 fire-and-forget 模式,无法暂停/恢复播放中的声音。`stopAll()` 可批量停止。 @@ -1185,16 +1154,16 @@ Box3JS 的容器 GUI 系统,与 Box3 的 UI 面板理念不同(Box3 是 2D U 客户端独立的本地持久化存储,与服务端 storage 完全隔离: -| Box3JS 客户端 API | 说明 | -| ---------------------------- | ---------------------------------------------------------- | +| Box3JS 客户端 API | 说明 | +| -------------------------------- | -------------------------------------------------------------- | | `storage.getDataStorage(key)` | 获取具类型的数据存储空间(客户端本地),返回 `GameDataStorage` | -| `ds.set(key, value)` | 同步写入键值 | -| `ds.get(key)` | 同步读取,返回 value 或 null | -| `ds.update(key, fn)` | 原子更新 | -| `ds.increment(key, delta?)` | 原子增减(数值类型) | -| `ds.list(options)` | 分页列出 `{ pageSize?, ascending? }` | -| `ds.remove(key)` | 删除键 | -| `ds.keys()` | 返回所有键数组 | +| `ds.set(key, value)` | 同步写入键值 | +| `ds.get(key)` | 同步读取,返回 value 或 null | +| `ds.update(key, fn)` | 原子更新 | +| `ds.increment(key, delta?)` | 原子增减(数值类型) | +| `ds.list(options)` | 分页列出 `{ pageSize?, ascending? }` | +| `ds.remove(key)` | 删除键 | +| `ds.keys()` | 返回所有键数组 | 客户端存储位置: `config/box3/storage/-client/`,与服务端存储文件分离。 @@ -1202,11 +1171,11 @@ Box3JS 的容器 GUI 系统,与 Box3 的 UI 面板理念不同(Box3 是 2D U 客户端本地 SQLite 数据库(需安装 `minecraft-sqlite-jdbc` 模组): -| Box3JS 客户端 API | 说明 | -| ------------------------------- | --------------------------------------------------------- | -| `db.isAvailable()` | 检查 SQLite 驱动是否可用 | -| `db.sql(sql, ...params)` | 执行 SQL 查询/更新,返回 `GameQueryResult` | -| `db.sql\`SELECT ... WHERE id = ${id}\`` | Tagged template 语法,自动参数化防注入 | +| Box3JS 客户端 API | 说明 | +| --------------------------------------- | ------------------------------------------ | +| `db.isAvailable()` | 检查 SQLite 驱动是否可用 | +| `db.sql(sql, ...params)` | 执行 SQL 查询/更新,返回 `GameQueryResult` | +| `db.sql\`SELECT ... WHERE id = ${id}\`` | Tagged template 语法,自动参数化防注入 | **GameQueryResult**: `rows` (数组), `firstRow`, `columnNames`, `columnCount`, `rowCount`, `affectedRows`, `isQuery` @@ -1216,10 +1185,10 @@ Box3JS 的容器 GUI 系统,与 Box3 的 UI 面板理念不同(Box3 是 2D U 客户端 HTTP 请求,支持同步和异步模式: -| Box3JS 客户端 API | 说明 | -| --------------------------------------- | ------------------------------------------------------------ | -| `http.fetch(url, options?)` | 发起 HTTP 请求,options 同服务端,额外支持 `async: true` + `onResponse/onError` 回调 | -| `Response` 对象 | `ok`, `status`, `headers`, `data`, `json()`, `text()`, `arrayBuffer()` | +| Box3JS 客户端 API | 说明 | +| --------------------------- | ------------------------------------------------------------------------------------ | +| `http.fetch(url, options?)` | 发起 HTTP 请求,options 同服务端,额外支持 `async: true` + `onResponse/onError` 回调 | +| `Response` 对象 | `ok`, `status`, `headers`, `data`, `json()`, `text()`, `arrayBuffer()` | > **客户端特有**: 支持 `async: true` 异步模式,通过 `onResponse`/`onError` 回调接收结果,不阻塞渲染帧。 @@ -1229,29 +1198,27 @@ Box3JS 的容器 GUI 系统,与 Box3 的 UI 面板理念不同(Box3 是 2D U ### 8.11 客户端控制台 (console) -| Box3JS API | 说明 | -| -------------------------- | ---------------------------- | -| `console.log(...args)` | 输出到客户端日志 | -| `console.warn(...args)` | 输出警告 | -| `console.error(...args)` | 输出错误 | -| `console.debug(...args)` | 输出调试信息 | -| `console.clear()` | 清空控制台 | -| `console.assert(cond, ...)` | 条件断言输出 | +| Box3JS API | 说明 | +| --------------------------- | ---------------- | +| `console.log(...args)` | 输出到客户端日志 | +| `console.warn(...args)` | 输出警告 | +| `console.error(...args)` | 输出错误 | +| `console.debug(...args)` | 输出调试信息 | +| `console.clear()` | 清空控制台 | +| `console.assert(cond, ...)` | 条件断言输出 | ### 8.12 与 Box3 客户端 API 差距 以下 Box3 客户端能力 Box3JS **暂未实现**: -| Box3 客户端功能 | 状态 | 说明 | -| ------------------------- | ---- | --------------------------------------------------------------------------------------------- | -| 2D UI 控件体系 | ❌ | Box3 的盒子/文本/图片/输入框/滚动框等完整 UI 框架。Box3JS 仅有 drawText 叠加层 + 容器 GUI | -| `media` 录音/播放 | ❌ | MC 无内置录音 API | -| `navigator` 设备信息 | ❌ | MC 无对应 API | -| 3D 渲染开关 | ❌ | MC 渲染始终开启 | -| 客户端世界 API | ❌ | Box3 客户端可控制 3D 渲染开关等,Box3JS 无对应 | -| 自定义模型渲染 | ❌ | Box3 的 `entity.mesh` / `meshColor` / `meshScale` 等 | - ---- +| Box3 客户端功能 | 状态 | 说明 | +| -------------------- | ---- | ----------------------------------------------------------------------------------------- | +| 2D UI 控件体系 | ❌ | Box3 的盒子/文本/图片/输入框/滚动框等完整 UI 框架。Box3JS 仅有 drawText 叠加层 + 容器 GUI | +| `media` 录音/播放 | ❌ | MC 无内置录音 API | +| `navigator` 设备信息 | ❌ | MC 无对应 API | +| 3D 渲染开关 | ❌ | MC 渲染始终开启 | +| 客户端世界 API | ❌ | Box3 客户端可控制 3D 渲染开关等,Box3JS 无对应 | +| 自定义模型渲染 | ❌ | Box3 的 `entity.mesh` / `meshColor` / `meshScale` 等 | ## 9. Box3JS 独有 MC 扩展 @@ -1368,8 +1335,6 @@ Box3JS 客户端引擎提供的 Box3 平台**不存在**的能力: - Client-side `db` — 客户端本地 SQLite 数据库 - Client-side `http.fetch(async: true)` — 非阻塞异步 HTTP 请求 ---- - ## 10. 总结 ### 10.1 实现统计 @@ -1415,6 +1380,7 @@ Box3JS 客户端引擎提供的 Box3 平台**不存在**的能力: Box3JS 虽然缺失部分 Box3 视觉/渲染/2D UI API,但提供了 Box3 平台**完全不具备**的能力: **服务端独有能力**: + - **完整的原版方块系统** — 数百种方块类型、红石、容器、刷怪笼 - **生物 AI/寻路/战斗** — 全套 MC 生物行为控制 - **原版物品/装备/附魔** — 完整的物品系统 @@ -1428,6 +1394,7 @@ Box3JS 虽然缺失部分 Box3 视觉/渲染/2D UI API,但提供了 Box3 平 - **结构放置** — 数据包结构模板 **客户端独有能力**: + - **HUD 精确绘制** — `ui.drawText` 屏幕坐标文字,FPS 监控 - **键盘快捷键** — `input.onKeyPress` 注册自定义热键 - **鼠标事件** — `input.onMouseClick` 检测点击类型和坐标 diff --git a/Box3JS-NeoForge-1.21.1/docs/README_en.md b/Box3JS-NeoForge-1.21.1/docs/README_en.md deleted file mode 100644 index b16526e..0000000 --- a/Box3JS-NeoForge-1.21.1/docs/README_en.md +++ /dev/null @@ -1,70 +0,0 @@ -# Box3JS Documentation - -Box3JS is a JavaScript/TypeScript scripting engine mod for Minecraft NeoForge 1.21.1. Write server-side gameplay scripts and client-side UI scripts without installing a JDK or compiling Java code. - -## Navigation - -### Getting Started - -Learn what Box3JS is, how to use it, and the principles behind it — from zero. - -| Doc | Content | -|------|------| -| [Quick Start](guide/getting-started_en.md) | Setup → first script → dev cycle → debugging → deployment | -| [Common Recipes](guide/recipes_en.md) | Feature templates: economy, teleport, shop, daily rewards, leaderboards, webhooks | -| [Architecture](guide/architecture_en.md) | Rhino engine, scope management, build pipeline, network communication | -| [JS vs Java](guide/js-vs-java_en.md) | Box3JS scripting vs native Java modding — pros, cons & when to choose | -| [FAQ](guide/faq_en.md) | Loading, build, runtime, database, HTTP, client, deployment | - -### Tutorials - -5 progressive tutorials, each 10–15 minutes with complete runnable code. - -| # | Tutorial | You'll learn | -|---|---------|-------------| -| 1 | [From Zero](tutorial/01-basics_en.md) | Create project → build → first script → chat commands → timers | -| 2 | [Players & Items](tutorial/02-player-items_en.md) | Teleport, flight, give items, enchantments, potion effects, game modes | -| 3 | [Events & Entities](tutorial/03-events-entities_en.md) | All event callbacks, spawn entities, AI control, patrol guards, collision | -| 4 | [Advanced Systems](tutorial/04-advanced-systems_en.md) | Scoreboards, BossBars, teams, world border, cross-script messaging | -| 5 | [Real Mini-Games](tutorial/05-examples_en.md) | Full PvP arena, particle effects, fireworks, wave spawning | -| 6 | [Client-Side Scripting](tutorial/06-client-scripting_en.md) | Keyboard input, screen UI, sound/music, local storage, SQLite, HTTP, remoteChannel | -| 📋 | [Tutorial Overview](tutorial/README_en.md) | Learning roadmap, prerequisites, pro tips | - -### API Reference - -Complete API docs organized by functional category. One document per global object/namespace. - -| Category | Doc | Globals | -|----------|-----|---------| -| **Server Overview** | [server](api/server_en.md) | Server runtime boundary, events, players/entities, blocks, data, cross-side communication | -| **World** | [world](api/world_en.md) | `world` — events, particles, fireworks, sound, scoreboards | -| **Entity** | [entity](api/entity_en.md) | `entity` — properties, AI, equipment, effects | -| **Player** | [player](api/player_en.md) | `player` — inventory, messages, flight, teleport | -| **Voxels** | [voxels](api/voxels_en.md) | `voxels` — block read/write, region fill | -| **Storage** | [storage](api/storage_en.md) | `storage` — JSON persistence | -| **Database** | [database](api/database_en.md) | `db` — SQLite database | -| **HTTP** | [http](api/http_en.md) | `http` — HTTP requests | -| **Client** | [client](api/client_en.md) | `audio` `client` `input` `ui` `chat` `gui` `remoteChannel` | -| **Registries** | [registries](api/registries_en.md) | `registries` — custom blocks/items/sounds | -| **Math** | [math](api/math_en.md) | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | -| **Commands** | [commands](api/commands_en.md) | `/box3script` CLI commands | -| **Task Lookup** | [API by Task](api/README_en.md) | Find APIs by "I want to..." | -| **Comparison** | [Box3 API Comparison](BOX3_API_COMPARISON.md) | Box3 platform API vs Box3JS implementation | - -### Version Info - -| Item | Version | -|------|---------| -| Minecraft | 1.21.1 | -| Mod Loader | NeoForge | -| Java | 21 | -| JS Engine | Mozilla Rhino 1.9.1 (ES5 compatible) | -| TypeScript | Compiled to ES5 via Babel | - -## Quick Links - -- **5-minute quickstart**: [Quick Start →](guide/getting-started_en.md) -- **I want to do X, which API?**: [API by Task →](api/README_en.md) -- **Why Box3JS over Java modding?**: [JS vs Java →](guide/js-vs-java_en.md) -- **How does Box3JS work internally?**: [Architecture →](guide/architecture_en.md) -- **Learn Box3JS scripting from zero**: [Tutorial 1 →](tutorial/01-basics_en.md) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README.md b/Box3JS-NeoForge-1.21.1/docs/api/README.md index 36512ae..ae9b6a4 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -1,6 +1,6 @@ # Box3JS API 参考 -Box3JS 是一个 Minecraft 模组,允许用 JavaScript/TypeScript 编写服务端脚本,并可选下发客户端脚本来实现本地 UI、输入和音效。每个脚本项目位于 `config/box3/script/<项目名>` 下。 +用 JavaScript/TypeScript 编写 Minecraft 服务端与客户端脚本。 ## 5 分钟快速开始 @@ -32,33 +32,35 @@ console.log("脚本已加载"); 每次修改后重新 `npm run build`,然后用 `/box3script reload mygame` 热重载。客户端逻辑放在 `src/client/app.ts`,构建后生成 `dist/client.js`,玩家安装 Box3JS 客户端 Mod 后会在加入服务器时自动接收。 -> **新手上路**: [快速开始指南](../guide/getting-started.md) | **原理深入**: [运行原理](../guide/architecture.md) | **JS vs Java**: [技术选型对比](../guide/js-vs-java.md) +::: tip 快速导航 +[快速开始指南](../guide/getting-started.md) | [运行原理](../guide/architecture.md) | [JS vs Java 对比](../guide/js-vs-java.md) +::: ## API 领域分类 Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服务端与客户端类型声明已拆分,`tsconfig.server.json` 不会包含客户端全局对象,`tsconfig.client.json` 也不会包含服务端 `world` / `voxels`。 -| 领域 | 运行环境 | 全局对象 | 说明 | -|------|---------|---------|------| -| **世界与实体** (服务端) | 服务端 | `world` `voxels` | 世界控制、方块操作、事件回调 | -| **玩家与数据** (服务端) | 服务端 | `entity` `player` `storage` `db` `http` | `entity`/`player` 来自回调或查询 | -| **客户端交互** (客户端) | 客户端 | `audio` `client` `input` `ui` `chat` `gui` | 需 Box3JS 客户端 Mod | -| **跨端通信** | 双端 | `remoteChannel` | 服务端↔客户端事件通信 | -| **注册与编译** | 编译时 | `registries` | 仅在 `/box3script compile` JAR 模式可用 | -| **数学与工具** | 双端 | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | 通过 `new` 构造 | -| **全局工具** | 双端 | `console` | 日志输出 | +| 领域 | 运行环境 | 全局对象 | 说明 | +| ----------------------- | -------- | --------------------------------------------------------------------------- | --------------------------------------- | +| **世界与实体** (服务端) | 服务端 | `world` `voxels` | 世界控制、方块操作、事件回调 | +| **玩家与数据** (服务端) | 服务端 | `entity` `player` `storage` `db` `http` | `entity`/`player` 来自回调或查询 | +| **客户端交互** (客户端) | 客户端 | `audio` `client` `input` `ui` `chat` `gui` | 需 Box3JS 客户端 Mod | +| **跨端通信** | 双端 | `remoteChannel` | 服务端↔客户端事件通信 | +| **注册与编译** | 编译时 | `registries` | 仅在 `/box3script compile` JAR 模式可用 | +| **数学与工具** | 双端 | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | 通过 `new` 构造 | +| **全局工具** | 双端 | `console` | 日志输出 | -> **服务端 API** 操作世界、实体、玩家、方块。脚本默认运行在服务端。 -> **客户端 API** 仅在安装了 Box3JS 客户端 Mod 时可用,用于 UI、输入、音效。 -> **注册 API** 仅在编译 JAR 模式下可用(`/box3script start` 解释模式中 `registries` 为 `undefined`)。 +::: info API 分类 +**服务端 API** 操作世界、实体、玩家、方块。脚本默认运行在服务端。**客户端 API** 仅在安装了 Box3JS 客户端 Mod 时可用,用于 UI、输入、音效。**注册 API** 仅在编译 JAR 模式下可用(`/box3script start` 解释模式中 `registries` 为 `undefined`)。 +::: ## 按运行环境阅读 -| 入口 | 适合场景 | 包含文档 | -|------|----------|----------| -| [服务端 API 总览](server.md) | 写玩法逻辑、事件、方块、实体、玩家、数据、服务端到客户端事件 | `world`、`entity`、`player`、`voxels`、`storage`、`db`、`http`、`registries` | -| [客户端 API 总览](client.md) | 写本地 UI、输入、音效、聊天辅助、本地数据、客户端到服务端事件 | `client`、`audio`、`input`、`ui`、`chat`、`gui`、`storage`、`db`、`http` | -| [共享工具](math.md) | 写双端都可用的数学、颜色、空间计算代码 | `GameVector3`、`GameBounds3`、`GameRGBColor`、`GameRGBAColor`、`GameQuaternion` | +| 入口 | 适合场景 | 包含文档 | +| ---------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| [服务端 API 总览](server.md) | 写玩法逻辑、事件、方块、实体、玩家、数据、服务端到客户端事件 | `world`、`entity`、`player`、`voxels`、`storage`、`db`、`http`、`registries` | +| [客户端 API 总览](client.md) | 写本地 UI、输入、音效、聊天辅助、本地数据、客户端到服务端事件 | `client`、`audio`、`input`、`ui`、`chat`、`gui`、`storage`、`db`、`http` | +| [共享工具](math.md) | 写双端都可用的数学、颜色、空间计算代码 | `GameVector3`、`GameBounds3`、`GameRGBColor`、`GameRGBAColor`、`GameQuaternion` | ## API 风格约定 @@ -73,225 +75,225 @@ Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服 ### 消息与聊天 -| 我想... | 用这个 | -|---------|--------| -| 全服广播 | `world.say("消息")` | -| 私密消息(只一人看到) | `player.directMessage("消息")` | -| 快捷栏上方文字 | `player.actionBar("消息")` | -| 屏幕中央大标题 | `player.title("标题", "副标题")` | -| 拦截/处理聊天 | `world.onChat((entity, msg) => { ... })` | +| 我想... | 用这个 | +| ---------------------- | ---------------------------------------- | +| 全服广播 | `world.say("消息")` | +| 私密消息(只一人看到) | `player.directMessage("消息")` | +| 快捷栏上方文字 | `player.actionBar("消息")` | +| 屏幕中央大标题 | `player.title("标题", "副标题")` | +| 拦截/处理聊天 | `world.onChat((entity, msg) => { ... })` | ### 玩家属性 -| 我想... | 用这个 | -|---------|--------| -| 获取/设置玩家位置 | `player.position` → `GameVector3` | -| 传送玩家 | `player.teleport(new GameVector3(x, y, z))` | -| 修改生命值 | `player.hp = 20` / `player.maxHp = 40` | -| 修改饱食度 | `player.food = 20` / `player.saturation = 10` | -| 切换游戏模式 | `player.gameMode = "creative"` | -| 切换飞行 | `player.canFly = true` / `player.flying = true` | -| 踢出玩家 | `player.kick("原因")` | -| 以玩家身份执行命令 | `player.runCommand("say hi")` | +| 我想... | 用这个 | +| ------------------ | ----------------------------------------------- | +| 获取/设置玩家位置 | `player.position` → `GameVector3` | +| 传送玩家 | `player.teleport(new GameVector3(x, y, z))` | +| 修改生命值 | `player.hp = 20` / `player.maxHp = 40` | +| 修改饱食度 | `player.food = 20` / `player.saturation = 10` | +| 切换游戏模式 | `player.gameMode = "creative"` | +| 切换飞行 | `player.canFly = true` / `player.flying = true` | +| 踢出玩家 | `player.kick("原因")` | +| 以玩家身份执行命令 | `player.runCommand("say hi")` | ### 物品与装备 -| 我想... | 用这个 | -|---------|--------| -| 给玩家普通物品 | `player.giveItem("minecraft:diamond", 1)` | -| 给带附魔的物品 | `player.giveEnchantedItem(...)` | -| 给带自定义名称的物品 | `player.giveNamedItem(...)` | -| 获取手持物品 | `player.getHeldItem()` | -| 清空背包 | `player.clearInventory()` | -| 设置实体装备 | `entity.setEquipment("head", "iron_helmet")` | +| 我想... | 用这个 | +| -------------------- | -------------------------------------------- | +| 给玩家普通物品 | `player.giveItem("minecraft:diamond", 1)` | +| 给带附魔的物品 | `player.giveEnchantedItem(...)` | +| 给带自定义名称的物品 | `player.giveNamedItem(...)` | +| 获取手持物品 | `player.getHeldItem()` | +| 清空背包 | `player.clearInventory()` | +| 设置实体装备 | `entity.setEquipment("head", "iron_helmet")` | ### 自定义注册表(方块/物品/音效) 🆕 -| 我想... | 用这个 | -|---------|--------| -| 注册自定义方块 | `registries/blocks.json`(编译时) | -| 注册自定义物品 | `registries/items.json`(编译时) | -| 注册自定义音效 | `registries/sounds.json`(编译时) | -| 注册创造标签页 | `registries/creativeTabs.json`(编译时) | -| 获取注册的方块 | `registries.getBlock("my_block")` | -| 获取注册的物品 | `registries.getItem("chocolate")` | -| 获取注册的音效 | `registries.getSound("victory_fanfare")` | -| 给予自定义方块/物品 | `player.giveItem(block.itemId, 1)` | -| 放置自定义方块 | `voxels.setVoxel(x, y, z, block.block)` | +| 我想... | 用这个 | +| ------------------------ | ----------------------------------------------- | +| 注册自定义方块 | `registries/blocks.json`(编译时) | +| 注册自定义物品 | `registries/items.json`(编译时) | +| 注册自定义音效 | `registries/sounds.json`(编译时) | +| 注册创造标签页 | `registries/creativeTabs.json`(编译时) | +| 获取注册的方块 | `registries.getBlock("my_block")` | +| 获取注册的物品 | `registries.getItem("chocolate")` | +| 获取注册的音效 | `registries.getSound("victory_fanfare")` | +| 给予自定义方块/物品 | `player.giveItem(block.itemId, 1)` | +| 放置自定义方块 | `voxels.setVoxel(x, y, z, block.block)` | | 播放自定义音效(服务端) | `world.playSound(sound.soundId, x, y, z, 1, 1)` | -| 播放自定义音效(客户端) | `audio.playSound("modId:soundId", 1.0, 1.0)` | +| 播放自定义音效(客户端) | `audio.playSound("modId:soundId", 1.0, 1.0)` | -> **仅服务端可用。** 客户端脚本中 `registries` 为 `undefined`。**仅在 `/box3script compile` 编译的 JAR 模式下可用。** 需客户端也安装该 JAR 以正确渲染纹理/模型。详见 [registries.md](registries.md) +::: warning +仅服务端可用。客户端脚本中 `registries` 为 `undefined`。仅在 `/box3script compile` 编译的 JAR 模式下可用。需客户端也安装该 JAR 以正确渲染纹理/模型。详见 [registries.md](registries.md) +::: ### 方块操作 -| 我想... | 用这个 | -|---------|--------| -| 读取某个位置的方块 | `voxels.getVoxel(x, y, z)` | -| 放置/替换方块 | `voxels.setVoxel(x, y, z, "minecraft:stone")` | -| 填充区域 | `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, "stone")` | -| 监听方块破坏 | `world.onVoxelDestroy((entity, x, y, z, voxel) => { ... })` | -| 监听方块放置 | `world.onBlockPlace((entity, x, y, z, voxel) => { ... })` | +| 我想... | 用这个 | +| ------------------ | ----------------------------------------------------------- | +| 读取某个位置的方块 | `voxels.getVoxel(x, y, z)` | +| 放置/替换方块 | `voxels.setVoxel(x, y, z, "minecraft:stone")` | +| 填充区域 | `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, "stone")` | +| 监听方块破坏 | `world.onVoxelDestroy((entity, x, y, z, voxel) => { ... })` | +| 监听方块放置 | `world.onBlockPlace((entity, x, y, z, voxel) => { ... })` | ### 实体操控 -| 我想... | 用这个 | -|---------|--------| -| 生成实体 | `world.spawnEntity("minecraft:zombie", pos)` | +| 我想... | 用这个 | +| -------------- | --------------------------------------------- | +| 生成实体 | `world.spawnEntity("minecraft:zombie", pos)` | | 带配置创建实体 | `world.createEntity({ type, position, ... })` | -| 设置实体名称 | `entity.setNameTag("§cBoss")` | -| 开关 AI | `entity.setAI(true)` | -| 实体导航 | `entity.navigateTo(x, y, z, speed)` | -| 设置攻击目标 | `entity.setTarget(otherEntity)` | -| 判断是否是玩家 | `entity.isPlayer()` | -| 获取实体类型 | `entity.entityType` | -| 获取实体标签 | `entity.tags()` / `entity.hasTag("boss")` | -| 查询附近实体 | `world.entitiesInRadius(pos, radius)` | -| 查询所有实体 | `world.querySelectorAll("*")` | +| 设置实体名称 | `entity.setNameTag("§cBoss")` | +| 开关 AI | `entity.setAI(true)` | +| 实体导航 | `entity.navigateTo(x, y, z, speed)` | +| 设置攻击目标 | `entity.setTarget(otherEntity)` | +| 判断是否是玩家 | `entity.isPlayer()` | +| 获取实体类型 | `entity.entityType` | +| 获取实体标签 | `entity.tags()` / `entity.hasTag("boss")` | +| 查询附近实体 | `world.entitiesInRadius(pos, radius)` | +| 查询所有实体 | `world.querySelectorAll("*")` | ### 客户端本地功能(需 Box3JS 客户端 Mod) -| 我想... | 用这个 | -|---------|--------| -| 客户端每帧执行 | `client.onTick(() => { ... })` | -| 检测按键按下 | `input.isKeyDown("space")` | -| 监听按键事件 | `input.onKeyPress("f", () => { ... })` | -| 播放客户端音效 | `audio.playSound("pling", 1.0, 1.0)` | -| 播放客户端音乐 | `audio.playMusic("minecraft:music.game", 0.5, 1.0)` | -| 停止所有声音 | `audio.stopAll()` | -| 获取/设置音量 | `audio.getVolume("music")` / `audio.setVolume("player", 0.8)` | -| 快捷栏上方显示文字 | `ui.showOverlay("文字")` | -| 显示屏幕大标题 | `ui.showTitle("标题", "副标题")` | -| 发送聊天消息 | `chat.sendMessage("消息")` | -| 接收聊天消息 | `chat.onMessage((msg, sender, isSystem) => { ... })` | -| 发送服务端事件 | `remoteChannel.sendServerEvent({ ... })` | -| 接收服务端事件 | `remoteChannel.onClientEvent((event) => { ... })` | -| 客户端本地存储 | `storage.getDataStorage("key")` | -| 设置雾颜色 | `client.setFogColor(255, 100, 50)` | -| 设置雾距离 | `client.setFogStartDistance(10)` / `client.setFogEndDistance(50)` | -| 重置雾效果 | `client.resetFog()` | +| 我想... | 用这个 | +| ------------------ | ----------------------------------------------------------------- | +| 客户端每帧执行 | `client.onTick(() => { ... })` | +| 检测按键按下 | `input.isKeyDown("space")` | +| 监听按键事件 | `input.onKeyPress("f", () => { ... })` | +| 播放客户端音效 | `audio.playSound("pling", 1.0, 1.0)` | +| 播放客户端音乐 | `audio.playMusic("minecraft:music.game", 0.5, 1.0)` | +| 停止所有声音 | `audio.stopAll()` | +| 获取/设置音量 | `audio.getVolume("music")` / `audio.setVolume("player", 0.8)` | +| 快捷栏上方显示文字 | `ui.showOverlay("文字")` | +| 显示屏幕大标题 | `ui.showTitle("标题", "副标题")` | +| 发送聊天消息 | `chat.sendMessage("消息")` | +| 接收聊天消息 | `chat.onMessage((msg, sender, isSystem) => { ... })` | +| 发送服务端事件 | `remoteChannel.sendServerEvent({ ... })` | +| 接收服务端事件 | `remoteChannel.onClientEvent((event) => { ... })` | +| 客户端本地存储 | `storage.getDataStorage("key")` | +| 设置雾颜色 | `client.setFogColor(255, 100, 50)` | +| 设置雾距离 | `client.setFogStartDistance(10)` / `client.setFogEndDistance(50)` | +| 重置雾效果 | `client.resetFog()` | ### 视觉效果 -| 我想... | 用这个 | -|---------|--------| -| 粒子效果 | `world.spawnParticle("flame", x, y, z, ...)` | -| 圆形粒子圈 | `world.spawnParticleCircle(x, y, z, radius, "heart", 20)` | -| 烟花 | `world.launchFirework(x, y, z, "red", "large_ball")` | -| 闪电 | `world.strikeLightning(x, y, z)` | -| 爆炸 | `world.explode(x, y, z, 4)` | -| 播放音效(全局) | `world.playSound("pling", pos, 1.0, 1.0)` | -| 播放音效(单人) | `player.playSound("pling", 1.0, 1.0)` | +| 我想... | 用这个 | +| ---------------- | --------------------------------------------------------- | +| 粒子效果 | `world.spawnParticle("flame", x, y, z, ...)` | +| 圆形粒子圈 | `world.spawnParticleCircle(x, y, z, radius, "heart", 20)` | +| 烟花 | `world.launchFirework(x, y, z, "red", "large_ball")` | +| 闪电 | `world.strikeLightning(x, y, z)` | +| 爆炸 | `world.explode(x, y, z, 4)` | +| 播放音效(全局) | `world.playSound("pling", pos, 1.0, 1.0)` | +| 播放音效(单人) | `player.playSound("pling", 1.0, 1.0)` | ### 药水效果 -| 我想... | 用这个 | -|---------|--------| +| 我想... | 用这个 | +| ------------ | --------------------------------------------------------------------- | | 施加药水效果 | `entity.addEffect("minecraft:speed", duration, level, hideParticles)` | -| 清除所有效果 | `entity.clearEffects()` | +| 清除所有效果 | `entity.clearEffects()` | ### 事件系统 -| 我想... | 用这个 | -|---------|--------| -| 每 tick 执行 | `world.onTick((info) => { ... })` | -| 玩家加入时 | `world.onPlayerJoin((entity, tick) => { ... })` | -| 玩家离开时 | `world.onPlayerLeave((entity, tick) => { ... })` | -| 实体死亡时 | `world.onEntityDeath((entity, killer, tick) => { ... })` | -| 实体受伤时 | `world.onEntityDamage((entity, amount, source, attacker, tick) => { ... })` | -| 右键实体时 | `world.onInteract((entity, target, tick) => { ... })` | -| 右键方块时 | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | -| 按钮按下时 | `world.onButtonPressed((entity, button, tick) => { ... })` | -| 玩家重生时 | `world.onPlayerRespawn((entity, tick) => { ... })` | -| 定时执行一次 | `setTimeout(() => { ... }, ticks)` | -| 定时循环执行 | `setInterval(() => { ... }, ticks)` | -| 取消事件监听 | `token.cancel()` | -| 检查事件是否活跃 | `token.active()` | +| 我想... | 用这个 | +| ---------------- | --------------------------------------------------------------------------- | +| 每 tick 执行 | `world.onTick((info) => { ... })` | +| 玩家加入时 | `world.onPlayerJoin((entity, tick) => { ... })` | +| 玩家离开时 | `world.onPlayerLeave((entity, tick) => { ... })` | +| 实体死亡时 | `world.onEntityDeath((entity, killer, tick) => { ... })` | +| 实体受伤时 | `world.onEntityDamage((entity, amount, source, attacker, tick) => { ... })` | +| 右键实体时 | `world.onInteract((entity, target, tick) => { ... })` | +| 右键方块时 | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | +| 按钮按下时 | `world.onButtonPressed((entity, button, tick) => { ... })` | +| 玩家重生时 | `world.onPlayerRespawn((entity, tick) => { ... })` | +| 定时执行一次 | `setTimeout(() => { ... }, ticks)` | +| 定时循环执行 | `setInterval(() => { ... }, ticks)` | +| 取消事件监听 | `token.cancel()` | +| 检查事件是否活跃 | `token.active()` | ### 数据持久化 -| 我想... | 用这个 | -|---------|--------| +| 我想... | 用这个 | +| -------------- | ------------------------------- | | 读写 JSON 数据 | `storage.getDataStorage("key")` | -| SQL 查询 | `db.sql("SELECT ...")` | -| SQL 写入 | `db.sql("INSERT INTO ...")` | +| SQL 查询 | `db.sql("SELECT ...")` | +| SQL 写入 | `db.sql("INSERT INTO ...")` | ### 网络请求 -| 我想... | 用这个 | -|---------|--------| -| GET 请求 | `http.fetch("https://...")` | +| 我想... | 用这个 | +| --------- | ---------------------------------------------------- | +| GET 请求 | `http.fetch("https://...")` | | POST JSON | `http.fetch(url, { method: "POST", headers, body })` | -| 解析 JSON | `resp.json()` 或 `{ responseType: "json" }` | -| 读取文本 | `resp.text()` | -| 设置超时 | `http.fetch(url, { timeout: 5000 })` | +| 解析 JSON | `resp.json()` 或 `{ responseType: "json" }` | +| 读取文本 | `resp.text()` | +| 设置超时 | `http.fetch(url, { timeout: 5000 })` | ### 游戏系统 -| 我想... | 用这个 | -|---------|--------| -| 创建计分板 | `world.addScoreboard("name")` | -| 设置分数 | `world.setScore("玩家", "计分板", 10)` | -| 显示计分板 | `world.showScoreboard("sidebar", "name")` | -| 显示 BossBar | `world.showBossbar("id", "标题", 0.5, "red")` | -| 创建队伍 | `world.createTeam("teamName", "color")` | -| 加入队伍 | `world.joinTeam(entity, "teamName")` | -| 设置世界边界 | `world.borderSize = 500` | -| 缩圈 | `world.shrinkBorder(100, 60)` | -| 修改世界时间 | `world.time = 6000` | -| 设置天气 | `world.rainDensity = 0` / `world.clearWeather()` | -| 修改游戏规则 | `world.setGameRule("keepInventory", true)` | +| 我想... | 用这个 | +| ------------ | ------------------------------------------------ | +| 创建计分板 | `world.addScoreboard("name")` | +| 设置分数 | `world.setScore("玩家", "计分板", 10)` | +| 显示计分板 | `world.showScoreboard("sidebar", "name")` | +| 显示 BossBar | `world.showBossbar("id", "标题", 0.5, "red")` | +| 创建队伍 | `world.createTeam("teamName", "color")` | +| 加入队伍 | `world.joinTeam(entity, "teamName")` | +| 设置世界边界 | `world.borderSize = 500` | +| 缩圈 | `world.shrinkBorder(100, 60)` | +| 修改世界时间 | `world.time = 6000` | +| 设置天气 | `world.rainDensity = 0` / `world.clearWeather()` | +| 修改游戏规则 | `world.setGameRule("keepInventory", true)` | ### 数学工具 -| 我想... | 用这个 | -|---------|--------| -| 三维坐标 | `new GameVector3(x, y, z)` | -| 向量运算 | `v.add(other)`, `v.scale(n)`, `v.length()` | -| 包围盒 | `new GameBounds3(min, max)` | -| 颜色 | `new GameRGBColor(r, g, b)` / `new GameRGBAColor(r, g, b, a)` | +| 我想... | 用这个 | +| -------- | ------------------------------------------------------------- | +| 三维坐标 | `new GameVector3(x, y, z)` | +| 向量运算 | `v.add(other)`, `v.scale(n)`, `v.length()` | +| 包围盒 | `new GameBounds3(min, max)` | +| 颜色 | `new GameRGBColor(r, g, b)` / `new GameRGBAColor(r, g, b, a)` | ### 跨脚本通信 -| 我想... | 用这个 | -|---------|--------| -| 发送消息给其他脚本 | `world.sendMessage("projectName", data)` | +| 我想... | 用这个 | +| ------------------ | ------------------------------------------ | +| 发送消息给其他脚本 | `world.sendMessage("projectName", data)` | | 接收其他脚本的消息 | `world.onMessage((from, data) => { ... })` | ---- - ## 全局对象一览 -| 对象 | 类型 | 说明 | -|------|------|------| -| `world` | 服务端 | 世界控制,见 [world.md](world.md) | -| `voxels` | 服务端 | 方块操作,见 [voxels.md](voxels.md) | -| `entity` | 服务端值 | 实体包装(回调参数或 `world.spawnEntity` 创建),见 [entity.md](entity.md) | -| `player` | 服务端值 | 玩家包装(通过 `entity.player` 获取),见 [player.md](player.md) | -| `storage` | 双端 | 数据持久化,见 [storage.md](storage.md) | -| `db` | 双端 | SQLite 数据库,见 [database.md](database.md) | -| `http` | 双端 | HTTP 请求,见 [http.md](http.md) | -| `audio` | 客户端 | 客户端音效、音乐、音量控制,见 [client.md](client.md) | -| `client` | 客户端 | 客户端生命周期,见 [client.md](client.md) | -| `input` | 客户端 | 客户端键盘输入,见 [client.md](client.md) | -| `ui` | 客户端 | 客户端屏幕 UI,见 [client.md](client.md) | -| `chat` | 客户端 | 客户端聊天收发,见 [client.md](client.md) | -| `gui` | 客户端 | 自定义容器 GUI,见 [client.md](client.md) | -| `remoteChannel` | 双端 | 服务端↔客户端事件通信,见 [server.md](server.md) / [client.md](client.md) | -| `registries` | 服务端 | 自定义方块/物品/音效(编译模式),见 [registries.md](registries.md) | -| `console` | 双端 | 控制台日志输出(`log`/`warn`/`error`/`debug`) | -| `GameVector3` | 双端 | 三维向量,见 [math.md](math.md) | -| `GameBounds3` | 双端 | 包围盒,见 [math.md](math.md) | -| `GameRGBColor` | 双端 | RGB 颜色,见 [math.md](math.md) | -| `GameRGBAColor` | 双端 | RGBA 颜色,见 [math.md](math.md) | -| `GameQuaternion` | 双端 | 四元数,见 [math.md](math.md) | +| 对象 | 类型 | 说明 | +| ---------------- | -------- | -------------------------------------------------------------------------- | +| `world` | 服务端 | 世界控制,见 [world.md](world.md) | +| `voxels` | 服务端 | 方块操作,见 [voxels.md](voxels.md) | +| `entity` | 服务端值 | 实体包装(回调参数或 `world.spawnEntity` 创建),见 [entity.md](entity.md) | +| `player` | 服务端值 | 玩家包装(通过 `entity.player` 获取),见 [player.md](player.md) | +| `storage` | 双端 | 数据持久化,见 [storage.md](storage.md) | +| `db` | 双端 | SQLite 数据库,见 [database.md](database.md) | +| `http` | 双端 | HTTP 请求,见 [http.md](http.md) | +| `audio` | 客户端 | 客户端音效、音乐、音量控制,见 [client.md](client.md) | +| `client` | 客户端 | 客户端生命周期,见 [client.md](client.md) | +| `input` | 客户端 | 客户端键盘输入,见 [client.md](client.md) | +| `ui` | 客户端 | 客户端屏幕 UI,见 [client.md](client.md) | +| `chat` | 客户端 | 客户端聊天收发,见 [client.md](client.md) | +| `gui` | 客户端 | 自定义容器 GUI,见 [client.md](client.md) | +| `remoteChannel` | 双端 | 服务端↔客户端事件通信,见 [server.md](server.md) / [client.md](client.md) | +| `registries` | 服务端 | 自定义方块/物品/音效(编译模式),见 [registries.md](registries.md) | +| `console` | 双端 | 控制台日志输出(`log`/`warn`/`error`/`debug`) | +| `GameVector3` | 双端 | 三维向量,见 [math.md](math.md) | +| `GameBounds3` | 双端 | 包围盒,见 [math.md](math.md) | +| `GameRGBColor` | 双端 | RGB 颜色,见 [math.md](math.md) | +| `GameRGBAColor` | 双端 | RGBA 颜色,见 [math.md](math.md) | +| `GameQuaternion` | 双端 | 四元数,见 [math.md](math.md) | ## API 标注说明 -| 标注 | 含义 | -|------|------| -| ✅ **Box3 API** | 源自 Box3 平台,命名和语义与 Box3 保持一致 | -| ⬆ **MC 扩展** | 非 Box3 原有,利用 Minecraft 特性新增的 API | +| 标注 | 含义 | +| --------------- | ------------------------------------------- | +| ✅ **Box3 API** | 源自 Box3 平台,命名和语义与 Box3 保持一致 | +| ⬆ **MC 扩展** | 非 Box3 原有,利用 Minecraft 特性新增的 API | ## 文档风格约定 @@ -307,26 +309,26 @@ Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服 ## 详细文档索引 -| 文档 | 内容 | -|------|------| -| [server.md](server.md) | 服务端 API 总览:运行边界、全局对象、事件、玩家/实体、方块、数据、跨端通信 | -| [world.md](world.md) | 世界状态、事件回调、记分板、BossBar、队伍、边界、粒子、烟花、闪电、音效 | -| [entity.md](entity.md) | 实体属性、AI、装备、药水效果、寻路、标签、碰撞 | -| [player.md](player.md) | 背包、消息、飞行、游戏模式、传送、命令、经验值 | -| [voxels.md](voxels.md) | 方块读写、区域填充、刷怪笼 | -| [storage.md](storage.md) | 数据持久化存储 | -| [database.md](database.md) | SQLite 数据库 | -| [http.md](http.md) | HTTP 网络请求 | -| [client.md](client.md) | 客户端 API:生命周期、键盘输入、屏幕 UI、聊天、GUI、remoteChannel、客户端本地存储 | -| [registries.md](registries.md) | 自定义方块/物品/音效(blocks.json、items.json、sounds.json、creativeTabs.json) | -| [math.md](math.md) | GameVector3、GameBounds3、GameRGBColor、GameRGBAColor、GameQuaternion | -| [commands.md](commands.md) | `/box3script` 命令参考 | +| 文档 | 内容 | +| ------------------------------ | --------------------------------------------------------------------------------- | +| [server.md](server.md) | 服务端 API 总览:运行边界、全局对象、事件、玩家/实体、方块、数据、跨端通信 | +| [world.md](world.md) | 世界状态、事件回调、记分板、BossBar、队伍、边界、粒子、烟花、闪电、音效 | +| [entity.md](entity.md) | 实体属性、AI、装备、药水效果、寻路、标签、碰撞 | +| [player.md](player.md) | 背包、消息、飞行、游戏模式、传送、命令、经验值 | +| [voxels.md](voxels.md) | 方块读写、区域填充、刷怪笼 | +| [storage.md](storage.md) | 数据持久化存储 | +| [database.md](database.md) | SQLite 数据库 | +| [http.md](http.md) | HTTP 网络请求 | +| [client.md](client.md) | 客户端 API:生命周期、键盘输入、屏幕 UI、聊天、GUI、remoteChannel、客户端本地存储 | +| [registries.md](registries.md) | 自定义方块/物品/音效(blocks.json、items.json、sounds.json、creativeTabs.json) | +| [math.md](math.md) | GameVector3、GameBounds3、GameRGBColor、GameRGBAColor、GameQuaternion | +| [commands.md](commands.md) | `/box3script` 命令参考 | ## 文件模块 — TypeScript 构建管线 `/box3script create` 创建的项目自带完整的 TS 构建环境: -``` +```text config/box3/script/mygame/ ├── package.json ← esbuild + Babel + @babel/preset-typescript ├── tsconfig.base.json ← 公共 TS 编译选项 @@ -369,7 +371,7 @@ config/box3/script/mygame/ 开发调试完成后,将脚本编译为**独立 JAR 模组**,需与 Box3JS 一同部署在 NeoForge 服务器: -``` +```js /box3script compile <项目名> ``` @@ -379,30 +381,30 @@ config/box3/script/mygame/ ## Tick 换算 -| 时长 | Ticks | -|------|-------| -| 1 秒 | 20 | -| 5 秒 | 100 | -| 30 秒 | 600 | -| 1 分钟 | 1200 | -| 5 分钟 | 6000 | +| 时长 | Ticks | +| ------ | ----- | +| 1 秒 | 20 | +| 5 秒 | 100 | +| 30 秒 | 600 | +| 1 分钟 | 1200 | +| 5 分钟 | 6000 | ## 深入学习 -| 文档 | 内容 | -|------|------| -| [快速开始](../guide/getting-started.md) | 环境搭建、第一个脚本、开发循环、调试、发布 | -| [运行原理](../guide/architecture.md) | Rhino 引擎、作用域、事件回调、构建管线、网络通信 | -| [JS vs Java](../guide/js-vs-java.md) | Box3JS 脚本开发 vs 原生 Java 模组开发对比 | +| 文档 | 内容 | +| --------------------------------------- | ------------------------------------------------ | +| [快速开始](../guide/getting-started.md) | 环境搭建、第一个脚本、开发循环、调试、发布 | +| [运行原理](../guide/architecture.md) | Rhino 引擎、作用域、事件回调、构建管线、网络通信 | +| [JS vs Java](../guide/js-vs-java.md) | Box3JS 脚本开发 vs 原生 Java 模组开发对比 | ## 教程 从零开始学习 Box3JS 脚本开发,请阅读 `docs/tutorial/` 系列教程: -| 教程 | 内容 | -|------|------| -| [01-basics.md](../tutorial/01-basics.md) | 从零开始:第一个脚本、聊天命令、定时任务 | -| [02-player-items.md](../tutorial/02-player-items.md) | 玩家操控:传送、物品给予、药水效果、游戏模式 | -| [03-events-entities.md](../tutorial/03-events-entities.md) | 事件系统与实体操控:AI、战斗、巡逻 | -| [04-advanced-systems.md](../tutorial/04-advanced-systems.md) | 高级系统:计分板、BossBar、队伍、世界边界 | -| [05-examples.md](../tutorial/05-examples.md) | 实战示例:PvP 竞技场、特效、烟花、波次刷怪 | +| 教程 | 内容 | +| ------------------------------------------------------------ | -------------------------------------------- | +| [01-basics.md](../tutorial/01-basics.md) | 从零开始:第一个脚本、聊天命令、定时任务 | +| [02-player-items.md](../tutorial/02-player-items.md) | 玩家操控:传送、物品给予、药水效果、游戏模式 | +| [03-events-entities.md](../tutorial/03-events-entities.md) | 事件系统与实体操控:AI、战斗、巡逻 | +| [04-advanced-systems.md](../tutorial/04-advanced-systems.md) | 高级系统:计分板、BossBar、队伍、世界边界 | +| [05-examples.md](../tutorial/05-examples.md) | 实战示例:PvP 竞技场、特效、烟花、波次刷怪 | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md deleted file mode 100644 index da87597..0000000 --- a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md +++ /dev/null @@ -1,408 +0,0 @@ -# Box3JS API Reference - -Box3JS is a Minecraft mod that lets you write server-side scripts in JavaScript/TypeScript, with optional client scripts for local UI, input, and audio. Each script project lives under `config/box3/script/`. - -## 5-Minute Quick Start - -```bash -# 1. Create a project in-game -/box3script create mygame - -# 2. Install dependencies and build -cd config/box3/script/mygame -npm install && npm run build - -# 3. Start the script -/box3script start mygame -``` - -Open `src/server/app.ts` and write: - -```js -world.onChat((entity, message) => { - if (message === "!hello") { - entity.player.directMessage("Hello, " + entity.player.name + "!"); - return false; // suppress chat message - } - return true; -}); - -console.log("Script loaded"); -``` - -After each edit, re-run `npm run build`, then use `/box3script reload mygame` to hot-reload. Client logic goes in `src/client/app.ts`; after build it becomes `dist/client.js` and is sent automatically to players who have the Box3JS client mod installed. - -> **New here?** [Quick Start Guide](../guide/getting-started_en.md) | **How it works:** [Architecture](../guide/architecture_en.md) | **JS vs Java:** [Comparison](../guide/js-vs-java_en.md) - -## API Domain Map - -Box3JS APIs are split into server-side, client-side, and shared runtimes. The type declarations are separated: `tsconfig.server.json` does not include client globals, and `tsconfig.client.json` does not include server globals such as `world` / `voxels`. - -| Domain | Runtime | Globals | Description | -|--------|---------|---------|-------------| -| **World & Entities** (server) | Server | `world` `voxels` | World control, blocks, event callbacks | -| **Players & Data** (server) | Server | `entity` `player` `storage` `db` `http` | `entity`/`player` come from callbacks or queries | -| **Client Interaction** (client) | Client | `audio` `client` `input` `ui` `chat` `gui` | Requires Box3JS client mod | -| **Cross-Side** | Both | `remoteChannel` | Server↔Client event communication | -| **Registries** | Compile-time | `registries` | Only in `/box3script compile` JAR mode | -| **Math & Utilities** | Both | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | Constructed with `new` | -| **Global Tools** | Both | `console` | Log output | - -> **Server APIs** manipulate the world, entities, players, and blocks. Scripts run on the server by default. -> **Client APIs** are only available with the Box3JS client mod installed, for UI, input, and audio. -> **Registry APIs** are only available in compiled JAR mode (`registries` is `undefined` in interpreted mode). - -## Read By Runtime - -| Entry | Use it for | Includes | -|-------|------------|----------| -| [Server API Overview](server_en.md) | Gameplay logic, events, blocks, entities, players, data, server-to-client events | `world`, `entity`, `player`, `voxels`, `storage`, `db`, `http`, `registries` | -| [Client API Overview](client_en.md) | Local UI, input, audio, chat helpers, local data, client-to-server events | `client`, `audio`, `input`, `ui`, `chat`, `gui`, `storage`, `db`, `http` | -| [Shared Utilities](math_en.md) | Math, color, and spatial code usable on both sides | `GameVector3`, `GameBounds3`, `GameRGBColor`, `GameRGBAColor`, `GameQuaternion` | - -## API Style Rules - -- Every `onXxx(...)` event registration API returns a `GameEventHandlerToken`; call `token.cancel()` to unsubscribe and `token.active()` to check whether it is still live. -- Server APIs are only typed in `src/server/app.ts`; client APIs are only typed in `src/client/app.ts`. Shared APIs are `storage`, `db`, `http`, `remoteChannel`, `console`, and the math classes. -- Cross-side data travels through `remoteChannel` as JSON-serializable objects: clients use `sendServerEvent` / `onClientEvent`; servers use `sendClientEvent` / `broadcastClientEvent` / `onServerEvent`. -- Coordinate APIs that accept `GameVector3` usually also support an `x, y, z` overload; server block coordinates are handled as integers. - -## Find by Task — I want to... - -Find APIs by what you want to do, not by which global object they live on. - -### Messages & Chat - -| I want to... | Use this | -|-------------|----------| -| Broadcast to server | `world.say("message")` | -| Send private message | `player.directMessage("message")` | -| Show action bar text | `player.actionBar("message")` | -| Show screen title | `player.title("Title", "Subtitle")` | -| Handle chat input | `world.onChat((entity, msg) => { ... })` | - -### Player Properties - -| I want to... | Use this | -|-------------|----------| -| Get/set player position | `player.position` → `GameVector3` | -| Teleport player | `player.teleport(new GameVector3(x, y, z))` | -| Change health | `player.hp = 20` / `player.maxHp = 40` | -| Change hunger | `player.food = 20` / `player.saturation = 10` | -| Switch game mode | `player.gameMode = "creative"` | -| Toggle flight | `player.canFly = true` / `player.flying = true` | -| Kick player | `player.kick("reason")` | -| Run command as player | `player.runCommand("say hi")` | - -### Items & Equipment - -| I want to... | Use this | -|-------------|----------| -| Give a basic item | `player.giveItem("minecraft:diamond", 1)` | -| Give enchanted item | `player.giveEnchantedItem(...)` | -| Give named item | `player.giveNamedItem(...)` | -| Get held item | `player.getHeldItem()` | -| Clear inventory | `player.clearInventory()` | -| Set entity equipment | `entity.setEquipment("head", "iron_helmet")` | - -### Custom Registries (Blocks, Items & Sounds) 🆕 - -| I want to... | Use this | -|-------------|----------| -| Register custom blocks | `registries/blocks.json` (at compile time) | -| Register custom items | `registries/items.json` (at compile time) | -| Register custom sounds | `registries/sounds.json` (at compile time) | -| Register creative tabs | `registries/creativeTabs.json` (at compile time) | -| Get a registered block | `registries.getBlock("my_block")` | -| Get a registered item | `registries.getItem("chocolate")` | -| Get a registered sound | `registries.getSound("victory_fanfare")` | -| Give a custom block/item | `player.giveItem(block.itemId, 1)` | -| Place a custom block | `voxels.setVoxel(x, y, z, block.block)` | -| Play a custom sound (server) | `world.playSound(sound.soundId, x, y, z, 1, 1)` | -| Play a custom sound (client) | `audio.playSound("modId:soundId", 1.0, 1.0)` | - -> **Server-side only.** `registries` is `undefined` in client scripts. **Only available in `/box3script compile` JAR mode.** Client must also install the JAR for textures/models. See [registries_en.md](registries_en.md) - -### Block Operations - -| I want to... | Use this | -|-------------|----------| -| Read a block | `voxels.getVoxel(x, y, z)` | -| Place/replace a block | `voxels.setVoxel(x, y, z, "minecraft:stone")` | -| Fill a region | `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, "stone")` | -| Listen for block breaks | `world.onVoxelDestroy((entity, x, y, z, voxel) => { ... })` | -| Listen for block placement | `world.onBlockPlace((entity, x, y, z, voxel) => { ... })` | - -### Entity Manipulation - -| I want to... | Use this | -|-------------|----------| -| Spawn an entity | `world.spawnEntity("minecraft:zombie", pos)` | -| Create with config | `world.createEntity({ type, position, ... })` | -| Set entity name | `entity.setNameTag("§cBoss")` | -| Toggle AI | `entity.setAI(true)` | -| Navigate to position | `entity.navigateTo(x, y, z, speed)` | -| Set attack target | `entity.setTarget(otherEntity)` | -| Check if player | `entity.isPlayer()` | -| Get entity type | `entity.entityType` | -| Get entity tags | `entity.tags()` / `entity.hasTag("boss")` | -| Query nearby entities | `world.entitiesInRadius(pos, radius)` | -| Query all entities | `world.querySelectorAll("*")` | - -### Client-side Features (requires Box3JS client mod) - -| I want to... | Use this | -|--------------|----------| -| Run every client tick | `client.onTick(() => { ... })` | -| Check key held down | `input.isKeyDown("space")` | -| Listen for key press | `input.onKeyPress("f", () => { ... })` | -| Play sound effect | `audio.playSound("pling", 1.0, 1.0)` | -| Play music | `audio.playMusic("minecraft:music.game", 0.5, 1.0)` | -| Stop all sounds | `audio.stopAll()` | -| Get/set volume | `audio.getVolume("music")` / `audio.setVolume("player", 0.8)` | -| Show action bar text | `ui.showOverlay("text")` | -| Show screen title | `ui.showTitle("Title", "Subtitle")` | -| Send chat message | `chat.sendMessage("message")` | -| Receive chat messages | `chat.onMessage((msg, sender, isSystem) => { ... })` | -| Send event to server | `remoteChannel.sendServerEvent({ ... })` | -| Receive event from server | `remoteChannel.onClientEvent((event) => { ... })` | -| Client-side local storage | `storage.getDataStorage("key")` | -| Set fog colour | `client.setFogColor(255, 100, 50)` | -| Set fog distance | `client.setFogStartDistance(10)` / `client.setFogEndDistance(50)` | -| Reset fog | `client.resetFog()` | - -### Visual Effects - -| I want to... | Use this | -|-------------|----------| -| Spawn particles | `world.spawnParticle("flame", x, y, z, ...)` | -| Particle circle | `world.spawnParticleCircle(x, y, z, radius, "heart", 20)` | -| Firework | `world.launchFirework(x, y, z, "red", "large_ball")` | -| Lightning | `world.strikeLightning(x, y, z)` | -| Explosion | `world.explode(x, y, z, power)` | -| Play sound (global) | `world.playSound("pling", pos, 1.0, 1.0)` | -| Play sound (per-player) | `player.playSound("pling", 1.0, 1.0)` | - -### Potion Effects - -| I want to... | Use this | -|-------------|----------| -| Apply an effect | `entity.addEffect("minecraft:speed", duration, level, hideParticles)` | -| Clear all effects | `entity.clearEffects()` | - -### Event System - -| I want to... | Use this | -|-------------|----------| -| Run every tick | `world.onTick((info) => { ... })` | -| On player join | `world.onPlayerJoin((entity, tick) => { ... })` | -| On player leave | `world.onPlayerLeave((entity, tick) => { ... })` | -| On entity death | `world.onEntityDeath((entity, killer, tick) => { ... })` | -| On entity damaged | `world.onEntityDamage((entity, amount, source, attacker, tick) => { ... })` | -| On right-click entity | `world.onInteract((entity, target, tick) => { ... })` | -| On right-click block | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | -| On button pressed | `world.onButtonPressed((entity, button, tick) => { ... })` | -| On player respawn | `world.onPlayerRespawn((entity, tick) => { ... })` | -| Run once after delay | `setTimeout(() => { ... }, ticks)` | -| Run on interval | `setInterval(() => { ... }, ticks)` | -| Cancel event listener | `token.cancel()` | -| Check if active | `token.active()` | - -### Data Persistence - -| I want to... | Use this | -|-------------|----------| -| Read/write JSON data | `storage.getDataStorage("key")` | -| SQL query | `db.sql("SELECT ...")` | -| SQL write | `db.sql("INSERT INTO ...")` | - -### HTTP Requests - -| I want to... | Use this | -|-------------|----------| -| GET request | `http.fetch("https://...")` | -| POST JSON | `http.fetch(url, { method: "POST", headers, body })` | -| Parse JSON | `resp.json()` or `{ responseType: "json" }` | -| Read text | `resp.text()` | -| Set timeout | `http.fetch(url, { timeout: 5000 })` | - -### Game Systems - -| I want to... | Use this | -|-------------|----------| -| Create scoreboard | `world.addScoreboard("name")` | -| Set score | `world.setScore("player", "board", 10)` | -| Display scoreboard | `world.showScoreboard("sidebar", "name")` | -| Show BossBar | `world.showBossbar("id", "title", 0.5, "red")` | -| Create team | `world.createTeam("teamName", "color")` | -| Join team | `world.joinTeam(entity, "teamName")` | -| Set world border | `world.borderSize = 500` | -| Shrink border | `world.shrinkBorder(100, 60)` | -| Change time | `world.time = 6000` | -| Set weather | `world.rainDensity = 0` / `world.clearWeather()` | -| Change game rule | `world.setGameRule("keepInventory", true)` | - -### Math Tools - -| I want to... | Use this | -|-------------|----------| -| 3D coordinate | `new GameVector3(x, y, z)` | -| Vector math | `v.add(other)`, `v.scale(n)`, `v.length()` | -| Bounding box | `new GameBounds3(min, max)` | -| Color | `new GameRGBColor(r, g, b)` / `new GameRGBAColor(r, g, b, a)` | - -### Cross-Script Messaging - -| I want to... | Use this | -|-------------|----------| -| Send to another script | `world.sendMessage("projectName", data)` | -| Receive from other scripts | `world.onMessage((from, data) => { ... })` | - ---- - -## Global Objects - -| Object | Type | Description | -|--------|------|-------------| -| `world` | Server | World control, see [world_en.md](world_en.md) | -| `voxels` | Server | Block operations, see [voxels_en.md](voxels_en.md) | -| `entity` | Server value | Entity wrapper (from callbacks or `world.spawnEntity`), see [entity_en.md](entity_en.md) | -| `player` | Server value | Player wrapper (via `entity.player`), see [player_en.md](player_en.md) | -| `storage` | Both | Data persistence, see [storage_en.md](storage_en.md) | -| `db` | Both | SQLite database, see [database_en.md](database_en.md) | -| `http` | Both | HTTP requests, see [http_en.md](http_en.md) | -| `audio` | Client | Client sound, music, volume control, see [client_en.md](client_en.md) | -| `client` | Client | Client lifecycle, see [client_en.md](client_en.md) | -| `input` | Client | Client keyboard input, see [client_en.md](client_en.md) | -| `ui` | Client | Client screen UI, see [client_en.md](client_en.md) | -| `chat` | Client | Client chat send/receive, see [client_en.md](client_en.md) | -| `gui` | Client | Custom container GUI, see [client_en.md](client_en.md) | -| `remoteChannel` | Both | Server↔client event channel, see [server_en.md](server_en.md) / [client_en.md](client_en.md) | -| `registries` | Server | Custom blocks, items & sounds (compiled mode), see [registries_en.md](registries_en.md) | -| `console` | Both | Console logging (`log`/`warn`/`error`/`debug`) | -| `GameVector3` | Both | 3D vector, see [math_en.md](math_en.md) | -| `GameBounds3` | Both | Bounding box, see [math_en.md](math_en.md) | -| `GameRGBColor` | Both | RGB color, see [math_en.md](math_en.md) | -| `GameRGBAColor` | Both | RGBA color, see [math_en.md](math_en.md) | -| `GameQuaternion` | Both | Quaternion, see [math_en.md](math_en.md) | - -## API Legend - -| Label | Meaning | -|-------|---------| -| ✅ **Box3 API** | Originates from the Box3 platform; naming and semantics match Box3 | -| ⬆ **MC Extension** | Not in original Box3; added using Minecraft-specific features | - -## Documentation Style - -Each API document should follow this structure. Use the same style when adding future APIs: - -1. State the runtime at the top: server, client, or shared. -2. List globals and core concepts before method details. -3. Use `object.method(parameters)` for method headings. -4. Document parameters in tables with name, type, default, and meaning. -5. Prefer TypeScript/JavaScript examples and identify server or client context when needed. -6. For cross-side APIs, always state the direction: server → client, or client → server. -7. If docs and types disagree, treat `types/server/index.d.ts` and `types/client/index.d.ts` as the source of truth, then update the docs. - -## Detailed Document Index - -| Document | Content | -|----------|---------| -| [server_en.md](server_en.md) | Server API overview: runtime boundary, globals, events, players/entities, blocks, data, cross-side communication | -| [world_en.md](world_en.md) | World state, events, scoreboard, bossbar, teams, border, particles, fireworks, lightning, sounds | -| [entity_en.md](entity_en.md) | Entity properties, AI, equipment, potion effects, pathfinding, tags, collisions | -| [player_en.md](player_en.md) | Inventory, messaging, flight, game mode, teleport, commands, XP | -| [voxels_en.md](voxels_en.md) | Block read/write, region fill, spawner control | -| [storage_en.md](storage_en.md) | Persistent data storage | -| [database_en.md](database_en.md) | SQLite database API | -| [http_en.md](http_en.md) | HTTP request API | -| [client_en.md](client_en.md) | Client API: lifecycle, keyboard, screen UI, chat, GUI, remoteChannel, client-side storage | -| [registries_en.md](registries_en.md) | Custom blocks, items & sounds (blocks.json, items.json, sounds.json, creativeTabs.json) | -| [math_en.md](math_en.md) | GameVector3, GameBounds3, GameRGBColor, GameRGBAColor, GameQuaternion | -| [commands_en.md](commands_en.md) | `/box3script` command reference | - -## File Modules — TypeScript Build Pipeline - -Projects created with `/box3script create` come with a complete TS build environment: - -``` -config/box3/script/mygame/ -├── package.json ← esbuild + Babel + @babel/preset-typescript -├── tsconfig.base.json ← Shared TS compiler options -├── tsconfig.server.json ← Server-side TS config -├── tsconfig.client.json ← Client-side TS config -├── build.mjs ← Babel TS→JS → esbuild bundle → dist/ -├── types/ -│ ├── shared.d.ts ← Shared types (server & client) -│ ├── server/ -│ │ ├── index.d.ts ← Server type entry point -│ │ ├── server.d.ts -│ │ ├── entity.d.ts -│ │ ├── player.d.ts -│ │ ├── world.d.ts -│ │ └── voxels.d.ts -│ └── client/ -│ ├── index.d.ts ← Client type entry point -│ ├── client.d.ts -│ ├── audio.d.ts -│ ├── input.d.ts -│ ├── ui.d.ts -│ ├── chat.d.ts -│ └── gui.d.ts -├── src/ -│ ├── server/ -│ │ ├── app.ts ← Server entry point -│ │ └── ... -│ └── client/ -│ ├── app.ts ← Client entry point -│ └── ... -└── dist/ - ├── server.js ← Server compiled output - ├── client.js ← Client compiled output - └── -.jar ← Standalone JAR (/box3script compile) -``` - -Run `npm run build` to build. Use `/box3script watch` to enable file watching for auto hot-reload. - -## Deployment - -When ready to distribute, compile your script into a **standalone JAR mod** that runs on any NeoForge server alongside Box3JS: - -``` -/box3script compile -``` - -Outputs `-.jar` (metadata read from `package.json`: name, displayName, version, description, author, license, homepage, logoFile). Drop it into `mods/` and start the server. - -See [full command reference →](commands_en.md#box3script-compile-project) - -## Tick Conversion - -| Duration | Ticks | -|----------|-------| -| 1 second | 20 | -| 5 seconds | 100 | -| 30 seconds | 600 | -| 1 minute | 1200 | -| 5 minutes | 6000 | - -## Deep Dive - -| Doc | Content | -|-----|---------| -| [Quick Start](../guide/getting-started_en.md) | Setup, first script, dev cycle, debugging, deployment | -| [Architecture](../guide/architecture_en.md) | Rhino engine, scopes, event callbacks, build pipeline, network | -| [JS vs Java](../guide/js-vs-java_en.md) | Box3JS scripting vs native Java modding comparison | - -## Tutorials - -Learn Box3JS from scratch with the tutorial series in `docs/tutorial/`: - -| Tutorial | Content | -|----------|---------| -| [01-basics.md](../tutorial/01-basics.md) | From zero: first script, chat commands, timers | -| [02-player-items.md](../tutorial/02-player-items.md) | Player controls: teleport, items, potion effects, game modes | -| [03-events-entities.md](../tutorial/03-events-entities.md) | Events & entities: AI, combat, patrols | -| [04-advanced-systems.md](../tutorial/04-advanced-systems.md) | Advanced: scoreboard, BossBar, teams, world border | -| [05-examples.md](../tutorial/05-examples.md) | Real-world: PvP arena, effects, fireworks, wave mobs | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client.md b/Box3JS-NeoForge-1.21.1/docs/api/client.md index 8eb5a28..56c528a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client.md @@ -17,8 +17,9 @@ | `gui` | `GameGUI` | 自定义容器 GUI 界面 | | `remoteChannel` | `RemoteChannel` | 客户端 ↔ 服务端事件通信 | -> **前置条件:** 客户端必须安装 Box3JS mod,服务端必须启用该项目的客户端脚本并通过网络自动下发。 -> 客户端脚本放在 `src/client/` 目录下,服务端脚本放在 `src/server/` 目录下。客户端类型入口是 `types/client/index.d.ts`,不会包含服务端 `world` / `voxels` API。 +::: info 前置条件 +客户端必须安装 Box3JS mod,服务端必须启用该项目的客户端脚本并通过网络自动下发。客户端脚本放在 `src/client/` 目录下,服务端脚本放在 `src/server/` 目录下。客户端类型入口是 `types/client/index.d.ts`,不会包含服务端 `world` / `voxels` API。 +::: 客户端脚本不能直接修改服务端世界。需要改变方块、玩家、实体或计分板时,应发送事件给服务端: @@ -114,7 +115,9 @@ const token = client.onTick(() => { // token.cancel(); ``` -> **注意:** 服务端也有 `world.onTick()`,但参数为 `TickInfo` 对象。客户端 `client.onTick()` 无参数。 +::: info 注意 +服务端也有 `world.onTick()`,但参数为 `TickInfo` 对象。客户端 `client.onTick()` 无参数。 +::: ### client.getFPS() @@ -517,8 +520,9 @@ remoteChannel.onClientEvent((event) => { }); ``` -> 服务端对应 API 为 `remoteChannel.sendClientEvent()` / `broadcastClientEvent()` / `onServerEvent()`。 -> 详见 `server.d.ts` 中的类型声明。 +::: info +服务端对应 API 为 `remoteChannel.sendClientEvent()` / `broadcastClientEvent()` / `onServerEvent()`。详见 `server.d.ts` 中的类型声明。 +::: ## storage — 客户端存储 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/commands.md b/Box3JS-NeoForge-1.21.1/docs/api/commands.md index 6c89a4b..8403f50 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/commands.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/commands.md @@ -8,13 +8,13 @@ 显示项目状态概览。 -``` +```js /box3script ``` 输出示例: -``` +```text ══ Box3JS Script Engine ══ Watch: ● Active Sandbox: ● 1 project(s) @@ -39,7 +39,7 @@ 创建新的 TypeScript 脚本项目。生成完整的 TS 脚手架,默认**禁用**。 -``` +```js /box3script create mygame ``` @@ -56,7 +56,7 @@ npm install && npm run build 启用并加载项目。**不带参数** = 启用全部。**带项目名** = 只启用指定项目。**`all`** = 显式启用全部。 -``` +```js /box3script start # 启用全部 /box3script start all # 启用全部(同无参数) /box3script start mygame # 只启用 mygame @@ -66,7 +66,7 @@ npm install && npm run build 禁用并卸载项目。**不带参数** = 禁用全部。**带项目名** = 只禁用指定项目。**`all`** = 显式禁用全部。 -``` +```js /box3script stop # 禁用全部 /box3script stop all # 禁用全部(同无参数) /box3script stop mygame # 只禁用 mygame @@ -76,7 +76,7 @@ npm install && npm run build 重载脚本。**不带参数** = 停止全部,重新加载所有已启用项目。**带项目名** = 重载指定项目。 -``` +```js /box3script reload # 重载全部已启用项目 /box3script reload mygame # 只重载 mygame ``` @@ -87,7 +87,7 @@ npm install && npm run build 开启/关闭文件监听。监听所有项目的 `dist/` 目录,`.js` 文件变化时自动重载对应项目。 -``` +```js /box3script watch # 切换 开/关 ``` @@ -95,7 +95,7 @@ npm install && npm run build 切换沙盒模式。开启后自动追踪该项目所有的方块/实体/世界状态变更,关闭时回滚并显示摘要。 -``` +```js /box3script sandbox mygame # 切换 开/关 ``` @@ -103,7 +103,7 @@ npm install && npm run build 典型工作流: -``` +```js /box3script sandbox mygame # 开启沙盒 /box3script start mygame # 启用项目 # ... 测试 ... @@ -112,19 +112,25 @@ npm install && npm run build /box3script sandbox mygame # 关闭沙盒 → 回滚全部修改 ``` -> **注意:** 沙盒仅追踪通过脚本 API 修改的方块(`setVoxel`/`setVoxelId`/`fillVoxel`),手动挖掘不受影响。 +::: warning +沙盒仅追踪通过脚本 API 修改的方块(`setVoxel`/`setVoxelId`/`fillVoxel`),手动挖掘不受影响。 +::: ### `/box3script compile ` 将脚本项目编译为**轻量独立 JAR 模组**(~50KB),依赖 Box3JS 模组提供 Rhino 运行时和 API 绑定。 -``` +```js /box3script compile mygame ``` -> **依赖:** 脚本 JAR 不包含 Rhino 或 Box3JS API 类,需将 Box3JS 模组(`box3js`)一同放入 `mods/`。 +::: warning 依赖 +脚本 JAR 不包含 Rhino 或 Box3JS API 类,需将 Box3JS 模组(`box3js`)一同放入 `mods/`。 +::: -> **自定义注册表:** 如果存在 `registries/blocks.json`、`items.json`、`sounds.json`、`creativeTabs.json` 和 `assets/`,编译时会自动注册方块/物品/音效,并将资源打包进 JAR。客户端也需安装该 JAR 才能正常渲染。详见 [registries.md](registries.md)。 +::: info 自定义注册表 +如果存在 `registries/blocks.json`、`items.json`、`sounds.json`、`creativeTabs.json` 和 `assets/`,编译时会自动注册方块/物品/音效,并将资源打包进 JAR。客户端也需安装该 JAR 才能正常渲染。详见 [registries.md](registries.md)。 +::: 编译时**从 `package.json` 读取以下字段**写入 `neoforge.mods.toml`: @@ -140,7 +146,9 @@ npm install && npm run build | `bugs.url` | `issueTrackerURL` | 问题反馈链接 | | `logoFile` | `logoFile` | 模组图标(项目中的 PNG 路径,打包为 `logo.png`) | -> **`logoFile` 使用说明:** 填写项目根目录下的 PNG 文件相对路径(如 `"logoFile": "logo.png"`),编译时自动打包为 JAR 根目录的 `logo.png`,无需在 `neoforge.mods.toml` 中手动配置。NeoForge 建议尺寸 128×128 或 256×256,仅支持 PNG 格式。不填则使用默认模组图标。 +::: tip logoFile 使用说明 +填写项目根目录下的 PNG 文件相对路径(如 `"logoFile": "logo.png"`),编译时自动打包为 JAR 根目录的 `logo.png`,无需在 `neoforge.mods.toml` 中手动配置。NeoForge 建议尺寸 128×128 或 256×256,仅支持 PNG 格式。不填则使用默认模组图标。 +::: 输出文件名格式:`dist/-.jar`。编译在后台线程运行,不阻塞服务器 tick,完成后聊天栏通知输出路径。 @@ -151,7 +159,7 @@ npm install && npm run build **输出 JAR 内容:** -``` +```text mygame-1.0.0.jar ├── META-INF/neoforge.mods.toml ← 模组元数据(依赖 box3js) ├── logo.png ← 模组图标(如有指定) @@ -168,7 +176,7 @@ mygame-1.0.0.jar **部署:** 将脚本 JAR 与 Box3JS 模组一起放入 `mods/`: -``` +```text mods/ ├── box3js-1.0.0.jar ← Box3JS 主模组 └── mygame-1.0.0.jar ← 编译的脚本模组 @@ -185,7 +193,9 @@ mods/ | 热重载 | 支持 | 不支持(JAR 重启才生效) | | 适用场景 | 开发调试 | 分发部署 | -> **注意:** 编译后的 JAR 是标准 NeoForge mod,由 NeoForge mod loader 管理,**不受** `/box3script start/stop/reload` 控制。多个编译 JAR 可同时放入 `mods/`,各自独立运行,互不干扰。 +::: warning +编译后的 JAR 是标准 NeoForge mod,由 NeoForge mod loader 管理,**不受** `/box3script start/stop/reload` 控制。多个编译 JAR 可同时放入 `mods/`,各自独立运行,互不干扰。 +::: ## 配置文件 @@ -200,7 +210,7 @@ mods/ ## 脚本目录结构 -``` +```text config/box3/ ├── scripts.json ← 项目开关配置 ├── script/ ← 脚本目录 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/database.md b/Box3JS-NeoForge-1.21.1/docs/api/database.md index 5bcf02f..6bb703c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/database.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/database.md @@ -2,7 +2,9 @@ Box3JS 通过全局 `db` 对象提供 SQLite 数据库能力,无需手动管理连接。 -> **运行环境:** 服务端和客户端都可用。服务端数据库位于 `config/box3/data/.db`;客户端数据库位于本地游戏目录的 `box3/client-db/.db`。两端数据库互不共享,需要同步数据时请使用 `remoteChannel`。 +::: info 运行环境 +服务端和客户端都可用。服务端数据库位于 `config/box3/data/.db`;客户端数据库位于本地游戏目录的 `box3/client-db/.db`。两端数据库互不共享,需要同步数据时请使用 `remoteChannel`。 +::: ## 依赖与降级行为 @@ -16,10 +18,10 @@ db API requires SQLite JDBC driver. Install the minecraft-sqlite-jdbc mod, then 安装 `minecraft-sqlite-jdbc` 并重启服务器后,`db` API 即可恢复可用。 -> **NeoForge 开发环境提示:** -> -> - 请将 `minecraft-sqlite-jdbc` 放到 `run/mods/`。 -> - 模组文件必须是 `.jar`(例如 `xxx.jar`),不要使用 `.zip`,否则不会被 NeoForge 加载。 +::: warning NeoForge 开发环境 +- 请将 `minecraft-sqlite-jdbc` 放到 `run/mods/`。 +- 模组文件必须是 `.jar`(例如 `xxx.jar`),不要使用 `.zip`,否则不会被 NeoForge 加载。 +::: ## `db.isAvailable()` @@ -279,15 +281,23 @@ db.sql( ); ``` -> **重要:只有值(值)用 `${}`,标识符(表名、列名)不能做绑定参数。** -> -> ```ts -> // ✅ 正确 — 表名硬编码在模板字符串中 -> db.sql`SELECT * FROM players WHERE name = ${name}`; -> -> // ❌ 错误 — 表名不能用占位符,会报 SQL syntax error -> db.sql`SELECT * FROM ${table} WHERE name = ${name}`; -> ``` +::: warning +只有值用 `${}`,标识符(表名、列名)不能做绑定参数。 +::: + +```ts +// ✅ 正确 — 表名硬编码在模板字符串中 +db.sql`SELECT * FROM players WHERE name = ${name}`; +``` + +::: danger 常见错误 +表名不能用占位符,会报 SQL syntax error: + +```js +// ❌ 错误 +db.sql`SELECT * FROM ${table} WHERE name = ${name}`; +``` +::: ## Rhino 兼容性注意事项 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity.md b/Box3JS-NeoForge-1.21.1/docs/api/entity.md index 564e4f1..1ce6169 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity.md @@ -1,6 +1,6 @@ # entity — 实体 API -`entity` 代表 Minecraft 世界中的任意实体(怪物、动物、掉落物、玩家)。通过 `world.spawnEntity()`、`world.createEntity()`、`world.querySelector()`、`world.searchBox()`、`world.entitiesInRadius()` 或事件回调参数获取。 +`entity` 代表 Minecraft 世界中的任意实体(怪物、动物、掉落物、玩家)。 通过 `entity.player` 可获取该实体对应的 `player` 对象(仅当是玩家时非 null)。 @@ -415,7 +415,9 @@ entity.setAttribute("minecraft:generic.knockback_resistance", 1.0); entity.setAttribute("minecraft:generic.armor", 10); ``` -> 注意:`maxHp` / `hp` / `walkSpeed` / `jumpPower` 等 Box3 便捷属性内部也使用这些 attribute,推荐优先使用便捷属性。仅当需要访问未封装的属性时才用 `setAttribute`。 +::: tip +`maxHp` / `hp` / `walkSpeed` / `jumpPower` 等 Box3 便捷属性内部也使用这些 attribute,推荐优先使用便捷属性。仅当需要访问未封装的属性时才用 `setAttribute`。 +::: ## 生命周期 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/http.md b/Box3JS-NeoForge-1.21.1/docs/api/http.md index 91c8eed..a1339f9 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/http.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/http.md @@ -1,8 +1,10 @@ # HTTP API -Box3JS 通过全局 `http` 对象提供 HTTP 请求能力,支持全部 HTTP 方法、超时、自定义请求头、自动解析、二进制上传,以及同步/异步两种调用方式。 +通过全局 `http` 对象发起 HTTP 请求。 -> **运行环境:** 服务端和客户端都可用。服务端同步请求会阻塞服务器 tick,避免在高频回调中执行长时间请求。客户端同步请求会阻塞客户端渲染/逻辑线程。**异步请求**(`async: true`)通过回调接收结果。 +::: info 运行环境 +服务端和客户端都可用。同步请求会阻塞当前线程,避免在高频回调中执行长时间请求。**异步请求**(`async: true`)通过回调接收结果。 +::: ## `http.fetch(url, options?)` @@ -27,9 +29,11 @@ Box3JS 通过全局 `http` 对象提供 HTTP 请求能力,支持全部 HTTP | `onResponse` | `function` | — | 异步请求成功回调,参数为 `GameHttpFetchResponse` | | `onError` | `function` | — | 异步请求失败回调,参数为错误信息字符串 | -> 设置 `responseType` 后,解析结果可直接通过 `resp.data` 获取,无需手动调 `resp.json()` 等。 -> -> 异步模式下 `fetch()` 返回 `null`,结果通过回调接收。 +::: info +设置 `responseType` 后,解析结果可直接通过 `resp.data` 获取,无需手动调 `resp.json()` 等。 + +异步模式下 `fetch()` 返回 `null`,结果通过回调接收。 +::: ## GameHttpFetchResponse diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math.md b/Box3JS-NeoForge-1.21.1/docs/api/math.md index 5d730f9..5344dce 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/math.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md @@ -9,14 +9,14 @@ ### 构造 ```js -var v = new GameVector3(); // 零向量 (0, 0, 0) +var v = new GameVector3(); // 零向量 (0, 0, 0) var v = new GameVector3(x, y, z); // 指定坐标 ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| +| 属性 | 类型 | 说明 | +| ----- | -------- | ------------------------- | | `v.x` | `number` | X 分量 (东西方向),可读写 | | `v.y` | `number` | Y 分量 (上下方向),可读写 | | `v.z` | `number` | Z 分量 (南北方向),可读写 | @@ -25,57 +25,57 @@ var v = new GameVector3(x, y, z); // 指定坐标 #### 原地修改 (返回 this) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `v.set(x, y, z)` | `GameVector3` | 设置所有分量 | -| `v.copy(w)` | `GameVector3` | 从 `w` 复制所有分量 | -| `v.addEq(w)` | `GameVector3` | 原地加法:`v += w` | -| `v.subEq(w)` | `GameVector3` | 原地减法:`v -= w` | -| `v.mulEq(w)` | `GameVector3` | 原地逐分量乘法:`v.x *= w.x` … | -| `v.divEq(w)` | `GameVector3` | 原地逐分量除法,除以 0 跳过该分量 | -| `v.scaleEq(n)` | `GameVector3` | 原地标量乘法:`v.x *= n` … | -| `v.negEq()` | `GameVector3` | 原地取反:`v = -v` | +| 方法 | 返回值 | 说明 | +| ---------------- | ------------- | --------------------------------- | +| `v.set(x, y, z)` | `GameVector3` | 设置所有分量 | +| `v.copy(w)` | `GameVector3` | 从 `w` 复制所有分量 | +| `v.addEq(w)` | `GameVector3` | 原地加法:`v += w` | +| `v.subEq(w)` | `GameVector3` | 原地减法:`v -= w` | +| `v.mulEq(w)` | `GameVector3` | 原地逐分量乘法:`v.x *= w.x` … | +| `v.divEq(w)` | `GameVector3` | 原地逐分量除法,除以 0 跳过该分量 | +| `v.scaleEq(n)` | `GameVector3` | 原地标量乘法:`v.x *= n` … | +| `v.negEq()` | `GameVector3` | 原地取反:`v = -v` | #### 创建新向量 (不修改自身) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `v.clone()` | `GameVector3` | 深拷贝,返回相同值的独立新向量 | -| `v.add(w)` | `GameVector3` | 向量加法:`v + w` | -| `v.sub(w)` | `GameVector3` | 向量减法:`v - w` | -| `v.mul(w)` | `GameVector3` | 逐分量乘法 | -| `v.div(w)` | `GameVector3` | 逐分量除法,除以 0 得 0 | -| `v.scale(n)` | `GameVector3` | 标量乘法:每个分量乘以 `n` | -| `v.cross(w)` | `GameVector3` | 叉积:`v × w` | -| `v.normalize()` | `GameVector3` | 单位化,零向量返回 `(0,0,0)` | -| `v.lerp(w, t)` | `GameVector3` | 线性插值:`t=0` 为自身,`t=1` 为 `w` | -| `v.towards(w)` | `GameVector3` | 指向 `w` 的方向向量 (已单位化) | -| `v.max(w)` | `GameVector3` | 逐分量取较大值 | -| `v.min(w)` | `GameVector3` | 逐分量取较小值 | -| `v.neg()` | `GameVector3` | 取反:`-v` | -| `v.moveTowards(target, maxDelta)` | `GameVector3` | 向目标移动不超过 `maxDelta` 距离 | -| `v.floor()` | `GameVector3` | 逐分量向下取整 | -| `v.ceil()` | `GameVector3` | 逐分量向上取整 | -| `v.clampLength(max)` | `GameVector3` | 限制长度至 `max`,超长则等比缩放 | +| 方法 | 返回值 | 说明 | +| --------------------------------- | ------------- | ------------------------------------ | +| `v.clone()` | `GameVector3` | 深拷贝,返回相同值的独立新向量 | +| `v.add(w)` | `GameVector3` | 向量加法:`v + w` | +| `v.sub(w)` | `GameVector3` | 向量减法:`v - w` | +| `v.mul(w)` | `GameVector3` | 逐分量乘法 | +| `v.div(w)` | `GameVector3` | 逐分量除法,除以 0 得 0 | +| `v.scale(n)` | `GameVector3` | 标量乘法:每个分量乘以 `n` | +| `v.cross(w)` | `GameVector3` | 叉积:`v × w` | +| `v.normalize()` | `GameVector3` | 单位化,零向量返回 `(0,0,0)` | +| `v.lerp(w, t)` | `GameVector3` | 线性插值:`t=0` 为自身,`t=1` 为 `w` | +| `v.towards(w)` | `GameVector3` | 指向 `w` 的方向向量 (已单位化) | +| `v.max(w)` | `GameVector3` | 逐分量取较大值 | +| `v.min(w)` | `GameVector3` | 逐分量取较小值 | +| `v.neg()` | `GameVector3` | 取反:`-v` | +| `v.moveTowards(target, maxDelta)` | `GameVector3` | 向目标移动不超过 `maxDelta` 距离 | +| `v.floor()` | `GameVector3` | 逐分量向下取整 | +| `v.ceil()` | `GameVector3` | 逐分量向上取整 | +| `v.clampLength(max)` | `GameVector3` | 限制长度至 `max`,超长则等比缩放 | #### 数值计算 -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `v.dot(w)` | `number` | 点积 (内积):`v · w` | -| `v.mag()` | `number` | 向量长度 (模) | -| `v.sqrMag()` | `number` | 长度平方,比 `mag()` 更快 | -| `v.distance(w)` | `number` | 与 `w` 的欧几里得距离 | -| `v.angle(w)` | `number` | 与 `w` 的夹角 (弧度, 0–π) | +| 方法 | 返回值 | 说明 | +| ------------------ | -------- | --------------------------------------- | +| `v.dot(w)` | `number` | 点积 (内积):`v · w` | +| `v.mag()` | `number` | 向量长度 (模) | +| `v.sqrMag()` | `number` | 长度平方,比 `mag()` 更快 | +| `v.distance(w)` | `number` | 与 `w` 的欧几里得距离 | +| `v.angle(w)` | `number` | 与 `w` 的夹角 (弧度, 0–π) | | `v.sqrDistance(w)` | `number` | 与 `w` 的距离平方,比 `distance()` 更快 | #### 比较 -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `v.equals(w)` | `boolean` | 近似相等,容差 1e-6 | -| `v.exactEquals(w)` | `boolean` | 精确相等,分量完全一致 | -| `v.isZero()` | `boolean` | 是否为 (接近) 零向量,容差 1e-6 | +| 方法 | 返回值 | 说明 | +| ------------------ | --------- | ------------------------------- | +| `v.equals(w)` | `boolean` | 近似相等,容差 1e-6 | +| `v.exactEquals(w)` | `boolean` | 精确相等,分量完全一致 | +| `v.isZero()` | `boolean` | 是否为 (接近) 零向量,容差 1e-6 | ```js var pos = new GameVector3(0, 100, 0); @@ -93,8 +93,8 @@ var angle = pos.angle(target); // 弧度 // 比较 var a = new GameVector3(1, 2, 3); var b = new GameVector3(1.0000001, 2.0000001, 3.0000001); -a.equals(b); // true (容差内) -a.exactEquals(b); // false +a.equals(b); // true (容差内) +a.exactEquals(b); // false // 传送实体 (LiveVec3) entity.position.set(0, 100, 0); @@ -117,8 +117,6 @@ var v = new GameVector3(1, 2, 3); v.toString(); // "GameVector3(1.0, 2.0, 3.0)" ``` ---- - ## GameBounds3 轴对齐包围盒 (AABB),由两个对角顶点 `lo` (最小角) 和 `hi` (最大角) 定义。 @@ -128,41 +126,41 @@ v.toString(); // "GameVector3(1.0, 2.0, 3.0)" ```js var bounds = new GameBounds3( new GameVector3(-1, 0, -1), // lo (最小角) - new GameVector3(1, 2, 1), // hi (最大角) + new GameVector3(1, 2, 1), // hi (最大角) ); ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| +| 属性 | 类型 | 说明 | +| ----------- | ------------- | ----------------------------------- | | `bounds.lo` | `GameVector3` | 最小角 (三个分量均为最小值),可读写 | | `bounds.hi` | `GameVector3` | 最大角 (三个分量均为最大值),可读写 | ### 实例方法 -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `bounds.set(lox, loy, loz, hix, hiy, hiz)` | `GameBounds3` | 原地设置所有边界,返回自身 | -| `bounds.copy(b)` | `GameBounds3` | 原地复制 `b` 的值,返回自身 | -| `bounds.intersects(other)` | `boolean` | 是否与 `other` 相交 | -| `bounds.intersect(other)` | `GameBounds3 \| null` | 计算交集包围盒,不相交返回 `null` | -| `bounds.contains(v)` | `boolean` | 点 `v` 是否在包围盒内 (含边界) | -| `bounds.containsBounds(b)` | `boolean` | 是否完全包含另一个包围盒 `b` | -| `bounds.center()` | `GameVector3` | 包围盒中心点 | -| `bounds.size()` | `GameVector3` | 包围盒尺寸 (宽, 高, 深) | -| `bounds.expand(delta)` | `GameBounds3` | 各面向外扩展 `delta`,返回新包围盒 | -| `bounds.expandEq(delta)` | `GameBounds3` | 原地各面向外扩展 `delta`,返回自身 | -| `bounds.growToInclude(v)` | `GameBounds3` | 原地扩展以包含点 `v`,返回自身 | -| `bounds.closestPoint(v)` | `GameVector3` | 包围盒上离点 `v` 最近的点 | -| `bounds.move(offset)` | `GameBounds3` | 平移 `offset`,返回新包围盒 | -| `bounds.moveEq(offset)` | `GameBounds3` | 原地平移 `offset`,返回自身 | +| 方法 | 返回值 | 说明 | +| ------------------------------------------ | --------------------- | ---------------------------------- | +| `bounds.set(lox, loy, loz, hix, hiy, hiz)` | `GameBounds3` | 原地设置所有边界,返回自身 | +| `bounds.copy(b)` | `GameBounds3` | 原地复制 `b` 的值,返回自身 | +| `bounds.intersects(other)` | `boolean` | 是否与 `other` 相交 | +| `bounds.intersect(other)` | `GameBounds3 \| null` | 计算交集包围盒,不相交返回 `null` | +| `bounds.contains(v)` | `boolean` | 点 `v` 是否在包围盒内 (含边界) | +| `bounds.containsBounds(b)` | `boolean` | 是否完全包含另一个包围盒 `b` | +| `bounds.center()` | `GameVector3` | 包围盒中心点 | +| `bounds.size()` | `GameVector3` | 包围盒尺寸 (宽, 高, 深) | +| `bounds.expand(delta)` | `GameBounds3` | 各面向外扩展 `delta`,返回新包围盒 | +| `bounds.expandEq(delta)` | `GameBounds3` | 原地各面向外扩展 `delta`,返回自身 | +| `bounds.growToInclude(v)` | `GameBounds3` | 原地扩展以包含点 `v`,返回自身 | +| `bounds.closestPoint(v)` | `GameVector3` | 包围盒上离点 `v` 最近的点 | +| `bounds.move(offset)` | `GameBounds3` | 平移 `offset`,返回新包围盒 | +| `bounds.moveEq(offset)` | `GameBounds3` | 原地平移 `offset`,返回自身 | ### 静态方法 ```js // 从 GameVector3 数组创建最小包围盒 -var points = [new GameVector3(0,0,0), new GameVector3(5,10,3)]; +var points = [new GameVector3(0, 0, 0), new GameVector3(5, 10, 3)]; var box = GameBounds3.fromPoints(points); // 返回 GameBounds3 或 null ``` @@ -182,8 +180,6 @@ if (bounds.contains(player.position)) { } ``` ---- - ## GameRGBColor RGB 颜色,三个通道范围 0.0–1.0。 @@ -191,15 +187,15 @@ RGB 颜色,三个通道范围 0.0–1.0。 ### 构造 ```js -var red = new GameRGBColor(1, 0, 0); -var blue = new GameRGBColor(0, 0, 1); -var gray = new GameRGBColor(0.5, 0.5, 0.5); +var red = new GameRGBColor(1, 0, 0); +var blue = new GameRGBColor(0, 0, 1); +var gray = new GameRGBColor(0.5, 0.5, 0.5); ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| +| 属性 | 类型 | 说明 | +| --------- | -------- | ---------------------- | | `color.r` | `number` | 红色通道 (0–1),可读写 | | `color.g` | `number` | 绿色通道 (0–1),可读写 | | `color.b` | `number` | 蓝色通道 (0–1),可读写 | @@ -208,29 +204,29 @@ var gray = new GameRGBColor(0.5, 0.5, 0.5); #### 原地修改 (返回 this) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `c.set(r, g, b)` | `GameRGBColor` | 设置所有通道 | -| `c.copy(o)` | `GameRGBColor` | 从另一个颜色复制所有通道 | -| `c.addEq(o)` | `GameRGBColor` | 原地加法:`c += o` | -| `c.subEq(o)` | `GameRGBColor` | 原地减法:`c -= o` | -| `c.mulEq(o)` | `GameRGBColor` | 原地逐通道乘法 | -| `c.divEq(o)` | `GameRGBColor` | 原地逐通道除法,除以 0 跳过该通道 | -| `c.scaleEq(n)` | `GameRGBColor` | 原地标量乘法:每个通道乘以 `n` | +| 方法 | 返回值 | 说明 | +| ---------------- | -------------- | --------------------------------- | +| `c.set(r, g, b)` | `GameRGBColor` | 设置所有通道 | +| `c.copy(o)` | `GameRGBColor` | 从另一个颜色复制所有通道 | +| `c.addEq(o)` | `GameRGBColor` | 原地加法:`c += o` | +| `c.subEq(o)` | `GameRGBColor` | 原地减法:`c -= o` | +| `c.mulEq(o)` | `GameRGBColor` | 原地逐通道乘法 | +| `c.divEq(o)` | `GameRGBColor` | 原地逐通道除法,除以 0 跳过该通道 | +| `c.scaleEq(n)` | `GameRGBColor` | 原地标量乘法:每个通道乘以 `n` | #### 创建新颜色 (不修改自身) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `c.clone()` | `GameRGBColor` | 深拷贝 | -| `c.add(o)` | `GameRGBColor` | 逐通道加法 | -| `c.sub(o)` | `GameRGBColor` | 逐通道减法 | -| `c.mul(o)` | `GameRGBColor` | 逐通道乘法 | -| `c.div(o)` | `GameRGBColor` | 逐通道除法,除以 0 得 0 | +| 方法 | 返回值 | 说明 | +| -------------- | -------------- | ------------------------------------ | +| `c.clone()` | `GameRGBColor` | 深拷贝 | +| `c.add(o)` | `GameRGBColor` | 逐通道加法 | +| `c.sub(o)` | `GameRGBColor` | 逐通道减法 | +| `c.mul(o)` | `GameRGBColor` | 逐通道乘法 | +| `c.div(o)` | `GameRGBColor` | 逐通道除法,除以 0 得 0 | | `c.lerp(o, t)` | `GameRGBColor` | 线性插值:`t=0` 为自身,`t=1` 为 `o` | -| `c.scale(n)` | `GameRGBColor` | 标量乘法:每个通道乘以 `n` | -| `c.equals(o)` | `boolean` | 近似相等,容差 1e-6 | -| `c.toRGBA()` | `string` | 转为 CSS 格式:`"rgba(r,g,b,1.0)"` | +| `c.scale(n)` | `GameRGBColor` | 标量乘法:每个通道乘以 `n` | +| `c.equals(o)` | `boolean` | 近似相等,容差 1e-6 | +| `c.toRGBA()` | `string` | 转为 CSS 格式:`"rgba(r,g,b,1.0)"` | ### 静态方法 @@ -244,8 +240,6 @@ var randomColor = GameRGBColor.random(); // 每个通道 0–1 随机值 new GameRGBColor(1, 0.5, 0).toString(); // "GameRGBColor(1.0, 0.5, 0.0)" ``` ---- - ## GameRGBAColor 带 Alpha 通道的颜色,四个分量范围 0.0–1.0。 @@ -254,45 +248,45 @@ new GameRGBColor(1, 0.5, 0).toString(); // "GameRGBColor(1.0, 0.5, 0.0)" ```js var semiRed = new GameRGBAColor(1, 0, 0, 0.5); -var opaque = new GameRGBAColor(0, 1, 0, 1.0); +var opaque = new GameRGBAColor(0, 1, 0, 1.0); ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| -| `color.r` | `number` | 红色通道 (0–1),可读写 | -| `color.g` | `number` | 绿色通道 (0–1),可读写 | -| `color.b` | `number` | 蓝色通道 (0–1),可读写 | +| 属性 | 类型 | 说明 | +| --------- | -------- | ---------------------------- | +| `color.r` | `number` | 红色通道 (0–1),可读写 | +| `color.g` | `number` | 绿色通道 (0–1),可读写 | +| `color.b` | `number` | 蓝色通道 (0–1),可读写 | | `color.a` | `number` | Alpha 不透明度 (0–1),可读写 | ### 实例方法 #### 原地修改 (返回 this) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `c.set(r, g, b, a)` | `GameRGBAColor` | 设置所有四个通道 | -| `c.copy(o)` | `GameRGBAColor` | 从另一个 RGBA 颜色复制所有通道 | -| `c.addEq(o)` | `GameRGBAColor` | 原地加法 | -| `c.subEq(o)` | `GameRGBAColor` | 原地减法 | -| `c.mulEq(o)` | `GameRGBAColor` | 原地逐通道乘法 | -| `c.divEq(o)` | `GameRGBAColor` | 原地逐通道除法,除以 0 跳过该通道 | -| `c.scaleEq(n)` | `GameRGBAColor` | 原地标量乘法:每个通道乘以 `n` | +| 方法 | 返回值 | 说明 | +| ------------------- | --------------- | --------------------------------- | +| `c.set(r, g, b, a)` | `GameRGBAColor` | 设置所有四个通道 | +| `c.copy(o)` | `GameRGBAColor` | 从另一个 RGBA 颜色复制所有通道 | +| `c.addEq(o)` | `GameRGBAColor` | 原地加法 | +| `c.subEq(o)` | `GameRGBAColor` | 原地减法 | +| `c.mulEq(o)` | `GameRGBAColor` | 原地逐通道乘法 | +| `c.divEq(o)` | `GameRGBAColor` | 原地逐通道除法,除以 0 跳过该通道 | +| `c.scaleEq(n)` | `GameRGBAColor` | 原地标量乘法:每个通道乘以 `n` | #### 创建新颜色 (不修改自身) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `c.clone()` | `GameRGBAColor` | 深拷贝 | -| `c.add(o)` | `GameRGBAColor` | 逐通道加法 | -| `c.sub(o)` | `GameRGBAColor` | 逐通道减法 | -| `c.mul(o)` | `GameRGBAColor` | 逐通道乘法 | -| `c.div(o)` | `GameRGBAColor` | 逐通道除法,除以 0 得 0 | -| `c.lerp(o, t)` | `GameRGBAColor` | 线性插值 | -| `c.scale(n)` | `GameRGBAColor` | 标量乘法:每个通道乘以 `n` | -| `c.equals(o)` | `boolean` | 近似相等,容差 1e-6 | -| `c.blendEq(rgb)` | `GameRGBColor` | Alpha 混合到 RGB 背景上,返回最终 RGB | +| 方法 | 返回值 | 说明 | +| ---------------- | --------------- | ------------------------------------- | +| `c.clone()` | `GameRGBAColor` | 深拷贝 | +| `c.add(o)` | `GameRGBAColor` | 逐通道加法 | +| `c.sub(o)` | `GameRGBAColor` | 逐通道减法 | +| `c.mul(o)` | `GameRGBAColor` | 逐通道乘法 | +| `c.div(o)` | `GameRGBAColor` | 逐通道除法,除以 0 得 0 | +| `c.lerp(o, t)` | `GameRGBAColor` | 线性插值 | +| `c.scale(n)` | `GameRGBAColor` | 标量乘法:每个通道乘以 `n` | +| `c.equals(o)` | `boolean` | 近似相等,容差 1e-6 | +| `c.blendEq(rgb)` | `GameRGBColor` | Alpha 混合到 RGB 背景上,返回最终 RGB | ### toString @@ -303,12 +297,10 @@ new GameRGBAColor(1, 0, 0, 0.5).toString(); // "GameRGBAColor(1.0, 0.0, 0.0, 0.5 ```js // Alpha 混合 var fg = new GameRGBAColor(1, 0, 0, 0.5); // 半透明红 -var bg = new GameRGBColor(1, 1, 1); // 白色背景 -var result = fg.blendEq(bg); // 得到混合后的 RGB 颜色 +var bg = new GameRGBColor(1, 1, 1); // 白色背景 +var result = fg.blendEq(bg); // 得到混合后的 RGB 颜色 ``` ---- - ## GameQuaternion 四元数,用于 3D 旋转。单位四元数 (模长=1) 表示纯旋转。 @@ -316,60 +308,60 @@ var result = fg.blendEq(bg); // 得到混合后的 RGB 颜色 ### 构造 ```js -var q = new GameQuaternion(); // 单位四元数 (1, 0, 0, 0) -var q = new GameQuaternion(w, x, y, z); // 指定分量 +var q = new GameQuaternion(); // 单位四元数 (1, 0, 0, 0) +var q = new GameQuaternion(w, x, y, z); // 指定分量 ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| +| 属性 | 类型 | 说明 | +| ----- | -------- | ----------------------- | | `q.w` | `number` | 实部 (标量分量),可读写 | -| `q.x` | `number` | 虚部 X 分量,可读写 | -| `q.y` | `number` | 虚部 Y 分量,可读写 | -| `q.z` | `number` | 虚部 Z 分量,可读写 | +| `q.x` | `number` | 虚部 X 分量,可读写 | +| `q.y` | `number` | 虚部 Y 分量,可读写 | +| `q.z` | `number` | 虚部 Z 分量,可读写 | ### 实例方法 #### 原地修改 (返回 this) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `q.set(w, x, y, z)` | `GameQuaternion` | 设置所有分量 | -| `q.copy(p)` | `GameQuaternion` | 从 `p` 复制所有分量 | +| 方法 | 返回值 | 说明 | +| ------------------- | ---------------- | ------------------- | +| `q.set(w, x, y, z)` | `GameQuaternion` | 设置所有分量 | +| `q.copy(p)` | `GameQuaternion` | 从 `p` 复制所有分量 | #### 创建新四元数 (不修改自身) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `q.clone()` | `GameQuaternion` | 深拷贝 | -| `q.add(p)` | `GameQuaternion` | 逐分量加法 | -| `q.sub(p)` | `GameQuaternion` | 逐分量减法 | -| `q.mul(p)` | `GameQuaternion` | 汉密尔顿积:`q × p` (不可交换) | -| `q.div(p)` | `GameQuaternion` | 除法:`q × p⁻¹` | -| `q.inv()` | `GameQuaternion` | 共轭 (对单位四元数等价于逆) | -| `q.normalize()` | `GameQuaternion` | 单位化,返回模长为 1 的新四元数 | +| 方法 | 返回值 | 说明 | +| --------------- | ---------------- | ---------------------------------------- | +| `q.clone()` | `GameQuaternion` | 深拷贝 | +| `q.add(p)` | `GameQuaternion` | 逐分量加法 | +| `q.sub(p)` | `GameQuaternion` | 逐分量减法 | +| `q.mul(p)` | `GameQuaternion` | 汉密尔顿积:`q × p` (不可交换) | +| `q.div(p)` | `GameQuaternion` | 除法:`q × p⁻¹` | +| `q.inv()` | `GameQuaternion` | 共轭 (对单位四元数等价于逆) | +| `q.normalize()` | `GameQuaternion` | 单位化,返回模长为 1 的新四元数 | | `q.slerp(p, t)` | `GameQuaternion` | 球面线性插值:`t=0` 为自身,`t=1` 为 `p` | #### 数值计算 -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `q.dot(p)` | `number` | 点积 | -| `q.mag()` | `number` | 模长 (范数) | -| `q.sqrMag()` | `number` | 模长平方 | -| `q.angle(p)` | `number` | 与 `p` 的角度差 (弧度) | -| `q.equals(p)` | `boolean` | 近似相等,容差 1e-6 | +| 方法 | 返回值 | 说明 | +| ------------- | --------- | ---------------------- | +| `q.dot(p)` | `number` | 点积 | +| `q.mag()` | `number` | 模长 (范数) | +| `q.sqrMag()` | `number` | 模长平方 | +| `q.angle(p)` | `number` | 与 `p` 的角度差 (弧度) | +| `q.equals(p)` | `boolean` | 近似相等,容差 1e-6 | #### 旋转操作 (绕自身坐标系旋转,返回新四元数) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `q.rotateX(rad)` | `GameQuaternion` | 绕 X 轴旋转 | -| `q.rotateY(rad)` | `GameQuaternion` | 绕 Y 轴旋转 | -| `q.rotateZ(rad)` | `GameQuaternion` | 绕 Z 轴旋转 | -| `q.rotateVector(v)` | `GameVector3` | 用此四元数旋转向量 `v` | -| `q.toEuler()` | `GameVector3` | 转为欧拉角 (YZX 顺序),返回 `(x, y, z)` 弧度 | +| 方法 | 返回值 | 说明 | +| ------------------- | ---------------- | -------------------------------------------- | +| `q.rotateX(rad)` | `GameQuaternion` | 绕 X 轴旋转 | +| `q.rotateY(rad)` | `GameQuaternion` | 绕 Y 轴旋转 | +| `q.rotateZ(rad)` | `GameQuaternion` | 绕 Z 轴旋转 | +| `q.rotateVector(v)` | `GameVector3` | 用此四元数旋转向量 `v` | +| `q.toEuler()` | `GameVector3` | 转为欧拉角 (YZX 顺序),返回 `(x, y, z)` 弧度 | #### 轴角分解 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math_en.md b/Box3JS-NeoForge-1.21.1/docs/api/math_en.md deleted file mode 100644 index efdff2c..0000000 --- a/Box3JS-NeoForge-1.21.1/docs/api/math_en.md +++ /dev/null @@ -1,413 +0,0 @@ -# Math Types - - The following data types are globally available in JS. - -## GameVector3 - -A 3D vector with double-precision components. Used for position, direction, velocity, etc. - -### Constructor - -```js -var v = new GameVector3(); // Zero vector (0, 0, 0) -var v = new GameVector3(x, y, z); // Specified coordinates -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `v.x` | `number` | X component (east/west), read/write | -| `v.y` | `number` | Y component (up/down), read/write | -| `v.z` | `number` | Z component (north/south), read/write | - -### Instance Methods - -#### Mutating (return this) - -| Method | Returns | Description | -|--------|---------|-------------| -| `v.set(x, y, z)` | `GameVector3` | Set all components | -| `v.copy(w)` | `GameVector3` | Copy all components from `w` | -| `v.addEq(w)` | `GameVector3` | In-place addition: `v += w` | -| `v.subEq(w)` | `GameVector3` | In-place subtraction: `v -= w` | -| `v.mulEq(w)` | `GameVector3` | In-place component-wise multiplication | -| `v.divEq(w)` | `GameVector3` | In-place component-wise division; divide-by-zero skips that component | -| `v.scaleEq(n)` | `GameVector3` | In-place scalar multiplication: `v.x *= n` … | -| `v.negEq()` | `GameVector3` | In-place negation: `v = -v` | - -#### Creating New Vectors (does not mutate) - -| Method | Returns | Description | -|--------|---------|-------------| -| `v.clone()` | `GameVector3` | Deep copy — independent vector with same values | -| `v.add(w)` | `GameVector3` | Vector addition: `v + w` | -| `v.sub(w)` | `GameVector3` | Vector subtraction: `v - w` | -| `v.mul(w)` | `GameVector3` | Component-wise multiplication | -| `v.div(w)` | `GameVector3` | Component-wise division; divide-by-zero → 0 | -| `v.scale(n)` | `GameVector3` | Scalar multiplication: each component × `n` | -| `v.cross(w)` | `GameVector3` | Cross product: `v × w` | -| `v.normalize()` | `GameVector3` | Unit vector; zero vector returns `(0,0,0)` | -| `v.lerp(w, t)` | `GameVector3` | Linear interpolation: `t=0` → this, `t=1` → `w` | -| `v.towards(w)` | `GameVector3` | Direction vector pointing toward `w` (normalized) | -| `v.max(w)` | `GameVector3` | Component-wise maximum | -| `v.min(w)` | `GameVector3` | Component-wise minimum | -| `v.neg()` | `GameVector3` | Negation: `-v` | -| `v.moveTowards(target, maxDelta)` | `GameVector3` | Move toward target by at most `maxDelta` distance | -| `v.floor()` | `GameVector3` | Component-wise floor | -| `v.ceil()` | `GameVector3` | Component-wise ceiling | -| `v.clampLength(max)` | `GameVector3` | Clamp magnitude to `max`, scale down proportionally if exceeded | - -#### Numeric Computations - -| Method | Returns | Description | -|--------|---------|-------------| -| `v.dot(w)` | `number` | Dot (inner) product: `v · w` | -| `v.mag()` | `number` | Magnitude (length) | -| `v.sqrMag()` | `number` | Squared magnitude — faster than `mag()` | -| `v.distance(w)` | `number` | Euclidean distance to `w` | -| `v.angle(w)` | `number` | Angle between `v` and `w` (radians, 0–π) | -| `v.sqrDistance(w)` | `number` | Squared distance to `w` — faster than `distance()` | - -#### Comparison - -| Method | Returns | Description | -|--------|---------|-------------| -| `v.equals(w)` | `boolean` | Approximate equality, tolerance 1e-6 | -| `v.exactEquals(w)` | `boolean` | Exact equality — components strictly equal | -| `v.isZero()` | `boolean` | Whether this is (approximately) a zero vector, tolerance 1e-6 | - -```js -var pos = new GameVector3(0, 100, 0); -var target = new GameVector3(10, 100, 10); - -// Distance -var dist = pos.distance(target); // ~14.14 - -// Direction vector -var dir = target.sub(pos).normalize(); - -// Angle -var angle = pos.angle(target); // radians - -// Comparison -var a = new GameVector3(1, 2, 3); -var b = new GameVector3(1.0000001, 2.0000001, 3.0000001); -a.equals(b); // true (within tolerance) -a.exactEquals(b); // false - -// Teleport entity (LiveVec3) -entity.position.set(0, 100, 0); -``` - -### Static Methods - -```js -// Spherical coordinates → vector -var v = GameVector3.fromPolar(mag, phi, theta); -// mag: radius -// phi: azimuth angle (radians, horizontal rotation around Y) -// theta: elevation angle (radians, from horizontal plane) -``` - -### toString - -```js -var v = new GameVector3(1, 2, 3); -v.toString(); // "GameVector3(1.0, 2.0, 3.0)" -``` - ---- - -## GameBounds3 - -Axis-aligned bounding box (AABB), defined by two opposing corners: `lo` (minimum corner) and `hi` (maximum corner). - -### Constructor - -```js -var bounds = new GameBounds3( - new GameVector3(-1, 0, -1), // lo (min corner) - new GameVector3(1, 2, 1), // hi (max corner) -); -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `bounds.lo` | `GameVector3` | Minimum corner, read/write | -| `bounds.hi` | `GameVector3` | Maximum corner, read/write | - -### Instance Methods - -| Method | Returns | Description | -|--------|---------|-------------| -| `bounds.set(lox, loy, loz, hix, hiy, hiz)` | `GameBounds3` | Set all boundaries in-place, returns this | -| `bounds.copy(b)` | `GameBounds3` | Copy values from `b` in-place, returns this | -| `bounds.intersects(other)` | `boolean` | Whether this intersects `other` | -| `bounds.intersect(other)` | `GameBounds3 \| null` | Intersection bounds, or `null` if no overlap | -| `bounds.contains(v)` | `boolean` | Whether point `v` is inside (inclusive) | -| `bounds.containsBounds(b)` | `boolean` | Whether this fully contains `b` | -| `bounds.center()` | `GameVector3` | Center point of the bounds | -| `bounds.size()` | `GameVector3` | Size of the bounds (width, height, depth) | -| `bounds.expand(delta)` | `GameBounds3` | Expand all faces outward by `delta`, returns new bounds | -| `bounds.expandEq(delta)` | `GameBounds3` | In-place expand all faces outward by `delta`, returns this | -| `bounds.growToInclude(v)` | `GameBounds3` | In-place grow to include point `v`, returns this | -| `bounds.closestPoint(v)` | `GameVector3` | Closest point on the bounds to point `v` | -| `bounds.move(offset)` | `GameBounds3` | Translate by `offset`, returns new bounds | -| `bounds.moveEq(offset)` | `GameBounds3` | In-place translate by `offset`, returns this | - -### Static Methods - -```js -// Create minimal bounds from an array of GameVector3 -var points = [new GameVector3(0,0,0), new GameVector3(5,10,3)]; -var box = GameBounds3.fromPoints(points); // returns GameBounds3 or null -``` - -### toString - -```js -bounds.toString(); // "GameBounds3(GameVector3(-1.0, 0.0, -1.0), GameVector3(1.0, 2.0, 1.0))" -``` - -```js -// Query entities within bounds -var entities = world.searchBox(bounds); - -// Check if point is inside -if (bounds.contains(player.position)) { - // Player is inside the area -} -``` - ---- - -## GameRGBColor - -An RGB color with three channels ranging from 0.0 to 1.0. - -### Constructor - -```js -var red = new GameRGBColor(1, 0, 0); -var blue = new GameRGBColor(0, 0, 1); -var gray = new GameRGBColor(0.5, 0.5, 0.5); -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `color.r` | `number` | Red channel (0–1), read/write | -| `color.g` | `number` | Green channel (0–1), read/write | -| `color.b` | `number` | Blue channel (0–1), read/write | - -### Instance Methods - -#### Mutating (return this) - -| Method | Returns | Description | -|--------|---------|-------------| -| `c.set(r, g, b)` | `GameRGBColor` | Set all channels | -| `c.copy(o)` | `GameRGBColor` | Copy all channels from another color | -| `c.addEq(o)` | `GameRGBColor` | In-place addition: `c += o` | -| `c.subEq(o)` | `GameRGBColor` | In-place subtraction: `c -= o` | -| `c.mulEq(o)` | `GameRGBColor` | In-place channel-wise multiplication | -| `c.divEq(o)` | `GameRGBColor` | In-place channel-wise division; divide-by-zero skips | -| `c.scaleEq(n)` | `GameRGBColor` | In-place scalar multiplication: each channel × `n` | - -#### Creating New Colors (does not mutate) - -| Method | Returns | Description | -|--------|---------|-------------| -| `c.clone()` | `GameRGBColor` | Deep copy | -| `c.add(o)` | `GameRGBColor` | Channel-wise addition | -| `c.sub(o)` | `GameRGBColor` | Channel-wise subtraction | -| `c.mul(o)` | `GameRGBColor` | Channel-wise multiplication | -| `c.div(o)` | `GameRGBColor` | Channel-wise division; divide-by-zero → 0 | -| `c.lerp(o, t)` | `GameRGBColor` | Linear interpolation: `t=0` → this, `t=1` → `o` | -| `c.scale(n)` | `GameRGBColor` | Scalar multiplication: each channel × `n` | -| `c.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | -| `c.toRGBA()` | `string` | CSS format string: `"rgba(r,g,b,1.0)"` | - -### Static Methods - -```js -var randomColor = GameRGBColor.random(); // Each channel 0–1 random -``` - -### toString - -```js -new GameRGBColor(1, 0.5, 0).toString(); // "GameRGBColor(1.0, 0.5, 0.0)" -``` - ---- - -## GameRGBAColor - -An RGBA color with four channels ranging from 0.0 to 1.0. - -### Constructor - -```js -var semiRed = new GameRGBAColor(1, 0, 0, 0.5); -var opaque = new GameRGBAColor(0, 1, 0, 1.0); -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `color.r` | `number` | Red channel (0–1), read/write | -| `color.g` | `number` | Green channel (0–1), read/write | -| `color.b` | `number` | Blue channel (0–1), read/write | -| `color.a` | `number` | Alpha opacity (0–1), read/write | - -### Instance Methods - -#### Mutating (return this) - -| Method | Returns | Description | -|--------|---------|-------------| -| `c.set(r, g, b, a)` | `GameRGBAColor` | Set all four channels | -| `c.copy(o)` | `GameRGBAColor` | Copy all channels from another RGBA color | -| `c.addEq(o)` | `GameRGBAColor` | In-place addition | -| `c.subEq(o)` | `GameRGBAColor` | In-place subtraction | -| `c.mulEq(o)` | `GameRGBAColor` | In-place channel-wise multiplication | -| `c.divEq(o)` | `GameRGBAColor` | In-place channel-wise division; divide-by-zero skips | -| `c.scaleEq(n)` | `GameRGBAColor` | In-place scalar multiplication: each channel × `n` | - -#### Creating New Colors (does not mutate) - -| Method | Returns | Description | -|--------|---------|-------------| -| `c.clone()` | `GameRGBAColor` | Deep copy | -| `c.add(o)` | `GameRGBAColor` | Channel-wise addition | -| `c.sub(o)` | `GameRGBAColor` | Channel-wise subtraction | -| `c.mul(o)` | `GameRGBAColor` | Channel-wise multiplication | -| `c.div(o)` | `GameRGBAColor` | Channel-wise division; divide-by-zero → 0 | -| `c.lerp(o, t)` | `GameRGBAColor` | Linear interpolation | -| `c.scale(n)` | `GameRGBAColor` | Scalar multiplication: each channel × `n` | -| `c.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | -| `c.blendEq(rgb)` | `GameRGBColor` | Alpha-blend onto an RGB background, returns displayed RGB | - -### toString - -```js -new GameRGBAColor(1, 0, 0, 0.5).toString(); // "GameRGBAColor(1.0, 0.0, 0.0, 0.5)" -``` - -```js -// Alpha blending -var fg = new GameRGBAColor(1, 0, 0, 0.5); // Semi-transparent red -var bg = new GameRGBColor(1, 1, 1); // White background -var result = fg.blendEq(bg); // Blended RGB color -``` - ---- - -## GameQuaternion - -A quaternion used for 3D rotation. Unit quaternions (magnitude=1) represent pure rotations. - -### Constructor - -```js -var q = new GameQuaternion(); // Identity (1, 0, 0, 0) -var q = new GameQuaternion(w, x, y, z); // Specified components -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `q.w` | `number` | Real (scalar) component, read/write | -| `q.x` | `number` | Imaginary X component, read/write | -| `q.y` | `number` | Imaginary Y component, read/write | -| `q.z` | `number` | Imaginary Z component, read/write | - -### Instance Methods - -#### Mutating (return this) - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.set(w, x, y, z)` | `GameQuaternion` | Set all components | -| `q.copy(p)` | `GameQuaternion` | Copy all components from `p` | - -#### Creating New Quaternions (does not mutate) - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.clone()` | `GameQuaternion` | Deep copy | -| `q.add(p)` | `GameQuaternion` | Component-wise addition | -| `q.sub(p)` | `GameQuaternion` | Component-wise subtraction | -| `q.mul(p)` | `GameQuaternion` | Hamilton product: `q × p` (NOT commutative) | -| `q.div(p)` | `GameQuaternion` | Division: `q × p⁻¹` | -| `q.inv()` | `GameQuaternion` | Conjugate (equals inverse for unit quaternions) | -| `q.normalize()` | `GameQuaternion` | Normalize, returns unit quaternion | - -#### Interpolation - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.slerp(p, t)` | `GameQuaternion` | Spherical linear interpolation: `t=0` → this, `t=1` → `p` | - -#### Numeric Computations - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.dot(p)` | `number` | Dot product | -| `q.mag()` | `number` | Magnitude (norm) | -| `q.sqrMag()` | `number` | Squared magnitude | -| `q.angle(p)` | `number` | Angular difference from `p` (radians) | -| `q.equals(p)` | `boolean` | Approximate equality, tolerance 1e-6 | - -#### Rotation Operations (rotate around local axes, returns new quaternion) - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.rotateX(rad)` | `GameQuaternion` | Rotate around X axis | -| `q.rotateY(rad)` | `GameQuaternion` | Rotate around Y axis | -| `q.rotateZ(rad)` | `GameQuaternion` | Rotate around Z axis | -| `q.rotateVector(v)` | `GameVector3` | Rotate vector `v` by this quaternion | -| `q.toEuler()` | `GameVector3` | Convert to Euler angles (YZX order), returns `(x, y, z)` in radians | - -#### Axis-Angle Decomposition - -```js -var result = q.getAxisAngle(); -// result.angle — rotation angle (radians) -// result.axis — rotation axis (unit GameVector3) -``` - -### Static Methods - -```js -// Create from axis-angle representation -var q1 = GameQuaternion.fromAxisAngle(axis, rad); -// axis: GameVector3 (auto-normalized) -// rad: rotation angle (radians) - -// Create from Euler angles (YZX rotation order: Y → Z → X) -var q2 = GameQuaternion.fromEuler(x, y, z); -// x, y, z: rotation around each axis in radians - -// Shortest-arc quaternion rotating from vector a to b -var q3 = GameQuaternion.rotationBetween(fromVec, toVec); - -// Create quaternion from look-at direction (from → to) -var q4 = GameQuaternion.lookAt(from, to, up); -// from: GameVector3 — observer position -// to: GameVector3 — target point -// up: GameVector3 — up direction (default (0,1,0)) -``` - -### toString - -```js -q.toString(); // "GameQuaternion(0.707, 0.0, 0.707, 0.0)" -``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player.md b/Box3JS-NeoForge-1.21.1/docs/api/player.md index 4825dea..d9bc281 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -1,6 +1,6 @@ # player — 玩家 API -`player` 对象通过 `entity.player` 获取,代表登录的玩家。它拥有 `entity` 的全部属性和方法(如 `hp`、`position`、`tags()` 等),并额外提供玩家专属功能:背包、经验、飞行、消息、传送等。 +`player` 通过 `entity.player` 获取,拥有 `entity` 的全部属性并额外提供背包、经验、飞行、消息、传送等玩家专属功能。 ```js world.onPlayerJoin(function(entity, tick) { diff --git a/Box3JS-NeoForge-1.21.1/docs/api/registries.md b/Box3JS-NeoForge-1.21.1/docs/api/registries.md index 2fc848d..899dc2f 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/registries.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/registries.md @@ -1,16 +1,14 @@ # 自定义注册(registries API) -> **仅服务端可用。** 客户端中 `registries` 为 `undefined`。客户端代码请直接使用 ResourceLocation 字符串(如 `audio.playSound("colorzone:victory_fanfare", 1.0, 1.0)`)。 -> -> **仅在编译 JAR 模式(`/box3script compile`)下可用。** 解释模式(`/box3script start`)中 `registries` 为 `undefined`。 -> -> **需要服务端和客户端都安装编译后的 JAR** 才能正确渲染方块纹理/模型。客户端没有 JAR 的话,方块会显示为紫黑缺失方块。 +::: warning +仅编译 JAR 模式(`/box3script compile`)**服务端**可用。客户端及解释模式中 `registries` 为 `undefined`。JAR 需双端安装才能渲染。 +::: 方块、物品和音效事件在 JSON 配置文件中声明,编译时生成 `DeferredRegister` 代码注入 `@Mod` 类。资源文件从项目 `assets/` 目录打包进 JAR。 ## 项目布局 -``` +```text mygame/ ├── registries/ │ ├── blocks.json ← 方块定义 @@ -137,7 +135,9 @@ mygame/ } ``` -> **注意:** `creativeTab` 图标会自动从物品中查找(优先物品,其次方块)。如果 `creativeTabs.json` 的 `icon` 匹配某个物品 key,会使用该物品作为图标。 +::: tip +`creativeTab` 图标会自动从物品中查找(优先物品,其次方块)。如果 `creativeTabs.json` 的 `icon` 匹配某个物品 key,会使用该物品作为图标。 +::: ### 装备类型(工具 & 盔甲) @@ -198,7 +198,9 @@ mygame/ } ``` -> **注意:** 装备的 `maxStackSize` 固定为 1(不可堆叠),无需手动设置。`nutrition`/`saturation`/`alwaysEdible` 仅用于 `"food"` 类型。 +::: tip +装备的 `maxStackSize` 固定为 1(不可堆叠),无需手动设置。`nutrition`/`saturation`/`alwaysEdible` 仅用于 `"food"` 类型。 +::: ## sounds.json @@ -234,7 +236,7 @@ mygame/ 对应文件: -``` +```js assets/sounds/victory_fanfare.ogg assets/sounds/skill_cast.ogg assets/sounds/background_music.ogg @@ -266,7 +268,7 @@ audio.playSound("colorzone:victory_fanfare", 1.0, 1.0); 与 Minecraft 资源包结构一致: -``` +```text assets// ├── blockstates/.json ← 自动生成,可自定义覆盖 ├── models/block/.json ← 自动生成,可自定义覆盖 @@ -281,7 +283,9 @@ assets// └── item/.png ``` -> **注意:** `` 来自 `package.json` 的 `name` 字段(从第二个 `/` 后取,如 `@scope/mygame` → `mygame`)。 +::: tip +`` 来自 `package.json` 的 `name` 字段(从第二个 `/` 后取,如 `@scope/mygame` → `mygame`)。 +::: 编译时自动将 `assets/` 打包为 `assets//`。 @@ -289,7 +293,7 @@ assets// 语言文件需要在 `assets/lang/` 目录下**手动创建**,不会被自动生成。至少应提供 `en_us.json` 和 `zh_cn.json`,也可添加更多语言: -``` +```text mygame/ └── assets/ └── lang/ diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server.md b/Box3JS-NeoForge-1.21.1/docs/api/server.md index e8a65d0..2380190 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/server.md @@ -2,7 +2,9 @@ 服务端脚本运行在 Minecraft 服务器线程上,入口文件是 `src/server/app.ts`,构建产物是 `dist/server.js`。服务端 API 负责世界状态、实体和玩家、方块读写、事件回调、持久化数据、网络请求以及服务端到客户端的事件下发。 -> 客户端 UI、键盘输入、本地音效和本地 GUI 不在服务端 API 中。相关能力见 [client.md](client.md)。 +::: info +客户端 UI、键盘输入、本地音效和本地 GUI 不在服务端 API 中。相关能力见 [client.md](client.md)。 +::: ## 服务端全局对象 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage.md b/Box3JS-NeoForge-1.21.1/docs/api/storage.md index 6b575a6..10df6bf 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -2,7 +2,9 @@ `storage` 提供 JSON 文件持久化存储,带内存缓存加速读写。 -> **运行环境:** 服务端和客户端都可用。服务端数据保存在 `config/box3/storage/<项目名>/`;客户端数据保存在本地游戏目录的 `box3/client-storage/<项目名>/`。每个项目自动拥有独立命名空间。 +::: info 运行环境 +服务端和客户端都可用。服务端数据保存在 `config/box3/storage/<项目名>/`;客户端数据保存在本地游戏目录的 `box3/client-storage/<项目名>/`。每个项目自动拥有独立命名空间。 +::: ## 获取存储实例 @@ -18,7 +20,9 @@ 获取**跨项目共享**存储。所有项目通过同一 `name` 访问同一份数据(底层使用 `__shared__/` 命名空间)。适合做全服排行榜、全局配置等。 -> 仅服务端可用。客户端本地存储只提供 `getDataStorage(name)`。 +::: warning +仅服务端可用。客户端本地存储只提供 `getDataStorage(name)`。 +::: ```js var store = storage.getDataStorage("leaderboard"); @@ -45,7 +49,9 @@ var winner = store.get("lastWinner"); // "Steve" (string) var cfg = store.get("config"); // {difficulty: "hard", ...} (object) ``` -> **注意:** 当数据从磁盘重新加载后,复杂对象会以普通 JSON 对象形式返回(例如 `Map` 风格对象),请避免依赖原始 JS 原型方法。 +::: warning 注意 +当数据从磁盘重新加载后,复杂对象会以普通 JSON 对象形式返回(例如 `Map` 风格对象),请避免依赖原始 JS 原型方法。 +::: ### store.keys() diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md index 95149e7..a668a2d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -1,6 +1,6 @@ # world — 世界 API -`world` 是全局单例,代表 Minecraft 服务端的世界状态。控制天气、时间、游戏规则、实体生成,注册事件回调,管理记分板/Bossbar/队伍,以及发射粒子、烟花、闪电等视觉效果。 +`world` 是全局单例,代表 Minecraft 服务端的世界状态。 ## 世界属性 diff --git a/Box3JS-NeoForge-1.21.1/docs/en/README.md b/Box3JS-NeoForge-1.21.1/docs/en/README.md new file mode 100644 index 0000000..499ecfa --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/README.md @@ -0,0 +1,77 @@ +--- +layout: home + +hero: + name: "Box3JS" + text: "Minecraft JS/TS Scripting Engine" + tagline: Same programming experience as Box3 — build mini-games in Minecraft with JS/TS + actions: + - theme: brand + text: Get Started + link: /en/guide/getting-started + +features: + - icon: 🎮 + title: Server & Client Dual-Side Scripting + details: "Server: world manipulation, entities, recipes. Client: keyboard input, screen UI, audio, SQLite storage, HTTP requests." + - icon: 📦 + title: TypeScript-First + details: DTS type definitions for all 17 global objects. Built-in esbuild + Babel pipeline transpiles modern TS to Rhino-compatible ES5. + - icon: 🔄 + title: Hot Reload + details: Script changes take effect instantly — no server restart needed. File watcher auto-reloads on save. + - icon: 🌐 + title: Bidirectional Communication + details: remoteChannel for server↔client event messaging. Server broadcasts to all players, clients reply independently. + - icon: 🗄️ + title: Dual-Side Storage & Database + details: JSON file persistence and SQLite on both server and client. Paginated queries, atomic updates, counters, tagged templates. + - icon: 🧩 + title: Custom Blocks & Items + details: Block textures, item models, equipment, sounds, creative tabs — all configured via JSON registries (standalone/JAR mode). + - icon: 📚 + title: Comprehensive Docs + details: 50+ pages covering API reference, progressive tutorials, recipes, architecture deep-dive, and FAQ — bilingual CN/EN. + - icon: 🚀 + title: Standalone JAR Mode + details: Compile script projects into independent JAR mods with zero runtime dependencies — drop into mods folder and go. + +--- + +## Quick Start + +```bash +# In-game: create a new project +/box3script create mygame + +# Build & watch +cd config/box3/script/mygame +npm install +npm run build -- --watch + +# TypeScript type check +npm run check +``` + +```ts +// src/server/app.ts — your first script +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage(`Hello, ${entity.player.name}!`); + return false; + } + return true; +}); +``` + +[View Full Docs →](/en/guide/getting-started) + +## Version Info + +| Component | Version | +|-----------|---------| +| Minecraft | 1.21.1 | +| Mod Loader | NeoForge | +| Java | 21 | +| JS Engine | Mozilla Rhino 1.9.1 (ES5) | +| TypeScript | Via Babel → ES5 | diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/README.md b/Box3JS-NeoForge-1.21.1/docs/en/api/README.md new file mode 100644 index 0000000..cbcb548 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/README.md @@ -0,0 +1,413 @@ +--- +--- + +# Box3JS API Reference + +Write Minecraft server and client scripts in JavaScript/TypeScript. + +## 5-Minute Quick Start + +```bash +# 1. Create a project in-game +/box3script create mygame + +# 2. Install dependencies and build +cd config/box3/script/mygame +npm install && npm run build + +# 3. Start the script +/box3script start mygame +``` + +Open `src/server/app.ts` and write: + +```js +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage("Hello, " + entity.player.name + "!"); + return false; // suppress chat message + } + return true; +}); + +console.log("Script loaded"); +``` + +After each edit, re-run `npm run build`, then use `/box3script reload mygame` to hot-reload. Client logic goes in `src/client/app.ts`; after build it becomes `dist/client.js` and is sent automatically to players who have the Box3JS client mod installed. + +::: tip Quick Navigation +[Quick Start Guide](../guide/getting-started.md) | [Architecture](../guide/architecture.md) | [JS vs Java Comparison](../guide/js-vs-java.md) +::: + +## API Domain Map + +Box3JS APIs are split into server-side, client-side, and shared runtimes. The type declarations are separated: `tsconfig.server.json` does not include client globals, and `tsconfig.client.json` does not include server globals such as `world` / `voxels`. + +| Domain | Runtime | Globals | Description | +| ------------------------------- | ------------ | --------------------------------------------------------------------------- | ------------------------------------------------ | +| **World & Entities** (server) | Server | `world` `voxels` | World control, blocks, event callbacks | +| **Players & Data** (server) | Server | `entity` `player` `storage` `db` `http` | `entity`/`player` come from callbacks or queries | +| **Client Interaction** (client) | Client | `audio` `client` `input` `ui` `chat` `gui` | Requires Box3JS client mod | +| **Cross-Side** | Both | `remoteChannel` | Server↔Client event communication | +| **Registries** | Compile-time | `registries` | Only in `/box3script compile` JAR mode | +| **Math & Utilities** | Both | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | Constructed with `new` | +| **Global Tools** | Both | `console` | Log output | + +::: info API Classification +**Server APIs** manipulate the world, entities, players, and blocks. Scripts run on the server by default. **Client APIs** are only available with the Box3JS client mod installed, for UI, input, and audio. **Registry APIs** are only available in compiled JAR mode (`registries` is `undefined` in interpreted mode). +::: + +## Read By Runtime + +| Entry | Use it for | Includes | +| ----------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| [Server API Overview](server.md) | Gameplay logic, events, blocks, entities, players, data, server-to-client events | `world`, `entity`, `player`, `voxels`, `storage`, `db`, `http`, `registries` | +| [Client API Overview](client.md) | Local UI, input, audio, chat helpers, local data, client-to-server events | `client`, `audio`, `input`, `ui`, `chat`, `gui`, `storage`, `db`, `http` | +| [Shared Utilities](math.md) | Math, color, and spatial code usable on both sides | `GameVector3`, `GameBounds3`, `GameRGBColor`, `GameRGBAColor`, `GameQuaternion` | + +## API Style Rules + +- Every `onXxx(...)` event registration API returns a `GameEventHandlerToken`; call `token.cancel()` to unsubscribe and `token.active()` to check whether it is still live. +- Server APIs are only typed in `src/server/app.ts`; client APIs are only typed in `src/client/app.ts`. Shared APIs are `storage`, `db`, `http`, `remoteChannel`, `console`, and the math classes. +- Cross-side data travels through `remoteChannel` as JSON-serializable objects: clients use `sendServerEvent` / `onClientEvent`; servers use `sendClientEvent` / `broadcastClientEvent` / `onServerEvent`. +- Coordinate APIs that accept `GameVector3` usually also support an `x, y, z` overload; server block coordinates are handled as integers. + +## Find by Task — I want to... + +Find APIs by what you want to do, not by which global object they live on. + +### Messages & Chat + +| I want to... | Use this | +| -------------------- | ---------------------------------------- | +| Broadcast to server | `world.say("message")` | +| Send private message | `player.directMessage("message")` | +| Show action bar text | `player.actionBar("message")` | +| Show screen title | `player.title("Title", "Subtitle")` | +| Handle chat input | `world.onChat((entity, msg) => { ... })` | + +### Player Properties + +| I want to... | Use this | +| ----------------------- | ----------------------------------------------- | +| Get/set player position | `player.position` → `GameVector3` | +| Teleport player | `player.teleport(new GameVector3(x, y, z))` | +| Change health | `player.hp = 20` / `player.maxHp = 40` | +| Change hunger | `player.food = 20` / `player.saturation = 10` | +| Switch game mode | `player.gameMode = "creative"` | +| Toggle flight | `player.canFly = true` / `player.flying = true` | +| Kick player | `player.kick("reason")` | +| Run command as player | `player.runCommand("say hi")` | + +### Items & Equipment + +| I want to... | Use this | +| -------------------- | -------------------------------------------- | +| Give a basic item | `player.giveItem("minecraft:diamond", 1)` | +| Give enchanted item | `player.giveEnchantedItem(...)` | +| Give named item | `player.giveNamedItem(...)` | +| Get held item | `player.getHeldItem()` | +| Clear inventory | `player.clearInventory()` | +| Set entity equipment | `entity.setEquipment("head", "iron_helmet")` | + +### Custom Registries (Blocks, Items & Sounds) 🆕 + +| I want to... | Use this | +| ---------------------------- | ------------------------------------------------ | +| Register custom blocks | `registries/blocks.json` (at compile time) | +| Register custom items | `registries/items.json` (at compile time) | +| Register custom sounds | `registries/sounds.json` (at compile time) | +| Register creative tabs | `registries/creativeTabs.json` (at compile time) | +| Get a registered block | `registries.getBlock("my_block")` | +| Get a registered item | `registries.getItem("chocolate")` | +| Get a registered sound | `registries.getSound("victory_fanfare")` | +| Give a custom block/item | `player.giveItem(block.itemId, 1)` | +| Place a custom block | `voxels.setVoxel(x, y, z, block.block)` | +| Play a custom sound (server) | `world.playSound(sound.soundId, x, y, z, 1, 1)` | +| Play a custom sound (client) | `audio.playSound("modId:soundId", 1.0, 1.0)` | + +::: warning +Server-side only. `registries` is `undefined` in client scripts. Only available in `/box3script compile` JAR mode. Client must also install the JAR for textures/models. See [registries.md](registries.md) +::: + +### Block Operations + +| I want to... | Use this | +| -------------------------- | ----------------------------------------------------------- | +| Read a block | `voxels.getVoxel(x, y, z)` | +| Place/replace a block | `voxels.setVoxel(x, y, z, "minecraft:stone")` | +| Fill a region | `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, "stone")` | +| Listen for block breaks | `world.onVoxelDestroy((entity, x, y, z, voxel) => { ... })` | +| Listen for block placement | `world.onBlockPlace((entity, x, y, z, voxel) => { ... })` | + +### Entity Manipulation + +| I want to... | Use this | +| --------------------- | --------------------------------------------- | +| Spawn an entity | `world.spawnEntity("minecraft:zombie", pos)` | +| Create with config | `world.createEntity({ type, position, ... })` | +| Set entity name | `entity.setNameTag("§cBoss")` | +| Toggle AI | `entity.setAI(true)` | +| Navigate to position | `entity.navigateTo(x, y, z, speed)` | +| Set attack target | `entity.setTarget(otherEntity)` | +| Check if player | `entity.isPlayer()` | +| Get entity type | `entity.entityType` | +| Get entity tags | `entity.tags()` / `entity.hasTag("boss")` | +| Query nearby entities | `world.entitiesInRadius(pos, radius)` | +| Query all entities | `world.querySelectorAll("*")` | + +### Client-side Features (requires Box3JS client mod) + +| I want to... | Use this | +| ------------------------- | ----------------------------------------------------------------- | +| Run every client tick | `client.onTick(() => { ... })` | +| Check key held down | `input.isKeyDown("space")` | +| Listen for key press | `input.onKeyPress("f", () => { ... })` | +| Play sound effect | `audio.playSound("pling", 1.0, 1.0)` | +| Play music | `audio.playMusic("minecraft:music.game", 0.5, 1.0)` | +| Stop all sounds | `audio.stopAll()` | +| Get/set volume | `audio.getVolume("music")` / `audio.setVolume("player", 0.8)` | +| Show action bar text | `ui.showOverlay("text")` | +| Show screen title | `ui.showTitle("Title", "Subtitle")` | +| Send chat message | `chat.sendMessage("message")` | +| Receive chat messages | `chat.onMessage((msg, sender, isSystem) => { ... })` | +| Send event to server | `remoteChannel.sendServerEvent({ ... })` | +| Receive event from server | `remoteChannel.onClientEvent((event) => { ... })` | +| Client-side local storage | `storage.getDataStorage("key")` | +| Set fog colour | `client.setFogColor(255, 100, 50)` | +| Set fog distance | `client.setFogStartDistance(10)` / `client.setFogEndDistance(50)` | +| Reset fog | `client.resetFog()` | + +### Visual Effects + +| I want to... | Use this | +| ----------------------- | --------------------------------------------------------- | +| Spawn particles | `world.spawnParticle("flame", x, y, z, ...)` | +| Particle circle | `world.spawnParticleCircle(x, y, z, radius, "heart", 20)` | +| Firework | `world.launchFirework(x, y, z, "red", "large_ball")` | +| Lightning | `world.strikeLightning(x, y, z)` | +| Explosion | `world.explode(x, y, z, power)` | +| Play sound (global) | `world.playSound("pling", pos, 1.0, 1.0)` | +| Play sound (per-player) | `player.playSound("pling", 1.0, 1.0)` | + +### Potion Effects + +| I want to... | Use this | +| ----------------- | --------------------------------------------------------------------- | +| Apply an effect | `entity.addEffect("minecraft:speed", duration, level, hideParticles)` | +| Clear all effects | `entity.clearEffects()` | + +### Event System + +| I want to... | Use this | +| --------------------- | --------------------------------------------------------------------------- | +| Run every tick | `world.onTick((info) => { ... })` | +| On player join | `world.onPlayerJoin((entity, tick) => { ... })` | +| On player leave | `world.onPlayerLeave((entity, tick) => { ... })` | +| On entity death | `world.onEntityDeath((entity, killer, tick) => { ... })` | +| On entity damaged | `world.onEntityDamage((entity, amount, source, attacker, tick) => { ... })` | +| On right-click entity | `world.onInteract((entity, target, tick) => { ... })` | +| On right-click block | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | +| On button pressed | `world.onButtonPressed((entity, button, tick) => { ... })` | +| On player respawn | `world.onPlayerRespawn((entity, tick) => { ... })` | +| Run once after delay | `setTimeout(() => { ... }, ticks)` | +| Run on interval | `setInterval(() => { ... }, ticks)` | +| Cancel event listener | `token.cancel()` | +| Check if active | `token.active()` | + +### Data Persistence + +| I want to... | Use this | +| -------------------- | ------------------------------- | +| Read/write JSON data | `storage.getDataStorage("key")` | +| SQL query | `db.sql("SELECT ...")` | +| SQL write | `db.sql("INSERT INTO ...")` | + +### HTTP Requests + +| I want to... | Use this | +| ------------ | ---------------------------------------------------- | +| GET request | `http.fetch("https://...")` | +| POST JSON | `http.fetch(url, { method: "POST", headers, body })` | +| Parse JSON | `resp.json()` or `{ responseType: "json" }` | +| Read text | `resp.text()` | +| Set timeout | `http.fetch(url, { timeout: 5000 })` | + +### Game Systems + +| I want to... | Use this | +| ------------------ | ------------------------------------------------ | +| Create scoreboard | `world.addScoreboard("name")` | +| Set score | `world.setScore("player", "board", 10)` | +| Display scoreboard | `world.showScoreboard("sidebar", "name")` | +| Show BossBar | `world.showBossbar("id", "title", 0.5, "red")` | +| Create team | `world.createTeam("teamName", "color")` | +| Join team | `world.joinTeam(entity, "teamName")` | +| Set world border | `world.borderSize = 500` | +| Shrink border | `world.shrinkBorder(100, 60)` | +| Change time | `world.time = 6000` | +| Set weather | `world.rainDensity = 0` / `world.clearWeather()` | +| Change game rule | `world.setGameRule("keepInventory", true)` | + +### Math Tools + +| I want to... | Use this | +| ------------- | ------------------------------------------------------------- | +| 3D coordinate | `new GameVector3(x, y, z)` | +| Vector math | `v.add(other)`, `v.scale(n)`, `v.length()` | +| Bounding box | `new GameBounds3(min, max)` | +| Color | `new GameRGBColor(r, g, b)` / `new GameRGBAColor(r, g, b, a)` | + +### Cross-Script Messaging + +| I want to... | Use this | +| -------------------------- | ------------------------------------------ | +| Send to another script | `world.sendMessage("projectName", data)` | +| Receive from other scripts | `world.onMessage((from, data) => { ... })` | + +## Global Objects + +| Object | Type | Description | +| ---------------- | ------------ | -------------------------------------------------------------------------------------------- | +| `world` | Server | World control, see [world.md](world.md) | +| `voxels` | Server | Block operations, see [voxels.md](voxels.md) | +| `entity` | Server value | Entity wrapper (from callbacks or `world.spawnEntity`), see [entity.md](entity.md) | +| `player` | Server value | Player wrapper (via `entity.player`), see [player.md](player.md) | +| `storage` | Both | Data persistence, see [storage.md](storage.md) | +| `db` | Both | SQLite database, see [database.md](database.md) | +| `http` | Both | HTTP requests, see [http.md](http.md) | +| `audio` | Client | Client sound, music, volume control, see [client.md](client.md) | +| `client` | Client | Client lifecycle, see [client.md](client.md) | +| `input` | Client | Client keyboard input, see [client.md](client.md) | +| `ui` | Client | Client screen UI, see [client.md](client.md) | +| `chat` | Client | Client chat send/receive, see [client.md](client.md) | +| `gui` | Client | Custom container GUI, see [client.md](client.md) | +| `remoteChannel` | Both | Server↔client event channel, see [server.md](server.md) / [client.md](client.md) | +| `registries` | Server | Custom blocks, items & sounds (compiled mode), see [registries.md](registries.md) | +| `console` | Both | Console logging (`log`/`warn`/`error`/`debug`) | +| `GameVector3` | Both | 3D vector, see [math.md](math.md) | +| `GameBounds3` | Both | Bounding box, see [math.md](math.md) | +| `GameRGBColor` | Both | RGB color, see [math.md](math.md) | +| `GameRGBAColor` | Both | RGBA color, see [math.md](math.md) | +| `GameQuaternion` | Both | Quaternion, see [math.md](math.md) | + +## API Legend + +| Label | Meaning | +| ------------------ | ------------------------------------------------------------------ | +| ✅ **Box3 API** | Originates from the Box3 platform; naming and semantics match Box3 | +| ⬆ **MC Extension** | Not in original Box3; added using Minecraft-specific features | + +## Documentation Style + +Each API document should follow this structure. Use the same style when adding future APIs: + +1. State the runtime at the top: server, client, or shared. +2. List globals and core concepts before method details. +3. Use `object.method(parameters)` for method headings. +4. Document parameters in tables with name, type, default, and meaning. +5. Prefer TypeScript/JavaScript examples and identify server or client context when needed. +6. For cross-side APIs, always state the direction: server → client, or client → server. +7. If docs and types disagree, treat `types/server/index.d.ts` and `types/client/index.d.ts` as the source of truth, then update the docs. + +## Detailed Document Index + +| Document | Content | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| [server.md](server.md) | Server API overview: runtime boundary, globals, events, players/entities, blocks, data, cross-side communication | +| [world.md](world.md) | World state, events, scoreboard, bossbar, teams, border, particles, fireworks, lightning, sounds | +| [entity.md](entity.md) | Entity properties, AI, equipment, potion effects, pathfinding, tags, collisions | +| [player.md](player.md) | Inventory, messaging, flight, game mode, teleport, commands, XP | +| [voxels.md](voxels.md) | Block read/write, region fill, spawner control | +| [storage.md](storage.md) | Persistent data storage | +| [database.md](database.md) | SQLite database API | +| [http.md](http.md) | HTTP request API | +| [client.md](client.md) | Client API: lifecycle, keyboard, screen UI, chat, GUI, remoteChannel, client-side storage | +| [registries.md](registries.md) | Custom blocks, items & sounds (blocks.json, items.json, sounds.json, creativeTabs.json) | +| [math.md](math.md) | GameVector3, GameBounds3, GameRGBColor, GameRGBAColor, GameQuaternion | +| [commands.md](commands.md) | `/box3script` command reference | + +## File Modules — TypeScript Build Pipeline + +Projects created with `/box3script create` come with a complete TS build environment: + +```text +config/box3/script/mygame/ +├── package.json ← esbuild + Babel + @babel/preset-typescript +├── tsconfig.base.json ← Shared TS compiler options +├── tsconfig.server.json ← Server-side TS config +├── tsconfig.client.json ← Client-side TS config +├── build.mjs ← Babel TS→JS → esbuild bundle → dist/ +├── types/ +│ ├── shared.d.ts ← Shared types (server & client) +│ ├── server/ +│ │ ├── index.d.ts ← Server type entry point +│ │ ├── server.d.ts +│ │ ├── entity.d.ts +│ │ ├── player.d.ts +│ │ ├── world.d.ts +│ │ └── voxels.d.ts +│ └── client/ +│ ├── index.d.ts ← Client type entry point +│ ├── client.d.ts +│ ├── audio.d.ts +│ ├── input.d.ts +│ ├── ui.d.ts +│ ├── chat.d.ts +│ └── gui.d.ts +├── src/ +│ ├── server/ +│ │ ├── app.ts ← Server entry point +│ │ └── ... +│ └── client/ +│ ├── app.ts ← Client entry point +│ └── ... +└── dist/ + ├── server.js ← Server compiled output + ├── client.js ← Client compiled output + └── -.jar ← Standalone JAR (/box3script compile) +``` + +Run `npm run build` to build. Use `/box3script watch` to enable file watching for auto hot-reload. + +## Deployment + +When ready to distribute, compile your script into a **standalone JAR mod** that runs on any NeoForge server alongside Box3JS: + +```js +/box3script compile +``` + +Outputs `-.jar` (metadata read from `package.json`: name, displayName, version, description, author, license, homepage, logoFile). Drop it into `mods/` and start the server. + +See [full command reference →](commands.md#box3script-compile-project) + +## Tick Conversion + +| Duration | Ticks | +| ---------- | ----- | +| 1 second | 20 | +| 5 seconds | 100 | +| 30 seconds | 600 | +| 1 minute | 1200 | +| 5 minutes | 6000 | + +## Deep Dive + +| Doc | Content | +| --------------------------------------------- | -------------------------------------------------------------- | +| [Quick Start](../guide/getting-started.md) | Setup, first script, dev cycle, debugging, deployment | +| [Architecture](../guide/architecture.md) | Rhino engine, scopes, event callbacks, build pipeline, network | +| [JS vs Java](../guide/js-vs-java.md) | Box3JS scripting vs native Java modding comparison | + +## Tutorials + +Learn Box3JS from scratch with the tutorial series in `docs/tutorial/`: + +| Tutorial | Content | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| [01-basics.md](../tutorial/01-basics.md) | From zero: first script, chat commands, timers | +| [02-player-items.md](../tutorial/02-player-items.md) | Player controls: teleport, items, potion effects, game modes | +| [03-events-entities.md](../tutorial/03-events-entities.md) | Events & entities: AI, combat, patrols | +| [04-advanced-systems.md](../tutorial/04-advanced-systems.md) | Advanced: scoreboard, BossBar, teams, world border | +| [05-examples.md](../tutorial/05-examples.md) | Real-world: PvP arena, effects, fireworks, wave mobs | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/client.md similarity index 95% rename from Box3JS-NeoForge-1.21.1/docs/api/client_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/client.md index d9d8acd..383325a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/client.md @@ -1,3 +1,6 @@ +--- +--- + # client — Client-side API Client scripts run locally on the player's Minecraft client. The entry file is `src/client/app.ts`, and the compiled output is `dist/client.js`. Client APIs handle local UI, input, audio, chat helpers, local storage, local HTTP/SQLite, and cross-side events. @@ -17,8 +20,9 @@ Client scripts access APIs through these globals: | `gui` | `GameGUI` | Custom container GUI interface | | `remoteChannel` | `RemoteChannel` | Client ↔ Server event communication | -> **Prerequisite:** The client must have the Box3JS mod installed. The server must enable the project's client script, which is automatically sent to connecting players. -> Client scripts go in `src/client/`, server scripts in `src/server/`. The client type entry is `types/client/index.d.ts`; it does not include server APIs such as `world` / `voxels`. +::: info +The client must have the Box3JS mod installed. Client scripts go in `src/client/`, server scripts in `src/server/`. The client type entry is `types/client/index.d.ts`; it does not include server APIs such as `world` / `voxels`. +::: Client scripts cannot directly modify the server world. To change blocks, players, entities, or scoreboards, send an event to the server: @@ -114,7 +118,9 @@ const token = client.onTick(() => { // token.cancel(); ``` -> **Note:** Server-side `world.onTick()` receives a `TickInfo` object. Client-side `client.onTick()` receives nothing. +::: info Note +Server-side `world.onTick()` receives a `TickInfo` object. Client-side `client.onTick()` receives nothing. +::: ### client.getFPS() @@ -517,8 +523,9 @@ remoteChannel.onClientEvent((event) => { }); ``` -> Server-side equivalents: `remoteChannel.sendClientEvent()` / `broadcastClientEvent()` / `onServerEvent()`. -> See type declarations in `server.d.ts`. +::: info +Server-side equivalents: `remoteChannel.sendClientEvent()` / `broadcastClientEvent()` / `onServerEvent()`. See type declarations in `server.d.ts`. +::: ## storage — Client-side Storage @@ -530,7 +537,7 @@ store.set("volume", 0.8); var volume = store.get("volume"); // 0.8 ``` -Full API reference: [storage_en.md](storage_en.md). +Full API reference: [storage.md](storage.md). ## Complete Client Example diff --git a/Box3JS-NeoForge-1.21.1/docs/api/commands_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/commands.md similarity index 84% rename from Box3JS-NeoForge-1.21.1/docs/api/commands_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/commands.md index ac2058d..79ee38c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/commands_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/commands.md @@ -1,3 +1,6 @@ +--- +--- + # /box3script Command Reference All commands require **OP level 2** (default admin permission). All `` arguments support **Tab completion**. @@ -8,13 +11,13 @@ All commands require **OP level 2** (default admin permission). All `` Shows project status overview. -``` +```js /box3script ``` Example output: -``` +```text ══ Box3JS Script Engine ══ Watch: ● Active Sandbox: ● 1 project(s) @@ -39,7 +42,7 @@ Example output: Creates a new TypeScript script project. Generates a complete TS scaffold, **disabled** by default. -``` +```js /box3script create mygame ``` @@ -56,7 +59,7 @@ Then enable with `/box3script start mygame`. Enable and load projects. **No args** = all projects. **Project name** = only that project. **`all`** = explicitly all. -``` +```js /box3script start # enable all /box3script start all # enable all (same as no args) /box3script start mygame # enable only mygame @@ -66,7 +69,7 @@ Enable and load projects. **No args** = all projects. **Project name** = only th Disable and unload projects. **No args** = all projects. **Project name** = only that project. **`all`** = explicitly all. -``` +```js /box3script stop # disable all /box3script stop all # disable all (same as no args) /box3script stop mygame # disable only mygame @@ -76,7 +79,7 @@ Disable and unload projects. **No args** = all projects. **Project name** = only Reload scripts. **No args** = stop all, reload all enabled projects. **With project name** = reload only that project. -``` +```js /box3script reload # reload all enabled projects /box3script reload mygame # reload only mygame ``` @@ -87,7 +90,7 @@ After editing code and running `npm run build`, use `reload` to apply changes. O Toggle file watching. When on, monitors `dist/` across all projects and auto-reloads on `.js` file changes. -``` +```js /box3script watch # toggle on/off ``` @@ -95,13 +98,13 @@ Toggle file watching. When on, monitors `dist/` across all projects and auto-rel Toggle sandbox mode. When enabled, tracks all block/entity/world state changes. When disabled, rolls back and shows summary. -``` +```js /box3script sandbox mygame # toggle on/off ``` Typical workflow: -``` +```js /box3script sandbox mygame # enable sandbox /box3script start mygame # load project # ... test ... @@ -110,19 +113,25 @@ Typical workflow: /box3script sandbox mygame # disable sandbox → full rollback ``` -> **Note:** Sandbox only tracks blocks placed through script APIs (`setVoxel`/`setVoxelId`/`fillVoxel`). Manual mining is unaffected. +::: warning +Sandbox only tracks blocks placed through script APIs (`setVoxel`/`setVoxelId`/`fillVoxel`). Manual mining is unaffected. +::: ### `/box3script compile ` Compiles a script project into a **lightweight standalone JAR mod** (~50KB) that depends on the Box3JS mod for Rhino runtime and API bindings. -``` +```js /box3script compile mygame ``` -> **Dependency:** Script JARs do not bundle Rhino or Box3JS API classes. Place the Box3JS mod (`box3js`) alongside your script JAR(s) in `mods/`. +::: warning Dependency +Script JARs do not bundle Rhino or Box3JS API classes. Place the Box3JS mod (`box3js`) alongside your script JAR(s) in `mods/`. +::: -> **Custom registries:** If `registries/blocks.json`, `items.json`, `sounds.json`, `creativeTabs.json` and `assets/` are present, blocks/items/sounds are registered and resources are bundled into the JAR. The client must also install the JAR for rendering. See [registries_en.md](registries_en.md). +::: info Custom registries +If `registries/blocks.json`, `items.json`, `sounds.json`, `creativeTabs.json` and `assets/` are present, blocks/items/sounds are registered and resources are bundled into the JAR. The client must also install the JAR for rendering. See [registries.md](registries.md). +::: The compiler **reads the following `package.json` fields** and writes them to `neoforge.mods.toml`: @@ -138,7 +147,9 @@ The compiler **reads the following `package.json` fields** and writes them to `n | `bugs.url` | `issueTrackerURL` | Issue tracker link | | `logoFile` | `logoFile` | Mod icon (PNG path in project, bundled as `logo.png`) | -> **`logoFile` usage:** Set to a relative path of a PNG file in the project root (e.g. `"logoFile": "logo.png"`). The file is automatically bundled as `logo.png` in the JAR root — no manual `neoforge.mods.toml` config needed. NeoForge recommends 128×128 or 256×256, PNG format only. Leave empty for the default mod icon. +::: tip logoFile usage +Set to a relative path of a PNG file in the project root (e.g. `"logoFile": "logo.png"`). The file is automatically bundled as `logo.png` in the JAR root — no manual `neoforge.mods.toml` config needed. NeoForge recommends 128×128 or 256×256, PNG format only. Leave empty for the default mod icon. +::: Output filename format: `dist/-.jar`. Compilation runs on a background thread — no server tick blocking. The output path is shown in chat on completion. @@ -149,7 +160,7 @@ Output filename format: `dist/-.jar`. Compilation runs on a backg **Output JAR contents:** -``` +```text mygame-1.0.0.jar ├── META-INF/neoforge.mods.toml ← mod metadata (depends on box3js) ├── logo.png ← mod icon (if specified) @@ -166,7 +177,7 @@ mygame-1.0.0.jar **Deployment:** Place the script JAR alongside the Box3JS mod in `mods/`: -``` +```text mods/ ├── box3js-1.0.0.jar ← Box3JS main mod └── mygame-1.0.0.jar ← compiled script mod @@ -183,7 +194,9 @@ mods/ | Hot reload | Yes | No (restart server to update) | | Use case | Development & debugging | Distribution & deployment | -> **Note:** Compiled JARs are standard NeoForge mods managed by the NeoForge mod loader. They are **not** controlled by `/box3script start/stop/reload`. Multiple compiled JARs can coexist in `mods/` — each runs independently with its own hardcoded metadata. +::: warning +Compiled JARs are standard NeoForge mods managed by the NeoForge mod loader. They are **not** controlled by `/box3script start/stop/reload`. Multiple compiled JARs can coexist in `mods/` — each runs independently with its own hardcoded metadata. +::: ## Configuration File @@ -198,7 +211,7 @@ Enable/disable state is saved in `config/box3/scripts.json`: ## Script Directory Structure -``` +```text config/box3/ ├── scripts.json ← project enable/disable config ├── script/ ← scripts directory diff --git a/Box3JS-NeoForge-1.21.1/docs/api/database_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/database.md similarity index 94% rename from Box3JS-NeoForge-1.21.1/docs/api/database_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/database.md index 9cb46af..f7cb4d6 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/database_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/database.md @@ -1,8 +1,13 @@ +--- +--- + # Database API Box3JS exposes SQLite capabilities through the global `db` object. Connections are managed automatically. -> **Runtime:** Available on both server and client. Server databases live at `config/box3/data/.db`; client databases live under the local game directory at `box3/client-db/.db`. The two sides do not share database files; use `remoteChannel` when data must be synchronized. +::: info Runtime +Available on both server and client. Server databases live at `config/box3/data/.db`; client databases live under the local game directory at `box3/client-db/.db`. The two sides do not share database files; use `remoteChannel` when data must be synchronized. +::: ## Dependency & Graceful Fallback @@ -16,10 +21,10 @@ db API requires SQLite JDBC driver. Install the minecraft-sqlite-jdbc mod, then After installing `minecraft-sqlite-jdbc` and restarting the server, the `db` API becomes available. -> **NeoForge dev environment note:** -> -> - Put `minecraft-sqlite-jdbc` under `run/mods/`. -> - The file must be a `.jar` (for example, `xxx.jar`), not `.zip`, otherwise NeoForge will not load it. +::: warning NeoForge dev environment +- Put `minecraft-sqlite-jdbc` under `run/mods/`. +- The file must be a `.jar` (for example, `xxx.jar`), not `.zip`, otherwise NeoForge will not load it. +::: ## `db.isAvailable()` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/entity.md similarity index 96% rename from Box3JS-NeoForge-1.21.1/docs/api/entity_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/entity.md index f218985..8eaa364 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/entity.md @@ -1,6 +1,9 @@ +--- +--- + # entity — Entity API -`entity` represents any entity in the Minecraft world (mobs, animals, items, players). Obtain via `world.spawnEntity()`, `world.createEntity()`, `world.querySelector()`, `world.searchBox()`, `world.entitiesInRadius()`, or event callback parameters. +`entity` represents any entity in the Minecraft world (mobs, animals, items, players). Use `entity.player` to get the corresponding `player` object (non-null only when the entity is a player). @@ -415,7 +418,9 @@ entity.setAttribute("minecraft:generic.knockback_resistance", 1.0); entity.setAttribute("minecraft:generic.armor", 10); ``` -> Note: `maxHp` / `hp` / `walkSpeed` / `jumpPower` and other Box3 convenience properties use these attributes internally. Prefer the convenience properties; use `setAttribute` only for attributes not exposed as properties. +::: tip +`maxHp` / `hp` / `walkSpeed` / `jumpPower` and other Box3 convenience properties use these attributes internally. Prefer the convenience properties; use `setAttribute` only for attributes not exposed as properties. +::: ## Lifecycle diff --git a/Box3JS-NeoForge-1.21.1/docs/api/http_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/http.md similarity index 84% rename from Box3JS-NeoForge-1.21.1/docs/api/http_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/http.md index 2b419c4..b0542fe 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/http_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/http.md @@ -1,8 +1,13 @@ +--- +--- + # HTTP API -Box3JS provides HTTP request capabilities via the global `http` object, supporting all HTTP methods, timeout, custom headers, auto-parsing, binary uploads, and both synchronous and asynchronous calling modes. +HTTP requests via the global `http` object. -> **Runtime:** Available on both server and client. Server-side synchronous requests block the server tick, so avoid long-running requests in high-frequency callbacks. Client-side synchronous requests block the client render/logic thread. **Async requests** (`async: true`) deliver results via callbacks. +::: info Runtime +Available on both server and client. Synchronous requests block the current thread — avoid long requests in high-frequency callbacks. **Async requests** (`async: true`) deliver results via callbacks. +::: ## `http.fetch(url, options?)` @@ -27,9 +32,11 @@ Sends an HTTP request and returns `GameHttpFetchResponse`. | `onResponse` | `function` | — | Callback on async success, receives `GameHttpFetchResponse` | | `onError` | `function` | — | Callback on async failure, receives error message string | -> When `responseType` is set, the parsed result is available via `resp.data` — no need to call `resp.json()` manually. -> -> In async mode `fetch()` returns `null`. Results are delivered via callbacks. +::: info +When `responseType` is set, the parsed result is available via `resp.data` — no need to call `resp.json()` manually. + +In async mode `fetch()` returns `null`. Results are delivered via callbacks. +::: ## GameHttpFetchResponse diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/math.md b/Box3JS-NeoForge-1.21.1/docs/en/api/math.md new file mode 100644 index 0000000..1915335 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/math.md @@ -0,0 +1,408 @@ +--- +--- + +# Math Types + +The following data types are globally available in JS. + +## GameVector3 + +A 3D vector with double-precision components. Used for position, direction, velocity, etc. + +### Constructor + +```js +var v = new GameVector3(); // Zero vector (0, 0, 0) +var v = new GameVector3(x, y, z); // Specified coordinates +``` + +### Properties + +| Property | Type | Description | +| -------- | -------- | ------------------------------------- | +| `v.x` | `number` | X component (east/west), read/write | +| `v.y` | `number` | Y component (up/down), read/write | +| `v.z` | `number` | Z component (north/south), read/write | + +### Instance Methods + +#### Mutating (return this) + +| Method | Returns | Description | +| ---------------- | ------------- | --------------------------------------------------------------------- | +| `v.set(x, y, z)` | `GameVector3` | Set all components | +| `v.copy(w)` | `GameVector3` | Copy all components from `w` | +| `v.addEq(w)` | `GameVector3` | In-place addition: `v += w` | +| `v.subEq(w)` | `GameVector3` | In-place subtraction: `v -= w` | +| `v.mulEq(w)` | `GameVector3` | In-place component-wise multiplication | +| `v.divEq(w)` | `GameVector3` | In-place component-wise division; divide-by-zero skips that component | +| `v.scaleEq(n)` | `GameVector3` | In-place scalar multiplication: `v.x *= n` … | +| `v.negEq()` | `GameVector3` | In-place negation: `v = -v` | + +#### Creating New Vectors (does not mutate) + +| Method | Returns | Description | +| --------------------------------- | ------------- | --------------------------------------------------------------- | +| `v.clone()` | `GameVector3` | Deep copy — independent vector with same values | +| `v.add(w)` | `GameVector3` | Vector addition: `v + w` | +| `v.sub(w)` | `GameVector3` | Vector subtraction: `v - w` | +| `v.mul(w)` | `GameVector3` | Component-wise multiplication | +| `v.div(w)` | `GameVector3` | Component-wise division; divide-by-zero → 0 | +| `v.scale(n)` | `GameVector3` | Scalar multiplication: each component × `n` | +| `v.cross(w)` | `GameVector3` | Cross product: `v × w` | +| `v.normalize()` | `GameVector3` | Unit vector; zero vector returns `(0,0,0)` | +| `v.lerp(w, t)` | `GameVector3` | Linear interpolation: `t=0` → this, `t=1` → `w` | +| `v.towards(w)` | `GameVector3` | Direction vector pointing toward `w` (normalized) | +| `v.max(w)` | `GameVector3` | Component-wise maximum | +| `v.min(w)` | `GameVector3` | Component-wise minimum | +| `v.neg()` | `GameVector3` | Negation: `-v` | +| `v.moveTowards(target, maxDelta)` | `GameVector3` | Move toward target by at most `maxDelta` distance | +| `v.floor()` | `GameVector3` | Component-wise floor | +| `v.ceil()` | `GameVector3` | Component-wise ceiling | +| `v.clampLength(max)` | `GameVector3` | Clamp magnitude to `max`, scale down proportionally if exceeded | + +#### Numeric Computations + +| Method | Returns | Description | +| ------------------ | -------- | -------------------------------------------------- | +| `v.dot(w)` | `number` | Dot (inner) product: `v · w` | +| `v.mag()` | `number` | Magnitude (length) | +| `v.sqrMag()` | `number` | Squared magnitude — faster than `mag()` | +| `v.distance(w)` | `number` | Euclidean distance to `w` | +| `v.angle(w)` | `number` | Angle between `v` and `w` (radians, 0–π) | +| `v.sqrDistance(w)` | `number` | Squared distance to `w` — faster than `distance()` | + +#### Comparison + +| Method | Returns | Description | +| ------------------ | --------- | ------------------------------------------------------------- | +| `v.equals(w)` | `boolean` | Approximate equality, tolerance 1e-6 | +| `v.exactEquals(w)` | `boolean` | Exact equality — components strictly equal | +| `v.isZero()` | `boolean` | Whether this is (approximately) a zero vector, tolerance 1e-6 | + +```js +var pos = new GameVector3(0, 100, 0); +var target = new GameVector3(10, 100, 10); + +// Distance +var dist = pos.distance(target); // ~14.14 + +// Direction vector +var dir = target.sub(pos).normalize(); + +// Angle +var angle = pos.angle(target); // radians + +// Comparison +var a = new GameVector3(1, 2, 3); +var b = new GameVector3(1.0000001, 2.0000001, 3.0000001); +a.equals(b); // true (within tolerance) +a.exactEquals(b); // false + +// Teleport entity (LiveVec3) +entity.position.set(0, 100, 0); +``` + +### Static Methods + +```js +// Spherical coordinates → vector +var v = GameVector3.fromPolar(mag, phi, theta); +// mag: radius +// phi: azimuth angle (radians, horizontal rotation around Y) +// theta: elevation angle (radians, from horizontal plane) +``` + +### toString + +```js +var v = new GameVector3(1, 2, 3); +v.toString(); // "GameVector3(1.0, 2.0, 3.0)" +``` + +## GameBounds3 + +Axis-aligned bounding box (AABB), defined by two opposing corners: `lo` (minimum corner) and `hi` (maximum corner). + +### Constructor + +```js +var bounds = new GameBounds3( + new GameVector3(-1, 0, -1), // lo (min corner) + new GameVector3(1, 2, 1), // hi (max corner) +); +``` + +### Properties + +| Property | Type | Description | +| ----------- | ------------- | -------------------------- | +| `bounds.lo` | `GameVector3` | Minimum corner, read/write | +| `bounds.hi` | `GameVector3` | Maximum corner, read/write | + +### Instance Methods + +| Method | Returns | Description | +| ------------------------------------------ | --------------------- | ---------------------------------------------------------- | +| `bounds.set(lox, loy, loz, hix, hiy, hiz)` | `GameBounds3` | Set all boundaries in-place, returns this | +| `bounds.copy(b)` | `GameBounds3` | Copy values from `b` in-place, returns this | +| `bounds.intersects(other)` | `boolean` | Whether this intersects `other` | +| `bounds.intersect(other)` | `GameBounds3 \| null` | Intersection bounds, or `null` if no overlap | +| `bounds.contains(v)` | `boolean` | Whether point `v` is inside (inclusive) | +| `bounds.containsBounds(b)` | `boolean` | Whether this fully contains `b` | +| `bounds.center()` | `GameVector3` | Center point of the bounds | +| `bounds.size()` | `GameVector3` | Size of the bounds (width, height, depth) | +| `bounds.expand(delta)` | `GameBounds3` | Expand all faces outward by `delta`, returns new bounds | +| `bounds.expandEq(delta)` | `GameBounds3` | In-place expand all faces outward by `delta`, returns this | +| `bounds.growToInclude(v)` | `GameBounds3` | In-place grow to include point `v`, returns this | +| `bounds.closestPoint(v)` | `GameVector3` | Closest point on the bounds to point `v` | +| `bounds.move(offset)` | `GameBounds3` | Translate by `offset`, returns new bounds | +| `bounds.moveEq(offset)` | `GameBounds3` | In-place translate by `offset`, returns this | + +### Static Methods + +```js +// Create minimal bounds from an array of GameVector3 +var points = [new GameVector3(0, 0, 0), new GameVector3(5, 10, 3)]; +var box = GameBounds3.fromPoints(points); // returns GameBounds3 or null +``` + +### toString + +```js +bounds.toString(); // "GameBounds3(GameVector3(-1.0, 0.0, -1.0), GameVector3(1.0, 2.0, 1.0))" +``` + +```js +// Query entities within bounds +var entities = world.searchBox(bounds); + +// Check if point is inside +if (bounds.contains(player.position)) { + // Player is inside the area +} +``` + +## GameRGBColor + +An RGB color with three channels ranging from 0.0 to 1.0. + +### Constructor + +```js +var red = new GameRGBColor(1, 0, 0); +var blue = new GameRGBColor(0, 0, 1); +var gray = new GameRGBColor(0.5, 0.5, 0.5); +``` + +### Properties + +| Property | Type | Description | +| --------- | -------- | ------------------------------- | +| `color.r` | `number` | Red channel (0–1), read/write | +| `color.g` | `number` | Green channel (0–1), read/write | +| `color.b` | `number` | Blue channel (0–1), read/write | + +### Instance Methods + +#### Mutating (return this) + +| Method | Returns | Description | +| ---------------- | -------------- | ---------------------------------------------------- | +| `c.set(r, g, b)` | `GameRGBColor` | Set all channels | +| `c.copy(o)` | `GameRGBColor` | Copy all channels from another color | +| `c.addEq(o)` | `GameRGBColor` | In-place addition: `c += o` | +| `c.subEq(o)` | `GameRGBColor` | In-place subtraction: `c -= o` | +| `c.mulEq(o)` | `GameRGBColor` | In-place channel-wise multiplication | +| `c.divEq(o)` | `GameRGBColor` | In-place channel-wise division; divide-by-zero skips | +| `c.scaleEq(n)` | `GameRGBColor` | In-place scalar multiplication: each channel × `n` | + +#### Creating New Colors (does not mutate) + +| Method | Returns | Description | +| -------------- | -------------- | ----------------------------------------------- | +| `c.clone()` | `GameRGBColor` | Deep copy | +| `c.add(o)` | `GameRGBColor` | Channel-wise addition | +| `c.sub(o)` | `GameRGBColor` | Channel-wise subtraction | +| `c.mul(o)` | `GameRGBColor` | Channel-wise multiplication | +| `c.div(o)` | `GameRGBColor` | Channel-wise division; divide-by-zero → 0 | +| `c.lerp(o, t)` | `GameRGBColor` | Linear interpolation: `t=0` → this, `t=1` → `o` | +| `c.scale(n)` | `GameRGBColor` | Scalar multiplication: each channel × `n` | +| `c.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | +| `c.toRGBA()` | `string` | CSS format string: `"rgba(r,g,b,1.0)"` | + +### Static Methods + +```js +var randomColor = GameRGBColor.random(); // Each channel 0–1 random +``` + +### toString + +```js +new GameRGBColor(1, 0.5, 0).toString(); // "GameRGBColor(1.0, 0.5, 0.0)" +``` + +## GameRGBAColor + +An RGBA color with four channels ranging from 0.0 to 1.0. + +### Constructor + +```js +var semiRed = new GameRGBAColor(1, 0, 0, 0.5); +var opaque = new GameRGBAColor(0, 1, 0, 1.0); +``` + +### Properties + +| Property | Type | Description | +| --------- | -------- | ------------------------------- | +| `color.r` | `number` | Red channel (0–1), read/write | +| `color.g` | `number` | Green channel (0–1), read/write | +| `color.b` | `number` | Blue channel (0–1), read/write | +| `color.a` | `number` | Alpha opacity (0–1), read/write | + +### Instance Methods + +#### Mutating (return this) + +| Method | Returns | Description | +| ------------------- | --------------- | ---------------------------------------------------- | +| `c.set(r, g, b, a)` | `GameRGBAColor` | Set all four channels | +| `c.copy(o)` | `GameRGBAColor` | Copy all channels from another RGBA color | +| `c.addEq(o)` | `GameRGBAColor` | In-place addition | +| `c.subEq(o)` | `GameRGBAColor` | In-place subtraction | +| `c.mulEq(o)` | `GameRGBAColor` | In-place channel-wise multiplication | +| `c.divEq(o)` | `GameRGBAColor` | In-place channel-wise division; divide-by-zero skips | +| `c.scaleEq(n)` | `GameRGBAColor` | In-place scalar multiplication: each channel × `n` | + +#### Creating New Colors (does not mutate) + +| Method | Returns | Description | +| ---------------- | --------------- | --------------------------------------------------------- | +| `c.clone()` | `GameRGBAColor` | Deep copy | +| `c.add(o)` | `GameRGBAColor` | Channel-wise addition | +| `c.sub(o)` | `GameRGBAColor` | Channel-wise subtraction | +| `c.mul(o)` | `GameRGBAColor` | Channel-wise multiplication | +| `c.div(o)` | `GameRGBAColor` | Channel-wise division; divide-by-zero → 0 | +| `c.lerp(o, t)` | `GameRGBAColor` | Linear interpolation | +| `c.scale(n)` | `GameRGBAColor` | Scalar multiplication: each channel × `n` | +| `c.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | +| `c.blendEq(rgb)` | `GameRGBColor` | Alpha-blend onto an RGB background, returns displayed RGB | + +### toString + +```js +new GameRGBAColor(1, 0, 0, 0.5).toString(); // "GameRGBAColor(1.0, 0.0, 0.0, 0.5)" +``` + +```js +// Alpha blending +var fg = new GameRGBAColor(1, 0, 0, 0.5); // Semi-transparent red +var bg = new GameRGBColor(1, 1, 1); // White background +var result = fg.blendEq(bg); // Blended RGB color +``` + +## GameQuaternion + +A quaternion used for 3D rotation. Unit quaternions (magnitude=1) represent pure rotations. + +### Constructor + +```js +var q = new GameQuaternion(); // Identity (1, 0, 0, 0) +var q = new GameQuaternion(w, x, y, z); // Specified components +``` + +### Properties + +| Property | Type | Description | +| -------- | -------- | ----------------------------------- | +| `q.w` | `number` | Real (scalar) component, read/write | +| `q.x` | `number` | Imaginary X component, read/write | +| `q.y` | `number` | Imaginary Y component, read/write | +| `q.z` | `number` | Imaginary Z component, read/write | + +### Instance Methods + +#### Mutating (return this) + +| Method | Returns | Description | +| ------------------- | ---------------- | ---------------------------- | +| `q.set(w, x, y, z)` | `GameQuaternion` | Set all components | +| `q.copy(p)` | `GameQuaternion` | Copy all components from `p` | + +#### Creating New Quaternions (does not mutate) + +| Method | Returns | Description | +| --------------- | ---------------- | ----------------------------------------------- | +| `q.clone()` | `GameQuaternion` | Deep copy | +| `q.add(p)` | `GameQuaternion` | Component-wise addition | +| `q.sub(p)` | `GameQuaternion` | Component-wise subtraction | +| `q.mul(p)` | `GameQuaternion` | Hamilton product: `q × p` (NOT commutative) | +| `q.div(p)` | `GameQuaternion` | Division: `q × p⁻¹` | +| `q.inv()` | `GameQuaternion` | Conjugate (equals inverse for unit quaternions) | +| `q.normalize()` | `GameQuaternion` | Normalize, returns unit quaternion | + +#### Interpolation + +| Method | Returns | Description | +| --------------- | ---------------- | --------------------------------------------------------- | +| `q.slerp(p, t)` | `GameQuaternion` | Spherical linear interpolation: `t=0` → this, `t=1` → `p` | + +#### Numeric Computations + +| Method | Returns | Description | +| ------------- | --------- | ------------------------------------- | +| `q.dot(p)` | `number` | Dot product | +| `q.mag()` | `number` | Magnitude (norm) | +| `q.sqrMag()` | `number` | Squared magnitude | +| `q.angle(p)` | `number` | Angular difference from `p` (radians) | +| `q.equals(p)` | `boolean` | Approximate equality, tolerance 1e-6 | + +#### Rotation Operations (rotate around local axes, returns new quaternion) + +| Method | Returns | Description | +| ------------------- | ---------------- | ------------------------------------------------------------------- | +| `q.rotateX(rad)` | `GameQuaternion` | Rotate around X axis | +| `q.rotateY(rad)` | `GameQuaternion` | Rotate around Y axis | +| `q.rotateZ(rad)` | `GameQuaternion` | Rotate around Z axis | +| `q.rotateVector(v)` | `GameVector3` | Rotate vector `v` by this quaternion | +| `q.toEuler()` | `GameVector3` | Convert to Euler angles (YZX order), returns `(x, y, z)` in radians | + +#### Axis-Angle Decomposition + +```js +var result = q.getAxisAngle(); +// result.angle — rotation angle (radians) +// result.axis — rotation axis (unit GameVector3) +``` + +### Static Methods + +```js +// Create from axis-angle representation +var q1 = GameQuaternion.fromAxisAngle(axis, rad); +// axis: GameVector3 (auto-normalized) +// rad: rotation angle (radians) + +// Create from Euler angles (YZX rotation order: Y → Z → X) +var q2 = GameQuaternion.fromEuler(x, y, z); +// x, y, z: rotation around each axis in radians + +// Shortest-arc quaternion rotating from vector a to b +var q3 = GameQuaternion.rotationBetween(fromVec, toVec); + +// Create quaternion from look-at direction (from → to) +var q4 = GameQuaternion.lookAt(from, to, up); +// from: GameVector3 — observer position +// to: GameVector3 — target point +// up: GameVector3 — up direction (default (0,1,0)) +``` + +### toString + +```js +q.toString(); // "GameQuaternion(0.707, 0.0, 0.707, 0.0)" +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/player.md similarity index 98% rename from Box3JS-NeoForge-1.21.1/docs/api/player_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/player.md index d76bb5f..126bbbd 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/player.md @@ -1,6 +1,9 @@ +--- +--- + # player — Player API -The `player` object is obtained via `entity.player` and represents a logged-in player. It includes all `entity` capabilities (like `hp`, `position`, `tags()`, etc.) plus player-specific features: inventory, XP, flight, messaging, teleport, etc. +Obtained via `entity.player`. Includes all `entity` capabilities plus inventory, XP, flight, messaging, teleport, etc. ```js world.onPlayerJoin(function(entity, tick) { diff --git a/Box3JS-NeoForge-1.21.1/docs/api/registries_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/registries.md similarity index 92% rename from Box3JS-NeoForge-1.21.1/docs/api/registries_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/registries.md index b1511a7..080e1b5 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/registries_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/registries.md @@ -1,16 +1,17 @@ +--- +--- + # Custom Registries (registries API) -> **Server-side only.** `registries` is `undefined` on the client. For client code, use the ResourceLocation string directly (e.g. `audio.playSound("colorzone:victory_fanfare", 1.0, 1.0)`). -> -> **Only available in compiled JAR mode (`/box3script compile`).** In interpreted mode (`/box3script start`), `registries` is `undefined`. -> -> **The compiled JAR must be installed on both server and client** for block textures/models to render. Without it on the client, blocks appear as purple/black missing textures. +::: warning +Available only in compiled JAR mode (`/box3script compile`) on the **server**. `registries` is `undefined` on client and in interpreted mode. JAR must be installed on both sides to render. +::: Blocks, items, and sound events are declared in JSON config files. At compile time, `DeferredRegister` code is generated and injected into the `@Mod` class. Assets are bundled from the project's `assets/` directory into the JAR. ## Project Layout -``` +```text mygame/ ├── registries/ │ ├── blocks.json ← block definitions @@ -137,7 +138,9 @@ Item textures follow the same pattern as block textures: `assets/textures/item/< } ``` -> **Note:** Creative tab icon lookup searches items first, then blocks. If `creativeTabs.json`'s `icon` matches an item key, that item will be used as the icon. +::: tip +Creative tab icon lookup searches items first, then blocks. If `creativeTabs.json`'s `icon` matches an item key, that item will be used as the icon. +::: ### Equipment Types (Tools & Armor) @@ -198,7 +201,9 @@ Equipment examples: } ``` -> **Note:** Equipment items always have `maxStackSize` fixed to 1 (unstackable). `nutrition`/`saturation`/`alwaysEdible` only apply to `"food"` type. +::: tip +Equipment items always have `maxStackSize` fixed to 1 (unstackable). `nutrition`/`saturation`/`alwaysEdible` only apply to `"food"` type. +::: ## sounds.json @@ -234,7 +239,7 @@ The compiler auto-generates `assets//sounds.json` in the standard Minecra Corresponding files: -``` +```js assets/sounds/victory_fanfare.ogg assets/sounds/skill_cast.ogg assets/sounds/background_music.ogg @@ -266,7 +271,7 @@ audio.playSound("colorzone:victory_fanfare", 1.0, 1.0); Follows the standard Minecraft resource pack structure: -``` +```text assets// ├── blockstates/.json ← auto-generated unless you provide custom ├── models/block/.json ← auto-generated; can override @@ -281,7 +286,9 @@ assets// └── item/.png ``` -> **Note:** `` is taken from the `name` field in `package.json` (text after the last `/`, e.g. `@scope/mygame` → `mygame`). +::: tip +`` is taken from the `name` field in `package.json` (text after the last `/`, e.g. `@scope/mygame` → `mygame`). +::: At compile time, `assets/` is automatically bundled as `assets//` in the JAR. @@ -289,7 +296,7 @@ At compile time, `assets/` is automatically bundled as `assets//` in the Language files must be **created manually** in `assets/lang/`. They are not auto-generated. At minimum, provide `en_us.json` and `zh_cn.json`. You can also add more languages: -``` +```text mygame/ └── assets/ └── lang/ diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/server.md similarity index 90% rename from Box3JS-NeoForge-1.21.1/docs/api/server_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/server.md index ea0d25c..c0358a7 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/server.md @@ -1,8 +1,13 @@ +--- +--- + # server — Server-side API Overview Server scripts run on the Minecraft server thread. The entry file is `src/server/app.ts`, and the compiled output is `dist/server.js`. Server APIs handle world state, entities and players, block read/write, event callbacks, persistence, HTTP, and server-to-client event delivery. -> Client UI, keyboard input, local sounds, and local GUI are not server APIs. See [client_en.md](client_en.md). +::: info +Client UI, keyboard input, local sounds, and local GUI are not server APIs. See [client.md](client.md). +::: ## Server Globals @@ -215,15 +220,15 @@ Custom content files: | Document | Content | |----------|---------| -| [world_en.md](world_en.md) | World state, events, scoreboard, bossbar, teams, border, particles, sounds | -| [entity_en.md](entity_en.md) | Entity properties, AI, equipment, effects, navigation, tags | -| [player_en.md](player_en.md) | Player inventory, messaging, teleport, flight, game mode, XP | -| [voxels_en.md](voxels_en.md) | Block read/write, region fill, spawners | -| [storage_en.md](storage_en.md) | JSON persistence | -| [database_en.md](database_en.md) | SQLite database | -| [http_en.md](http_en.md) | HTTP requests | -| [registries_en.md](registries_en.md) | Custom registries and compiled JAR mode | -| [math_en.md](math_en.md) | Vectors, bounds, colors, quaternions | +| [world.md](world.md) | World state, events, scoreboard, bossbar, teams, border, particles, sounds | +| [entity.md](entity.md) | Entity properties, AI, equipment, effects, navigation, tags | +| [player.md](player.md) | Player inventory, messaging, teleport, flight, game mode, XP | +| [voxels.md](voxels.md) | Block read/write, region fill, spawners | +| [storage.md](storage.md) | JSON persistence | +| [database.md](database.md) | SQLite database | +| [http.md](http.md) | HTTP requests | +| [registries.md](registries.md) | Custom registries and compiled JAR mode | +| [math.md](math.md) | Vectors, bounds, colors, quaternions | ## Recommended Style diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/storage.md similarity index 90% rename from Box3JS-NeoForge-1.21.1/docs/api/storage_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/storage.md index 2765df9..e33d556 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/storage.md @@ -1,8 +1,13 @@ +--- +--- + # storage — Data Storage API `storage` provides JSON file persistence with in-memory caching for fast reads/writes. -> **Runtime:** Available on both server and client. Server data is saved under `config/box3/storage//`; client data is saved under the local game directory at `box3/client-storage//`. Each project automatically gets an independent namespace. +::: info Runtime +Available on both server and client. Server data is saved under `config/box3/storage//`; client data is saved under the local game directory at `box3/client-storage//`. Each project automatically gets an independent namespace. +::: ## Getting a Storage Instance @@ -18,7 +23,9 @@ Gets or creates a named storage. Same name returns the same instance. Gets a **cross-project shared** storage. All projects access the same data via the same `name` (uses `__shared__/` namespace internally). Useful for global leaderboards, shared config, etc. -> Server-side only. Client local storage only provides `getDataStorage(name)`. +::: warning +Server-side only. Client local storage only provides `getDataStorage(name)`. +::: ```js var store = storage.getDataStorage("leaderboard"); @@ -45,7 +52,9 @@ var winner = store.get("lastWinner"); // "Steve" (string) var cfg = store.get("config"); // {difficulty: "hard", ...} (object) ``` -> **Note:** After data is reloaded from disk, complex values are returned as plain JSON objects (for example map-like objects). Avoid relying on original JS prototype methods. +::: warning Note +After data is reloaded from disk, complex values are returned as plain JSON objects (for example map-like objects). Avoid relying on original JS prototype methods. +::: ### store.keys() diff --git a/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md similarity index 99% rename from Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md index ffe5726..4e55b7a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md @@ -1,3 +1,6 @@ +--- +--- + # voxels — Block Operations API `voxels` provides pure block-level read/write operations. No entity logic is involved. diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/world.md similarity index 99% rename from Box3JS-NeoForge-1.21.1/docs/api/world_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/world.md index b9974fe..e90b74a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/world.md @@ -1,6 +1,9 @@ +--- +--- + # world — World API -`world` is a global singleton representing the Minecraft server's world state. Controls weather, time, game rules, entity spawning; registers event callbacks; manages scoreboards, bossbars, teams; and fires particles, fireworks, lightning, and other visual effects. +`world` is a global singleton representing the Minecraft server's world state. ## World Properties diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/README_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/README.md similarity index 86% rename from Box3JS-NeoForge-1.21.1/docs/guide/README_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/README.md index 1d9c709..0668a17 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/README.md @@ -1,21 +1,22 @@ -# Box3JS Guides +--- +--- -Start here whether you want to get your hands dirty quickly or dive deep into internals. +# Box3JS Guides ## Pick Your Path | I want to... | Read this | |-------------|----------| -| Understand where Box3JS comes from and why | [Box3JS & Box3](about-box3js_en.md) | -| Write my first script in 10 minutes | [Quick Start](getting-started_en.md) | -| Use ready-made templates to implement features | [Common Recipes](recipes_en.md) | -| Understand how Box3JS works internally | [Architecture](architecture_en.md) | -| Decide between Box3JS and Java modding | [JS vs Java](js-vs-java_en.md) | -| Troubleshoot problems | [FAQ](faq_en.md) | +| Understand where Box3JS comes from and why | [Box3JS & Box3](about-box3js.md) | +| Write my first script in 10 minutes | [Quick Start](getting-started.md) | +| Use ready-made templates to implement features | [Common Recipes](recipes.md) | +| Understand how Box3JS works internally | [Architecture](architecture.md) | +| Decide between Box3JS and Java modding | [JS vs Java](js-vs-java.md) | +| Troubleshoot problems | [FAQ](faq.md) | ## Learning Path -``` +```text Box3JS & Box3 → Quick Start Architecture JS vs Java │ │ │ │ Setup │ Rhino engine │ Pros & cons diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md similarity index 67% rename from Box3JS-NeoForge-1.21.1/docs/guide/about-box3js_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md index 2386259..b4fcceb 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md @@ -1,6 +1,7 @@ -# Box3JS and Box3 (Shenqi Code Island) +--- +--- -This page explains where Box3JS comes from, why it exists, and what unique advantages it offers. +# Box3JS and Box3 (Shenqi Code Island) ## What is Box3 (神奇代码岛) @@ -38,12 +39,12 @@ But Box3 runs on its own closed platform. Creators can't reach the Minecraft eco ### Who Box3JS is For -| User | Why | -|------|-----| -| Box3 platform developers | Same API style — reuse existing skills with zero learning curve | -| MC server owners wanting custom gameplay | No Java, Gradle, or Mixin needed — just write JS | -| Programming education | TypeScript + hot reload + sandbox rollback = ideal teaching environment | -| Developers who don't want to write Java mods | Ready-to-use APIs cover common needs with no build pipeline burden | +| User | Why | +| -------------------------------------------- | ----------------------------------------------------------------------- | +| Box3 platform developers | Same API style — reuse existing skills with zero learning curve | +| MC server owners wanting custom gameplay | No Java, Gradle, or Mixin needed — just write JS | +| Programming education | TypeScript + hot reload + sandbox rollback = ideal teaching environment | +| Developers who don't want to write Java mods | Ready-to-use APIs cover common needs with no build pipeline burden | ## Unique Advantages of Box3JS @@ -88,7 +89,7 @@ Box3JS directly operates the Minecraft world — real blocks, vanilla entities, When development is done, compile into a standalone JAR with one command: -``` +```js /box3script compile mygame ``` @@ -96,7 +97,7 @@ Generates `mygame-1.0.0.jar` — drop it into any NeoForge 1.21.1 server's `mods ### 5. Dual-Side Architecture -``` +```text Server (authoritative) Client (presentation) world.* / voxels.* client.* / input.* entity.* / player.* ←→ ui.* / audio.* / gui.* @@ -117,23 +118,21 @@ Server handles authoritative game logic; client handles local presentation (UI, Box3JS is not a 1:1 copy of Box3's API. Differences stem from the fundamental differences between the two platforms: -| Area | Box3 Platform | Box3JS (MC) | -|------|--------------|-------------| -| Rendering | Custom 3D engine | Minecraft vanilla renderer | -| Physics | Custom physics engine | Minecraft vanilla physics | -| Weather | Independent rain/snow/fog (rich parameter control) | MC vanilla weather (rainDensity/thunderDensity) | -| Lighting | Manual/natural light modes (lightMode/sunFrequency) | MC vanilla lighting | -| Custom models | Built-in editor | Requires Resource Pack (MC mechanism) | -| Custom blocks/items | Runtime registration | Requires JAR compilation (`registries` + `/box3script compile`) | -| Database | Built-in KV storage | JSON storage + SQLite (requires sqlite-jdbc mod) | -| Networking | Platform-managed | `remoteChannel` custom payloads | +| Area | Box3 Platform | Box3JS (MC) | +| ------------------- | --------------------------------------------------- | --------------------------------------------------------------- | +| Rendering | Custom 3D engine | Minecraft vanilla renderer | +| Physics | Custom physics engine | Minecraft vanilla physics | +| Weather | Independent rain/snow/fog (rich parameter control) | MC vanilla weather (rainDensity/thunderDensity) | +| Lighting | Manual/natural light modes (lightMode/sunFrequency) | MC vanilla lighting | +| Custom models | Built-in editor | Requires Resource Pack (MC mechanism) | +| Custom blocks/items | Runtime registration | Requires JAR compilation (`registries` + `/box3script compile`) | +| Database | Built-in KV storage | JSON storage + SQLite (requires sqlite-jdbc mod) | +| Networking | Platform-managed | `remoteChannel` custom payloads | **Design principle:** Keep API naming and semantics consistent where possible, but don't force-fit MC-incompatible features. For a detailed comparison, see [Box3 API vs Box3JS](../BOX3_API_COMPARISON.md). ---- - ## Next Steps -- **Get started**: [Quick Start Guide](getting-started_en.md) — write your first MC script in 10 minutes -- **How it works**: [Architecture](architecture_en.md) — Rhino engine, scope isolation, build pipeline -- **API reference**: [API by Task](../api/README_en.md) — find APIs by "I want to..." +- **Get started**: [Quick Start Guide](getting-started.md) — write your first MC script in 10 minutes +- **How it works**: [Architecture](architecture.md) — Rhino engine, scope isolation, build pipeline +- **API reference**: [API by Task](../api/README.md) — find APIs by "I want to..." diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/architecture.md similarity index 90% rename from Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/architecture.md index c26fc78..360bf23 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/architecture.md @@ -1,25 +1,11 @@ -# Box3JS Architecture - -A deep dive into Box3JS's internals: how the JS engine is embedded in Minecraft, how scopes are managed, how the build pipeline works, and how network communication is implemented. - -## Table of Contents - -1. [Overall Architecture](#overall-architecture) -2. [Rhino Engine](#rhino-engine) -3. [Scopes & Isolation](#scopes--isolation) -4. [Global Object Injection](#global-object-injection) -5. [Event Callback Mechanism](#event-callback-mechanism) -6. [Build Pipeline](#build-pipeline) -7. [Network Communication](#network-communication) -8. [Sandbox System](#sandbox-system) -9. [File Watching & Hot Reload](#file-watching--hot-reload) -10. [Compiled Release Mode](#compiled-release-mode) - +--- --- +# Box3JS Architecture + ## Overall Architecture -``` +```text ┌──────────────────────────┐ │ Minecraft Server │ │ (NeoForge) │ @@ -57,7 +43,7 @@ A deep dive into Box3JS's internals: how the JS engine is embedded in Minecraft, ### Key Package Structure -``` +```text com.box3lab.box3js ├── Box3JS.java ← @Mod entry point ├── script/ ← Server engine @@ -80,19 +66,18 @@ com.box3lab.box3js └── ... ``` ---- - ## Rhino Engine ### Why Rhino -| Engine | Type | Speed | JVM Integration | ES Version | -|--------|------|-------|----------------|------------| -| Mozilla Rhino | Interpreted | Medium | Native (Java impl) | ES5 | -| GraalJS | JIT | Fast | Requires config | ES2023 | -| Nashorn | JIT | Fast | JDK built-in (removed) | ES6 | +| Engine | Type | Speed | JVM Integration | ES Version | +| ------------- | ----------- | ------ | ---------------------- | ---------- | +| Mozilla Rhino | Interpreted | Medium | Native (Java impl) | ES5 | +| GraalJS | JIT | Fast | Requires config | ES2023 | +| Nashorn | JIT | Fast | JDK built-in (removed) | ES6 | Reasons for choosing Rhino: + - **Pure Java implementation**, zero-config JVM embedding, no startup overhead - **Mature and stable**, widely validated in the Minecraft modding community - **Compatible with NeoForge classloader**, no special configuration needed @@ -122,24 +107,22 @@ cx.evaluateReader(scope, scriptReader, "server.js", 1, null); When Java objects are exposed to JS, Rhino automatically handles type conversion: -| Java Type | JS Type | -|-----------|---------| -| `String` | `string` | -| `int` / `double` | `number` | -| `boolean` | `boolean` | -| `Map` | `object` | -| `List` | `array` | +| Java Type | JS Type | +| ------------------------- | --------- | +| `String` | `string` | +| `int` / `double` | `number` | +| `boolean` | `boolean` | +| `Map` | `object` | +| `List` | `array` | | Java object (method call) | JS object | Most Box3JS return values are **native Java objects** (e.g., `ServerPlayer` wrappers). Complex returns (e.g., `querySelectorAll`) return Java `List`, which Rhino maps to JS arrays. ---- - ## Scopes & Isolation ### Per-Project Independent Scopes -``` +```text Rhino Context │ ┌──────────────┼──────────────┐ @@ -153,6 +136,7 @@ Most Box3JS return values are **native Java objects** (e.g., `ServerPlayer` wrap ``` Each project has: + - **Independent top-level scope** — variables don't cross-contaminate - **Independent event callback lists** — stored by `Box3JSEventBus` keyed by project name - **Independent storage namespace** — `storage.getDataStorage("coins")` reads per-project data @@ -161,18 +145,17 @@ Each project has: ### Cleanup Mechanism When stopping a project: + 1. `Box3JSEventBus` clears all event callbacks for that project 2. Scoreboards/BossBars/teams created by the project are removed 3. If sandbox was enabled, all block and entity changes are rolled back 4. Rhino scope is released, GC collects ---- - ## Global Object Injection ### Server-Side Injection -``` +```text Box3ScriptEngine.setupScope(scope) │ ├── scope.put("world", scope, new Box3JSWorld(...)) @@ -194,7 +177,7 @@ Box3ScriptEngine.setupScope(scope) ### Client-Side Injection -``` +```text Box3JSClientEngine.init(scope) │ ├── scope.put("audio", scope, audioObj) @@ -218,23 +201,29 @@ The Java `Box3JSConsole` method signature is `log(Object... args)` (varargs). Rh ```js // CONSOLE_INIT_JS — injected into every scope console = { - log: function() { return _jConsole.log.apply(_jConsole, arguments); }, - debug: function() { return _jConsole.debug.apply(_jConsole, arguments); }, - warn: function() { return _jConsole.warn.apply(_jConsole, arguments); }, - error: function() { return _jConsole.error.apply(_jConsole, arguments); }, + log: function () { + return _jConsole.log.apply(_jConsole, arguments); + }, + debug: function () { + return _jConsole.debug.apply(_jConsole, arguments); + }, + warn: function () { + return _jConsole.warn.apply(_jConsole, arguments); + }, + error: function () { + return _jConsole.error.apply(_jConsole, arguments); + }, // ... }; ``` `.apply()` ensures multiple arguments are correctly forwarded to the Java varargs method. ---- - ## Event Callback Mechanism ### Complete Chain -``` +```text Minecraft event fires │ ▼ @@ -293,6 +282,7 @@ token.active(); // check if still active ``` Java side: + ```java public class GameEventHandlerToken { private boolean active = true; @@ -301,11 +291,9 @@ public class GameEventHandlerToken { } ``` ---- - ## Build Pipeline -``` +```text src/server/app.ts (TypeScript + ES2020 syntax) │ ▼ @@ -349,7 +337,7 @@ import babel from "@babel/core"; // 1. Babel: TS → ES5 JS const es5Code = babel.transformSync(tsCode, { presets: ["@babel/preset-typescript"], - targets: { rhino: "1.9.1" } + targets: { rhino: "1.9.1" }, }); // 2. esbuild: bundle @@ -358,17 +346,15 @@ await build({ bundle: true, outfile: "dist/server.js", target: "es5", - format: "iife" + format: "iife", }); ``` ---- - ## Network Communication ### remoteChannel Architecture -``` +```text ┌──────────────────────┐ ┌──────────────────────┐ │ Server (Java) │ │ Client (Java) │ │ │ │ │ @@ -392,7 +378,8 @@ await build({ ### Data Flow **Server → Client:** -``` + +```text JS: remoteChannel.sendClientEvent(player, { type: "boss_bar", hp: 50 }) → Box3JSRemoteChannel.java → JSON.stringify(eventData) @@ -405,7 +392,8 @@ JS: remoteChannel.sendClientEvent(player, { type: "boss_bar", hp: 50 }) ``` **Client → Server:** -``` + +```text JS: remoteChannel.sendServerEvent({ key: "space" }) → Box3JSClientEngine → JSON.stringify @@ -418,18 +406,17 @@ JS: remoteChannel.sendServerEvent({ key: "space" }) ### Data Format All data crossing the network must be **JSON-serializable**: + - `string`, `number`, `boolean`, `null` - Plain objects `{ key: value }` - Arrays `[1, 2, 3]` - NOT supported: functions, `GameVector3` instances, Java objects ---- - ## Sandbox System ### How It Works -``` +```text /box3script sandbox mygame ← enable sandbox │ ▼ @@ -468,13 +455,11 @@ class SandboxTracker { - **Player testing** — let players try new features, rollback after without affecting the live server - **Debugging** — test destructive operations (explode, fillVoxel) ---- - ## File Watching & Hot Reload ### Workflow -``` +```text /box3script watch ← enable file watching │ ▼ @@ -499,13 +484,11 @@ New code takes effect (no manual reload needed) - 300ms debounce prevents multiple reloads during esbuild multi-chunk writes - Reload is atomic: stops old script (cleans up callbacks + resources) before loading new one ---- - ## Compiled Release Mode ### `/box3script compile` Flow -``` +```text Input: config/box3/script/mygame/ │ ▼ @@ -566,18 +549,16 @@ public static final DeferredBlock RUBY_BLOCK = **Note:** `registries` is only available in compiled JAR mode. In interpreted mode (`/box3script start`), `registries` is `undefined`. ---- - ## Performance Considerations ### Overhead Sources -| Layer | Overhead | Notes | -|-------|---------|-------| -| NeoForge event dispatch | Low | Same as vanilla Minecraft | -| Box3JS event forwarding | Medium | Java → JS argument boxing | -| Rhino execution | **Medium-High** | Interpreted, no JIT | -| JS code itself | Depends on code | Loops in `onTick` are most sensitive | +| Layer | Overhead | Notes | +| ----------------------- | --------------- | ------------------------------------ | +| NeoForge event dispatch | Low | Same as vanilla Minecraft | +| Box3JS event forwarding | Medium | Java → JS argument boxing | +| Rhino execution | **Medium-High** | Interpreted, no JIT | +| JS code itself | Depends on code | Loops in `onTick` are most sensitive | ### Performance Tips diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/faq.md similarity index 93% rename from Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/faq.md index 40bf166..7db7df3 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/faq.md @@ -1,12 +1,14 @@ -# FAQ & Troubleshooting +--- +--- -Common issues encountered during Box3JS development and how to resolve them. +# FAQ & Troubleshooting ## Script Loading ### Q: My script doesn't run. `/box3script` shows the project as ○ (not loaded) Checklist: + 1. Did `npm run build` succeed? Is `dist/server.js` present? 2. Has `/box3script start ` been run? 3. Check the server console for errors prefixed with `[Box3JS]` @@ -44,6 +46,7 @@ Run `npm install` once after creating or cloning a project. After that, only `np ### Q: TypeScript reports type errors but the script runs fine TypeScript only checks types at build time. At runtime, Rhino doesn't enforce types. To fix: + 1. Check the `.d.ts` signatures in `types/server/` and `types/client/` for correctness 2. If the type is genuinely wrong, use `// @ts-expect-error` as a temporary bypass 3. Consider fixing the `.d.ts` file for proper type coverage @@ -51,6 +54,7 @@ TypeScript only checks types at build time. At runtime, Rhino doesn't enforce ty ### Q: `npm run build` succeeds but the script throws syntax errors at runtime Babel compiles to ES5 targeting Rhino 1.9.1 (ES5 only). Common pitfalls: + - Don't use `async/await` in `src/` (Babel doesn't fully compile these to ES5) - Don't use `Promise` (Rhino 1.9.1 doesn't support it) - `let`/`const`, `=>` arrow functions, and template literals are handled by Babel — safe to use @@ -73,7 +77,8 @@ Babel compiles to ES5 targeting Rhino 1.9.1 (ES5 only). Common pitfalls: ### Q: API says "xxx is not a function" Check: -1. Is the method name spelled correctly? See [API reference](../api/README_en.md) + +1. Is the method name spelled correctly? See [API reference](../api/README.md) 2. Is it on the right global object? e.g. `world.say()` not `server.say()` 3. Does it need `new`? e.g. `new GameVector3(x, y, z)` 4. Are you calling a client API from a server script? (`audio`/`input`/`ui`/`chat` only work in `src/client/`) @@ -81,6 +86,7 @@ Check: ### Q: Script runs slowly / server lags Rhino is an interpreted engine (no JIT). Optimization tips: + - **Avoid heavy work in onTick** — Use `setInterval` to reduce frequency - **Cache query results** — Don't call `querySelectorAll` every tick - **Minimize JS ↔ Java crossings** — Batch operations are faster than individual calls @@ -106,6 +112,7 @@ Install the `minecraft-sqlite-jdbc` mod. If you don't use `db`, you don't need i ### Q: How do I prevent SQL injection? Use parameterized queries (recommended): + ```js // ✅ Safe: parameterized db.sql("SELECT * FROM t WHERE name = ?", userInput); @@ -148,6 +155,7 @@ No manual detection is needed. `remoteChannel.sendClientEvent()` uses optional p ### Q: Can client and server use `remoteChannel` at the same time? Yes. `remoteChannel` provides bidirectional channels: + - Client → Server: `remoteChannel.sendServerEvent()` → `remoteChannel.onServerEvent()` - Server → Client (targeted): `remoteChannel.sendClientEvent(entity, ...)` → `remoteChannel.onClientEvent()` - Server → Client (broadcast): `remoteChannel.broadcastClientEvent(...)` → `remoteChannel.onClientEvent()` @@ -162,7 +170,7 @@ Cannot send: functions, `GameVector3` instances, Java objects. To send coordinat ### Q: How do I distribute my script? -``` +```js /box3script compile ``` @@ -170,17 +178,15 @@ Outputs `-.jar`. Drop it into `mods/`. Recipients also need Bo ### Q: What's the difference between compiled JAR and interpreted mode? -| Feature | Interpreted Mode | Compiled JAR | -|---------|-----------------|--------------| -| registries | Not available | Available (custom blocks/items/sounds) | -| Hot reload | ✅ | ❌ (requires restart) | -| Distribution | Copy entire project directory | Single JAR file | -| Updates | Edit JS files directly | Recompile | +| Feature | Interpreted Mode | Compiled JAR | +| ------------ | ----------------------------- | -------------------------------------- | +| registries | Not available | Available (custom blocks/items/sounds) | +| Hot reload | ✅ | ❌ (requires restart) | +| Distribution | Copy entire project directory | Single JAR file | +| Updates | Edit JS files directly | Recompile | ### Q: Why is registries only available in compiled mode? Custom blocks/items/sounds require NeoForge's `DeferredRegister`, which must be registered during mod startup. Interpreted mode has no startup registration phase, so `registries` only works when compiled as a JAR. ---- - For more questions, ask on [GitHub Issues](https://github.com/box3lab/Box3JS). diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/getting-started.md similarity index 82% rename from Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/getting-started.md index 957b598..56672a4 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/getting-started.md @@ -1,36 +1,23 @@ -# Quick Start: From Zero to Your First Box3JS Script - -This guide is for readers with **zero modding experience**. If you know JavaScript, you can write your first Minecraft server script in 10 minutes. By the end of this guide, you'll understand both how to use the APIs and why they're designed the way they are. - -## Table of Contents +--- +--- -1. [What is Box3JS](#what-is-box3js) -2. [Setup](#setup) -3. [Create a Project](#create-a-project) -4. [Understanding Project Structure](#understanding-project-structure) -5. [Your First Script: Line by Line](#your-first-script-line-by-line) -6. [Core Design Philosophy: Why the API Works This Way](#core-design-philosophy-why-the-api-works-this-way) -7. [API Quick Tour](#api-quick-tour) -8. [Dev Cycle](#dev-cycle) -9. [Debugging](#debugging) -10. [Deployment](#deployment) -11. [Next Steps](#next-steps) +# Quick Start: From Zero to Your First Box3JS Script ---- +For readers with **zero modding experience**. Just need to know JavaScript. ## What is Box3JS -Box3JS is a **Minecraft mod** (NeoForge 1.21.1) that embeds a complete JavaScript runtime inside the Minecraft server, letting you write gameplay logic in JS/TypeScript. It also optionally delivers client-side scripts for key listeners, screen UI, client audio, and other local interactions. - -> Curious where Box3JS comes from and how it relates to the Box3 platform? → [Box3JS & Box3](about-box3js_en.md) +Box3JS is a **Minecraft mod** that embeds a JavaScript runtime inside the server, letting you write gameplay in JS/TypeScript. Client-side scripts are optionally delivered for key listeners, screen UI, and local audio. -### In One Sentence +Box3JS inherits its API design from **[Box3](https://box3.fun) (神奇代码岛)**, a browser-based multiplayer 3D game creation platform where thousands of creators build games with JavaScript. Box3's API has been battle-tested by its community — clean, intuitive, and efficient. Box3JS brings that same API paradigm to Minecraft, so you can build mini-games and custom gameplay the Box3 way. -> Box3JS is like Node.js inside your Minecraft server — but you don't need to know Java or modding. +::: tip More background +→ [Box3JS & Box3](about-box3js.md) +::: ### Architecture at a Glance -``` +```text You write Build tools Minecraft runs TypeScript ───→ compile to ES5 ───→ Rhino engine executes │ @@ -49,17 +36,17 @@ You write Build tools Minecraft runs ### What You Can Do -| Category | Examples | -|----------|---------| -| Chat Commands | `!heal`, `!home`, `!shop` | -| Event Response | Welcome on join, death penalty, block break logging | -| Entity Control | Spawn mobs, set AI, custom bosses | -| Mini-Games | PvP arena, parkour, wave survival | +| Category | Examples | +| ------------------ | ------------------------------------------------------- | +| Chat Commands | `!heal`, `!home`, `!shop` | +| Event Response | Welcome on join, death penalty, block break logging | +| Entity Control | Spawn mobs, set AI, custom bosses | +| Mini-Games | PvP arena, parkour, wave survival | | World Manipulation | Place/replace blocks, fill regions, change weather/time | -| Data Persistence | JSON storage, SQLite database | -| Game Systems | Scoreboards, BossBars, teams, world borders | -| HTTP Requests | Web API calls, webhook notifications | -| Client Scripts | Key listeners, screen UI, client audio, custom GUIs | +| Data Persistence | JSON storage, SQLite database | +| Game Systems | Scoreboards, BossBars, teams, world borders | +| HTTP Requests | Web API calls, webhook notifications | +| Client Scripts | Key listeners, screen UI, client audio, custom GUIs | ### What You Can't Do @@ -68,8 +55,6 @@ You write Build tools Minecraft runs - **Modify vanilla mechanics** — changing recipes or mob AI behavior requires Mixin - **Use modern JS syntax at runtime** — Rhino supports ES5 only, but you can use modern TypeScript in source (the build step handles transpilation) ---- - ## Setup ### What You Need @@ -82,27 +67,25 @@ You write Build tools Minecraft runs In-game, run: -``` +```js /box3script ``` If you see the project status panel, Box3JS is running. -``` +```text ══ Box3JS Script Engine ══ Watch: ○ Inactive Sandbox: ○ Inactive Projects: 0 enabled | 0 loaded ``` ---- - ## Create a Project ### One-Command Creation In-game: -``` +```js /box3script create mygame ``` @@ -110,7 +93,7 @@ This generates a complete TypeScript project at `config/box3/script/mygame/`. ### Understanding Project Structure -``` +```text config/box3/script/mygame/ ├── package.json ← Project config (name, version, build deps) ├── tsconfig.base.json ← Shared TS compiler options @@ -145,6 +128,7 @@ config/box3/script/mygame/ ``` **Key insights:** + - The `.d.ts` files in `types/` are your API reference — VS Code uses them for IntelliSense - `tsconfig.server.json` and `tsconfig.client.json` are **mutually exclusive** — server code never sees client globals like `client`, `input`, etc. - You only need to write code in `src/server/app.ts` (and optionally `src/client/app.ts`); the build tooling handles everything else @@ -158,8 +142,6 @@ npm install Run `npm install` once (installs esbuild, Babel, TypeScript build tooling). ---- - ## Your First Script: Line by Line Open `src/server/app.ts`, clear the contents, and write the code below. Let's understand each part. @@ -186,7 +168,14 @@ world.onPlayerJoin((entity) => { // Particle welcome effect const { position: pos } = p; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); ``` @@ -215,7 +204,7 @@ world.onChat((entity, message) => { if (message === "!pos") { const { position: pos2 } = p; p.directMessage( - `§eYour position: §f${Math.floor(pos2.x)}, ${Math.floor(pos2.y)}, ${Math.floor(pos2.z)}` + `§eYour position: §f${Math.floor(pos2.x)}, ${Math.floor(pos2.y)}, ${Math.floor(pos2.z)}`, ); return false; } @@ -254,18 +243,16 @@ setInterval(() => { ### Tick Conversion Table -| Duration | Ticks | -|----------|-------| -| 1 second | 20 | -| 5 seconds | 100 | -| 30 seconds | 600 | -| 1 minute | 1,200 | -| 5 minutes | 6,000 | +| Duration | Ticks | +| ---------- | ------ | +| 1 second | 20 | +| 5 seconds | 100 | +| 30 seconds | 600 | +| 1 minute | 1,200 | +| 5 minutes | 6,000 | | 10 minutes | 12,000 | | 30 minutes | 36,000 | ---- - ## Core Design Philosophy: Why the API Works This Way Understanding the design rationale behind Box3JS APIs helps you write more efficient and safer scripts. Here are the most important design decisions and their reasons. @@ -330,7 +317,7 @@ A unified `GameEventHandlerToken` pattern solves this: ### Design 4: Per-Project Scope Isolation -``` +```text Server runs 3 script projects simultaneously, completely independent: ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ @@ -354,7 +341,7 @@ Box3JS gives each project an **independent Rhino top-level scope**, backed by se ### Design 5: Dual-Side Architecture + remoteChannel -``` +```text ┌──────────────────────┐ ┌──────────────────────┐ │ Server │ │ Client │ │ │ │ │ @@ -391,7 +378,7 @@ remoteChannel.sendServerEvent({ key: "space", pressed: true }); ### Design 6: TypeScript Source + Babel Compilation to ES5 -``` +```text src/server/app.ts Babel esbuild dist/server.js (TypeScript, ES2020) ───→ ES5 JavaScript ───→ bundle ───→ (single file) ``` @@ -403,6 +390,7 @@ src/server/app.ts Babel esbuild dist/server.js - **esbuild bundles** — You can write multiple `.ts` files, but Rhino has no module system. esbuild merges everything into a single IIFE **You can safely use in source:** + - `const` / `let` (transpiled to `var`) - Arrow functions `() => {}` (transpiled to `function(){}`) - Template literals `` `hello ${name}` `` (transpiled to `"hello " + name`) @@ -425,7 +413,7 @@ world.onChat((entity, message) => { ### Design 8: Sandbox Mode — Safe Testing -``` +```js /box3script sandbox mygame # enable sandbox # ... test script (spawn entities, modify blocks, explode)... /box3script sandbox mygame # disable → auto-rollback all changes @@ -433,11 +421,9 @@ world.onChat((entity, message) => { **Why?** Once a script modifies the world, those changes are permanent (blocks replaced, entities spawned). Sandbox mode tracks all world modifications made by the script and auto-rolls them back when disabled. This lets developers fearlessly test destructive operations without damaging the live server. ---- - ## API Quick Tour -Organized by "what do I want to do?" — the most commonly used APIs. For the complete reference, see the [API docs](../api/README_en.md). +Organized by "what do I want to do?" — the most commonly used APIs. For the complete reference, see the [API docs](../api/README.md). ### Messages & Chat @@ -469,17 +455,17 @@ world.onChat((entity, message) => { ```js // Get player info const name = player.name; -const pos = player.position; // GameVector3 { x, y, z } +const pos = player.position; // GameVector3 { x, y, z } const hp = player.hp; -const mode = player.gameMode; // "survival", "creative", "adventure", "spectator" +const mode = player.gameMode; // "survival", "creative", "adventure", "spectator" // Modify player state -player.hp = 20; // full heal -player.maxHp = 40; // increase max health -player.food = 20; // full hunger +player.hp = 20; // full heal +player.maxHp = 40; // increase max health +player.food = 20; // full hunger player.gameMode = "creative"; // switch to creative -player.canFly = true; // allow flight -player.flying = true; // start flying +player.canFly = true; // allow flight +player.flying = true; // start flying // Teleport player.teleport(new GameVector3(100, 64, 100)); @@ -560,13 +546,17 @@ world.onInteract((entity, target, _tick) => { world.onBlockActivate((entity, x, y, z, voxel, _tick) => { // Player right-clicked a block if (voxel === "minecraft:chest") { - entity.player.directMessage(`You clicked a chest at ${String(x)}, ${String(y)}, ${String(z)}`); + entity.player.directMessage( + `You clicked a chest at ${String(x)}, ${String(y)}, ${String(z)}`, + ); } }); world.onBlockPlace((entity, x, y, z, voxel, _voxelId, _tick) => { // Player placed a block - console.log(`${entity.player.name} placed ${voxel} at ${String(x)}, ${String(y)}, ${String(z)}`); + console.log( + `${entity.player.name} placed ${voxel} at ${String(x)}, ${String(y)}, ${String(z)}`, + ); }); world.onVoxelDestroy((entity, _x, _y, _z, voxel, _tick) => { @@ -595,8 +585,10 @@ interval.cancel(); ```js // Spawn an entity (returns GameEntity | null) -const zombie = world.spawnEntity("minecraft:zombie", - new GameVector3(100, 64, 100)); +const zombie = world.spawnEntity( + "minecraft:zombie", + new GameVector3(100, 64, 100), +); if (zombie) { // use zombie ... } @@ -616,9 +608,9 @@ if (boss) { boss.setEquipment("chest", "minecraft:diamond_chestplate"); // Control entities - boss.setAI(false); // disable AI (stands still) - boss.invulnerable = true; // invincible - boss.navigateTo(110, 64, 100, 1.5); // navigate to target + boss.setAI(false); // disable AI (stands still) + boss.invulnerable = true; // invincible + boss.navigateTo(110, 64, 100, 1.5); // navigate to target // Potion effects boss.addEffect("minecraft:strength", 600, 2, false); @@ -632,13 +624,13 @@ if (boss) { // Tags (for marking and querying) boss.addTag("boss"); boss.addTag("stage_1"); - boss.hasTag("boss"); // → true + boss.hasTag("boss"); // → true } // Query entities -const nearby = world.entitiesInRadius(pos, 10); // within 10 blocks -const all = world.querySelectorAll("*"); // all entities -const players = world.querySelectorAll("player"); // all players +const nearby = world.entitiesInRadius(pos, 10); // within 10 blocks +const all = world.querySelectorAll("*"); // all entities +const players = world.querySelectorAll("player"); // all players const monsters = world.querySelectorAll("monster"); // all monsters ``` @@ -667,7 +659,7 @@ const store = storage.getDataStorage("coins"); store.set("player1", 100); const coins = store.get("player1"); // → 100 store.delete("player1"); -const keys = store.keys(); // → array of all keys +const keys = store.keys(); // → array of all keys // SQLite database db.sql("CREATE TABLE IF NOT EXISTS players (name TEXT, score INT)"); @@ -698,14 +690,14 @@ world.createTeam("blue", "blue"); world.joinTeam(entity, "red"); // World border -world.borderSize = 500; // set border size -world.shrinkBorder(100, 1200); // shrink to 100 over 1200 ticks +world.borderSize = 500; // set border size +world.shrinkBorder(100, 1200); // shrink to 100 over 1200 ticks // Weather & time -world.time = 6000; // set time (0=dawn, 6000=noon, 12000=dusk, 18000=midnight) -world.rainDensity = 0; // stop rain -world.clearWeather(); // clear skies -world.thunderDensity = 1; // thunderstorm +world.time = 6000; // set time (0=dawn, 6000=noon, 12000=dusk, 18000=midnight) +world.rainDensity = 0; // stop rain +world.clearWeather(); // clear skies +world.thunderDensity = 1; // thunderstorm // Game rules world.setGameRule("keepInventory", true); @@ -778,7 +770,7 @@ input.onKeyPress("f", () => { }); // Screen UI -ui.showOverlay("§ePress F to open menu"); // above hotbar +ui.showOverlay("§ePress F to open menu"); // above hotbar ui.showTitle("§6BOSS SPAWNED!", "§cGet ready!"); // centered screen title // Client audio @@ -787,10 +779,10 @@ audio.playMusic("minecraft:music.game", 0.5, 1.0); audio.stopAll(); // Fog control (client-side rendering) -client.setFogColor(255, 100, 50); // reddish fog appearance -client.setFogStartDistance(10); // fog begins at 10 blocks -client.setFogEndDistance(50); // fully obscured at 50 blocks -client.resetFog(); // restore default +client.setFogColor(255, 100, 50); // reddish fog appearance +client.setFogStartDistance(10); // fog begins at 10 blocks +client.setFogEndDistance(50); // fully obscured at 50 blocks +client.resetFog(); // restore default // Chat chat.sendMessage("Hello everyone!"); @@ -808,15 +800,13 @@ remoteChannel.onClientEvent((event) => { }); ``` ---- - ## Dev Cycle ### Standard Flow After each code change: -``` +```js Edit code → npm run build → /box3script reload mygame → test ``` @@ -828,7 +818,7 @@ npm run build Output: -``` +```js dist/server.js 7.1kb Done in 240ms ``` @@ -843,7 +833,7 @@ What the build does: In-game: -``` +```js /box3script start mygame # first launch /box3script reload mygame # reload after changes (no server restart) ``` @@ -854,7 +844,7 @@ In-game: Enable file watching so builds auto-trigger reload: -``` +```js /box3script watch ``` @@ -862,7 +852,7 @@ Enable file watching so builds auto-trigger reload: ### Multi-Project Management -``` +```js /box3script start mygame lobby # start multiple projects /box3script stop mygame # stop one /box3script stopall # stop all @@ -870,8 +860,6 @@ Enable file watching so builds auto-trigger reload: /box3script # view all project statuses ``` ---- - ## Debugging ### Troubleshooting Order @@ -884,28 +872,29 @@ Enable file watching so builds auto-trigger reload: ### Common Errors -| Error | Cause | Fix | -|-------|-------|-----| -| `console is not defined` | Engine init failed | Check mod installation | -| `world is not defined` | Scope issue | Ensure code is at global scope, not inside a nested function | -| `Cannot find name 'xxx'` | TypeScript type error | Check spelling, or look up the correct API name in `types/` `.d.ts` | -| `npm run build` fails | JS syntax error | Check ESLint output or terminal error line numbers | -| Script not executing | Project not enabled | Check `/box3script` status | -| Timer never fires | Tick count miscalculation | Remember: 1 sec = 20 ticks, not 1000 | -| Client script not working | Player lacks client mod | Box3JS client mod must be installed | -| remoteChannel not receiving | Data isn't JSON | Ensure you're sending plain objects, not Java objects or `GameVector3` instances | +| Error | Cause | Fix | +| --------------------------- | ------------------------- | -------------------------------------------------------------------------------- | +| `console is not defined` | Engine init failed | Check mod installation | +| `world is not defined` | Scope issue | Ensure code is at global scope, not inside a nested function | +| `Cannot find name 'xxx'` | TypeScript type error | Check spelling, or look up the correct API name in `types/` `.d.ts` | +| `npm run build` fails | JS syntax error | Check ESLint output or terminal error line numbers | +| Script not executing | Project not enabled | Check `/box3script` status | +| Timer never fires | Tick count miscalculation | Remember: 1 sec = 20 ticks, not 1000 | +| Client script not working | Player lacks client mod | Box3JS client mod must be installed | +| remoteChannel not receiving | Data isn't JSON | Ensure you're sending plain objects, not Java objects or `GameVector3` instances | ### Sandbox Testing Sandbox mode enables safe testing: all world modifications are tracked and rolled back when disabled. -``` +```js /box3script sandbox mygame # enable sandbox # ... test script (spawn entities, modify blocks, explode, etc.)... /box3script sandbox mygame # disable → auto-rollback all changes ``` **Use cases:** + - **First test of a new script** — unsure what it does? Sandbox first - **Player play-testing** — let players try new features, rollback after without affecting the live server - **Debugging destructive operations** — test `fillVoxel`, `explode`, etc. @@ -921,21 +910,20 @@ Box3JS scripts run on the server main thread — unreasonable code affects TPS: A typical parkour script consumes < 0.5ms/tick, with virtually no impact on server TPS. ---- - ## Deployment ### Dev Mode → Production Release When development is done, compile your script into a **standalone JAR mod**: -``` +```js /box3script compile mygame ``` Generates `mygame-1.0.0.jar` (version from `package.json`). Drop it into any NeoForge server's `mods/` directory. **Notes:** + - Box3JS must also be installed as a dependency (provides the Rhino runtime) - If you use `registries` (custom blocks/items), clients must also install the JAR - The JAR contains compiled JS — no source code needed @@ -960,23 +948,21 @@ These metadata fields are written into the JAR's `mods.toml` and shown in the ga ### Dev Mode vs Compiled Mode -| | Dev Mode (`/box3script start`) | Compiled Mode (`/box3script compile`) | -|---|---|---| -| Code changes | Hot reload, no restart | Must recompile | -| `registries` | `undefined` | ✅ Available | -| Distribution | Source code required | JAR only | -| Use case | Development, testing | Release, distribution | - ---- +| | Dev Mode (`/box3script start`) | Compiled Mode (`/box3script compile`) | +| ------------ | ------------------------------ | ------------------------------------- | +| Code changes | Hot reload, no restart | Must recompile | +| `registries` | `undefined` | ✅ Available | +| Distribution | Source code required | JAR only | +| Use case | Development, testing | Release, distribution | ## Next Steps Now you understand Box3JS's core design philosophy and basic API usage. Next: -- **API details**: [API by Task](../api/README_en.md) — find APIs by "I want to..." +- **API details**: [API by Task](../api/README.md) — find APIs by "I want to..." - **Event system**: [Tutorial 3: Events & Entities](../tutorial/03-events-entities.md) -- **Client APIs**: [Client API docs](../api/client_en.md) — key listeners, screen UI, client audio -- **Internals**: [Architecture](architecture_en.md) — Rhino engine, scopes, build pipeline, networking -- **Tech choice**: [JS vs Java](js-vs-java_en.md) — Box3JS scripting vs native Java modding -- **FAQ**: [Frequently Asked Questions](faq_en.md) -- **Recipes**: [Code Snippets & Recipes](recipes_en.md) — copy-paste solutions for common tasks +- **Client APIs**: [Client API docs](../api/client.md) — key listeners, screen UI, client audio +- **Internals**: [Architecture](architecture.md) — Rhino engine, scopes, build pipeline, networking +- **Tech choice**: [JS vs Java](js-vs-java.md) — Box3JS scripting vs native Java modding +- **FAQ**: [Frequently Asked Questions](faq.md) +- **Recipes**: [Code Snippets & Recipes](recipes.md) — copy-paste solutions for common tasks diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/js-vs-java.md similarity index 59% rename from Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/js-vs-java.md index 1a0e269..264d23d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/js-vs-java.md @@ -1,26 +1,25 @@ -# JS Scripting vs Native Java Mod Development +--- +--- -This guide helps you decide: **when to use Box3JS scripting, and when to write a native Java mod.** +# JS Scripting vs Native Java Mod Development ## Overview -| Aspect | Box3JS (JS/TS) | Native Java Mod | -|--------|---------------|-----------------| -| **Barrier to entry** | JavaScript knowledge enough | Requires Java + Gradle + MC modding knowledge | -| **Dev speed** | Edit → build → reload (seconds) | Edit → compile → restart MC (minutes) | -| **Hot reload** | Supported (`/box3script reload`) | Not supported; restart required per change | -| **Publishing** | `/box3script compile` → JAR | `gradlew build` → JAR | -| **Performance** | Medium (Rhino interpreted) | High (JIT-compiled bytecode) | -| **API coverage** | High-level wrappers (100+ methods) | Full Minecraft/NeoForge API | -| **Type safety** | TypeScript declarations | Java static types | -| **Debugging** | console.log + server output | IDE breakpoint debugging | -| **Dependency mgmt** | npm (build-time only) | Gradle/Maven | -| **Client features** | Limited (UI/input/audio/chat) | Full (rendering, models, GUI, net protocol) | -| **Custom blocks/items** | JSON config + compile-time gen | Java classes + registration | -| **Modify vanilla behavior** | No (no Mixin) | Yes (Mixin/ASM/CoreMod) | -| **Team collaboration** | JS source + Git | Java source + Git + Gradle | - ---- +| Aspect | Box3JS (JS/TS) | Native Java Mod | +| --------------------------- | ---------------------------------- | --------------------------------------------- | +| **Barrier to entry** | JavaScript knowledge enough | Requires Java + Gradle + MC modding knowledge | +| **Dev speed** | Edit → build → reload (seconds) | Edit → compile → restart MC (minutes) | +| **Hot reload** | Supported (`/box3script reload`) | Not supported; restart required per change | +| **Publishing** | `/box3script compile` → JAR | `gradlew build` → JAR | +| **Performance** | Medium (Rhino interpreted) | High (JIT-compiled bytecode) | +| **API coverage** | High-level wrappers (100+ methods) | Full Minecraft/NeoForge API | +| **Type safety** | TypeScript declarations | Java static types | +| **Debugging** | console.log + server output | IDE breakpoint debugging | +| **Dependency mgmt** | npm (build-time only) | Gradle/Maven | +| **Client features** | Limited (UI/input/audio/chat) | Full (rendering, models, GUI, net protocol) | +| **Custom blocks/items** | JSON config + compile-time gen | Java classes + registration | +| **Modify vanilla behavior** | No (no Mixin) | Yes (Mixin/ASM/CoreMod) | +| **Team collaboration** | JS source + Git | Java source + Git + Gradle | ## Dev Experience Comparison @@ -64,11 +63,11 @@ public class HealMod { This is Box3JS's **single biggest productivity advantage**. -| Action | Box3JS | Java Mod | -|--------|--------|---------| -| Change one line | build(3s) + reload(1s) = **4 seconds** | compile(10-60s) + restartMC(30-120s) = **40-180 seconds** | -| Test a chat command | Edit → build → reload in-game | Edit → compile → restart MC → enter world | -| Iterations per day | **50+** | 5–10 | +| Action | Box3JS | Java Mod | +| ------------------- | -------------------------------------- | --------------------------------------------------------- | +| Change one line | build(3s) + reload(1s) = **4 seconds** | compile(10-60s) + restartMC(30-120s) = **40-180 seconds** | +| Test a chat command | Edit → build → reload in-game | Edit → compile → restart MC → enter world | +| Iterations per day | **50+** | 5–10 | For gameplay scripts (mini-games, RPG mechanics, economy systems), hot reload is **irreplaceable** — gameplay needs constant tuning, and you can't afford to wait for restarts. @@ -96,6 +95,7 @@ world.setScore("Steve", "kills", 5); #### 4. One-Click Project Scaffolding `/box3script create` generates a complete project with: + - TypeScript config + type declarations - Build pipeline (Babel + esbuild) - ESLint code checking @@ -107,27 +107,25 @@ Compare: a Java mod requires manually creating a Gradle project, configuring Neo Before committing to a full Java mod, prototype gameplay with Box3JS: -``` +```js Idea → 30min Box3JS script → test with friends → tweak → gameplay validated ↓ Decide to ship full mod → rewrite in Java ``` ---- - ### Box3JS Disadvantages #### 1. Performance Overhead Rhino is an **interpreted** JS engine (no JIT), single-threaded. Performance-sensitive operations (e.g., scanning thousands of entities per tick) can bottleneck. -| Scenario | Box3JS | Java | -|----------|--------|------| -| Chat commands | Imperceptible | Imperceptible | -| 100 entities per tick | Acceptable | Acceptable | -| 10,000 entities per tick | **May lag** | Acceptable | -| Complex pathfinding math | **Noticeably slow** | Fast | -| Fill entire Y=0 chunk region | **Very slow** | Fast | +| Scenario | Box3JS | Java | +| ---------------------------- | ------------------- | ------------- | +| Chat commands | Imperceptible | Imperceptible | +| 100 entities per tick | Acceptable | Acceptable | +| 10,000 entities per tick | **May lag** | Acceptable | +| Complex pathfinding math | **Noticeably slow** | Fast | +| Fill entire Y=0 chunk region | **Very slow** | Fast | **Rule of thumb:** If `onTick` takes >1ms, consider optimizing or switching to Java. @@ -135,16 +133,16 @@ Rhino is an **interpreted** JS engine (no JIT), single-threaded. Performance-sen Box3JS wraps 100+ common APIs, but not everything: -| What you want | Box3JS | Java | -|-------------|--------|------| -| Modify recipes | No | Yes `RecipeManager` | -| Custom GUI (chest UI) | No | Yes `MenuProvider` / `Screen` | -| Modify mob AI | Partial (setAI/setTarget) | Yes Brain/Memory system | -| Custom dimensions | No | Yes `DimensionType` | -| Datapacks / loot tables | No | Yes full support | -| Network protocol | High-level (remoteChannel) | Yes low-level `CustomPayload` | -| Modify vanilla classes | No | Yes Mixin / ASM | -| Render custom models | No | Yes full render pipeline | +| What you want | Box3JS | Java | +| ----------------------- | -------------------------- | ----------------------------- | +| Modify recipes | No | Yes `RecipeManager` | +| Custom GUI (chest UI) | No | Yes `MenuProvider` / `Screen` | +| Modify mob AI | Partial (setAI/setTarget) | Yes Brain/Memory system | +| Custom dimensions | No | Yes `DimensionType` | +| Datapacks / loot tables | No | Yes full support | +| Network protocol | High-level (remoteChannel) | Yes low-level `CustomPayload` | +| Modify vanilla classes | No | Yes Mixin / ASM | +| Render custom models | No | Yes full render pipeline | #### 3. No Breakpoint Debugging @@ -153,12 +151,14 @@ Only `console.log` output for debugging. No IDE breakpoints, variable watches, o #### 4. Limited Client Features Client scripts can do: + - Key input detection - Screen UI display - Sound/music playback - Chat send/receive But cannot do: + - Custom rendering (models, particles, GUI) - HUD modification - Custom shaders @@ -167,6 +167,7 @@ But cannot do: #### 5. ES5 Limitations Rhino 1.9.1 only supports ES5 syntax. You cannot use: + - `let` / `const` (Babel compiles to `var`) - Arrow functions (Babel compiles to `function`) - `async` / `await` @@ -181,11 +182,9 @@ But **Babel compiles everything to ES5**, so you write modern TS and the build c Compiled JARs depend on Box3JS as a runtime. Users need both Box3JS + your JAR installed. Pure Java mods are self-contained. ---- - ## Decision Tree -``` +```text What do you want to build? │ ├─ Mini-game (PvP/parkour/racing) @@ -224,13 +223,11 @@ What do you want to build? Lots of content → Native Java mod ``` ---- - ## Hybrid Approach Best practice: **Box3JS for gameplay, Java for infrastructure**. -``` +```text ┌──────────────────────────────────┐ │ Java Mod (low-level capabilities)│ │ - Custom blocks/items registry │ @@ -250,22 +247,21 @@ Best practice: **Box3JS for gameplay, Java for infrastructure**. ``` A real-world architecture example: + - Java mod adds custom weapons, custom mobs, a new dimension - Box3JS scripts define wave rules, boss skill patterns, quest triggers - Gameplay designers can independently edit scripts without touching Java ---- - ## Summary -| Choose Box3JS | Choose Java | -|--------------|------------| -| You're building gameplay/mini-games | You need to modify vanilla mechanics | -| You need rapid iteration | You need custom rendering/models | -| Your team has JS developers | Your team is primarily Java developers | -| Logic is complex but no rendering | Project has many custom blocks/entities/dims | -| You want to prototype before committing | You're publishing to CurseForge/Modrinth | -| You need hot reload | You need maximum performance | -| Project is server-side focused | Project needs client-side rendering | +| Choose Box3JS | Choose Java | +| --------------------------------------- | -------------------------------------------- | +| You're building gameplay/mini-games | You need to modify vanilla mechanics | +| You need rapid iteration | You need custom rendering/models | +| Your team has JS developers | Your team is primarily Java developers | +| Logic is complex but no rendering | Project has many custom blocks/entities/dims | +| You want to prototype before committing | You're publishing to CurseForge/Modrinth | +| You need hot reload | You need maximum performance | +| Project is server-side focused | Project needs client-side rendering | **Neither is better — only better-suited to the current project.** For server-side gameplay development, Box3JS's productivity advantages are overwhelming: hot reload + low barrier + rich API. For projects needing vanilla mechanic modification or custom rendering, Java is essential. diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/recipes.md similarity index 90% rename from Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/recipes.md index 1af00cc..2063b64 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/recipes.md @@ -1,23 +1,9 @@ -# Common Recipes: Box3JS Feature Templates - -This guide is "cookbook" style — not a per-API walkthrough, but a series of "want to implement X? Copy this template and tweak." All code is build-verified. - -## Contents +--- +--- -1. [Chat Commands](#chat-commands) -2. [Economy System](#economy-system) -3. [Teleport System](#teleport-system) -4. [Respawn Protection](#respawn-protection) -5. [Shop / NPC](#shop--npc) -6. [Daily Rewards](#daily-rewards) -7. [Leaderboards](#leaderboards) -8. [Wave Spawning](#wave-spawning) -9. [Shrinking Zone](#shrinking-zone) -10. [HTTP Webhook](#http-webhook) -11. [Client HUD](#client-hud) -12. [Cross-Script Integration](#cross-script-integration) +# Common Recipes ---- +"Want to implement X? Copy this template and tweak." All code is build-verified. ## Chat Commands @@ -73,8 +59,6 @@ if (message === "!admin") { } ``` ---- - ## Economy System Scoreboard-based economy. Data persists through `/box3script reload` (scoreboards are independent of script lifecycle). @@ -150,8 +134,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## Teleport System ### Home TP (in-memory, lost on restart) @@ -243,24 +225,20 @@ world.onChat((entity, message) => { }); ``` ---- - ## Respawn Protection ```js // Give brief invulnerability after respawn world.onPlayerRespawn((entity) => { const p = entity.player; - p.addEffect("minecraft:resistance", 100, 4, true); // 5s Resistance V (near-invulnerable) - p.addEffect("minecraft:regeneration", 100, 2, true); // 5s Regen III - p.addEffect("minecraft:fire_resistance", 100, 0, true); // 5s Fire Resistance + p.addEffect("minecraft:resistance", 100, 4, true); // 5s Resistance V (near-invulnerable) + p.addEffect("minecraft:regeneration", 100, 2, true); // 5s Regen III + p.addEffect("minecraft:fire_resistance", 100, 0, true); // 5s Fire Resistance p.directMessage("§a5 seconds of respawn protection"); p.playSound("minecraft:block.beacon.activate", 1.0, 1.5); }); ``` ---- - ## Shop / NPC Right-click an entity (e.g. villager) to open a dialog/shop: @@ -318,12 +296,11 @@ world.onChat((entity, message) => { }); ``` ---- - ## Daily Rewards ```js -const dailyRewards = storage.getDataStorage<{ lastClaimed: number }>("daily-rewards"); +const dailyRewards = + storage.getDataStorage < { lastClaimed: number } > "daily-rewards"; world.onChat((entity, message) => { const p = entity.player; @@ -331,9 +308,9 @@ world.onChat((entity, message) => { if (message === "!daily") { const now = Date.now(); const record = dailyRewards.get(p.userId); - const cooldown = 24 * 60 * 60 * 1000; // 24 hours + const cooldown = 24 * 60 * 60 * 1000; // 24 hours - if (record && (now - record.lastClaimed) < cooldown) { + if (record && now - record.lastClaimed < cooldown) { const hours = Math.ceil((record.lastClaimed + cooldown - now) / 3600000); p.directMessage(`§cPlease wait ${hours} hours before claiming again`); return false; @@ -346,7 +323,9 @@ world.onChat((entity, message) => { const coins = world.getScore(p.name, "coins"); world.setScore(p.name, "coins", coins + bonus); dailyRewards.set(p.userId, { lastClaimed: now }); - p.directMessage(`§aDaily reward claimed! 3 diamonds + 8 XP bottles + ${bonus} coins`); + p.directMessage( + `§aDaily reward claimed! 3 diamonds + 8 XP bottles + ${bonus} coins`, + ); p.playSound("minecraft:entity.player.levelup", 1.0, 1.0); return false; } @@ -354,8 +333,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## Leaderboards ```js @@ -382,8 +359,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## Wave Spawning Complete wave system with scaling difficulty: @@ -424,8 +399,6 @@ function spawnWave(pos: GameVector3): void { } ``` ---- - ## Shrinking Zone ```js @@ -460,8 +433,6 @@ startShrinkPhase(0, 0, [ ]); ``` ---- - ## HTTP Webhook ```js @@ -478,8 +449,12 @@ world.onEntityDeath((entity, killer) => { }), timeout: 5000, async: true, - onResponse: (resp) => { console.log(`Webhook sent: ${resp.status}`); }, - onError: (err) => { console.warn(`Webhook failed: ${err}`); }, + onResponse: (resp) => { + console.log(`Webhook sent: ${resp.status}`); + }, + onError: (err) => { + console.warn(`Webhook failed: ${err}`); + }, }); } }); @@ -503,8 +478,6 @@ setInterval(() => { }, 6000); ``` ---- - ## Client HUD Combine `remoteChannel` for a custom client-side HUD (server provides data, client displays it): @@ -559,8 +532,6 @@ remoteChannel.onClientEvent((event) => { }); ``` ---- - ## Cross-Script Integration Multiple script projects communicating: @@ -589,6 +560,4 @@ function endGame(): void { } ``` ---- - -Each recipe is self-contained — grab what you need. See the [API reference](../api/README_en.md) and [tutorials](../tutorial/README_en.md) for more detail. +Each recipe is self-contained — grab what you need. See the [API reference](../api/README.md) and [tutorials](../tutorial/README.md) for more detail. diff --git a/Box3JS-NeoForge-1.21.1/docs/en/index.md b/Box3JS-NeoForge-1.21.1/docs/en/index.md new file mode 100644 index 0000000..9f5cfba --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/index.md @@ -0,0 +1,78 @@ +--- +layout: home + +hero: + name: "Box3JS" + text: "JS/TS Scripting Engine for Minecraft" + tagline: Build custom gameplay, mini-games, and UIs — no JDK, no Java compilation required. + actions: + - theme: brand + text: Get Started + link: /en/guide/getting-started + +features: + - icon: 🎮 + title: Server & Client Scripting + details: Server-side world manipulation, entities, recipes. Client-side keyboard input, screen UI, sounds, SQLite storage, and HTTP requests. + - icon: 📦 + title: TypeScript First + details: Full DTS type definitions for all 17 global objects. Built-in esbuild + Babel pipeline transpiles modern TS to Rhino-compatible ES5. + - icon: 🔄 + title: Hot Reload + details: Edit scripts and see changes instantly without restarting the server. File watcher auto-reloads on save. + - icon: 🌐 + title: Bidirectional Communication + details: remoteChannel enables server↔client event messaging. Server broadcasts to all players; clients reply independently. + - icon: 🗄️ + title: Dual-Side Storage & Database + details: JSON file persistence and SQLite on both server and client. Pagination, atomic updates, counters, and tagged-template queries. + - icon: 🧩 + title: Custom Blocks & Items + details: Block textures, item models, equipment, sounds, and creative tabs — all registered from JSON configs (standalone/JAR mode). + - icon: 📚 + title: Comprehensive Docs + details: 50+ pages across API reference, progressive tutorials, cookbook recipes, architecture deep-dive, and FAQ — in Chinese and English. + - icon: 🚀 + title: Standalone JAR Mode + details: Compile your script project into a self-contained JAR mod. No runtime dependency on Box3JS — just drop it in your mods folder. + +--- + +## Quick Start + +```bash +# In-game: create a new project +/box3script create mygame + +# Build and watch +cd config/box3/script/mygame +npm install +npm run build -- --watch + +# TypeScript type checking +npm run check +``` + +```ts +// src/server/app.ts — your first script +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage(`Hello, ${entity.player.name}!`); + return false; + } + return true; +}); +``` + +[Read full docs →](/en/guide/getting-started_en) + +## Version Info + +| Component | Version | +|-----------|---------| +| Minecraft | 1.21.1 | +| Mod Loader | NeoForge | +| Java | 21 | +| JS Engine | Mozilla Rhino 1.9.1 (ES5) | +| TypeScript | via Babel → ES5 | + diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/01-basics.md similarity index 79% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/01-basics.md index c2f46c4..5cb5022 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/01-basics.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 1: 5-Minute Quick Start Get from zero to your first running Box3JS script — no Minecraft modding experience needed, just JavaScript knowledge. @@ -11,7 +14,7 @@ Get from zero to your first running Box3JS script — no Minecraft modding exper Run one command in-game: -``` +```js /box3script create hello ``` @@ -46,7 +49,7 @@ world.onPlayerJoin((entity) => { Back in-game: -``` +```js /box3script start hello ``` @@ -62,14 +65,12 @@ entity.player.directMessage(`§6Hello, ${entity.player.name}!`); Save, run `npm run build`, then in-game: -``` +```js /box3script reload hello ``` No server restart required — changes take effect immediately. ---- - These 5 steps form the complete dev cycle: **edit code → build → reload**. The rest of this tutorial dives into everything you can build. ## Message System @@ -94,22 +95,22 @@ player.title("§6§lMain Title", "§7Subtitle"); player.title("§c§lBOSS", "Ancient Dragon", 10, 60, 10); ``` -| Method | Location | Visibility | -|--------|----------|------------| -| `world.say()` | Chat | Server-wide | -| `player.directMessage()` | Chat | Single player | -| `player.actionBar()` | Above hotbar | Single player | -| `player.title()` | Screen center | Single player | +| Method | Location | Visibility | +| ------------------------ | ------------- | ------------- | +| `world.say()` | Chat | Server-wide | +| `player.directMessage()` | Chat | Single player | +| `player.actionBar()` | Above hotbar | Single player | +| `player.title()` | Screen center | Single player | ### console Logging `console` outputs to the server console in the format `[Box3JS] [projectName] message`: ```js -console.log("Info"); // [Box3JS] [hello] Info -console.debug("Debug"); // [Box3JS] [hello] [DEBUG] Debug +console.log("Info"); // [Box3JS] [hello] Info +console.debug("Debug"); // [Box3JS] [hello] [DEBUG] Debug console.warn("Warning"); // [Box3JS] [hello] [WARN] Warning -console.error("Error"); // [Box3JS] [hello] [ERROR] Error +console.error("Error"); // [Box3JS] [hello] [ERROR] Error ``` ## Chat Command System @@ -128,7 +129,7 @@ world.onChat((entity, message) => { p.directMessage("§f!pos §7- Check position"); p.directMessage("§f!day §7- Set to daytime"); p.directMessage("§f!clear §7- Clear weather"); - return false; // ★ Return false to suppress the chat message + return false; // ★ Return false to suppress the chat message case "!hello": p.directMessage(`§eHello, ${p.name}!`); @@ -141,7 +142,7 @@ world.onChat((entity, message) => { case "!pos": { const pos = p.position; p.directMessage( - `§eYour position: §f${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)}` + `§eYour position: §f${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)}`, ); return false; } @@ -156,7 +157,7 @@ world.onChat((entity, message) => { world.say(`§e${p.name} §fcleared the weather`); return false; } - return true; // Non-command messages pass through normally + return true; // Non-command messages pass through normally }); ``` @@ -175,7 +176,14 @@ world.onPlayerJoin((entity) => { // Particle circle + sound const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); ``` @@ -189,41 +197,41 @@ Effect: when a player joins, they see a screen title, hear a bell chime, and gre setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7Online: §f${count} §7players`); -}, 6000); // 6000 ticks = 5 minutes +}, 6000); // 6000 ticks = 5 minutes // Run once after 30 seconds setTimeout(() => { world.say("§6Server has been running for 30 seconds"); -}, 600); // 600 ticks = 30 seconds +}, 600); // 600 ticks = 30 seconds ``` **Tick conversion:** 20 ticks = 1 second -| Duration | Ticks | -|----------|-------| -| 1 second | 20 | -| 5 seconds | 100 | -| 30 seconds | 600 | -| 1 minute | 1200 | -| 5 minutes | 6000 | +| Duration | Ticks | +| ---------- | ----- | +| 1 second | 20 | +| 5 seconds | 100 | +| 30 seconds | 600 | +| 1 minute | 1200 | +| 5 minutes | 6000 | ## World Properties ```js // Time -world.time = 6000; // Noon (0=sunrise, 6000=noon, 12000=sunset, 18000=midnight) +world.time = 6000; // Noon (0=sunrise, 6000=noon, 12000=sunset, 18000=midnight) // Weather -world.rainDensity = 1.0; // Full rain +world.rainDensity = 1.0; // Full rain world.thunderDensity = 0.5; // Thunderstorm -world.clearWeather(); // Clear skies +world.clearWeather(); // Clear skies // Difficulty -world.difficulty = "hard"; // peaceful / easy / normal / hard +world.difficulty = "hard"; // peaceful / easy / normal / hard // Game rules -world.setGameRule("keepInventory", true); // Keep inventory on death -world.setGameRule("doFireTick", false); // Fire doesn't spread +world.setGameRule("keepInventory", true); // Keep inventory on death +world.setGameRule("doFireTick", false); // Fire doesn't spread world.setGameRule("doMobSpawning", false); // Disable mob spawning ``` @@ -241,9 +249,22 @@ console.log("[Hello] Script loaded"); // ── Welcome effects ── world.onPlayerJoin((entity) => { const p = entity.player; - p.title("§6§lWelcome to the server!", "§7Type §f!help §7for commands", 5, 70, 10); + p.title( + "§6§lWelcome to the server!", + "§7Type §f!help §7for commands", + 5, + 70, + 10, + ); const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); @@ -268,7 +289,9 @@ world.onChat((entity, message) => { return false; case "!pos": { const pos = p.position; - p.directMessage(`§ePosition: §f${Math.floor(pos.x)} ${Math.floor(pos.y)} ${Math.floor(pos.z)}`); + p.directMessage( + `§ePosition: §f${Math.floor(pos.x)} ${Math.floor(pos.y)} ${Math.floor(pos.z)}`, + ); return false; } case "!online": @@ -291,13 +314,13 @@ world.onChat((entity, message) => { ### Dev Cycle -``` +```js Edit code → npm run build → /box3script reload hello → Test ``` Enable file watching for auto hot-reload (no manual reload needed): -``` +```js /box3script watch ``` @@ -305,7 +328,7 @@ Enable file watching for auto hot-reload (no manual reload needed): With sandbox enabled, all world modifications by the script are tracked and can be rolled back with one command: -``` +```js /box3script sandbox hello # Enable # ... test your script ... /box3script sandbox hello # Disable → all changes rolled back @@ -314,6 +337,7 @@ With sandbox enabled, all world modifications by the script are tracked and can ### Debugging When something goes wrong, check in this order: + 1. Check the server console for errors (`console.log` output appears here) 2. Verify the script is loaded: run `/box3script` and check if the project shows `◉` (loaded and running) 3. Verify the build succeeded: `npm run build` should complete without errors @@ -321,4 +345,4 @@ When something goes wrong, check in this order: ## Next Step -[Tutorial 2: Players & Items](../tutorial/02-player-items_en.md) — teleport, items, enchantments, potion effects, game modes. +[Tutorial 2: Players & Items](../tutorial/02-player-items.md) — teleport, items, enchantments, potion effects, game modes. diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/02-player-items_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/02-player-items.md similarity index 99% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/02-player-items_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/02-player-items.md index b5c1dd7..f3c3540 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/02-player-items_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/02-player-items.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 2: Players & Items This tutorial covers player properties, teleportation, giving items, potion effects, game modes, and more. diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/03-events-entities.md similarity index 99% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/03-events-entities.md index f943be2..76bac8c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/03-events-entities.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 3: Events & Entities This tutorial dives into event callbacks, block interactions, entity spawning, AI, and combat events. @@ -265,7 +268,7 @@ world.onEntitySeparate((entityA, entityB, _tick) => { ## 3.8 Common Entity Types -``` +```js minecraft:zombie Zombie minecraft:skeleton Skeleton minecraft:creeper Creeper diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/04-advanced-systems.md similarity index 99% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/04-advanced-systems.md index 863a72f..f169307 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/04-advanced-systems.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 4: Advanced Game Systems This tutorial covers scoreboards, BossBars, teams, world border, and cross-script communication. diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/05-examples.md similarity index 85% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/05-examples.md index 27f9d1e..94a8275 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/05-examples.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 5: Visual Effects & Complete Mini-Games This tutorial covers particles, fireworks, lightning, explosions, and other visual effects, plus three verified complete mini-games. @@ -19,21 +22,21 @@ world.spawnParticleCircle(0, 100, 0, 4.0, "minecraft:end_rod", 36); Common particles: -| Particle ID | Effect | -|-------------|--------| -| `minecraft:flame` | Fire | -| `minecraft:cloud` | Smoke | -| `minecraft:happy_villager` | Green particles (positive) | -| `minecraft:witch` | Purple particles | -| `minecraft:portal` | Portal | -| `minecraft:end_rod` | End rod light | -| `minecraft:heart` | Hearts | -| `minecraft:note` | Music notes | -| `minecraft:dragon_breath` | Dragon's breath | -| `minecraft:angry_villager` | Angry particles (red) | -| `minecraft:soul_fire_flame` | Soul fire (blue) | -| `minecraft:redstone` | Redstone particles | -| `minecraft:explosion` | Explosion particles | +| Particle ID | Effect | +| --------------------------- | -------------------------- | +| `minecraft:flame` | Fire | +| `minecraft:cloud` | Smoke | +| `minecraft:happy_villager` | Green particles (positive) | +| `minecraft:witch` | Purple particles | +| `minecraft:portal` | Portal | +| `minecraft:end_rod` | End rod light | +| `minecraft:heart` | Hearts | +| `minecraft:note` | Music notes | +| `minecraft:dragon_breath` | Dragon's breath | +| `minecraft:angry_villager` | Angry particles (red) | +| `minecraft:soul_fire_flame` | Soul fire (blue) | +| `minecraft:redstone` | Redstone particles | +| `minecraft:explosion` | Explosion particles | ### Spiral Rising Particles @@ -70,7 +73,16 @@ Firework shapes: `"ball"` `"large_ball"` `"star"` `"creeper"` `"burst"` ### Sequential Firework Show ```js -const colors = ["red", "gold", "green", "blue", "purple", "white", "pink", "aqua"]; +const colors = [ + "red", + "gold", + "green", + "blue", + "purple", + "white", + "pink", + "aqua", +]; const shapes = ["ball", "large_ball", "star", "creeper", "burst"]; for (let i = 0; i < 8; i++) { @@ -81,7 +93,8 @@ for (let i = 0; i < 8; i++) { pos.x + (Math.random() - 0.5) * 10, pos.y + 5 + Math.random() * 8, pos.z + (Math.random() - 0.5) * 10, - c, s + c, + s, ); }, i * 300); } @@ -91,9 +104,9 @@ for (let i = 0; i < 8; i++) { ```js // Lightning: (x, y, z, damage) -world.strikeLightning(0, 100, 0); // Default damage -world.strikeLightning(0, 100, 0, 10); // 10 damage -world.strikeLightning(0, 100, 0, 0); // No damage, visual only +world.strikeLightning(0, 100, 0); // Default damage +world.strikeLightning(0, 100, 0, 10); // 10 damage +world.strikeLightning(0, 100, 0, 0); // No damage, visual only // Summon lightning around a player for (let i = 0; i < 3; i++) { @@ -110,13 +123,23 @@ world.playSound("minecraft:entity.lightning_bolt.thunder", pos, 1.0, 1.0); ```js // Explosion: (x, y, z, power, causesFire) -world.explode(0, 100, 0, 4, false); // Power 4, no fire -world.explode(0, 100, 0, 8, true); // Power 8, causes fire +world.explode(0, 100, 0, 4, false); // Power 4, no fire +world.explode(0, 100, 0, 8, true); // Power 8, causes fire // Player-triggered self-destruct (3-second countdown) world.playSound("minecraft:block.note_block.bass", pos, 1.0, 0.5); setTimeout(() => { - world.spawnParticle("minecraft:explosion", pos.x, pos.y, pos.z, 1, 0, 0, 0, 0); + world.spawnParticle( + "minecraft:explosion", + pos.x, + pos.y, + pos.z, + 1, + 0, + 0, + 0, + 0, + ); setTimeout(() => { world.explode(pos.x, pos.y, pos.z, 4, false); world.playSound("minecraft:entity.generic.explode", pos, 1.0, 1.0); @@ -137,22 +160,22 @@ player.playSound("minecraft:entity.player.levelup", 1.0, 1.0); Common sounds: -| Sound ID | Use | -|----------|-----| -| `minecraft:block.note_block.pling` | Bell chime | -| `minecraft:block.note_block.bass` | Bass note | -| `minecraft:entity.experience_orb.pickup` | XP orb pickup | -| `minecraft:entity.player.levelup` | Level up | -| `minecraft:entity.ender_dragon.growl` | Dragon roar (boss entrance) | -| `minecraft:entity.wither.spawn` | Wither spawn (menacing) | -| `minecraft:entity.lightning_bolt.thunder` | Thunder | -| `minecraft:entity.generic.explode` | Explosion | -| `minecraft:entity.witch.throw` | Potion throw | -| `minecraft:block.beacon.activate` | Beacon activation | -| `minecraft:block.anvil.land` | Anvil landing | -| `minecraft:ui.toast.challenge_complete` | Challenge complete | -| `minecraft:entity.player.burp` | Eating sound | -| `minecraft:entity.enderman.teleport` | Teleport sound | +| Sound ID | Use | +| ----------------------------------------- | --------------------------- | +| `minecraft:block.note_block.pling` | Bell chime | +| `minecraft:block.note_block.bass` | Bass note | +| `minecraft:entity.experience_orb.pickup` | XP orb pickup | +| `minecraft:entity.player.levelup` | Level up | +| `minecraft:entity.ender_dragon.growl` | Dragon roar (boss entrance) | +| `minecraft:entity.wither.spawn` | Wither spawn (menacing) | +| `minecraft:entity.lightning_bolt.thunder` | Thunder | +| `minecraft:entity.generic.explode` | Explosion | +| `minecraft:entity.witch.throw` | Potion throw | +| `minecraft:block.beacon.activate` | Beacon activation | +| `minecraft:block.anvil.land` | Anvil landing | +| `minecraft:ui.toast.challenge_complete` | Challenge complete | +| `minecraft:entity.player.burp` | Eating sound | +| `minecraft:entity.enderman.teleport` | Teleport sound | ## 5.6 Player Join/Leave Effects @@ -160,12 +183,29 @@ Common sounds: world.onPlayerJoin((entity, _tick) => { const pos = entity.position; world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); }); world.onPlayerLeave((entity, _tick) => { const pos = entity.position; - world.spawnParticle("minecraft:cloud", pos.x, pos.y, pos.z, 10, 0.3, 0.3, 0.3, 0.01); + world.spawnParticle( + "minecraft:cloud", + pos.x, + pos.y, + pos.z, + 10, + 0.3, + 0.3, + 0.3, + 0.01, + ); }); ``` @@ -174,6 +214,7 @@ world.onPlayerLeave((entity, _tick) => { This is a practical application of the design patterns from Tutorial 4 — a full red-vs-blue PvP mini-game integrating events, BossBar, scoreboard, teams, particles, fireworks, shrinking border, airdrops, and more. **Commands:** + - `!pvp join` — Join the game - `!pvp leave` — Leave the queue - `!pvp start` — (OP) Start the game @@ -181,6 +222,7 @@ This is a practical application of the design patterns from Tutorial 4 — a ful - `!pvp status` — Check status **Features:** + - Lobby countdown 30s → game duration 300s - Auto-assign red/blue teams + team prefixes - Kill scoring + global announcements + firework effects @@ -591,8 +633,6 @@ world.onChat((entity, message, _tick) => { }); ``` ---- - All example code has been verified with `tsc --noEmit`, `eslint`, and `node build.mjs`. Ready to use. For more API details, refer to the complete API docs in the `docs/api/` directory. diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/06-client-scripting.md similarity index 95% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/06-client-scripting.md index fe17f91..bd9ccbe 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/06-client-scripting.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 6: Client-Side Scripting This tutorial covers all 9 global objects available in Box3JS client scripts: lifecycle, keyboard input, screen UI, chat control, sounds/music, local storage, SQLite, HTTP requests, and bidirectional communication. @@ -157,7 +160,9 @@ const color = client.getFogColor(); // returns GameRGBColor or null client.resetFog(); ``` -> **Note**: Fog changes take effect locally on each client. Use `remoteChannel` to let the server trigger fog changes on clients, enabling server-controlled weather effects. +::: warning +Fog changes take effect locally on each client. Use `remoteChannel` to let the server trigger fog changes on clients, enabling server-controlled weather effects. +::: ## 6.9 storage — Client-Side Local Storage @@ -264,7 +269,9 @@ function searchMobs(keyword: string): void { } ``` -> When `minecraft-sqlite-jdbc` is not installed, `db.isAvailable()` returns `false` and all SQL calls silently return empty results. +::: warning +When `minecraft-sqlite-jdbc` is not installed, `db.isAvailable()` returns `false` and all SQL calls silently return empty results. +::: ## 6.11 http — Client HTTP Requests @@ -389,7 +396,9 @@ No manual detection is needed. `remoteChannel.sendClientEvent()` uses optional p ### Data Format -> **Important:** Data sent across the network must be JSON-serializable (string, number, boolean, null, plain objects, arrays). You cannot send functions, Java objects, or `GameVector3`. +::: warning +Data sent across the network must be JSON-serializable (string, number, boolean, null, plain objects, arrays). You cannot send functions, Java objects, or `GameVector3`. +::: ## 6.13 Practical Example: Custom HUD Status Bar @@ -470,4 +479,4 @@ Troubleshooting order: ## Next Steps -[API Reference →](../api/client_en.md) Complete client API docs · [Tutorial 1](01-basics_en.md) Back to basics +[API Reference →](../api/client.md) Complete client API docs · [Tutorial 1](01-basics.md) Back to basics diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/README.md similarity index 57% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/README.md index bf5548a..cfb76e0 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/README.md @@ -1,10 +1,11 @@ -# Box3JS Tutorials +--- +--- -Learn Box3JS scripting from scratch. Each tutorial takes 10–15 minutes and includes complete runnable code. +# Box3JS Tutorials ## Learning Path -``` +```text Beginner Intermediate Advanced │ │ │ │ Tutorial 1: Basics │ Tutorial 3: Events │ Tutorial 5: Mini-Games @@ -21,20 +22,20 @@ Beginner Intermediate Advanced │ ▼ Want to go deeper? - → [Architecture](../guide/architecture_en.md) - → [JS vs Java](../guide/js-vs-java_en.md) + → [Architecture](../guide/architecture.md) + → [JS vs Java](../guide/js-vs-java.md) ``` ## Tutorial List | # | Tutorial | You'll learn | |---|---------|-------------| -| 1 | [From Zero](01-basics_en.md) | Create project → build → first script → chat commands → timers | -| 2 | [Players & Items](02-player-items_en.md) | Teleport, flight, give items, enchantments, potion effects, game modes | -| 3 | [Events & Entities](03-events-entities_en.md) | All event callbacks, spawn entities, AI control, patrol guards, collision | -| 4 | [Advanced Systems](04-advanced-systems_en.md) | Scoreboards, BossBars, teams, world border, cross-script messaging | -| 5 | [Real Mini-Games](05-examples_en.md) | Full PvP arena, particle effects, fireworks, wave spawning, home TP | -| 6 | [Client-Side Scripting](06-client-scripting_en.md) | Keyboard input, screen UI, sound/music, local storage, SQLite, HTTP, remoteChannel | +| 1 | [From Zero](01-basics.md) | Create project → build → first script → chat commands → timers | +| 2 | [Players & Items](02-player-items.md) | Teleport, flight, give items, enchantments, potion effects, game modes | +| 3 | [Events & Entities](03-events-entities.md) | All event callbacks, spawn entities, AI control, patrol guards, collision | +| 4 | [Advanced Systems](04-advanced-systems.md) | Scoreboards, BossBars, teams, world border, cross-script messaging | +| 5 | [Real Mini-Games](05-examples.md) | Full PvP arena, particle effects, fireworks, wave spawning, home TP | +| 6 | [Client-Side Scripting](06-client-scripting.md) | Keyboard input, screen UI, sound/music, local storage, SQLite, HTTP, remoteChannel | ## Prerequisites @@ -42,7 +43,7 @@ Beginner Intermediate Advanced - **Environment:** All server-side code runs on the server; players need nothing installed. Client scripts require the Box3JS client mod. - **Hot Reload:** Edit code → `npm run build` → `/box3script reload` — no server restart needed. - **Deployment:** When done, `/box3script compile` packages your script into a standalone JAR for `mods/`. -- **API Lookup:** Stuck on "which API does X"? Check the [API Task Reference](../api/README_en.md). +- **API Lookup:** Stuck on "which API does X"? Check the [API Task Reference](../api/README.md). ## Quick Example @@ -69,25 +70,25 @@ setInterval(() => { | I want to... | Read this | |-------------|----------| -| Look up a specific API | [API Reference](../api/README_en.md) | -| Understand Box3JS internals | [Architecture](../guide/architecture_en.md) | -| Ship my script as a standalone mod | [Quick Start - Deployment](../guide/getting-started_en.md#deployment) | -| Register custom blocks/items/sounds | [registries API](../api/registries_en.md) | -| Write client scripts (UI/input/audio) | [client API](../api/client_en.md) | -| Decide Box3JS vs Java modding | [JS vs Java](../guide/js-vs-java_en.md) | +| Look up a specific API | [API Reference](../api/README.md) | +| Understand Box3JS internals | [Architecture](../guide/architecture.md) | +| Ship my script as a standalone mod | [Quick Start - Deployment](../guide/getting-started.md#deployment) | +| Register custom blocks/items/sounds | [registries API](../api/registries.md) | +| Write client scripts (UI/input/audio) | [client API](../api/client.md) | +| Decide Box3JS vs Java modding | [JS vs Java](../guide/js-vs-java.md) | ## Full API Docs | Doc | Description | |-----|-------------| -| [world](../api/world_en.md) | World state, events, particles, fireworks, sound | -| [entity](../api/entity_en.md) | Entity properties, AI, equipment, effects | -| [player](../api/player_en.md) | Inventory, messages, flight, teleport | -| [voxels](../api/voxels_en.md) | Block read/write, region fill | -| [storage](../api/storage_en.md) | JSON data persistence | -| [database](../api/database_en.md) | SQLite database | -| [http](../api/http_en.md) | HTTP network requests | -| [client](../api/client_en.md) | Client scripts (UI/input/chat/audio) | -| [registries](../api/registries_en.md) | Custom blocks/items/sounds | -| [math](../api/math_en.md) | GameVector3, Color, Quaternion | -| [commands](../api/commands_en.md) | `/box3script` command reference | +| [world](../api/world.md) | World state, events, particles, fireworks, sound | +| [entity](../api/entity.md) | Entity properties, AI, equipment, effects | +| [player](../api/player.md) | Inventory, messages, flight, teleport | +| [voxels](../api/voxels.md) | Block read/write, region fill | +| [storage](../api/storage.md) | JSON data persistence | +| [database](../api/database.md) | SQLite database | +| [http](../api/http.md) | HTTP network requests | +| [client](../api/client.md) | Client scripts (UI/input/chat/audio) | +| [registries](../api/registries.md) | Custom blocks/items/sounds | +| [math](../api/math.md) | GameVector3, Color, Quaternion | +| [commands](../api/commands.md) | `/box3script` command reference | diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/README.md b/Box3JS-NeoForge-1.21.1/docs/guide/README.md index fe4b34a..291190d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/README.md @@ -1,7 +1,5 @@ # Box3JS 指南 -从零开始了解 Box3JS——无论你是想快速上手还是深入原理。 - ## 按需求选择 | 我想... | 读这个 | @@ -15,7 +13,7 @@ ## 学习路径 -``` +```text Box3JS 与神奇代码岛 → 快速开始 运行原理 JS vs Java │ │ │ │ 环境搭建 │ Rhino 引擎 │ 优劣势对比 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md b/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md index 77b551a..0752e76 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md @@ -1,7 +1,5 @@ # Box3JS 与神奇代码岛 -本文介绍 Box3JS 的起源:它从哪来、为什么做、以及相比其他方案有什么独特优势。 - ## 神奇代码岛是什么 [神奇代码岛](https://dao3.fun)(Box3)是一款**多人联机 3D 游戏创作平台**。在这里,用户不需要学游戏引擎,只要会 JavaScript,就能在浏览器里创建竞速、对战、RPG、FPS 甚至 MOBA 等各类 3D 游戏。 @@ -38,12 +36,12 @@ Box3JS 是一个**社区驱动的 Minecraft 模组**,它在 Minecraft 服务 ### 谁适合用 Box3JS -| 用户画像 | 为什么适合 | -|----------|-----------| -| 神奇代码岛开发者 | API 风格一致,已有技能直接复用,零学习成本 | -| 想给 MC 服务器写玩法的服主 | 不需要学 Java、Gradle、Mixin,写 JS 就行 | -| 编程教育场景 | TypeScript + 热重载 + 沙盒回滚 = 理想的编程教学环境 | -| 不想写 Java 模组的开发者 | 开箱即用的 API 覆盖常用功能,无编译管线负担 | +| 用户画像 | 为什么适合 | +| -------------------------- | --------------------------------------------------- | +| 神奇代码岛开发者 | API 风格一致,已有技能直接复用,零学习成本 | +| 想给 MC 服务器写玩法的服主 | 不需要学 Java、Gradle、Mixin,写 JS 就行 | +| 编程教育场景 | TypeScript + 热重载 + 沙盒回滚 = 理想的编程教学环境 | +| 不想写 Java 模组的开发者 | 开箱即用的 API 覆盖常用功能,无编译管线负担 | ## Box3JS 的独特优势 @@ -88,7 +86,7 @@ Box3JS 直接操作 Minecraft 的世界——真实的方块、原版实体、 开发完成后,一键编译为独立 JAR 模组: -``` +```js /box3script compile mygame ``` @@ -96,7 +94,7 @@ Box3JS 直接操作 Minecraft 的世界——真实的方块、原版实体、 ### 5. 双端架构 -``` +```text 服务端(权威) 客户端(表现) world.* / voxels.* client.* / input.* entity.* / player.* ←→ ui.* / audio.* / gui.* @@ -117,19 +115,17 @@ storage / db / http storage / db / http Box3JS 并非 1:1 复制 Box3 的 API。差异源于两个平台的根本不同: -| 差异领域 | 神奇代码岛 | Box3JS (MC) | -|----------|-----------|-------------| -| 渲染引擎 | 自研 3D 引擎 | Minecraft 原版渲染 | -| 物理引擎 | 自研物理 | Minecraft 原版物理 | -| 天气系统 | 独立的雨/雪/雾系统(丰富的参数控制) | 复用 MC 原版天气(rainDensity/thunderDensity) | -| 光照系统 | 手动/自然光照模式(lightMode/sunFrequency) | 复用 MC 原版光照 | -| 自定义模型 | 内置编辑器 | 需 Resource Pack(MC 机制) | -| 数据库 | 内置 KV 存储 | 内置 KV 存储 + SQLite | +| 差异领域 | 神奇代码岛 | Box3JS (MC) | +| ---------- | ------------------------------------------- | ---------------------------------------------- | +| 渲染引擎 | 自研 3D 引擎 | Minecraft 原版渲染 | +| 物理引擎 | 自研物理 | Minecraft 原版物理 | +| 天气系统 | 独立的雨/雪/雾系统(丰富的参数控制) | 复用 MC 原版天气(rainDensity/thunderDensity) | +| 光照系统 | 手动/自然光照模式(lightMode/sunFrequency) | 复用 MC 原版光照 | +| 自定义模型 | 内置编辑器 | 需 Resource Pack(MC 机制) | +| 数据库 | 内置 KV 存储 | 内置 KV 存储 + SQLite | **设计原则:** 尽量保持 API 命名和语义一致,但对于 MC 无法支持或与 MC 机制冲突的功能,不强行模拟。详细的 API 对照见 [Box3 API vs Box3JS 对比](../BOX3_API_COMPARISON.md)。 ---- - ## 下一步 - **从零开始**: [快速开始指南](getting-started.md) — 10 分钟写出第一个 MC 脚本 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md b/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md index f31b990..2e1288d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md @@ -1,25 +1,8 @@ # Box3JS 运行原理 -本文深入讲解 Box3JS 的内部架构:JS 引擎如何嵌入 Minecraft、作用域如何管理、构建管线如何工作、网络通信如何实现。 - -## 目录 - -1. [整体架构](#整体架构) -2. [Rhino 引擎](#rhino-引擎) -3. [作用域与隔离](#作用域与隔离) -4. [全局对象注入](#全局对象注入) -5. [事件回调机制](#事件回调机制) -6. [构建管线](#构建管线) -7. [网络通信](#网络通信) -8. [沙盒系统](#沙盒系统) -9. [文件监控与热重载](#文件监控与热重载) -10. [编译发布模式](#编译发布模式) - ---- - ## 整体架构 -``` +```text ┌──────────────────────────┐ │ Minecraft Server │ │ (NeoForge) │ @@ -57,7 +40,7 @@ ### 关键包结构 -``` +```text com.box3lab.box3js ├── Box3JS.java ← @Mod 入口 ├── script/ ← 服务端引擎 @@ -83,8 +66,6 @@ com.box3lab.box3js └── ... ``` ---- - ## Rhino 引擎 ### 为什么是 Rhino @@ -136,13 +117,11 @@ Java 对象暴露给 JS 时,Rhino 自动处理类型转换: Box3JS 返回的多是 **Java 原生对象**(如 `ServerPlayer` 包装器),JS 侧可直接调用方法。复杂的返回值(如 `querySelectorAll`)返回 Java `List`,Rhino 映射为 JS 数组。 ---- - ## 作用域与隔离 ### 每个项目独立作用域 -``` +```text Rhino Context │ ┌──────────────┼──────────────┐ @@ -169,13 +148,11 @@ Box3JS 返回的多是 **Java 原生对象**(如 `ServerPlayer` 包装器) 3. 如果沙盒开启,回滚所有方块和实体修改 4. 释放 Rhino scope,GC 回收 ---- - ## 全局对象注入 ### 注入流程 -``` +```text Box3ScriptEngine.setupScope(scope) │ ├── scope.put("world", scope, new Box3JSWorld(...)) @@ -197,7 +174,7 @@ Box3ScriptEngine.setupScope(scope) ### 客户端注入 -``` +```text Box3JSClientEngine.init(scope) │ ├── scope.put("audio", scope, audioObj) @@ -231,13 +208,11 @@ console = { `.apply()` 确保多个参数正确传递给 Java varargs 方法。 ---- - ## 事件回调机制 ### 完整链路 -``` +```text Minecraft 事件发生 │ ▼ @@ -304,11 +279,9 @@ public class GameEventHandlerToken { } ``` ---- - ## 构建管线 -``` +```text src/server/app.ts (TypeScript + ES2020 语法) │ ▼ @@ -364,13 +337,11 @@ await build({ }); ``` ---- - ## 网络通信 ### remoteChannel 架构 -``` +```text ┌──────────────────────┐ ┌──────────────────────┐ │ Server (Java) │ │ Client (Java) │ │ │ │ │ @@ -394,7 +365,7 @@ await build({ ### 数据流 **服务端 → 客户端:** -``` +```text JS: remoteChannel.sendClientEvent(player, { type: "boss_bar", hp: 50 }) → Box3JSRemoteChannel.java → JSON.stringify(eventData) @@ -407,7 +378,7 @@ JS: remoteChannel.sendClientEvent(player, { type: "boss_bar", hp: 50 }) ``` **客户端 → 服务端:** -``` +```text JS: remoteChannel.sendServerEvent({ key: "space" }) → Box3JSClientEngine → JSON.stringify @@ -425,13 +396,11 @@ JS: remoteChannel.sendServerEvent({ key: "space" }) - 数组 `[1, 2, 3]` - 不支持:函数、`GameVector3` 实例、Java 对象 ---- - ## 沙盒系统 ### 工作原理 -``` +```text /box3script sandbox mygame ← 开启沙盒 │ ▼ @@ -470,13 +439,11 @@ class SandboxTracker { - **玩家测试** — 让玩家试玩新功能,结束时回滚不影响正式服 - **调试** — 测试有破坏性的操作(explode、fillVoxel) ---- - ## 文件监控与热重载 ### 工作流程 -``` +```text /box3script watch ← 开启文件监控 │ ▼ @@ -501,13 +468,11 @@ Box3ScriptWatcher 启动 - 300ms 防抖避免 esbuild 写入多个 chunk 时多次 reload - reload 是原子的:先停止旧脚本(清理回调 + 资源),再加载新脚本 ---- - ## 编译发布模式 ### `/box3script compile` 流程 -``` +```text 输入: config/box3/script/mygame/ │ ▼ @@ -568,8 +533,6 @@ public static final DeferredBlock RUBY_BLOCK = **注意:** `registries` 只在编译 JAR 模式下可用。解释模式(`/box3script start`)中 `registries` 为 `undefined`。 ---- - ## 性能考虑 ### 开销来源 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/faq.md b/Box3JS-NeoForge-1.21.1/docs/guide/faq.md index f0de694..412a983 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/faq.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/faq.md @@ -1,12 +1,11 @@ # 常见问题与故障排查 -Box3JS 开发中经常遇到的问题及解决方案。 - ## 脚本加载 ### Q: 脚本不执行,`/box3script` 显示项目为 ○(未加载) 检查顺序: + 1. `npm run build` 是否成功?`dist/` 下是否生成了 `server.js`? 2. `/box3script start <项目名>` 是否执行过? 3. 服务端控制台是否有 `[Box3JS]` 前缀的错误日志? @@ -44,6 +43,7 @@ npm install ### Q: TypeScript 报类型错误但运行正常 TypeScript 只检查构建时类型,实际运行时 Rhino 不会做类型检查。修复步骤: + 1. 检查 `.d.ts` 中的 API 签名是否正确(`types/server/` 和 `types/client/`) 2. 如果类型确实不对,可以加 `// @ts-expect-error` 临时跳过 3. 同时考虑修复 `.d.ts` 文件 @@ -51,6 +51,7 @@ TypeScript 只检查构建时类型,实际运行时 Rhino 不会做类型检 ### Q: `npm run build` 成功但脚本报语法错误 Babel 编译为 ES5 后,目标引擎是 Rhino 1.9.1(仅支持 ES5)。常见问题: + - 不要在 `src/` 中使用 `async/await`(Babel 不会完整编译为 ES5) - 不要使用 `Promise`(Rhino 1.9.1 不支持) - `let`/`const`、`=>` 箭头函数、模板字符串由 Babel 处理,可以放心使用 @@ -73,6 +74,7 @@ Babel 编译为 ES5 后,目标引擎是 Rhino 1.9.1(仅支持 ES5)。常 ### Q: API 报 "xxx is not a function" 先确认: + 1. 方法名拼写是否正确?参考 [API 文档](../api/README.md) 2. 所在全局对象是否正确?如 `world.say()` 不是 `server.say()` 3. 是否需要 `new`?如 `new GameVector3(x, y, z)` @@ -81,6 +83,7 @@ Babel 编译为 ES5 后,目标引擎是 Rhino 1.9.1(仅支持 ES5)。常 ### Q: 脚本执行很慢/服务器卡顿 Rhino 是解释型引擎(无 JIT),需要优化: + - **不在 onTick 中做密集操作** — 用 `setInterval` 降低频率 - **缓存查询结果** — 不要每到 tick 都调用 `querySelectorAll` - **减少 JS ↔ Java 交互** — 批处理比逐个调用快 @@ -106,6 +109,7 @@ Rhino 是解释型引擎(无 JIT),需要优化: ### Q: 如何防止 SQL 注入? 用参数化查询(推荐): + ```js // ✅ 安全:参数化 db.sql("SELECT * FROM t WHERE name = ?", userInput); @@ -148,6 +152,7 @@ db.sql("SELECT * FROM t WHERE name = '" + userInput + "'"); ### Q: 客户端和服务端可以同时使用 `remoteChannel` 吗? 可以。`remoteChannel` 提供双向通道: + - 客户端 → 服务端:`remoteChannel.sendServerEvent()` → `remoteChannel.onServerEvent()` - 服务端 → 客户端(定向):`remoteChannel.sendClientEvent(entity, ...)` → `remoteChannel.onClientEvent()` - 服务端 → 客户端(广播):`remoteChannel.broadcastClientEvent(...)` → `remoteChannel.onClientEvent()` @@ -162,7 +167,7 @@ db.sql("SELECT * FROM t WHERE name = '" + userInput + "'"); ### Q: 如何发布我的脚本? -``` +```js /box3script compile <项目名> ``` @@ -170,17 +175,15 @@ db.sql("SELECT * FROM t WHERE name = '" + userInput + "'"); ### Q: 编译的 JAR 和解释模式有什么区别? -| 特性 | 解释模式 | 编译 JAR | -|------|---------|---------| -| registries | 不可用 | 可用(自定义方块/物品/音效) | -| 热重载 | ✅ | ❌(需重启) | -| 分发 | 复制整个项目目录 | 单个 JAR 文件 | -| 更新 | 直接编辑 JS 文件 | 重新编译 | +| 特性 | 解释模式 | 编译 JAR | +| ---------- | ---------------- | ---------------------------- | +| registries | 不可用 | 可用(自定义方块/物品/音效) | +| 热重载 | ✅ | ❌(需重启) | +| 分发 | 复制整个项目目录 | 单个 JAR 文件 | +| 更新 | 直接编辑 JS 文件 | 重新编译 | ### Q: registries 为什么只在编译模式可用? 自定义方块/物品/音效需要 NeoForge 的 `DeferredRegister`,这必须在模组启动时注册。解释模式没有启动期注册阶段,所以 `registries` 只能在编译为 JAR 时使用。 ---- - 更多问题请在 [GitHub Issues](https://github.com/box3lab/Box3JS) 提出。 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md index 7485cb1..8511dce 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md @@ -1,36 +1,20 @@ # 快速开始:从零到第一个 Box3JS 脚本 -本指南面向**零模组开发经验**的读者。你只需要会 JavaScript,就能在 10 分钟内写出第一个 Minecraft 服务端脚本,并在读完本指南后理解 Box3JS 的核心设计理念。 - -## 目录 - -1. [Box3JS 是什么](#box3js-是什么) -2. [环境搭建](#环境搭建) -3. [创建项目](#创建项目) -4. [理解项目结构](#理解项目结构) -5. [第一个脚本:逐行详解](#第一个脚本逐行详解) -6. [核心设计理念:为什么这样设计 API](#核心设计理念为什么这样设计-api) -7. [API 实战速览](#api-实战速览) -8. [开发循环](#开发循环) -9. [调试技巧](#调试技巧) -10. [发布部署](#发布部署) -11. [下一步](#下一步) - ---- +面向**零模组开发经验**的读者,只需会 JavaScript 即可。 ## Box3JS 是什么 -Box3JS 是一个 **Minecraft 模组**,它在 Minecraft 服务器内部嵌入了一个完整的 JavaScript 运行时,让你用 JS/TypeScript 编写游戏玩法逻辑。同时,可选下发客户端脚本,实现按键监听、屏幕 UI、客户端音效等本地交互。 +Box3JS 是一个 **Minecraft 模组**,在服务器内嵌入 JavaScript 运行时,让你用 JS/TypeScript 编写游戏玩法。客户端脚本可选下发,实现按键监听、屏幕 UI 等本地交互。 -> 了解 Box3JS 的起源和它与神奇代码岛的关系?→ [Box3JS 与神奇代码岛](about-box3js.md) +Box3JS 的 API 设计继承自**[神奇代码岛](https://dao3.fun)(Box3)**——一款浏览器端的多人 3D 游戏创作平台,成千上万的创作者在上面用 JS 写游戏。神奇代码岛的 API 经过长期社区验证,简洁直观。Box3JS 把这套 API 带到了 Minecraft,让你用同一种编程范式在 MC 里构建小游戏、自定义玩法。 -### 一句话概括 - -> Box3JS = Minecraft 服务端里的 Node.js,但不需要你懂 Java 或模组开发。 +::: tip 更多背景 +→ [Box3JS 与神奇代码岛](about-box3js.md) +::: ### 核心架构一览 -``` +```text 你在 VS Code 里写 构建工具帮你 Minecraft 帮你跑 TypeScript ───→ 编译成 ES5 JS ───→ Rhino 引擎执行 │ @@ -47,29 +31,6 @@ Box3JS 是一个 **Minecraft 模组**,它在 Minecraft 服务器内部嵌入 - **Rhino 在 JVM 内执行**,直接调用 Minecraft API - **服务端 + 客户端双端运行**,通过 `remoteChannel` 通信 -### 能做什么 - -| 类别 | 示例 | -| ---------- | ----------------------------------------- | -| 聊天命令 | `!heal`、`!home`、`!shop` | -| 事件响应 | 玩家进服欢迎、死亡惩罚、方块破坏记录 | -| 实体控制 | 生成怪物、设置 AI、自定义 Boss | -| 小游戏 | PvP 竞技场、跑酷、波次刷怪 | -| 世界操作 | 放置/替换方块、填充区域、修改天气时间 | -| 数据持久化 | JSON 存储、SQLite 数据库 | -| 游戏系统 | 计分板、BossBar、队伍、世界边界 | -| HTTP 请求 | 查询 Web API、Webhook 通知 | -| 客户端脚本 | 按键监听、屏幕 UI、客户端音效、自定义 GUI | - -### 不能做什么 - -- **渲染自定义模型/粒子** — 需要客户端资源包或 Java 模组 -- **添加新方块/物品(运行时)** — 需要编译为 JAR 模组(`/box3script compile`) -- **修改原版机制** — 如修改合成表、生物 AI 行为,这些需要 Mixin -- **使用现代 JS 语法在运行时** — Rhino 只支持 ES5,但源码中可以用 TypeScript 现代语法,构建时会转换 - ---- - ## 环境搭建 ### 你需要 @@ -82,27 +43,25 @@ Box3JS 是一个 **Minecraft 模组**,它在 Minecraft 服务器内部嵌入 进入游戏,执行: -``` +```js /box3script ``` 如果看到项目状态面板,说明 Box3JS 已正常运行。 -``` +```text ══ Box3JS Script Engine ══ Watch: ○ Inactive Sandbox: ○ Inactive Projects: 0 enabled | 0 loaded ``` ---- - ## 创建项目 ### 一键创建 在游戏内执行: -``` +```js /box3script create mygame ``` @@ -110,7 +69,7 @@ Box3JS 是一个 **Minecraft 模组**,它在 Minecraft 服务器内部嵌入 ### 理解项目结构 -``` +```text config/box3/script/mygame/ ├── package.json ← 项目配置(名称、版本、构建依赖) ├── tsconfig.base.json ← TypeScript 公共编译选项 @@ -161,8 +120,6 @@ npm install `npm install` 只需执行一次(安装 esbuild、Babel、TypeScript 等构建工具)。 ---- - ## 第一个脚本:逐行详解 打开 `src/server/app.ts`,清空已有内容,写入以下代码。我们来逐行理解。 @@ -274,8 +231,6 @@ setInterval(() => { | 10 分钟 | 12,000 | | 30 分钟 | 36,000 | ---- - ## 核心设计理念:为什么这样设计 API 理解 Box3JS API 的设计理念,能让你写出更高效、更安全的脚本。以下是最重要的几个设计决策及其原因。 @@ -340,7 +295,7 @@ if (token.active()) { ### 设计 4:项目作用域隔离 -``` +```text 服务端同时运行 3 个脚本项目,互不影响: ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ @@ -364,7 +319,7 @@ Box3JS 给每个项目分配**独立的 Rhino 顶级作用域**,存储在独 ### 设计 5:双端架构 + remoteChannel -``` +```text ┌──────────────────────┐ ┌──────────────────────┐ │ 服务端 (Server) │ │ 客户端 (Client) │ │ │ │ │ @@ -401,7 +356,7 @@ remoteChannel.sendServerEvent({ key: "space", pressed: true }); ### 设计 6:TypeScript 源码 + Babel 编译为 ES5 -``` +```text src/server/app.ts Babel esbuild dist/server.js (TypeScript, ES2020) ───→ ES5 JavaScript ───→ bundle ───→ (一个文件) ``` @@ -436,7 +391,7 @@ world.onChat((entity, message) => { ### 设计 8:沙盒模式 — 安全测试 -``` +```js /box3script sandbox mygame # 开启沙盒 # ... 测试脚本(生成实体、修改方块、爆炸)... /box3script sandbox mygame # 关闭 → 自动回滚所有修改 @@ -444,8 +399,6 @@ world.onChat((entity, message) => { **为什么?** 一旦脚本修改了世界,这些修改是永久性的(方块被替换、实体被生成)。沙盒模式追踪脚本对世界的所有修改,关闭时自动回滚。这让开发者可以大胆测试破坏性操作,不用担心搞坏正式服。 ---- - ## API 实战速览 以下按"我想做什么"组织,覆盖最常用的 API。完整的 API 参考见 [API 文档](../api/README.md)。 @@ -804,10 +757,10 @@ audio.playMusic("minecraft:music.game", 0.5, 1.0); audio.stopAll(); // 雾效控制(客户端渲染) -client.setFogColor(255, 100, 50); // 红雾外观 -client.setFogStartDistance(10); // 雾从 10 格开始 -client.setFogEndDistance(50); // 50 格外完全遮挡 -client.resetFog(); // 恢复默认 +client.setFogColor(255, 100, 50); // 红雾外观 +client.setFogStartDistance(10); // 雾从 10 格开始 +client.setFogEndDistance(50); // 50 格外完全遮挡 +client.resetFog(); // 恢复默认 // 聊天 chat.sendMessage("大家好!"); @@ -825,15 +778,13 @@ remoteChannel.onClientEvent((event) => { }); ``` ---- - ## 开发循环 ### 标准流程 每次修改代码后: -``` +```js 改代码 → npm run build → /box3script reload mygame → 测试 ``` @@ -845,7 +796,7 @@ npm run build 输出: -``` +```js dist/server.js 7.1kb ⚡ Done in 240ms ``` @@ -860,7 +811,7 @@ npm run build 在游戏内: -``` +```js /box3script start mygame # 首次启动 /box3script reload mygame # 修改后重载(无需重启服务器) ``` @@ -871,7 +822,7 @@ npm run build 开启文件监控后,保存代码 + build 会自动触发 reload: -``` +```js /box3script watch ``` @@ -879,7 +830,7 @@ npm run build ### 多项目管理 -``` +```js /box3script start mygame lobby # 同时启动多个项目 /box3script stop mygame # 停止单个 /box3script stopall # 停止全部 @@ -887,8 +838,6 @@ npm run build /box3script # 查看所有项目状态 ``` ---- - ## 调试技巧 ### 排查顺序 @@ -918,7 +867,7 @@ npm run build 沙盒模式允许安全测试:开启后所有世界修改被追踪,关闭时一键回滚。 -``` +```js /box3script sandbox mygame # 开启沙盒 # ... 测试脚本(生成实体、修改方块、爆炸等)... /box3script sandbox mygame # 关闭 → 自动回滚所有修改 @@ -941,15 +890,13 @@ Box3JS 脚本运行在服务器主线程上,不合理的代码会影响 TPS: 一个跑酷脚本的性能消耗通常 < 0.5ms/tick,对服务器 TPS 几乎无影响。 ---- - ## 发布部署 ### 开发模式 → 生产发布 开发完成后,将脚本编译为**独立 JAR 模组**: -``` +```js /box3script compile mygame ``` @@ -988,8 +935,6 @@ Box3JS 脚本运行在服务器主线程上,不合理的代码会影响 TPS: | 分发 | 需要源码 | 只需 JAR | | 适用场景 | 开发、测试 | 发布、分发 | ---- - ## 下一步 现在你已经理解了 Box3JS 的核心设计理念和基本 API 用法。接下来: diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java.md b/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java.md index b3e3368..cf2457b 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java.md @@ -1,26 +1,22 @@ # JS 脚本 vs 原生 Java 模组开发对比 -本文帮助你判断:**什么时候用 Box3JS 写脚本,什么时候用 Java 写原生模组。** - ## 总览 -| 维度 | Box3JS (JS/TS) | 原生 Java 模组 | -|------|---------------|---------------| -| **上手门槛** | 会 JavaScript 即可 | 需要 Java + Gradle + Minecraft 模组开发知识 | -| **开发速度** | 改代码 → build → reload(秒级) | 改代码 → 编译 → 重启 MC(分钟级) | -| **热重载** | 支持(`/box3script reload`) | 不支持,每次改代码需重启客户端/服务端 | -| **发布方式** | `/box3script compile` 生成 JAR | `gradlew build` 生成 JAR | -| **执行性能** | 中等(Rhino 解释执行) | 高(JIT 编译为字节码) | -| **API 覆盖面** | 高层封装 API(100+ 方法) | 完整 Minecraft/NeoForge API | -| **类型安全** | TypeScript 类型声明 | Java 静态类型 | -| **调试工具** | console.log + 控制台输出 | IDE 断点调试 | -| **依赖管理** | npm(仅构建时) | Gradle/Maven | -| **客户端功能** | 有限(UI/输入/音效/聊天) | 完整(渲染、模型、GUI、网络协议) | -| **自定义方块/物品** | JSON 配置 + 编译时生成 | Java 类 + 注册 | -| **修改原版行为** | 不支持(无 Mixin) | 支持(Mixin/ASM/CoreMod) | -| **多人协作** | JS 源码 + Git | Java 源码 + Git + Gradle | - ---- +| 维度 | Box3JS (JS/TS) | 原生 Java 模组 | +| ------------------- | ------------------------------- | ------------------------------------------- | +| **上手门槛** | 会 JavaScript 即可 | 需要 Java + Gradle + Minecraft 模组开发知识 | +| **开发速度** | 改代码 → build → reload(秒级) | 改代码 → 编译 → 重启 MC(分钟级) | +| **热重载** | 支持(`/box3script reload`) | 不支持,每次改代码需重启客户端/服务端 | +| **发布方式** | `/box3script compile` 生成 JAR | `gradlew build` 生成 JAR | +| **执行性能** | 中等(Rhino 解释执行) | 高(JIT 编译为字节码) | +| **API 覆盖面** | 高层封装 API(100+ 方法) | 完整 Minecraft/NeoForge API | +| **类型安全** | TypeScript 类型声明 | Java 静态类型 | +| **调试工具** | console.log + 控制台输出 | IDE 断点调试 | +| **依赖管理** | npm(仅构建时) | Gradle/Maven | +| **客户端功能** | 有限(UI/输入/音效/聊天) | 完整(渲染、模型、GUI、网络协议) | +| **自定义方块/物品** | JSON 配置 + 编译时生成 | Java 类 + 注册 | +| **修改原版行为** | 不支持(无 Mixin) | 支持(Mixin/ASM/CoreMod) | +| **多人协作** | JS 源码 + Git | Java 源码 + Git + Gradle | ## 开发体验对比 @@ -64,11 +60,11 @@ public class HealMod { 这是 Box3JS **最大的生产力优势**。 -| 操作 | Box3JS | Java 模组 | -|------|--------|---------| -| 修改一行代码 | build(3s) + reload(1s) = **4 秒** | 编译(10-60s) + 重启MC(30-120s) = **40-180 秒** | -| 测试一个聊天命令 | 改代码 → build → 游戏内 reload | 改代码 → 编译 → 重启MC → 进入世界 | -| 一天迭代次数 | **50+** | 5-10 | +| 操作 | Box3JS | Java 模组 | +| ---------------- | --------------------------------- | ---------------------------------------------- | +| 修改一行代码 | build(3s) + reload(1s) = **4 秒** | 编译(10-60s) + 重启MC(30-120s) = **40-180 秒** | +| 测试一个聊天命令 | 改代码 → build → 游戏内 reload | 改代码 → 编译 → 重启MC → 进入世界 | +| 一天迭代次数 | **50+** | 5-10 | 对于玩法脚本(小游戏、RPG 机制、经济系统),热重载是**不可替代的**——玩法需要反复调参试错,等不起重启。 @@ -106,6 +102,7 @@ world.setScore("Steve", "kills", 5); #### 4. 一站式项目模板 `/box3script create` 生成完整的项目结构,包含: + - TypeScript 配置 + 类型声明 - 构建管线(Babel + esbuild) - ESLint 代码检查 @@ -117,27 +114,25 @@ world.setScore("Steve", "kills", 5); 在正式写 Java 模组前,用 Box3JS 快速验证玩法设计: -``` +```js 想法 → 30分钟写Box3JS脚本 → 和朋友试玩 → 调整 → 确认玩法可行 ↓ 决定做完整模组 → 用 Java 重写 ``` ---- - ### Box3JS 的劣势 #### 1. 性能开销 Rhino 是**解释型** JS 引擎(无 JIT),单线程执行。对性能敏感的操作(如每 tick 扫描大量实体)可能成为瓶颈。 -| 场景 | Box3JS | Java | -|------|--------|------| -| 聊天命令 | 无感知 | 无感知 | -| 每 tick 遍历 100 个实体 | 可接受 | 可接受 | +| 场景 | Box3JS | Java | +| ------------------------- | ------------ | ------ | +| 聊天命令 | 无感知 | 无感知 | +| 每 tick 遍历 100 个实体 | 可接受 | 可接受 | | 每 tick 遍历 10000 个实体 | **可能卡顿** | 可接受 | -| 复杂数学运算(路径算法) | **明显慢** | 快 | -| Y=0 区块全图填充 | **很慢** | 快 | +| 复杂数学运算(路径算法) | **明显慢** | 快 | +| Y=0 区块全图填充 | **很慢** | 快 | **经验法则**:如果 `onTick` 回调耗时超过 1ms,考虑优化或改用 Java。 @@ -145,16 +140,16 @@ Rhino 是**解释型** JS 引擎(无 JIT),单线程执行。对性能敏 Box3JS 封装了 100+ 常用 API,但不是全部: -| 你想做的 | Box3JS | Java | -|---------|--------|------| -| 修改合成表 | ❌ | ✅ `RecipeManager` | -| 自定义 GUI(箱子界面)| ❌ | ✅ `MenuProvider` / `Screen` | -| 修改生物 AI | 部分(setAI/setTarget) | ✅ Brain/Memory 系统 | -| 自定义维度 | ❌ | ✅ `DimensionType` | -| 数据包/战利品表 | ❌ | ✅ 完整支持 | -| 网络协议 | 高层(remoteChannel) | ✅ 底层 `CustomPayload` | -| 修改原版类行为 | ❌ | ✅ Mixin / ASM | -| 渲染自定义模型 | ❌ | ✅ 完整渲染管线 | +| 你想做的 | Box3JS | Java | +| ---------------------- | ----------------------- | ---------------------------- | +| 修改合成表 | ❌ | ✅ `RecipeManager` | +| 自定义 GUI(箱子界面) | ❌ | ✅ `MenuProvider` / `Screen` | +| 修改生物 AI | 部分(setAI/setTarget) | ✅ Brain/Memory 系统 | +| 自定义维度 | ❌ | ✅ `DimensionType` | +| 数据包/战利品表 | ❌ | ✅ 完整支持 | +| 网络协议 | 高层(remoteChannel) | ✅ 底层 `CustomPayload` | +| 修改原版类行为 | ❌ | ✅ Mixin / ASM | +| 渲染自定义模型 | ❌ | ✅ 完整渲染管线 | #### 3. 无断点调试 @@ -163,12 +158,14 @@ Box3JS 封装了 100+ 常用 API,但不是全部: #### 4. 客户端功能有限 客户端脚本可以做: + - 键盘输入检测 - 屏幕 UI 显示 - 音效/音乐播放 - 聊天收发 但不能做: + - 自定义渲染(模型、粒子、GUI) - 修改 HUD - 自定义着色器 @@ -177,6 +174,7 @@ Box3JS 封装了 100+ 常用 API,但不是全部: #### 5. ES5 限制 Rhino 1.9.1 仅支持 ES5 语法。不能使用: + - `let` / `const`(Babel 编译为 `var`) - 箭头函数(Babel 编译为 `function`) - `async` / `await` @@ -191,11 +189,9 @@ Rhino 1.9.1 仅支持 ES5 语法。不能使用: 编译后的 JAR 依赖 Box3JS 作为运行时。用户需要同时安装 Box3JS + 你的 JAR。而纯 Java 模组是自包含的。 ---- - ## 适用场景决策树 -``` +```text 你想做什么? │ ├─ 小游戏(PvP/跑酷/竞速) @@ -234,13 +230,11 @@ Rhino 1.9.1 仅支持 ES5 语法。不能使用: 大量内容 → Java 原生模组 ``` ---- - ## 混合方案 最佳实践:**Box3JS 做玩法,Java 做基础设施**。 -``` +```text ┌──────────────────────────────────┐ │ Java 模组(提供底层能力) │ │ - 自定义方块/物品注册 │ @@ -260,22 +254,21 @@ Rhino 1.9.1 仅支持 ES5 语法。不能使用: ``` 一个真实的示例架构: + - Java 模组添加了自定义武器、自定义怪物、新维度 - Box3JS 脚本定义怪物波次规则、Boss 技能、任务触发条件 - 玩法策划可以独立修改脚本,不需要碰 Java 代码 ---- - ## 总结 -| 选 Box3JS | 选 Java | -|-----------|--------| -| 你主要做玩法/小游戏 | 你需要修改原版机制 | -| 你需要快速迭代试错 | 你需要自定义渲染/模型 | -| 你的团队有 JS 开发者 | 你的团队主要是 Java 开发者 | +| 选 Box3JS | 选 Java | +| ------------------------ | -------------------------------- | +| 你主要做玩法/小游戏 | 你需要修改原版机制 | +| 你需要快速迭代试错 | 你需要自定义渲染/模型 | +| 你的团队有 JS 开发者 | 你的团队主要是 Java 开发者 | | 项目逻辑复杂但不涉及渲染 | 项目包含大量自定义方块/实体/维度 | -| 你想先验证玩法再正式开发 | 你要发布到 CurseForge/Modrinth | -| 你需要热重载 | 你需要极致性能 | -| 项目是服务端为主 | 项目需要客户端渲染 | +| 你想先验证玩法再正式开发 | 你要发布到 CurseForge/Modrinth | +| 你需要热重载 | 你需要极致性能 | +| 项目是服务端为主 | 项目需要客户端渲染 | **没有谁更好,只有谁更适合当前项目。** 对于服务端玩法开发,Box3JS 的生产力优势是压倒性的——热重载 + 低门槛 + 丰富 API。对于需要修改原版机制或自定义渲染的项目,Java 是必须的。 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md b/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md index 7ca0c83..d546262 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md @@ -1,23 +1,6 @@ -# 常用配方:Box3JS 功能模板 +# 常用配方 -本指南是"菜谱"风格——不逐 API 讲解,而是一个个"想实现 X 功能,照这个模板改就行"。所有代码段均经过编译验证。 - -## 目录 - -1. [聊天命令](#聊天命令) -2. [经济系统](#经济系统) -3. [传送系统](#传送系统) -4. [重生保护](#重生保护) -5. [商店/NPC](#商店npc) -6. [每日奖励](#每日奖励) -7. [排行榜](#排行榜) -8. [波次刷怪](#波次刷怪) -9. [缩圈机制](#缩圈机制) -10. [HTTP Webhook](#http-webhook) -11. [客户端 HUD](#客户端-hud) -12. [跨脚本联动](#跨脚本联动) - ---- +"想实现 X 功能,照模板改就行"。所有代码段均经过编译验证。 ## 聊天命令 @@ -73,8 +56,6 @@ if (message === "!admin") { } ``` ---- - ## 经济系统 基于计分板的经济系统,玩家可以用 `/box3script reload` 不丢失数据(计分板独立于脚本生命周期)。 @@ -150,8 +131,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## 传送系统 ### 家传送(内存,重启丢失) @@ -243,24 +222,20 @@ world.onChat((entity, message) => { }); ``` ---- - ## 重生保护 ```js // 玩家重生后给予短暂无敌 world.onPlayerRespawn((entity) => { const p = entity.player; - p.addEffect("minecraft:resistance", 100, 4, true); // 5秒 抗性V(无敌) - p.addEffect("minecraft:regeneration", 100, 2, true); // 5秒 生命恢复III + p.addEffect("minecraft:resistance", 100, 4, true); // 5秒 抗性V(无敌) + p.addEffect("minecraft:regeneration", 100, 2, true); // 5秒 生命恢复III p.addEffect("minecraft:fire_resistance", 100, 0, true); // 5秒 防火 p.directMessage("§a你已获得 5 秒重生保护"); p.playSound("minecraft:block.beacon.activate", 1.0, 1.5); }); ``` ---- - ## 商店/NPC 右键一个实体(如村民)弹出对话/交易: @@ -318,12 +293,11 @@ world.onChat((entity, message) => { }); ``` ---- - ## 每日奖励 ```js -const dailyRewards = storage.getDataStorage<{ lastClaimed: number }>("daily-rewards"); +const dailyRewards = + storage.getDataStorage < { lastClaimed: number } > "daily-rewards"; world.onChat((entity, message) => { const p = entity.player; @@ -331,9 +305,9 @@ world.onChat((entity, message) => { if (message === "!daily") { const now = Date.now(); const record = dailyRewards.get(p.userId); - const cooldown = 24 * 60 * 60 * 1000; // 24 小时 + const cooldown = 24 * 60 * 60 * 1000; // 24 小时 - if (record && (now - record.lastClaimed) < cooldown) { + if (record && now - record.lastClaimed < cooldown) { const hours = Math.ceil((record.lastClaimed + cooldown - now) / 3600000); p.directMessage(`§c请等待 ${hours} 小时后再领取`); return false; @@ -354,8 +328,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## 排行榜 ```js @@ -382,8 +354,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## 波次刷怪 完整波次系统,难度递增: @@ -424,8 +394,6 @@ function spawnWave(pos: GameVector3): void { } ``` ---- - ## 缩圈机制 ```js @@ -460,8 +428,6 @@ startShrinkPhase(0, 0, [ ]); ``` ---- - ## HTTP Webhook ```js @@ -478,8 +444,12 @@ world.onEntityDeath((entity, killer) => { }), timeout: 5000, async: true, - onResponse: (resp) => { console.log(`Webhook sent: ${resp.status}`); }, - onError: (err) => { console.warn(`Webhook failed: ${err}`); }, + onResponse: (resp) => { + console.log(`Webhook sent: ${resp.status}`); + }, + onError: (err) => { + console.warn(`Webhook failed: ${err}`); + }, }); } }); @@ -490,7 +460,7 @@ const WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_ID"; setInterval(() => { const playerCount = world.querySelectorAll("*").length; - const tps = "20"; // 正常情况 + const tps = "20"; // 正常情况 http.fetch(WEBHOOK_URL, { method: "POST", @@ -504,8 +474,6 @@ setInterval(() => { }, 6000); ``` ---- - ## 客户端 HUD 结合 `remoteChannel` 实现客户端自定义 HUD(服务端提供数据,客户端显示): @@ -560,8 +528,6 @@ remoteChannel.onClientEvent((event) => { }); ``` ---- - ## 跨脚本联动 多个脚本项目之间通信: @@ -590,6 +556,4 @@ function endGame(): void { } ``` ---- - 每个配方都是独立的,按需取用。更多细节参见 [API 文档](../api/README.md) 和 [教程系列](../tutorial/README.md)。 diff --git a/Box3JS-NeoForge-1.21.1/docs/index.md b/Box3JS-NeoForge-1.21.1/docs/index.md new file mode 100644 index 0000000..01e61bb --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/index.md @@ -0,0 +1,78 @@ +--- +layout: home + +hero: + name: "Box3JS" + text: "Minecraft JS/TS 脚本引擎" + tagline: 神奇代码岛同款编程体验,用 JS/TS 在 Minecraft 里创造小游戏 + actions: + - theme: brand + text: 快速开始 + link: /guide/getting-started + +features: + - icon: 🎮 + title: 服务端 & 客户端双端脚本 + details: 服务端:世界操作、实体、合成配方。客户端:键盘输入、屏幕 UI、音效、SQLite 存储、HTTP 请求。 + - icon: 📦 + title: TypeScript 优先 + details: 17 个全局对象全部提供 DTS 类型定义。内置 esbuild + Babel 构建管线,将现代 TS 转译为 Rhino 兼容的 ES5。 + - icon: 🔄 + title: 热重载 + details: 修改脚本即时生效,无需重启服务端。文件监听器在保存时自动重载。 + - icon: 🌐 + title: 双向通信 + details: remoteChannel 实现服务端↔客户端事件消息传递。服务端广播至所有玩家,客户端独立回复。 + - icon: 🗄️ + title: 双端存储 & 数据库 + details: 服务端和客户端均支持 JSON 文件持久化与 SQLite。分页查询、原子更新、计数器、标签模板查询。 + - icon: 🧩 + title: 自定义方块 & 物品 + details: 方块纹理、物品模型、装备、音效、创造标签页——全部通过 JSON 配置注册(独立/JAR 模式)。 + - icon: 📚 + title: 完善文档 + details: 50+ 页面,涵盖 API 参考、渐进式教程、常用配方、架构深入解析和常见问题——支持中英双语。 + - icon: 🚀 + title: 独立 JAR 模式 + details: 将脚本项目编译为独立 JAR 模组,无需依赖 Box3JS 运行时——放入 mods 文件夹即可使用。 + +--- + +## 快速开始 + +```bash +# 游戏内:创建新项目 +/box3script create mygame + +# 构建并监听 +cd config/box3/script/mygame +npm install +npm run build -- --watch + +# TypeScript 类型检查 +npm run check +``` + +```ts +// src/server/app.ts — 你的第一个脚本 +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage(`你好,${entity.player.name}!`); + return false; + } + return true; +}); +``` + +[查看完整文档 →](/guide/getting-started) + +## 版本信息 + +| 组件 | 版本 | +|------|------| +| Minecraft | 1.21.1 | +| 模组加载器 | NeoForge | +| Java | 21 | +| JS 引擎 | Mozilla Rhino 1.9.1 (ES5) | +| TypeScript | 通过 Babel → ES5 | + diff --git a/Box3JS-NeoForge-1.21.1/docs/README.md b/Box3JS-NeoForge-1.21.1/docs/overview.md similarity index 100% rename from Box3JS-NeoForge-1.21.1/docs/README.md rename to Box3JS-NeoForge-1.21.1/docs/overview.md diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md index 62e4c4f..91ed1da 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md @@ -11,7 +11,7 @@ 在游戏内执行一条命令: -``` +```js /box3script create hello ``` @@ -46,7 +46,7 @@ world.onPlayerJoin((entity) => { 回到游戏内: -``` +```js /box3script start hello ``` @@ -62,14 +62,12 @@ entity.player.directMessage(`§6你好,${entity.player.name}!`); 保存后执行 `npm run build`,然后在游戏内: -``` +```js /box3script reload hello ``` 不需要重启服务器,改动立刻生效。 ---- - 以上 5 步就是完整的开发循环:**改代码 → build → reload**。下文深入讲解你能用的所有能力。 ## 消息系统 @@ -94,22 +92,22 @@ player.title("§6§l主标题", "§7副标题"); player.title("§c§lBOSS", "远古巨龙", 10, 60, 10); ``` -| 方法 | 位置 | 可见范围 | -|------|------|---------| -| `world.say()` | 聊天栏 | 全服 | -| `player.directMessage()` | 聊天栏 | 单人 | -| `player.actionBar()` | 快捷栏上方 | 单人 | -| `player.title()` | 屏幕中央 | 单人 | +| 方法 | 位置 | 可见范围 | +| ------------------------ | ---------- | -------- | +| `world.say()` | 聊天栏 | 全服 | +| `player.directMessage()` | 聊天栏 | 单人 | +| `player.actionBar()` | 快捷栏上方 | 单人 | +| `player.title()` | 屏幕中央 | 单人 | ### console 日志 `console` 输出到服务端控制台,格式为 `[Box3JS] [项目名] message`: ```js -console.log("普通日志"); // [Box3JS] [hello] 普通日志 +console.log("普通日志"); // [Box3JS] [hello] 普通日志 console.debug("调试信息"); // [Box3JS] [hello] [DEBUG] 调试信息 -console.warn("警告"); // [Box3JS] [hello] [WARN] 警告 -console.error("错误"); // [Box3JS] [hello] [ERROR] 错误 +console.warn("警告"); // [Box3JS] [hello] [WARN] 警告 +console.error("错误"); // [Box3JS] [hello] [ERROR] 错误 ``` ## 聊天命令系统 @@ -128,7 +126,7 @@ world.onChat((entity, message) => { p.directMessage("§f!pos §7- 查看坐标"); p.directMessage("§f!day §7- 设为白天"); p.directMessage("§f!clear §7- 清除天气"); - return false; // ★ 返回 false 阻止消息显示在聊天栏 + return false; // ★ 返回 false 阻止消息显示在聊天栏 case "!hello": p.directMessage(`§e你好,${p.name}!`); @@ -141,7 +139,7 @@ world.onChat((entity, message) => { case "!pos": { const pos = p.position; p.directMessage( - `§e你的位置: §f${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)}` + `§e你的位置: §f${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)}`, ); return false; } @@ -156,7 +154,7 @@ world.onChat((entity, message) => { world.say(`§e${p.name} §f清除了天气`); return false; } - return true; // 不是命令的消息正常发送 + return true; // 不是命令的消息正常发送 }); ``` @@ -175,7 +173,14 @@ world.onPlayerJoin((entity) => { // 粒子圈 + 音效 const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); ``` @@ -189,41 +194,41 @@ world.onPlayerJoin((entity) => { setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7在线: §f${count} §7人`); -}, 6000); // 6000 ticks = 5 分钟 +}, 6000); // 6000 ticks = 5 分钟 // 30 秒后执行一次 setTimeout(() => { world.say("§6服务器已运行 30 秒"); -}, 600); // 600 ticks = 30 秒 +}, 600); // 600 ticks = 30 秒 ``` **Tick 换算:** 20 ticks = 1 秒 -| 时长 | Ticks | -|------|-------| -| 1 秒 | 20 | -| 5 秒 | 100 | -| 30 秒 | 600 | -| 1 分钟 | 1200 | -| 5 分钟 | 6000 | +| 时长 | Ticks | +| ------ | ----- | +| 1 秒 | 20 | +| 5 秒 | 100 | +| 30 秒 | 600 | +| 1 分钟 | 1200 | +| 5 分钟 | 6000 | ## 世界属性 ```js // 时间 -world.time = 6000; // 正午 (0=日出, 6000=正午, 12000=日落, 18000=午夜) +world.time = 6000; // 正午 (0=日出, 6000=正午, 12000=日落, 18000=午夜) // 天气 -world.rainDensity = 1.0; // 满强度下雨 +world.rainDensity = 1.0; // 满强度下雨 world.thunderDensity = 0.5; // 雷暴 -world.clearWeather(); // 晴天 +world.clearWeather(); // 晴天 // 难度 -world.difficulty = "hard"; // peaceful / easy / normal / hard +world.difficulty = "hard"; // peaceful / easy / normal / hard // 游戏规则 -world.setGameRule("keepInventory", true); // 死亡不掉落 -world.setGameRule("doFireTick", false); // 火焰不蔓延 +world.setGameRule("keepInventory", true); // 死亡不掉落 +world.setGameRule("doFireTick", false); // 火焰不蔓延 world.setGameRule("doMobSpawning", false); // 禁止刷怪 ``` @@ -243,7 +248,14 @@ world.onPlayerJoin((entity) => { const p = entity.player; p.title("§6§l欢迎来到服务器!", "§7输入 §f!help §7查看命令", 5, 70, 10); const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); @@ -268,7 +280,9 @@ world.onChat((entity, message) => { return false; case "!pos": { const pos = p.position; - p.directMessage(`§e位置: §f${Math.floor(pos.x)} ${Math.floor(pos.y)} ${Math.floor(pos.z)}`); + p.directMessage( + `§e位置: §f${Math.floor(pos.x)} ${Math.floor(pos.y)} ${Math.floor(pos.z)}`, + ); return false; } case "!online": @@ -291,13 +305,13 @@ world.onChat((entity, message) => { ### 开发循环 -``` +```js 改代码 → npm run build → /box3script reload hello → 测试 ``` 开启文件监控自动热重载(无需手动 reload): -``` +```js /box3script watch ``` @@ -305,7 +319,7 @@ world.onChat((entity, message) => { 开启沙盒后,脚本对世界的所有修改都会被追踪,关闭时一键回滚: -``` +```js /box3script sandbox hello # 开启 # ... 测试脚本 ... /box3script sandbox hello # 关闭 → 回滚所有修改 @@ -314,6 +328,7 @@ world.onChat((entity, message) => { ### 调试技巧 遇到问题时的排查顺序: + 1. 检查服务端控制台是否有报错(`console.log` 输出会出现在这里) 2. 确认脚本已加载:`/box3script` 看项目是否显示为 `◉`(已加载运行中) 3. 确认 build 成功:`npm run build` 应该没有错误 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md index e684d59..57af4a1 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md @@ -265,7 +265,7 @@ world.onEntitySeparate((entityA, entityB, _tick) => { ## 3.8 常用实体类型 -``` +```js minecraft:zombie 僵尸 minecraft:skeleton 骷髅 minecraft:creeper 苦力怕 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md index b7874e0..3b11b30 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md @@ -19,21 +19,21 @@ world.spawnParticleCircle(0, 100, 0, 4.0, "minecraft:end_rod", 36); 常用粒子: -| 粒子 ID | 效果 | -|---------|------| -| `minecraft:flame` | 火焰 | -| `minecraft:cloud` | 烟雾 | -| `minecraft:happy_villager` | 绿色粒子(正面) | -| `minecraft:witch` | 紫色粒子 | -| `minecraft:portal` | 传送门 | -| `minecraft:end_rod` | 末地烛光 | -| `minecraft:heart` | 爱心 | -| `minecraft:note` | 音符 | -| `minecraft:dragon_breath` | 龙息 | -| `minecraft:angry_villager` | 愤怒粒子(红色) | +| 粒子 ID | 效果 | +| --------------------------- | ---------------- | +| `minecraft:flame` | 火焰 | +| `minecraft:cloud` | 烟雾 | +| `minecraft:happy_villager` | 绿色粒子(正面) | +| `minecraft:witch` | 紫色粒子 | +| `minecraft:portal` | 传送门 | +| `minecraft:end_rod` | 末地烛光 | +| `minecraft:heart` | 爱心 | +| `minecraft:note` | 音符 | +| `minecraft:dragon_breath` | 龙息 | +| `minecraft:angry_villager` | 愤怒粒子(红色) | | `minecraft:soul_fire_flame` | 灵魂火焰(蓝色) | -| `minecraft:redstone` | 红石粒子 | -| `minecraft:explosion` | 爆炸粒子 | +| `minecraft:redstone` | 红石粒子 | +| `minecraft:explosion` | 爆炸粒子 | ### 螺旋上升粒子 @@ -70,7 +70,16 @@ world.launchFirework(0, 100, 0, "green", "creeper"); ### 连续烟花秀 ```js -const colors = ["red", "gold", "green", "blue", "purple", "white", "pink", "aqua"]; +const colors = [ + "red", + "gold", + "green", + "blue", + "purple", + "white", + "pink", + "aqua", +]; const shapes = ["ball", "large_ball", "star", "creeper", "burst"]; for (let i = 0; i < 8; i++) { @@ -81,7 +90,8 @@ for (let i = 0; i < 8; i++) { pos.x + (Math.random() - 0.5) * 10, pos.y + 5 + Math.random() * 8, pos.z + (Math.random() - 0.5) * 10, - c, s + c, + s, ); }, i * 300); } @@ -91,9 +101,9 @@ for (let i = 0; i < 8; i++) { ```js // 闪电: (x, y, z, 伤害) -world.strikeLightning(0, 100, 0); // 默认伤害 -world.strikeLightning(0, 100, 0, 10); // 10 点伤害 -world.strikeLightning(0, 100, 0, 0); // 无伤害,纯视觉效果 +world.strikeLightning(0, 100, 0); // 默认伤害 +world.strikeLightning(0, 100, 0, 10); // 10 点伤害 +world.strikeLightning(0, 100, 0, 0); // 无伤害,纯视觉效果 // 在玩家周围召唤闪电 for (let i = 0; i < 3; i++) { @@ -110,13 +120,23 @@ world.playSound("minecraft:entity.lightning_bolt.thunder", pos, 1.0, 1.0); ```js // 爆炸: (x, y, z, 威力, 是否引火) -world.explode(0, 100, 0, 4, false); // 威力 4,不引火 -world.explode(0, 100, 0, 8, true); // 威力 8,引火 +world.explode(0, 100, 0, 4, false); // 威力 4,不引火 +world.explode(0, 100, 0, 8, true); // 威力 8,引火 // 玩家引爆自身周围(3 秒倒计时) world.playSound("minecraft:block.note_block.bass", pos, 1.0, 0.5); setTimeout(() => { - world.spawnParticle("minecraft:explosion", pos.x, pos.y, pos.z, 1, 0, 0, 0, 0); + world.spawnParticle( + "minecraft:explosion", + pos.x, + pos.y, + pos.z, + 1, + 0, + 0, + 0, + 0, + ); setTimeout(() => { world.explode(pos.x, pos.y, pos.z, 4, false); world.playSound("minecraft:entity.generic.explode", pos, 1.0, 1.0); @@ -137,22 +157,22 @@ player.playSound("minecraft:entity.player.levelup", 1.0, 1.0); 常用音效: -| 音效 ID | 用途 | -|---------|------| -| `minecraft:block.note_block.pling` | 铃铛提示 | -| `minecraft:block.note_block.bass` | 低音提示 | -| `minecraft:entity.experience_orb.pickup` | 经验球拾取 | -| `minecraft:entity.player.levelup` | 升级 | -| `minecraft:entity.ender_dragon.growl` | 龙吼(Boss 出场) | -| `minecraft:entity.wither.spawn` | 凋零生成(压迫感) | -| `minecraft:entity.lightning_bolt.thunder` | 雷鸣 | -| `minecraft:entity.generic.explode` | 爆炸 | -| `minecraft:entity.witch.throw` | 药水投掷 | -| `minecraft:block.beacon.activate` | 信标激活 | -| `minecraft:block.anvil.land` | 铁砧落地 | -| `minecraft:ui.toast.challenge_complete` | 挑战完成 | -| `minecraft:entity.player.burp` | 吃食物音效 | -| `minecraft:entity.enderman.teleport` | 传送音效 | +| 音效 ID | 用途 | +| ----------------------------------------- | ------------------ | +| `minecraft:block.note_block.pling` | 铃铛提示 | +| `minecraft:block.note_block.bass` | 低音提示 | +| `minecraft:entity.experience_orb.pickup` | 经验球拾取 | +| `minecraft:entity.player.levelup` | 升级 | +| `minecraft:entity.ender_dragon.growl` | 龙吼(Boss 出场) | +| `minecraft:entity.wither.spawn` | 凋零生成(压迫感) | +| `minecraft:entity.lightning_bolt.thunder` | 雷鸣 | +| `minecraft:entity.generic.explode` | 爆炸 | +| `minecraft:entity.witch.throw` | 药水投掷 | +| `minecraft:block.beacon.activate` | 信标激活 | +| `minecraft:block.anvil.land` | 铁砧落地 | +| `minecraft:ui.toast.challenge_complete` | 挑战完成 | +| `minecraft:entity.player.burp` | 吃食物音效 | +| `minecraft:entity.enderman.teleport` | 传送音效 | ## 5.6 玩家进出特效 @@ -160,12 +180,29 @@ player.playSound("minecraft:entity.player.levelup", 1.0, 1.0); world.onPlayerJoin((entity, _tick) => { const pos = entity.position; world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); }); world.onPlayerLeave((entity, _tick) => { const pos = entity.position; - world.spawnParticle("minecraft:cloud", pos.x, pos.y, pos.z, 10, 0.3, 0.3, 0.3, 0.01); + world.spawnParticle( + "minecraft:cloud", + pos.x, + pos.y, + pos.z, + 10, + 0.3, + 0.3, + 0.3, + 0.01, + ); }); ``` @@ -174,6 +211,7 @@ world.onPlayerLeave((entity, _tick) => { 这是教程四中设计模式的实际应用——一个完整的红蓝两队 PvP 小游戏,整合了事件、BossBar、计分板、队伍、粒子、烟花、边界缩圈、空投等所有系统。 **命令:** + - `!pvp join` — 加入游戏 - `!pvp leave` — 退出等待 - `!pvp start` — (OP) 开始游戏 @@ -181,6 +219,7 @@ world.onPlayerLeave((entity, _tick) => { - `!pvp status` — 查看状态 **特性:** + - 大厅倒计时 30 秒 → 游戏时长 300 秒 - 红蓝两队自动分配 + 队伍前缀 - 击杀计分 + 全局播报 + 烟花特效 @@ -591,8 +630,6 @@ world.onChat((entity, message, _tick) => { }); ``` ---- - 所有示例代码均已通过 `tsc --noEmit`、`eslint` 和 `node build.mjs` 完整验证。可直接使用。 更多 API 细节请参考 `docs/api/` 目录中的完整 API 文档。 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md index 54ab070..2c42e82 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md @@ -157,7 +157,9 @@ const color = client.getFogColor(); // 返回 GameRGBColor 或 null client.resetFog(); ``` -> **注意**: 雾效修改在客户端本地生效。可通过 `remoteChannel` 让服务端指令触发客户端雾效变化,实现服务端控制的天气效果。 +::: warning +雾效修改在客户端本地生效。可通过 `remoteChannel` 让服务端指令触发客户端雾效变化,实现服务端控制的天气效果。 +::: ## 6.9 storage — 客户端本地存储 @@ -264,7 +266,9 @@ function searchMobs(keyword: string): void { } ``` -> 未安装 `minecraft-sqlite-jdbc` 时,`db.isAvailable()` 返回 `false`,所有 SQL 调用静默返回空结果。 +::: warning +未安装 `minecraft-sqlite-jdbc` 时,`db.isAvailable()` 返回 `false`,所有 SQL 调用静默返回空结果。 +::: ## 6.11 http — 客户端 HTTP 请求 @@ -389,7 +393,9 @@ remoteChannel.onServerEvent((event) => { ### 通讯数据格式 -> **重要:** 跨网络传输的数据必须是 JSON 可序列化的类型(string、number、boolean、null、普通对象、数组)。不能传函数、Java 对象或 `GameVector3`。 +::: warning +跨网络传输的数据必须是 JSON 可序列化的类型(string、number、boolean、null、普通对象、数组)。不能传函数、Java 对象或 `GameVector3`。 +::: ## 6.13 完整实战:客户端 HUD 状态栏 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md index 874d19d..d910e37 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md @@ -1,10 +1,8 @@ # Box3JS 教程 -从零开始学习 Box3JS 脚本开发。每个教程约 10-15 分钟,包含可直接运行的完整代码。 - ## 学习路径 -``` +```text 教程一 教程二 教程三 教程四 教程五 │ │ │ │ │ 从零开始 → 玩家操控 → 事件系统 → 高级游戏系统 → 实战小游戏 @@ -34,7 +32,7 @@ ## 技能进阶路线 -``` +```text 入门 进阶 高级 │ │ │ │ 教程一: 从零开始 │ 教程三: 事件与实体 │ 教程五: 实战小游戏 diff --git a/Box3JS-NeoForge-1.21.1/package-lock.json b/Box3JS-NeoForge-1.21.1/package-lock.json new file mode 100644 index 0000000..bf54d45 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/package-lock.json @@ -0,0 +1,2513 @@ +{ + "name": "box3js", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "box3js", + "version": "1.0.0", + "devDependencies": { + "vitepress": "^1.6.3" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.1.tgz", + "integrity": "sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.1.tgz", + "integrity": "sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.1.tgz", + "integrity": "sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.1.tgz", + "integrity": "sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.1.tgz", + "integrity": "sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.1.tgz", + "integrity": "sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.1.tgz", + "integrity": "sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.1.tgz", + "integrity": "sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.1.tgz", + "integrity": "sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.1.tgz", + "integrity": "sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.1.tgz", + "integrity": "sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.1.tgz", + "integrity": "sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.1.tgz", + "integrity": "sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.1.tgz", + "integrity": "sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.82", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.82.tgz", + "integrity": "sha512-4p978qHx8eD/QBOhgBzp/p7uS3OO2KCnVpFPJTUvuhuDXv1Hr4RcxcZ5MWc6ptkf/3Dlb1xb23068OtPyx10mA==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.1.tgz", + "integrity": "sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.18.1", + "@algolia/client-abtesting": "5.52.1", + "@algolia/client-analytics": "5.52.1", + "@algolia/client-common": "5.52.1", + "@algolia/client-insights": "5.52.1", + "@algolia/client-personalization": "5.52.1", + "@algolia/client-query-suggestions": "5.52.1", + "@algolia/client-search": "5.52.1", + "@algolia/ingestion": "1.52.1", + "@algolia/monitoring": "1.52.1", + "@algolia/recommend": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/package.json b/Box3JS-NeoForge-1.21.1/package.json new file mode 100644 index 0000000..de6d00e --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/package.json @@ -0,0 +1,15 @@ +{ + "name": "box3js", + "version": "1.0.0", + "private": true, + "description": "Box3JS — JavaScript/TypeScript scripting engine for Minecraft NeoForge 1.21.1", + "type": "module", + "scripts": { + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "devDependencies": { + "vitepress": "^1.6.3" + } +}