diff --git a/Box3JS-NeoForge-1.21.1/README.md b/Box3JS-NeoForge-1.21.1/README.md
index c296748..b8a12cd 100644
--- a/Box3JS-NeoForge-1.21.1/README.md
+++ b/Box3JS-NeoForge-1.21.1/README.md
@@ -1,185 +1,219 @@
# Box3JS — Minecraft 脚本引擎
-> **Beta** — 本项目处于早期测试阶段,API 可能变动,欢迎反馈。
+> **v0.1.0** — 首个公开测试版。API 可能变动,欢迎反馈。
[简体中文](Readme.md) | [English](README_en.md)
-**无需 Java 知识,用 TypeScript 为你的 Minecraft 服务器创造无限玩法。**
+**无需 Java 知识。用 TypeScript 为 Minecraft 创造无限玩法。**
+
+Box3JS 在 Minecraft 嵌入 Mozilla Rhino JavaScript 引擎,API 设计延续[神奇代码岛](https://dao3.fun)简洁高效的风格——告别 Gradle、告别重启、告别复杂的环境配置。PvP 竞技场、塔防、RPG 副本、派对小游戏,写 TypeScript,一键加载,即时生效。
+
+> Box3JS 与神奇代码岛的关系?→ [关于 Box3JS](docs/guide/about-box3js.md)
+
+```ts
+// 玩家加入时:弹窗欢迎 + 头顶粒子 + 升级音效
+world.onPlayerJoin((player) => {
+ player.player.dialog({ content: "欢迎来到 Box3JS 服务器!" });
+ const pos = new GameVector3(player.position.x, player.position.y + 1.8, player.position.z);
+ world.spawnParticle("minecraft:happy_villager", pos, 10, 0.5, 0.5, 0.5, 0.1);
+ world.playSound("minecraft:entity.player.levelup", pos, 1.0, 1.0);
+});
+
+// 聊天命令:!heal 回血、!firework 放烟花
+world.onChat((player, message) => {
+ if (message === "!heal") {
+ player.hp = player.maxHp;
+ player.player.actionBar("生命已恢复!");
+ return false; // 不广播此消息
+ }
+ if (message === "!firework") {
+ const pos = new GameVector3(player.position.x, player.position.y + 1, player.position.z);
+ world.launchFirework(pos, "green", "large_ball");
+ return false;
+ }
+});
+```
+
+
+对比:用传统 Java Mod 实现同样功能
+
+```java
+// Java — 需要 ~60 行、3 个类才能实现同等功能
+@Mod.EventBusSubscriber(modid = "mymod")
+public class PlayerJoinListener {
+ @SubscribeEvent
+ public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) {
+ ServerPlayer player = (ServerPlayer) event.getEntity();
+ // 发送对话框需要在客户端处理,服务端无法直接弹窗
+ player.sendSystemMessage(
+ Component.literal("欢迎来到服务器!").withStyle(ChatFormatting.GREEN)
+ );
+ // 粒子需要获取 ServerLevel、ParticleOptions
+ ServerLevel level = player.serverLevel();
+ Vec3 pos = player.position().add(0, 1.8, 0);
+ level.sendParticles(
+ ParticleTypes.HAPPY_VILLAGER,
+ pos.x, pos.y, pos.z,
+ 10, 0.5, 0.5, 0.5, 0.1
+ );
+ // 音效同理
+ level.playSound(
+ null, player.blockPosition(),
+ SoundEvents.PLAYER_LEVELUP, SoundSource.PLAYERS,
+ 1.0f, 1.0f
+ );
+ }
+}
+
+@Mod.EventBusSubscriber(modid = "mymod")
+public class ChatListener {
+ @SubscribeEvent
+ public static void onChat(ServerChatEvent event) {
+ ServerPlayer player = event.getPlayer();
+ String message = event.getMessage().getString();
+ if ("!heal".equals(message)) {
+ player.setHealth(player.getMaxHealth());
+ player.displayClientMessage(
+ Component.literal("生命已恢复!"), true
+ );
+ event.setCanceled(true);
+ }
+ if ("!firework".equals(message)) {
+ // 烟花需要手动构建 FireworkRocketEntity
+ FireworkRocketEntity firework = new FireworkRocketEntity(
+ player.level(),
+ player.getX(), player.getY() + 1, player.getZ(),
+ createFireworkItem("green", FireworkRocketEntity.Shape.LARGE_BALL)
+ );
+ player.level().addFreshEntity(firework);
+ event.setCanceled(true);
+ }
+ }
+ // 还需要 ~20 行构建烟花物品的辅助方法...
+}
+```
+
+
+## 为什么选择 Box3JS?
+
+**零门槛上手** — 会 JS/TS 就能写模组。不需要 Gradle、不需要 IDE、不需要重启服务端。`/box3script create` 一键生成完整 TypeScript 项目。
+
+**秒级热重载** — 改代码 → 构建 → 重载,改动即时生效。开启 `watch` 模式连构建步骤都省了,保存即更新。
+
+**写起来像 Box3,跑在 Minecraft 里** — API 命名和设计延续神奇代码岛的开发体验,学过 Box3 的创作者可以无缝过渡。同时充分利用 Minecraft 原生能力:原版生物 AI、粒子效果、结构、进度、配方等。
+
+**沙盒保护,放心测试** — 开启沙盒后所有脚本修改自动追踪,关闭沙盒完整回滚。测试时不用担心破坏地图,开发环境即生产环境。
+
+**TypeScript 原生支持** — 完整 `.d.ts` 类型声明,中英双语 JSDoc 注释。构建管线:esbuild 打包 → Babel 转译 ES5 → 正则清理,全程自动。
-Box3JS 是一个社区驱动的 Minecraft 模组,在服务端嵌入 Mozilla Rhino JavaScript 引擎。它的 API 设计延续了[神奇代码岛](https://box3.fun)(深圳奇梦岛科技有限公司)的风格——把 Box3 平台简洁高效的开发体验带进 Minecraft。告别复杂的 Java 模组开发:写 TypeScript,一键热重载,即时生效。PvP 竞技场、RPG 副本、派对小游戏、世界管理、社交工具,全能用脚本快速实现。
+**双端脚本** — 服务端处理游戏逻辑、实体 AI、方块操作;客户端处理键盘输入、屏幕 UI、聊天、音效。通过 `remoteChannel` 双向实时通信。
-> 了解 Box3JS 与神奇代码岛的关系?→ [Box3JS 与神奇代码岛](docs/guide/about-box3js.md)
+**数据持久化** — 内置 JSON 文件存储和 SQLite 数据库,排行榜、经济系统、玩家存档都能轻松实现。
+
+**独立分发** — `/box3script compile` 将脚本编译为独立 JAR 模组,像普通模组一样放入 `mods/` 即可运行,无需 Box3JS。
## 安装
1. 将 `box3js-.jar` 放入服务端 `mods/` 目录
-2. 如需使用 SQLite 数据库(`db` API),还需安装 [`minecraft-sqlite-jdbc`](https://modrinth.com/mod/minecraft-sqlite-jdbc)
+2. 如需 SQLite 数据库(`db` API),同时安装 [`minecraft-sqlite-jdbc`](https://modrinth.com/mod/minecraft-sqlite-jdbc)
3. 启动服务器
-## 5 分钟快速开始
+## 快速开始
-在游戏中(需要 OP 权限等级 ≥ 2):
+在聊天框输入(需 OP 权限 ≥ 2):
```
-/box3script create mygame
+/box3script create mygame # 创建 TypeScript 项目
```
-这会创建 TypeScript 项目:
-
-````
-config/box3/script/mygame/
-├── package.json ← npm 依赖(esbuild、Babel、TypeScript)
-├── tsconfig.base.json ← 公共 TS 编译选项
-├── tsconfig.server.json ← 服务端 TS 配置
-├── tsconfig.client.json ← 客户端 TS 配置
-├── build.mjs ← 构建脚本(esbuild → Babel → Rhino)
-├── eslint.config.mjs
-├── types/
-│ ├── shared.d.ts ← 服务端&客户端共享类型
-│ ├── server/ ← 服务端专属类型(server/entity/player/world/voxels)
-│ └── client/ ← 客户端专属类型(client/audio/input/ui/chat)
-└── src/
- ├── server/
- │ └── app.ts ← 服务端入口(游戏逻辑)
- └── client/
- └── app.ts ← 客户端入口(UI/按键/网络)
-
-构建并启动:
+然后构建并启动:
```bash
cd config/box3/script/mygame
-npm install && npm run build
-````
+npm install && npm run build # 安装依赖 + 构建
+```
```
-/box3script sandbox mygame # (推荐) 开启沙盒,放心测试
-/box3script start mygame # 启动脚本
+/box3script sandbox mygame # 开启沙盒(推荐,测试无忧)
+/box3script start mygame # 启动脚本
```
-打开 `src/app.ts` 写入代码,保存后 `npm run build`,再 `/box3script reload mygame` 即时生效——**不需要重启服务器**。
+打开 `src/server/app.ts` 写游戏逻辑,保存后 `npm run build`,再 `/box3script reload mygame` — **不需要重启服务器**。开启 `/box3script watch` 后,保存即自动重载。
-## 为什么选择 Box3JS?
+> [完整入门指南 →](docs/guide/getting-started.md)
+
+## 命令速查
+
+| 命令 | 说明 |
+|------|------|
+| `/box3script` | 查看所有项目运行状态 |
+| `/box3script create ` | 创建新 TypeScript 项目 |
+| `/box3script start [project\|all]` | 启用并加载项目 |
+| `/box3script stop [project\|all]` | 禁用并卸载项目 |
+| `/box3script reload [project]` | 重载脚本(开发用) |
+| `/box3script watch` | 切换文件监控(自动热重载) |
+| `/box3script sandbox ` | 切换沙盒(开=追踪修改 / 关=完整回滚) |
+| `/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 模组,便于分发部署 |
-
-## 命令
-
-| 命令 | 说明 |
-| ---------------------------------- | --------------------------------- |
-| `/box3script` | 查看项目状态总览 |
-| `/box3script create ` | 创建新 TypeScript 项目 |
-| `/box3script start [project\|all]` | 启用并加载项目 |
-| `/box3script stop [project\|all]` | 禁用并卸载项目 |
-| `/box3script reload [project]` | 重载脚本(开发用) |
-| `/box3script watch` | 切换文件监控(自动热重载) |
-| `/box3script sandbox ` | 切换沙盒模式(开=追踪 / 关=回滚) |
-| `/box3script compile ` | 预检 + 编译为独立 JAR 模组 |
-
-所有 `` 参数支持 **Tab 自动补全**。[完整命令文档 →](docs/api/commands.md)
-
-> 运行策略:若同名脚本 JAR 已被 NeoForge 加载,则 `/box3script start/reload/watch` 会跳过文件模式(jar 优先),避免重复执行。
+所有 `` 参数支持 **Tab 自动补全**。[完整命令参考 →](docs/api/commands.md)
## 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` | 四元数(旋转运算) |
-
-[文档首页 →](docs/README.md) · [API 总览 →](docs/api/README.md) · [按任务速查 →](docs/api/README.md#功能速查---我想)
+| 全局对象 | 用途 |
+|----------|------|
+| `world` | 世界事件、实体查询、粒子、烟花、闪电、音效、计分板、BossBar、队伍、边界 |
+| `entity` | 实体属性、AI 寻路/攻击、装备、药水效果、属性修改 |
+| `player` | 背包、飞行、游戏模式、传送、消息、经验 |
+| `voxels` | 方块读写、区域填充、替换、刷怪笼 |
+| `remoteChannel` | 服务端 ↔ 客户端 双向事件通道 |
+| `client` · `input` · `ui` · `chat` · `audio` · `gui` | 客户端 API:生命周期、键盘、屏幕文字、聊天、音频、自定义容器 |
+| `http` | HTTP 请求(同步 + 异步) |
+| `storage` · `db` | JSON 持久化 / SQLite 数据库(双端) |
+| `registries` | 自定义方块/物品/音效(编译 JAR 模式) |
+| `GameVector3` · `GameBounds3` · `GameRGBColor` · `GameQuaternion` | 数学类型:向量、包围盒、颜色、四元数 |
+
+[API 总览 →](docs/api/README.md) · [按功能速查 →](docs/api/README.md#功能速查---我想) · [完整文档 →](docs/README.md)
## 教程
-从零到完整小游戏,每个示例均经过 TypeScript 编译 + ESLint 验证:
+从零到完整小游戏,每个示例均经 TypeScript 编译 + ESLint 验证:
-| # | 教程 | 时长 | 学什么 |
-| --- | ----------------------------------------------------- | ------ | ---------------------------------------- |
-| 1 | [从零开始](docs/tutorial/01-basics.md) | 10 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 竞技场、粒子烟花、波次刷怪、特效大全 |
+| # | 教程 | 时长 | 学什么 |
+|---|------|------|--------|
+| 1 | [从零开始](docs/tutorial/01-basics.md) | 10 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 竞技场、粒子与烟花、波次刷怪 |
+| 6 | [客户端脚本](docs/tutorial/06-client-scripting.md) | 15 min | 键盘输入、屏幕 UI、音效音乐、本地存储、remoteChannel |
[教程总览 →](docs/tutorial/README.md)
-## 文档结构
-
-```
-docs/
-├── guide/ ← 入门指南
-│ ├── README.md 指南总览
-│ ├── about-box3js.md Box3JS 与神奇代码岛(起源、关系、优势)
-│ ├── getting-started.md 从零开始(环境、第一个脚本、调试、发布)
-│ ├── architecture.md 运行原理(Rhino 引擎、作用域、构建管线)
-│ └── js-vs-java.md JS vs Java 模组开发对比
-├── api/ ← API 参考
-│ ├── README.md 总览 + 功能速查
-│ ├── world.md 世界 API(事件、粒子、烟花、计分板...)
-│ ├── entity.md 实体 API(属性、AI、装备、效果...)
-│ ├── player.md 玩家 API(背包、消息、飞行、传送...)
-│ ├── voxels.md 方块 API(读写、填充、刷怪笼)
-│ ├── storage.md 存储 API(JSON 持久化)
-│ ├── database.md 数据库 API(SQLite)
-│ ├── http.md HTTP 请求 API
-│ ├── client.md 客户端 API(UI、输入、聊天、通讯)
-│ ├── registries.md 自定义方块/物品/音效
-│ ├── math.md 数学 API(Vector3、Color、Quaternion)
-│ └── commands.md /box3script 命令参考
-├── tutorial/ ← 入门教程
-│ ├── README.md 教程总览
-│ ├── 01-basics.md 从零开始
-│ ├── 02-player-items.md 玩家与物品
-│ ├── 03-events-entities.md 事件与实体
-│ ├── 04-advanced-systems.md 高级系统
-│ └── 05-examples.md 实战小游戏
-└── BOX3_API_COMPARISON.md ← Box3 平台 vs Box3JS API 对照表
-```
-
## 示例项目
-`run/config/box3/script/colorzone/` 包含完整的双向通讯游戏和 7 个功能示例,涵盖服务端逻辑、客户端 UI、键盘输入、HTTP 请求、数据库等全部教学场景。
+`run/config/box3/script/` 下包含多个完整游戏项目,可直接运行或作为参考:
+
+| 项目 | 类型 | 亮点 |
+|------|------|------|
+| `patdef` | 塔防 | 四类塔(箭/冰/火/雷)、波次刷怪、GUI 商店、攻击光束特效 |
+| `bedwar` | 团队竞技 | 双队对抗、资源生成、装备升级、陷阱、床破坏机制 |
+| `coredf` | 核心防守 | 中文消息、多阶段波次、经济系统 |
+| `az` | 多阶段游戏 | 复杂状态机、阶段切换、多玩法融合 |
+| `colorzone` | 跑酷 + 教程 | 双向客户端通信、UI 示例、7 个功能演示 |
+| `mygame` | API 测试 | 覆盖所有 Box3JS API 的功能测试用例 |
-## 依赖说明
+每个项目都有独立的客户端 `dist/client.js` 和服务端 `dist/server.js`,开箱即用。
-| 功能 | 依赖 |
-| ------------------ | ------------------------------------------------------------------------------------- |
-| 脚本引擎核心 | 内嵌 Rhino 1.9.1,无需额外安装 |
-| `db` API(SQLite) | 需安装 [`minecraft-sqlite-jdbc`](https://modrinth.com/mod/minecraft-sqlite-jdbc) 模组 |
-| 其他 API | 无额外依赖 |
+## 依赖
-> 未安装 `minecraft-sqlite-jdbc` 时,`db` 以外的所有 API 正常工作。只有调用 `db.sql()` 才会提示需要安装。
+| 功能 | 要求 |
+|------|------|
+| 脚本引擎核心 | 内嵌 Rhino 1.9.1,无需额外安装 |
+| `db` API(SQLite) | 需 [`minecraft-sqlite-jdbc`](https://modrinth.com/mod/minecraft-sqlite-jdbc) |
+| 其他所有 API | 无额外依赖 |
-## 技术栈
+> 即使不装 SQLite 模组,除 `db` 外所有功能正常工作。
-- **运行时:** Mozilla Rhino 1.9.1(Java 嵌入式 JS 引擎)
-- **编译工具:** esbuild 打包 → Babel 转译(Rhino 目标) → 正则清理
-- **语言:** TypeScript 编写,编译为 ES5 兼容 JS
-- **平台:** NeoForge 1.21.1,Java 21
## 许可证
diff --git a/Box3JS-NeoForge-1.21.1/README_en.md b/Box3JS-NeoForge-1.21.1/README_en.md
index 67a8664..63d17b2 100644
--- a/Box3JS-NeoForge-1.21.1/README_en.md
+++ b/Box3JS-NeoForge-1.21.1/README_en.md
@@ -1,14 +1,116 @@
# Box3JS — Minecraft Scripting Engine
-> **Beta** — This project is in early beta. APIs may change. Feedback is welcome.
+> **v0.1.0** — First public beta release. APIs may change. Feedback is welcome.
[简体中文](Readme.md) | [English](README_en.md)
**No Java knowledge required. Build unlimited Minecraft gameplay with TypeScript.**
-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.
+Box3JS embeds the Mozilla Rhino JavaScript engine in a Minecraft mod. Its API design is inspired by [Box3](https://dao3.fun) — bringing Box3's clean, efficient developer experience into Minecraft. No Gradle, no server restarts, no complex toolchains. PvP arenas, tower defense, RPG dungeons, party games — write TypeScript, load instantly, see changes live.
+
+> Box3JS & the Box3 platform? → [About Box3JS](docs/en/guide/about-box3js.md)
+
+```ts
+// On player join: welcome dialog + particles + sound
+world.onPlayerJoin((player) => {
+ player.player.dialog({ content: "Welcome to the Box3JS server!" });
+ const pos = new GameVector3(player.position.x, player.position.y + 1.8, player.position.z);
+ world.spawnParticle("minecraft:happy_villager", pos, 10, 0.5, 0.5, 0.5, 0.1);
+ world.playSound("minecraft:entity.player.levelup", pos, 1.0, 1.0);
+});
+
+// Chat commands: !heal to restore HP, !firework to launch a firework
+world.onChat((player, message) => {
+ if (message === "!heal") {
+ player.hp = player.maxHp;
+ player.player.actionBar("Healed!");
+ return false; // cancel broadcast
+ }
+ if (message === "!firework") {
+ const pos = new GameVector3(player.position.x, player.position.y + 1, player.position.z);
+ world.launchFirework(pos, "green", "large_ball");
+ return false;
+ }
+});
+```
+
+
+Versus: the same feature in a traditional Java mod
+
+```java
+// Java — ~60 lines, 3 classes for the same result
+@Mod.EventBusSubscriber(modid = "mymod")
+public class PlayerJoinListener {
+ @SubscribeEvent
+ public static void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) {
+ ServerPlayer player = (ServerPlayer) event.getEntity();
+ // Dialogs require client-side handling; server-side can only send chat
+ player.sendSystemMessage(
+ Component.literal("Welcome to the server!").withStyle(ChatFormatting.GREEN)
+ );
+ // Particles need ServerLevel and ParticleOptions
+ ServerLevel level = player.serverLevel();
+ Vec3 pos = player.position().add(0, 1.8, 0);
+ level.sendParticles(
+ ParticleTypes.HAPPY_VILLAGER,
+ pos.x, pos.y, pos.z,
+ 10, 0.5, 0.5, 0.5, 0.1
+ );
+ // Same for sound
+ level.playSound(
+ null, player.blockPosition(),
+ SoundEvents.PLAYER_LEVELUP, SoundSource.PLAYERS,
+ 1.0f, 1.0f
+ );
+ }
+}
+
+@Mod.EventBusSubscriber(modid = "mymod")
+public class ChatListener {
+ @SubscribeEvent
+ public static void onChat(ServerChatEvent event) {
+ ServerPlayer player = event.getPlayer();
+ String message = event.getMessage().getString();
+ if ("!heal".equals(message)) {
+ player.setHealth(player.getMaxHealth());
+ player.displayClientMessage(
+ Component.literal("Healed!"), true
+ );
+ event.setCanceled(true);
+ }
+ if ("!firework".equals(message)) {
+ // Fireworks need manual entity construction
+ FireworkRocketEntity firework = new FireworkRocketEntity(
+ player.level(),
+ player.getX(), player.getY() + 1, player.getZ(),
+ createFireworkItem("green", FireworkRocketEntity.Shape.LARGE_BALL)
+ );
+ player.level().addFreshEntity(firework);
+ event.setCanceled(true);
+ }
+ }
+ // + ~20 lines for the firework item builder...
+}
+```
+
+
+## Why Box3JS?
+
+**Zero barrier to entry** — If you know JS/TS, you can build mods. No Gradle, no IDE, no server restarts. `/box3script create` scaffolds a complete TypeScript project in one command.
+
+**Instant hot reload** — Edit → build → reload takes seconds. Enable `watch` mode and skip the build step too — save and it reloads automatically.
+
+**Box3-like, Minecraft-native** — The API design mirrors Box3's developer experience. Creators familiar with Box3 can transition seamlessly. Plus full access to Minecraft's native capabilities: mob AI, particles, structures, advancements, recipes.
+
+**Sandbox protection** — Enable sandbox to automatically track all script changes. Disable it to fully roll back. Test fearlessly — your map is always safe.
+
+**First-class TypeScript** — Complete `.d.ts` type declarations with bilingual JSDoc. Build pipeline: esbuild bundle → Babel transpile to ES5 → regex sanitize. Full IDE IntelliSense.
-> Curious about Box3JS's relationship with the Box3 platform? → [Box3JS & Box3](docs/guide/about-box3js_en.md)
+**Dual-side scripting** — Server handles game logic, entity AI, block manipulation. Client handles keyboard input, screen UI, chat, audio. Bidirectional real-time communication via `remoteChannel`.
+
+**Built-in persistence** — JSON file storage and SQLite database for leaderboards, economies, player saves — all accessible from both server and client scripts.
+
+**Standalone distribution** — `/box3script compile` packages your scripts into a standalone JAR mod. Drop it into `mods/` like any other mod — no Box3JS required.
## Installation
@@ -16,170 +118,101 @@ Box3JS is a community-driven Minecraft mod (NeoForge 1.21.1) that embeds the Moz
2. For SQLite database support (`db` API), also install [`minecraft-sqlite-jdbc`](https://modrinth.com/mod/minecraft-sqlite-jdbc)
3. Start the server
-## 5-Minute Quick Start
+## Quick Start
-In-game (requires OP level ≥ 2):
+In chat (requires OP level ≥ 2):
```
-/box3script create mygame
+/box3script create mygame # scaffold a TypeScript project
```
-This creates a TypeScript project:
-
-````
-config/box3/script/mygame/
-├── package.json ← npm dependencies (esbuild, Babel, TypeScript)
-├── tsconfig.base.json ← Shared TS compiler options
-├── tsconfig.server.json ← Server-side TS config
-├── tsconfig.client.json ← Client-side TS config
-├── build.mjs ← build script (esbuild → Babel → Rhino)
-├── eslint.config.mjs
-├── types/
-│ ├── shared.d.ts ← types shared by server & client
-│ ├── server/ ← server-only types (server/entity/player/world/voxels)
-│ └── client/ ← client-only types (client/audio/input/ui/chat)
-└── src/
- ├── server/
- │ └── app.ts ← server entry (game logic)
- └── client/
- └── app.ts ← client entry (UI/input/network)
-
-Build and start:
+Then build and start:
```bash
cd config/box3/script/mygame
-npm install && npm run build
-````
+npm install && npm run build # install deps + build
+```
```
-/box3script sandbox mygame # (recommended) enable sandbox for safe testing
-/box3script start mygame # start the script
+/box3script sandbox mygame # enable sandbox (recommended)
+/box3script start mygame # start the script
```
-Edit `src/app.ts`, re-run `npm run build`, then `/box3script reload mygame` — changes take effect **without restarting the server**.
-
-## Why Box3JS?
+Open `src/server/app.ts`, write game logic, `npm run build`, then `/box3script reload mygame` — **no server restart needed**. Enable `/box3script watch` and it auto-reloads on every save.
-| Feature | Description |
-| --------------------- | --------------------------------------------------------------------------------------------------------------------- |
-| **Zero barrier** | Know JS/TS? You can build. No Gradle, no IDE, no restarts |
-| **Hot reload** | Edit → build → reload in seconds. Enable `watch` for auto-reload |
-| **Sandbox** | Toggle sandbox to track all script changes; disable to fully roll back |
-| **TypeScript** | Full `.d.ts` type declarations, esbuild + Babel pipeline, IDE IntelliSense |
-| **20+ events** | onTick, onPlayerJoin, onChat, onEntityDeath, onBlockActivate, onButtonPressed... |
-| **Visual effects** | 13+ particles, fireworks, lightning, explosions, sounds |
-| **Client API** | Keyboard input, screen UI, chat interception, sound/music control, client storage, SQLite, HTTP, bidirectional events |
-| **Game systems** | Scoreboards, BossBar, teams, world border, cross-script messaging |
-| **Custom registries** | JSON-configured blocks, items (food/tools/armor), sounds & creative tabs, compiled to standalone JAR |
-| **Data persistence** | JSON storage + SQLite database (leaderboards, economy, player data) |
-| **Standalone JAR** | `/box3script compile` packages scripts into a standalone JAR mod for distribution |
+> [Full getting-started guide →](docs/en/guide/getting-started.md)
## Commands
-| Command | Description |
-| ---------------------------------- | ---------------------------------------- |
-| `/box3script` | Show project status overview |
-| `/box3script create ` | Create a new TypeScript project |
-| `/box3script start [project\|all]` | Enable and load projects |
-| `/box3script stop [project\|all]` | Disable and unload projects |
-| `/box3script reload [project]` | Reload scripts (for development) |
-| `/box3script watch` | Toggle file watching (auto hot-reload) |
-| `/box3script sandbox ` | Toggle sandbox (on=track / off=rollback) |
-| `/box3script compile ` | Preflight + compile to standalone JAR |
+| Command | Description |
+|---------|-------------|
+| `/box3script` | Show all project statuses |
+| `/box3script create ` | Scaffold a new TypeScript project |
+| `/box3script start [project\|all]` | Enable and load projects |
+| `/box3script stop [project\|all]` | Disable and unload projects |
+| `/box3script reload [project]` | Reload scripts (development) |
+| `/box3script watch` | Toggle file watching (auto hot-reload) |
+| `/box3script sandbox ` | Toggle sandbox (on=track / off=rollback) |
+| `/box3script compile ` | Compile to standalone JAR mod |
-All `` arguments support **Tab completion**. [Full command reference →](docs/api/commands_en.md)
-
-> Runtime policy: if a matching script JAR is already loaded by NeoForge, `/box3script start/reload/watch` skips filesystem mode (jar-first) to avoid duplicate execution.
+All `` arguments support **Tab completion**. [Full command reference →](docs/en/api/commands.md)
## API Overview
-| Global | Purpose |
-| -------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
-| `world` | World state, events, particles, fireworks, lightning, sounds, scoreboards, BossBar, teams, border |
-| `entity` | Entity properties, AI pathfinding, equipment, potion effects, tags, navigation |
-| `player` | Inventory, flight, game mode, teleport, messaging, XP, sounds |
-| `voxels` | Block read/write, region fill, spawner control |
-| `http` | HTTP requests (sync + async, GET/POST/JSON) |
-| `remoteChannel` | Server ↔ client bidirectional event channel |
-| `registries` | Custom blocks, items & sounds (compiled JAR mode), see [registries_en.md](docs/api/registries_en.md) |
-| `client` · `input` · `ui` · `chat` · `audio` | Client scripts: lifecycle, keyboard, screen text, chat, audio control |
-| `storage` | JSON data persistence (server & client) |
-| `db` | SQLite database (server & client) |
-| `console` | Console logging (`log`/`warn`/`error`/`debug`/`assert`/`clear`) |
-| `GameVector3` | 3D vector (coordinate math) |
-| `GameBounds3` | Bounding box |
-| `GameRGBColor` / `GameRGBAColor` | RGB / RGBA color |
-| `GameQuaternion` | Quaternion (rotation math) |
-
-[Docs Home →](docs/README_en.md) · [API Overview →](docs/api/README_en.md) · [Find by Task →](docs/api/README_en.md#find-by-task--i-want-to)
+| Global | Purpose |
+|--------|---------|
+| `world` | World events, entity queries, particles, fireworks, lightning, sounds, scoreboards, BossBar, teams, border |
+| `entity` | Entity properties, AI pathfinding/attack, equipment, potion effects, attribute modification |
+| `player` | Inventory, flight, game mode, teleport, messaging, XP |
+| `voxels` | Block read/write, region fill, replace, spawner control |
+| `remoteChannel` | Server ↔ client bidirectional event channel |
+| `client` · `input` · `ui` · `chat` · `audio` · `gui` | Client APIs: lifecycle, keyboard, screen text, chat, audio, custom containers |
+| `http` | HTTP requests (sync + async) |
+| `storage` · `db` | JSON persistence / SQLite database (server & client) |
+| `registries` | Custom blocks, items & sounds (compiled JAR mode) |
+| `GameVector3` · `GameBounds3` · `GameRGBColor` · `GameQuaternion` | Math types: vectors, bounding boxes, colors, quaternions |
+
+[API Overview →](docs/en/api/README.md) · [Find by Task →](docs/en/api/README.md#find-by-task--i-want-to) · [Full Docs →](docs/en/README.md)
## Tutorials
From zero to full mini-games. Every example is TypeScript-compiled and ESLint-verified:
-| # | Tutorial | Time | What you'll learn |
-| --- | -------------------------------------------------------- | ------ | ----------------------------------------------------------------- |
-| 1 | [Getting Started](docs/tutorial/01-basics.md) | 10 min | Project setup, first script, chat commands, timers |
-| 2 | [Players & Items](docs/tutorial/02-player-items.md) | 15 min | Teleport, flight, items, enchantments, potions |
-| 3 | [Events & Entities](docs/tutorial/03-events-entities.md) | 15 min | Event callbacks, entity spawning, AI, combat, patrols |
-| 4 | [Advanced Systems](docs/tutorial/04-advanced-systems.md) | 15 min | Scoreboards, BossBar, teams, world border, cross-script messaging |
-| 5 | [Mini-Games](docs/tutorial/05-examples.md) | 20 min | PvP arena, particles & fireworks, wave mobs, visual effects |
+| # | Tutorial | Time | What you'll learn |
+|---|----------|------|-------------------|
+| 1 | [Getting Started](docs/en/tutorial/01-basics.md) | 10 min | Project setup, first script, chat commands, timers |
+| 2 | [Players & Items](docs/en/tutorial/02-player-items.md) | 15 min | Teleport, flight, items, enchantments, potion effects |
+| 3 | [Events & Entities](docs/en/tutorial/03-events-entities.md) | 15 min | All event callbacks, entity spawning, AI control, patrol guards |
+| 4 | [Advanced Systems](docs/en/tutorial/04-advanced-systems.md) | 15 min | Scoreboards, BossBar, teams, world border, cross-script messaging |
+| 5 | [Mini-Games](docs/en/tutorial/05-examples.md) | 20 min | PvP arena, particles & fireworks, wave mob spawning |
+| 6 | [Client Scripting](docs/en/tutorial/06-client-scripting.md) | 15 min | Keyboard input, screen UI, audio/music, local storage, remoteChannel |
-[Tutorial overview →](docs/tutorial/README.md)
+[Tutorial Overview →](docs/en/tutorial/README.md)
-## Documentation Structure
+## Example Projects
-```
-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
-├── api/ ← API Reference
-│ ├── README.md Overview + find by task
-│ ├── world.md World API (events, particles, fireworks, scoreboards...)
-│ ├── entity.md Entity API (properties, AI, equipment, effects...)
-│ ├── player.md Player API (inventory, messaging, flight, teleport...)
-│ ├── voxels.md Voxels API (read/write, fill, spawner)
-│ ├── storage.md Storage API (JSON persistence)
-│ ├── database.md Database API (SQLite)
-│ ├── http.md HTTP request API
-│ ├── client.md Client API (UI, input, chat, events)
-│ ├── registries.md Custom blocks, items & sounds
-│ ├── math.md Math API (Vector3, Color, Quaternion)
-│ └── commands.md /box3script command reference
-├── tutorial/ ← Tutorials
-│ ├── README.md Learning path overview
-│ ├── 01-basics.md Getting started
-│ ├── 02-player-items.md Players & items
-│ ├── 03-events-entities.md Events & entities
-│ ├── 04-advanced-systems.md Advanced systems
-│ └── 05-examples.md Mini-games
-└── BOX3_API_COMPARISON.md ← Box3 platform vs Box3JS API comparison
-```
+`run/config/box3/script/` contains multiple full game projects ready to run or study:
-## Example Project
+| Project | Genre | Highlights |
+|---------|-------|------------|
+| `patdef` | Tower Defense | 4 tower types (arrow/ice/fire/lightning), wave spawning, GUI shop, attack beam VFX |
+| `bedwar` | Team PvP | Two-team combat, resource generators, gear upgrades, traps, bed destruction |
+| `coredf` | Core Defense | Chinese-localized, multi-phase waves, economy system |
+| `az` | Multi-Phase | Complex state machine, phase transitions, mixed gameplay |
+| `colorzone` | Parkour + Demo | Bidirectional client-server communication, UI examples, 7 feature demos |
+| `mygame` | API Tests | Functional test cases covering every Box3JS API |
-`run/config/box3/script/colorzone/` contains a complete bidirectional communication game and 7 verified feature examples covering every tutorial scenario — from server logic to client UI.
+Each project has its own client `dist/client.js` and server `dist/server.js` — drop in and play.
## Dependencies
-| Feature | Requirement |
-| ------------------ | -------------------------------------------------------------------------------------- |
-| Script engine core | Rhino 1.9.1 bundled — no extra install needed |
-| `db` API (SQLite) | Requires [`minecraft-sqlite-jdbc`](https://modrinth.com/mod/minecraft-sqlite-jdbc) mod |
-| All other APIs | No additional dependencies |
-
-> Without `minecraft-sqlite-jdbc`, all APIs except `db` work normally. Only calling `db.sql()` triggers an error asking you to install it.
-
-## Tech Stack
+| Feature | Requirement |
+|---------|-------------|
+| Script engine core | Rhino 1.9.1 bundled — no extra install needed |
+| `db` API (SQLite) | Requires [`minecraft-sqlite-jdbc`](https://modrinth.com/mod/minecraft-sqlite-jdbc) |
+| All other APIs | No additional dependencies |
-- **Runtime:** Mozilla Rhino 1.9.1 (embedded JS engine for JVM)
-- **Build tools:** esbuild bundle → Babel transpile (Rhino target) → regex sanitize
-- **Language:** TypeScript, compiled to ES5-compatible JS
-- **Platform:** NeoForge 1.21.1, Java 21
+> Without the SQLite mod, everything except `db` works normally. Only calling `db.sql()` triggers a reminder.
## License
diff --git a/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs b/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs
index 36f3c78..c10f27e 100644
--- a/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs
+++ b/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs
@@ -2,6 +2,7 @@ import { defineConfig } from "vitepress";
const cnNav = [
{ text: "首页", link: "/" },
+ { text: "文档", link: "/overview" },
{ text: "指南", link: "/guide/README" },
{ text: "教程", link: "/tutorial/README" },
{ text: "API", link: "/api/README" },
@@ -9,6 +10,7 @@ const cnNav = [
const enNav = [
{ text: "Home", link: "/en/" },
+ { text: "Docs", link: "/en/overview" },
{ text: "Guide", link: "/en/guide/README" },
{ text: "Tutorials", link: "/en/tutorial/README" },
{ text: "API", link: "/en/api/README" },
@@ -96,6 +98,7 @@ const enSidebar = [
text: "Get Started",
collapsed: false,
items: [
+ { text: "Documentation Index", link: "/en/overview" },
{ text: "Overview", link: "/en/guide/README" },
{ text: "Getting Started", link: "/en/guide/getting-started" },
{ text: "Recipes", link: "/en/guide/recipes" },
@@ -181,9 +184,8 @@ export default defineConfig({
description: "Minecraft 的 JavaScript/TypeScript 脚本引擎",
lastUpdated: true,
cleanUrls: true,
- ignoreDeadLinks: true,
+ ignoreDeadLinks: false,
base: "/box3js-mc/",
- head: [["link", { rel: "icon", href: "/favicon.ico" }]],
locales: {
root: {
label: "简体中文",
@@ -206,7 +208,7 @@ export default defineConfig({
next: "下一页",
},
footer: {
- message: "基于 MIT 许可证发布",
+ message: "基于 Apache License 2.0 发布",
},
},
},
@@ -231,7 +233,7 @@ export default defineConfig({
next: "Next page",
},
footer: {
- message: "Released under the MIT License.",
+ message: "Released under the Apache License 2.0.",
},
},
},
diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client.md b/Box3JS-NeoForge-1.21.1/docs/api/client.md
index 7287168..30e2e38 100644
--- a/Box3JS-NeoForge-1.21.1/docs/api/client.md
+++ b/Box3JS-NeoForge-1.21.1/docs/api/client.md
@@ -157,6 +157,52 @@ client.setFogEndDistance(50);
client.resetFog();
```
+## 相机控制 (Camera Control)
+
+### client.setCameraTarget(entityUuid)
+
+将相机切换到目标实体的视角(旁观模式效果)。
+
+```js
+// 切换到指定实体的视角
+client.setCameraTarget("550e8400-e29b-41d4-a716-446655440000");
+```
+
+### client.resetCamera()
+
+重置相机到本地玩家视角。
+
+```js
+client.resetCamera();
+```
+
+## 方块查询 (Block Query)
+
+### client.getBlockState(x, y, z)
+
+在客户端查询指定坐标的方块状态(从渲染区块缓存读取)。空气或未加载区块返回 `null`。
+
+```js
+var block = client.getBlockState(0, 100, 0);
+if (block) {
+ console.log("方块 ID: " + block.id);
+}
+```
+
+## 渲染回调 (Render Callback)
+
+### client.onRender(callback)
+
+注册每 Render 帧回调(仅在 HUD 渲染阶段触发)。回调接收 `partialTick`(0–1,帧间插值因子)。
+
+返回 `GameEventHandlerToken`,调用 `.cancel()` 取消。
+
+```js
+var token = client.onRender(function (partialTick) {
+ // 使用 partialTick 进行平滑自定义渲染
+});
+```
+
## 客户端完整示例
```js
diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity.md b/Box3JS-NeoForge-1.21.1/docs/api/entity.md
index 9e43314..2ef0876 100644
--- a/Box3JS-NeoForge-1.21.1/docs/api/entity.md
+++ b/Box3JS-NeoForge-1.21.1/docs/api/entity.md
@@ -95,6 +95,10 @@ zombie.hp = 100;
对实体造成 `amount` 点伤害(通用伤害类型,触发伤害事件)。
+### entity.hurt(amount, source)
+
+对实体造成伤害并指定伤害来源实体(用于击杀追踪)。`source` 为另一个 `GameEntity`。
+
### entity.heal(amount)
治疗实体 `amount` 点生命值(不超过 maxHp)。
@@ -325,6 +329,19 @@ entity.navigateTo(10, 100, 10, 1.0);
entity.navigateTo(target.position, 1.0);
```
+### entity.teleportTo(x, y, z)
+
+将实体传送到指定坐标。
+
+### entity.teleportTo(pos)
+
+⬆ GameVector3 重载。
+
+```js
+entity.teleportTo(0, 100, 0);
+entity.teleportTo(new GameVector3(10, 100, 20));
+```
+
### entity.lookAt(x, y, z)
实体面朝目标坐标。
@@ -338,6 +355,19 @@ entity.lookAt(0, 100, -10);
entity.lookAt(target.position);
```
+## 粒子
+
+⬆ MC 扩展。
+
+### entity.spawnParticle(type, count, dx, dy, dz, speed)
+
+在实体中心位置生成粒子。
+
+```js
+entity.spawnParticle("minecraft:flame", 10, 0.5, 0.5, 0.5, 0.1);
+entity.spawnParticle("minecraft:cloud", 5, 0.2, 0, 0.2, 0);
+```
+
## 药水效果
全部 ⬆ MC 扩展。
@@ -386,6 +416,20 @@ entity.setEquipment("chest", "minecraft:iron_chestplate");
entity.setEquipment("feet", "minecraft:leather_boots");
```
+### entity.setEquipmentWithEnchants(slot, itemId, enchants)
+
+给生物穿戴带附魔的装备。`enchants` 是 `{附魔ID: 等级}` 对象。
+
+```js
+entity.setEquipmentWithEnchants("mainhand", "minecraft:diamond_sword", {
+ "minecraft:sharpness": 5,
+ "minecraft:fire_aspect": 2,
+});
+entity.setEquipmentWithEnchants("head", "minecraft:diamond_helmet", {
+ "minecraft:protection": 4,
+});
+```
+
### entity.setDropChance(slot, chance)
设置装备槽物品的掉落概率,范围 0.0–1.0。`slot` 设为 `"all"` 可一次性设置所有槽位(包括主副手和四个护甲槽)。
@@ -426,6 +470,14 @@ entity.setAttribute("minecraft:generic.armor", 10);
销毁实体。如果通过 `setOnDestroy()` 设置了回调,会触发它。
+### entity.remove()
+
+立即从世界中移除实体,不触发回调。与 `destroy()` 不同,`remove()` 不会调用 `setOnDestroy` 注册的回调。
+
+```js
+entity.remove(); // 简单移除,无回调
+```
+
### entity.setOnDestroy(handler)
设置销毁回调。`handler` 接收一个参数 `(entity)`。
diff --git a/Box3JS-NeoForge-1.21.1/docs/api/input.md b/Box3JS-NeoForge-1.21.1/docs/api/input.md
index fc31504..6fd0650 100644
--- a/Box3JS-NeoForge-1.21.1/docs/api/input.md
+++ b/Box3JS-NeoForge-1.21.1/docs/api/input.md
@@ -45,6 +45,22 @@ var mx = input.getMouseX();
var my = input.getMouseY();
```
+## input.getMouseDeltaX()
+
+获取当前帧鼠标 X 轴增量(自上一帧的移动量)。
+
+```js
+var dx = input.getMouseDeltaX();
+```
+
+## input.getMouseDeltaY()
+
+获取当前帧鼠标 Y 轴增量(自上一帧的移动量)。
+
+```js
+var dy = input.getMouseDeltaY();
+```
+
## input.onMouseClick(callback)
注册鼠标按键回调。返回 `GameEventHandlerToken`,调用 `.cancel()` 取消。
diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math.md b/Box3JS-NeoForge-1.21.1/docs/api/math.md
index 5344dce..41b29e6 100644
--- a/Box3JS-NeoForge-1.21.1/docs/api/math.md
+++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md
@@ -155,6 +155,12 @@ var bounds = new GameBounds3(
| `bounds.closestPoint(v)` | `GameVector3` | 包围盒上离点 `v` 最近的点 |
| `bounds.move(offset)` | `GameBounds3` | 平移 `offset`,返回新包围盒 |
| `bounds.moveEq(offset)` | `GameBounds3` | 原地平移 `offset`,返回自身 |
+| `bounds.volume()` | `number` | 包围盒体积 (宽×高×深) |
+| `bounds.isEmpty()` | `boolean` | 包围盒是否为空 (任一维度 ≤ 0) |
+| `bounds.equals(b)` | `boolean` | 判断两个包围盒是否完全相等 |
+| `bounds.union(b)` | `GameBounds3` | 返回同时包围两者自身和 b 的最小盒 |
+| `bounds.inflate(amount)` | `GameBounds3` | 每面向外扩展 amount(返回新对象) |
+| `bounds.deflate(amount)` | `GameBounds3` | 每面向内收缩 amount(最小为 0) |
### 静态方法
diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player.md b/Box3JS-NeoForge-1.21.1/docs/api/player.md
index fdf11f6..8195a47 100644
--- a/Box3JS-NeoForge-1.21.1/docs/api/player.md
+++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md
@@ -472,6 +472,64 @@ console.log(held.id, held.count); // "minecraft:diamond_sword" 1
player.clearInventory();
```
+### player.removeItem(itemId, count)
+
+从背包中移除指定数量的物品。返回实际移除的数量。
+
+```js
+// 移除 3 个铁锭
+const removed = player.removeItem("minecraft:iron_ingot", 3);
+console.log("实际移除: " + removed);
+```
+
+### player.setHeldSlot(slot)
+
+设置玩家手持快捷栏槽位 (0–8)。
+
+```js
+player.setHeldSlot(0); // 切换到第 1 格
+player.setHeldSlot(4); // 切换到第 5 格
+```
+
+### player.inventoryFreeSlots
+
+只读 `number`。返回背包空余槽位数(0–36)。
+
+```js
+if (player.inventoryFreeSlots < 2) {
+ player.directMessage("背包空间不足!");
+}
+```
+
+### player.hasItem(itemId)
+
+检查背包中是否拥有某物品。
+
+```js
+if (player.hasItem("minecraft:diamond")) {
+ player.directMessage("你有一颗钻石!");
+}
+```
+
+### player.getItemCount(itemId)
+
+统计背包中某物品的总数量。
+
+```js
+var count = player.getItemCount("minecraft:iron_ingot");
+console.log("背包里有 " + count + " 个铁锭");
+```
+
+### player.givePotion(itemId, potionType, count)
+
+给予玩家指定类型的药水(带有 `PotionContents` 组件,适用于 1.21.1+)。
+
+```js
+player.givePotion("minecraft:potion", "minecraft:healing", 3);
+player.givePotion("minecraft:splash_potion", "minecraft:poison", 1);
+player.givePotion("minecraft:lingering_potion", "minecraft:regeneration", 2);
+```
+
## 自定义容器 GUI
为玩家打开脚本控制的容器 GUI(类似箱子界面),可自定义格子内容、点击行为和关闭回调。
@@ -579,6 +637,31 @@ player.grantAdvancement("minecraft:adventure/kill_a_mob");
player.revokeAdvancement("minecraft:story/mine_stone");
```
+## Bossbar
+
+### player.showBossbar(name, text, progress, color)
+
+向该玩家显示一个独立的 Bossbar。
+
+| 参数 | 类型 | 说明 |
+|------|------|------|
+| `name` | string | Bossbar 唯一标识名,用于后续移除 |
+| `text` | string | 显示文字 |
+| `progress` | number | 进度条 (0–1) |
+| `color` | string | 颜色:`"pink"` / `"blue"` / `"red"` / `"green"` / `"yellow"` / `"purple"` / `"white"` |
+
+```js
+player.showBossbar("boss_health", "§c§lBoss HP", 0.75, "red");
+```
+
+### player.removeBossbar(name)
+
+移除指定名称的 Bossbar。
+
+```js
+player.removeBossbar("boss_health");
+```
+
## Tab 列表
### player.setPlayerListName(name)
diff --git a/Box3JS-NeoForge-1.21.1/docs/api/ui.md b/Box3JS-NeoForge-1.21.1/docs/api/ui.md
index b6b3ea1..6ff772b 100644
--- a/Box3JS-NeoForge-1.21.1/docs/api/ui.md
+++ b/Box3JS-NeoForge-1.21.1/docs/api/ui.md
@@ -80,3 +80,59 @@ ui.removeDrawText(1);
```js
ui.clearDrawTexts();
```
+
+## ui.drawItem(id, x, y, itemId, scale?)
+
+在屏幕上绘制物品图标(每帧持续绘制,直到调用 `removeDrawItem` 移除)。
+
+| 参数 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `id` | number | (必需) | 图标 ID,用于后续移除或更新 |
+| `x` | number | (必需) | X 坐标(GUI 缩放坐标系) |
+| `y` | number | (必需) | Y 坐标(GUI 缩放坐标系) |
+| `itemId` | string | (必需) | 物品 ID(如 `"minecraft:diamond"`) |
+| `scale` | number | `16` | 图标尺寸(像素) |
+
+返回图标 ID(与传入的 `id` 相同)。
+
+```js
+ui.drawItem(1, 10, 10, "minecraft:diamond");
+ui.drawItem(2, 30, 10, "minecraft:golden_apple", 24);
+```
+
+## ui.removeDrawItem(id)
+
+移除指定 ID 的绘制图标。
+
+```js
+ui.removeDrawItem(1);
+```
+
+## ui.drawRect(id, x, y, w, h, color, alpha?)
+
+在屏幕上绘制矩形(每帧持续绘制,直到调用 `removeDrawRect` 移除)。
+
+| 参数 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `id` | number | (必需) | 矩形 ID,用于后续移除或更新 |
+| `x` | number | (必需) | 左上角 X 坐标 |
+| `y` | number | (必需) | 左上角 Y 坐标 |
+| `w` | number | (必需) | 宽度(像素) |
+| `h` | number | (必需) | 高度(像素) |
+| `color` | GameRGBColor | (必需) | 填充颜色(RGB) |
+| `alpha` | number | `255` | 透明度 0–255 |
+
+返回矩形 ID(与传入的 `id` 相同)。
+
+```js
+var red = new GameRGBColor(1, 0, 0);
+ui.drawRect(1, 50, 50, 100, 60, red, 128); // 半透明红色矩形
+```
+
+## ui.removeDrawRect(id)
+
+移除指定 ID 的绘制矩形。
+
+```js
+ui.removeDrawRect(1);
+```
diff --git a/Box3JS-NeoForge-1.21.1/docs/api/voxels.md b/Box3JS-NeoForge-1.21.1/docs/api/voxels.md
index 0310e84..3c25077 100644
--- a/Box3JS-NeoForge-1.21.1/docs/api/voxels.md
+++ b/Box3JS-NeoForge-1.21.1/docs/api/voxels.md
@@ -76,6 +76,10 @@ voxels.setVoxel(new GameVector3(0, 100, 0), "minecraft:oak_stairs", 2);
⬆ GameVector3 重载。
+### voxels.fillVoxel(bounds, voxel)
+
+⬆ GameBounds3 重载。
+
```js
// 填充一个 5×1×5 的平台
voxels.fillVoxel(-2, 100, -2, 2, 100, 2, "minecraft:white_concrete");
@@ -93,6 +97,77 @@ voxels.fillVoxel(
);
```
+### voxels.replace(x1, y1, z1, x2, y2, z2, fromBlock, toBlock)
+
+在矩形区域内,将所有 `fromBlock` 替换为 `toBlock`。
+
+### voxels.replace(pos1, pos2, fromBlock, toBlock)
+
+⬆ GameVector3 重载。
+
+### voxels.replace(bounds, fromBlock, toBlock)
+
+⬆ GameBounds3 重载。
+
+```js
+// 清除所有羊毛方块
+voxels.replace(-10, 60, -10, 10, 80, 10, "minecraft:white_wool", "minecraft:air");
+voxels.replace(
+ new GameVector3(-10, 60, -10),
+ new GameVector3(10, 80, 10),
+ "minecraft:red_wool",
+ "minecraft:blue_wool",
+);
+```
+
+### voxels.clone(x1, y1, z1, x2, y2, z2, destX, destY, destZ)
+
+将源区域的所有方块复制到目标位置(含方块状态和旋转)。
+
+### voxels.clone(pos1, pos2, destPos)
+
+⬆ GameVector3 重载。
+
+### voxels.clone(bounds, destX, destY, destZ)
+
+⬆ GameBounds3 重载,目标坐标为原始坐标。
+
+### voxels.clone(bounds, destPos)
+
+⬆ GameBounds3 重载,目标坐标为 GameVector3。
+
+```js
+// 复制一个 5×5×5 的结构到新位置
+voxels.clone(0, 100, 0, 5, 105, 5, 10, 100, 10);
+voxels.clone(
+ new GameVector3(0, 100, 0),
+ new GameVector3(5, 105, 5),
+ new GameVector3(10, 100, 10),
+);
+```
+
+### voxels.setVoxelState(x, y, z, voxel, state)
+
+放置方块并指定 BlockState 属性。`state` 是一个键值对对象,key 为属性名,value 为属性值。
+
+```js
+// 放置朝北的橡木楼梯
+voxels.setVoxelState(0, 100, 0, "minecraft:oak_stairs", {
+ facing: "north",
+ half: "top",
+});
+
+// 放置点燃的蜡烛
+voxels.setVoxelState(0, 100, 0, "minecraft:candle", {
+ candles: "3",
+ lit: "true",
+});
+```
+
+### voxels.setVoxelState(pos, voxel, state)
+
+⬆ GameVector3 重载。
+
## 读取方块
### voxels.getVoxel(x, y, z)
@@ -146,6 +221,10 @@ var name = voxels.getVoxelName(new GameVector3(0, 100, 0));
⬆ GameVector3 重载。
+### voxels.countVoxel(bounds, voxel)
+
+⬆ GameBounds3 重载。
+
```js
// 统计区域内有多少个钻石块
var count = voxels.countVoxel(
diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md
index 935d548..776dd0d 100644
--- a/Box3JS-NeoForge-1.21.1/docs/api/world.md
+++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md
@@ -258,17 +258,17 @@ var token = world.onTick(function (info) {
| `world.onPlayerJoin(fn)` | | `(entity, tick)` | 玩家登录 |
| `world.onPlayerLeave(fn)` | | `(entity, tick)` | 玩家退出 |
| `world.onChat(fn)` | | `(entity, message, tick) => boolean \| void` | 玩家发送聊天消息;返回 `false` 可取消 |
-| `world.onVoxelDestroy(fn)` | | `(entity, x, y, z, voxel, tick)` | 玩家破坏方块 |
-| `world.onBlockPlace(fn)` | ⬆ MC | `(entity, x, y, z, voxel, voxelId, tick)` | 玩家放置方块 |
-| `world.onBlockActivate(fn)` | ⬆ MC | `(entity, x, y, z, voxel, tick)` | 玩家右键方块 |
-| `world.onInteract(fn)` | | `(entity, target, tick)` | 玩家右键实体 |
+| `world.onVoxelDestroy(fn)` | | `(entity, x, y, z, voxel, tick) => boolean \| void` | 玩家破坏方块;返回 `false` 可取消 |
+| `world.onBlockPlace(fn)` | ⬆ MC | `(entity, x, y, z, voxel, voxelId, tick) => boolean \| void` | 玩家放置方块;返回 `false` 可取消 |
+| `world.onBlockActivate(fn)` | ⬆ MC | `(entity, x, y, z, voxel, tick) => boolean \| void` | 玩家右键方块;返回 `false` 可取消交互 |
+| `world.onInteract(fn)` | | `(entity, target, tick) => boolean \| void` | 玩家右键实体;返回 `false` 可取消交互 |
| `world.onVoxelContact(fn)` | | `(entity, voxelId, x, y, z, contactType, force, tick)` | 实体接触方块 |
| `world.onEntityContact(fn)` | | `(entity, other, tick)` | 两个实体接触 |
| `world.onEntitySeparate(fn)` | | `(entity, other, tick)` | 两个实体分离 |
| `world.onFluidEnter(fn)` | | `(entity, fluid, x, y, z, tick)` | 实体进入液体 |
| `world.onFluidLeave(fn)` | | `(entity, fluid, x, y, z, tick)` | 实体离开液体 |
| `world.onEntityDeath(fn)` | ⬆ MC | `(entity, killer, tick)` | 实体死亡;`killer` 可能为 null |
-| `world.onEntityDamage(fn)` | ⬆ MC | `(entity, amount, source, attacker, tick)` | 实体受伤(Pre 阶段) |
+| `world.onEntityDamage(fn)` | ⬆ MC | `(entity, amount, source, attacker, tick)` => `boolean\|void` | 实体受伤(Pre 阶段);返回 `false` 可取消伤害 |
| `world.onPlayerRespawn(fn)` | ⬆ MC | `(entity, tick)` | 玩家重生 |
| `world.onButtonPressed(fn)` | ⬆ MC | `(entity, button, tick)` | 玩家按下按钮 |
| `world.onMessage(fn)` | ⬆ MC | `(from, data)` | 收到 `world.sendMessage()` 消息 |
@@ -660,6 +660,14 @@ world.spawnParticle(
⬆ GameVector3 重载。
+### world.spawnParticleLine(x1, y1, z1, x2, y2, z2, type, count)
+
+在两点之间均匀生成粒子线。⬆ MC 扩展。
+
+### world.spawnParticleLine(from, to, type, count)
+
+⬆ GameVector3 重载。
+
```js
// 单点粒子
world.spawnParticle("minecraft:flame", 0, 100, 0, 10, 0.5, 0.5, 0.5, 0.1);
@@ -674,6 +682,15 @@ world.spawnParticleCircle(
20,
);
+// 粒子线(两点之间)
+world.spawnParticleLine(0, 100, 0, 10, 100, 10, "minecraft:flame", 20);
+world.spawnParticleLine(
+ new GameVector3(0, 100, 0),
+ new GameVector3(10, 100, 10),
+ "minecraft:flame",
+ 20,
+);
+
// 常用粒子:
// minecraft:flame, minecraft:cloud, minecraft:happy_villager
// minecraft:witch, minecraft:portal, minecraft:end_rod
@@ -787,6 +804,10 @@ if (result.hit) {
返回 AABB 包围盒内所有实体。
+### world.entitiesInArea(bounds)
+
+⬆ GameBounds3 重载。
+
### world.entitiesInRadius(x, y, z, radius)
返回球体范围内所有实体。`entitiesInArea` 的便捷封装。
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/client.md b/Box3JS-NeoForge-1.21.1/docs/en/api/client.md
index 8cee98a..9e70b13 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/api/client.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/api/client.md
@@ -157,6 +157,52 @@ Resets fog to Minecraft's default behaviour.
client.resetFog();
```
+## Camera Control
+
+### client.setCameraTarget(entityUuid)
+
+Switches the camera to spectate the given entity (spectator-style view).
+
+```js
+// Switch to spectate a specific entity
+client.setCameraTarget("550e8400-e29b-41d4-a716-446655440000");
+```
+
+### client.resetCamera()
+
+Resets the camera back to the local player.
+
+```js
+client.resetCamera();
+```
+
+## Block Query
+
+### client.getBlockState(x, y, z)
+
+Queries the block state at a position from the client's render chunk cache. Returns `{ id: string }` or `null` for air/unloaded chunks.
+
+```js
+var block = client.getBlockState(0, 100, 0);
+if (block) {
+ console.log("Block ID: " + block.id);
+}
+```
+
+## Render Callback
+
+### client.onRender(callback)
+
+Registers a callback invoked every render frame (during HUD render phase). The callback receives `partialTick` (0–1), the frame interpolation factor.
+
+Returns a `GameEventHandlerToken`; call `.cancel()` to unsubscribe.
+
+```js
+var token = client.onRender(function (partialTick) {
+ // Use partialTick for smooth custom rendering
+});
+```
+
## Complete Client Example
```js
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/entity.md b/Box3JS-NeoForge-1.21.1/docs/en/api/entity.md
index 2cf07a6..12361ca 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/api/entity.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/api/entity.md
@@ -98,6 +98,10 @@ zombie.hp = 100;
Deals `amount` generic damage to the entity (triggers damage events).
+### entity.hurt(amount, source)
+
+Deals damage to the entity with a source entity for kill tracking. `source` is another `GameEntity`.
+
### entity.heal(amount)
Heals the entity by `amount` (capped at maxHp).
@@ -328,6 +332,19 @@ entity.navigateTo(10, 100, 10, 1.0);
entity.navigateTo(target.position, 1.0);
```
+### entity.teleportTo(x, y, z)
+
+Teleports the entity to the given coordinates.
+
+### entity.teleportTo(pos)
+
+⬆ GameVector3 overload.
+
+```js
+entity.teleportTo(0, 100, 0);
+entity.teleportTo(new GameVector3(10, 100, 20));
+```
+
### entity.lookAt(x, y, z)
Makes the entity look at the given coordinates.
@@ -341,6 +358,19 @@ entity.lookAt(0, 100, -10);
entity.lookAt(target.position);
```
+## Particles
+
+⬆ MC Extension.
+
+### entity.spawnParticle(type, count, dx, dy, dz, speed)
+
+Spawns particles at the entity's center position.
+
+```js
+entity.spawnParticle("minecraft:flame", 10, 0.5, 0.5, 0.5, 0.1);
+entity.spawnParticle("minecraft:cloud", 5, 0.2, 0, 0.2, 0);
+```
+
## Status Effects
All ⬆ MC Extension.
@@ -389,6 +419,20 @@ entity.setEquipment("chest", "minecraft:iron_chestplate");
entity.setEquipment("feet", "minecraft:leather_boots");
```
+### entity.setEquipmentWithEnchants(slot, itemId, enchants)
+
+Equips an enchanted item onto a mob's equipment slot. `enchants` is an `{enchantmentId: level}` object.
+
+```js
+entity.setEquipmentWithEnchants("mainhand", "minecraft:diamond_sword", {
+ "minecraft:sharpness": 5,
+ "minecraft:fire_aspect": 2,
+});
+entity.setEquipmentWithEnchants("head", "minecraft:diamond_helmet", {
+ "minecraft:protection": 4,
+});
+```
+
### entity.setDropChance(slot, chance)
Sets the drop chance for an equipment slot, range 0.0–1.0. Use `"all"` for `slot` to set all slots at once (both hands + four armor slots).
@@ -429,6 +473,14 @@ entity.setAttribute("minecraft:generic.armor", 10);
Destroys the entity. If a callback was registered via `setOnDestroy()`, it will be invoked.
+### entity.remove()
+
+Immediately removes the entity from the world without triggering the destroy callback. Unlike `destroy()`, `remove()` does not invoke the `setOnDestroy` handler.
+
+```js
+entity.remove(); // Simple removal, no callback
+```
+
### entity.setOnDestroy(handler)
Registers a callback called when the entity is destroyed. `handler` receives one argument `(entity)`.
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/input.md b/Box3JS-NeoForge-1.21.1/docs/en/api/input.md
index bd37a8f..781a40a 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/api/input.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/api/input.md
@@ -45,6 +45,22 @@ Gets the current mouse Y position in screen pixels.
var my = input.getMouseY();
```
+## input.getMouseDeltaX()
+
+Gets the mouse X delta since the last frame.
+
+```js
+var dx = input.getMouseDeltaX();
+```
+
+## input.getMouseDeltaY()
+
+Gets the mouse Y delta since the last frame.
+
+```js
+var dy = input.getMouseDeltaY();
+```
+
## input.onMouseClick(callback)
Registers a mouse button callback. Returns `GameEventHandlerToken`; call `.cancel()` to unregister.
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/math.md b/Box3JS-NeoForge-1.21.1/docs/en/api/math.md
index 1915335..5f7dd56 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/api/math.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/api/math.md
@@ -158,6 +158,12 @@ var bounds = new GameBounds3(
| `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 |
+| `bounds.volume()` | `number` | Volume of the bounds (width × height × depth) |
+| `bounds.isEmpty()` | `boolean` | Whether the bounds is empty (any dimension ≤ 0) |
+| `bounds.equals(b)` | `boolean` | Whether this bounds exactly equals another |
+| `bounds.union(b)` | `GameBounds3` | Returns the smallest bounds containing both this and b |
+| `bounds.inflate(amount)` | `GameBounds3` | Expand outward by amount on all six faces (returns new object) |
+| `bounds.deflate(amount)` | `GameBounds3` | Shrink inward by amount on all six faces (clamped to zero) |
### Static Methods
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/player.md b/Box3JS-NeoForge-1.21.1/docs/en/api/player.md
index 92ac144..ba11700 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/api/player.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/api/player.md
@@ -475,6 +475,63 @@ Clears the entire inventory (including armor slots and offhand).
player.clearInventory();
```
+### player.removeItem(itemId, count)
+
+Removes a specified count of the given item from the player's inventory. Returns the actual number of items removed.
+
+```js
+var removed = player.removeItem("minecraft:iron_ingot", 3);
+console.log("Removed: " + removed);
+```
+
+### player.setHeldSlot(slot)
+
+Sets the player's selected hotbar slot (0–8).
+
+```js
+player.setHeldSlot(0); // Switch to first slot
+player.setHeldSlot(4); // Switch to fifth slot
+```
+
+### player.inventoryFreeSlots
+
+Readonly `number`. Number of empty slots in the player's main inventory (0–36).
+
+```js
+if (player.inventoryFreeSlots < 2) {
+ player.directMessage("Not enough inventory space!");
+}
+```
+
+### player.hasItem(itemId)
+
+Checks whether the player has at least one of the given item in inventory.
+
+```js
+if (player.hasItem("minecraft:diamond")) {
+ player.directMessage("You have a diamond!");
+}
+```
+
+### player.getItemCount(itemId)
+
+Counts the total number of the given item in the player's inventory.
+
+```js
+var count = player.getItemCount("minecraft:iron_ingot");
+console.log("You have " + count + " iron ingots");
+```
+
+### player.givePotion(itemId, potionType, count)
+
+Gives the player a potion with a `PotionContents` component (1.21.1+).
+
+```js
+player.givePotion("minecraft:potion", "minecraft:healing", 3);
+player.givePotion("minecraft:splash_potion", "minecraft:poison", 1);
+player.givePotion("minecraft:lingering_potion", "minecraft:regeneration", 2);
+```
+
## Custom Container GUI
Opens a script-controlled container GUI (chest-like screen) for the player, with custom slot contents, click behavior, and close callbacks.
@@ -577,6 +634,31 @@ player.grantAdvancement("minecraft:adventure/kill_a_mob");
player.revokeAdvancement("minecraft:story/mine_stone");
```
+## Bossbar
+
+### player.showBossbar(name, text, progress, color)
+
+Shows a per-player bossbar (adds the player to a `ServerBossEvent`).
+
+| Parameter | Type | Description |
+|------------|--------|-------------|
+| `name` | string | Unique bossbar identifier for later removal |
+| `text` | string | Display text |
+| `progress` | number | Progress bar fill (0–1) |
+| `color` | string | Color: `"pink"` / `"blue"` / `"red"` / `"green"` / `"yellow"` / `"purple"` / `"white"` |
+
+```js
+player.showBossbar("boss_health", "§c§lBoss HP", 0.75, "red");
+```
+
+### player.removeBossbar(name)
+
+Removes a per-player bossbar by name.
+
+```js
+player.removeBossbar("boss_health");
+```
+
## Tab List
### player.setPlayerListName(name)
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/ui.md b/Box3JS-NeoForge-1.21.1/docs/en/api/ui.md
index fe20fac..3d2f350 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/api/ui.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/api/ui.md
@@ -80,3 +80,59 @@ Clears all texts drawn via `drawText()`.
```js
ui.clearDrawTexts();
```
+
+## ui.drawItem(id, x, y, itemId, scale?)
+
+Draws an item icon on screen (persists every frame until removed via `removeDrawItem`).
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `id` | number | (required) | Icon ID for later removal or update |
+| `x` | number | (required) | X position (GUI-scaled coordinates) |
+| `y` | number | (required) | Y position (GUI-scaled coordinates) |
+| `itemId` | string | (required) | Item ID (e.g. `"minecraft:diamond"`) |
+| `scale` | number | `16` | Icon size in pixels |
+
+Returns the icon ID (same as the passed `id`).
+
+```js
+ui.drawItem(1, 10, 10, "minecraft:diamond");
+ui.drawItem(2, 30, 10, "minecraft:golden_apple", 24);
+```
+
+## ui.removeDrawItem(id)
+
+Removes the drawn item icon with the given ID.
+
+```js
+ui.removeDrawItem(1);
+```
+
+## ui.drawRect(id, x, y, w, h, color, alpha?)
+
+Draws a filled rectangle on screen (persists every frame until removed via `removeDrawRect`).
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `id` | number | (required) | Rectangle ID for later removal or update |
+| `x` | number | (required) | Top-left X position |
+| `y` | number | (required) | Top-left Y position |
+| `w` | number | (required) | Width in pixels |
+| `h` | number | (required) | Height in pixels |
+| `color` | GameRGBColor | (required) | Fill colour (RGB) |
+| `alpha` | number | `255` | Alpha / opacity (0–255) |
+
+Returns the rectangle ID (same as the passed `id`).
+
+```js
+var red = new GameRGBColor(1, 0, 0);
+ui.drawRect(1, 50, 50, 100, 60, red, 128); // Semi-transparent red rectangle
+```
+
+## ui.removeDrawRect(id)
+
+Removes the drawn rectangle with the given ID.
+
+```js
+ui.removeDrawRect(1);
+```
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md b/Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md
index ef9c650..3cfb4ac 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md
@@ -79,6 +79,10 @@ Fill a rectangular region with a block. Corner coordinates are auto-sorted (no n
⬆ GameVector3 overload.
+### voxels.fillVoxel(bounds, voxel)
+
+⬆ GameBounds3 overload.
+
```js
// Fill a 5×1×5 platform
voxels.fillVoxel(-2, 100, -2, 2, 100, 2, "minecraft:white_concrete");
@@ -96,6 +100,77 @@ voxels.fillVoxel(
);
```
+### voxels.replace(x1, y1, z1, x2, y2, z2, fromBlock, toBlock)
+
+Replaces all occurrences of `fromBlock` with `toBlock` within the rectangular region.
+
+### voxels.replace(pos1, pos2, fromBlock, toBlock)
+
+⬆ GameVector3 overload.
+
+### voxels.replace(bounds, fromBlock, toBlock)
+
+⬆ GameBounds3 overload.
+
+```js
+// Remove all wool blocks
+voxels.replace(-10, 60, -10, 10, 80, 10, "minecraft:white_wool", "minecraft:air");
+voxels.replace(
+ new GameVector3(-10, 60, -10),
+ new GameVector3(10, 80, 10),
+ "minecraft:red_wool",
+ "minecraft:blue_wool",
+);
+```
+
+### voxels.clone(x1, y1, z1, x2, y2, z2, destX, destY, destZ)
+
+Copies all blocks from the source region to the destination (preserving block state and rotation).
+
+### voxels.clone(pos1, pos2, destPos)
+
+⬆ GameVector3 overload.
+
+### voxels.clone(bounds, destX, destY, destZ)
+
+⬆ GameBounds3 overload with raw destination coordinates.
+
+### voxels.clone(bounds, destPos)
+
+⬆ GameBounds3 overload with GameVector3 destination.
+
+```js
+// Copy a 5×5×5 structure to a new location
+voxels.clone(0, 100, 0, 5, 105, 5, 10, 100, 10);
+voxels.clone(
+ new GameVector3(0, 100, 0),
+ new GameVector3(5, 105, 5),
+ new GameVector3(10, 100, 10),
+);
+```
+
+### voxels.setVoxelState(x, y, z, voxel, state)
+
+Place a block with specific BlockState properties. `state` is a key-value object mapping property names to values.
+
+```js
+// Place an oak stair facing north, top half
+voxels.setVoxelState(0, 100, 0, "minecraft:oak_stairs", {
+ facing: "north",
+ half: "top",
+});
+
+// Place three lit candles
+voxels.setVoxelState(0, 100, 0, "minecraft:candle", {
+ candles: "3",
+ lit: "true",
+});
+```
+
+### voxels.setVoxelState(pos, voxel, state)
+
+⬆ GameVector3 overload.
+
## Reading Blocks
### voxels.getVoxel(x, y, z)
@@ -149,6 +224,10 @@ Count matching blocks in a region. `voxel` can be a string or numeric ID.
⬆ GameVector3 overload.
+### voxels.countVoxel(bounds, voxel)
+
+⬆ GameBounds3 overload.
+
```js
// Count how many diamond blocks are in the region
var count = voxels.countVoxel(
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/world.md b/Box3JS-NeoForge-1.21.1/docs/en/api/world.md
index 9dd191f..d75df14 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/api/world.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/api/world.md
@@ -665,6 +665,14 @@ Spawn particles evenly on a horizontal circle.
⬆ GameVector3 overload.
+### world.spawnParticleLine(x1, y1, z1, x2, y2, z2, type, count)
+
+Spawn particles evenly along a line between two points. ⬆ MC Extension.
+
+### world.spawnParticleLine(from, to, type, count)
+
+⬆ GameVector3 overload.
+
```js
// Point particles
world.spawnParticle("minecraft:flame", 0, 100, 0, 10, 0.5, 0.5, 0.5, 0.1);
@@ -679,6 +687,15 @@ world.spawnParticleCircle(
20,
);
+// Particle line (between two points)
+world.spawnParticleLine(0, 100, 0, 10, 100, 10, "minecraft:flame", 20);
+world.spawnParticleLine(
+ new GameVector3(0, 100, 0),
+ new GameVector3(10, 100, 10),
+ "minecraft:flame",
+ 20,
+);
+
// Common particles:
// minecraft:flame, minecraft:cloud, minecraft:happy_villager
// minecraft:witch, minecraft:portal, minecraft:end_rod
@@ -792,6 +809,10 @@ if (result.hit) {
Returns all entities within the AABB defined by two corner positions.
+### world.entitiesInArea(bounds)
+
+⬆ GameBounds3 overload.
+
### world.entitiesInRadius(x, y, z, radius)
Returns all entities within a spherical radius. Convenience wrapper around `entitiesInArea`.
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md
index b4fcceb..a9fa2ac 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md
@@ -1,11 +1,8 @@
----
----
-
-# Box3JS and Box3 (Shenqi Code Island)
+# Box3JS and Box3
## 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.
+[Box3](https://box3.fun) (also known as 神奇代码岛 or "Box3") is a **multiplayer 3D game creation platform**. Users create racing games, PvP arenas, RPGs, FPS shooters, and even MOBAs — all in a browser, using nothing but JavaScript.
Key features:
@@ -67,7 +64,7 @@ world.onChat((entity, message) => {
});
```
-For a detailed API comparison, see [Box3 API vs Box3JS](../BOX3_API_COMPARISON.md).
+For a detailed API comparison, see [Box3 API vs Box3JS](../../BOX3_API_COMPARISON.md).
### 2. Real Minecraft World
@@ -129,7 +126,7 @@ Box3JS is not a 1:1 copy of Box3's API. Differences stem from the fundamental di
| 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).
+**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
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/index.md b/Box3JS-NeoForge-1.21.1/docs/en/index.md
index 9f5cfba..0dd46e7 100644
--- a/Box3JS-NeoForge-1.21.1/docs/en/index.md
+++ b/Box3JS-NeoForge-1.21.1/docs/en/index.md
@@ -64,7 +64,7 @@ world.onChat((entity, message) => {
});
```
-[Read full docs →](/en/guide/getting-started_en)
+[Read full docs →](/en/guide/getting-started)
## Version Info
diff --git a/Box3JS-NeoForge-1.21.1/docs/en/overview.md b/Box3JS-NeoForge-1.21.1/docs/en/overview.md
new file mode 100644
index 0000000..c4491d0
--- /dev/null
+++ b/Box3JS-NeoForge-1.21.1/docs/en/overview.md
@@ -0,0 +1,70 @@
+# 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 in JS/TS — no JDK, no Java compilation required.
+
+## Documentation Navigation
+
+### Getting Started
+
+Start from zero: what Box3JS is, how to use it, and how it works under the hood.
+
+| Doc | Content |
+|-----|---------|
+| [Getting Started](guide/getting-started.md) | Setup → first script → dev loop → debugging → deployment |
+| [Common Recipes](guide/recipes.md) | Templates: economy, teleport, shop, daily rewards, leaderboard, webhook |
+| [Architecture](guide/architecture.md) | Rhino engine, scope management, build pipeline, network communication |
+| [JS vs Java](guide/js-vs-java.md) | Advantages and trade-offs of Box3JS scripting vs native Java modding |
+| [FAQ](guide/faq.md) | Loading, build, runtime, database, HTTP, client-side, deployment |
+
+### Tutorials
+
+6 progressive tutorials, 10-15 minutes each, with complete runnable code.
+
+| # | Tutorial | What you'll learn |
+|---|----------|-------------------|
+| 1 | [Basics](tutorial/01-basics.md) | Create a project → build → first script → chat commands → timers |
+| 2 | [Players & Items](tutorial/02-player-items.md) | Teleport, flight, items, enchantments, potion effects, game mode |
+| 3 | [Events & Entities](tutorial/03-events-entities.md) | All event callbacks, entity spawning, AI control, patrol guards, collision |
+| 4 | [Advanced Systems](tutorial/04-advanced-systems.md) | Scoreboard rankings, BossBar countdown, teams, world border, cross-script messaging |
+| 5 | [Mini-Games](tutorial/05-examples.md) | PvP arena (fully playable), particle effects, fireworks, wave-based mob spawning |
+| 6 | [Client Scripting](tutorial/06-client-scripting.md) | Keyboard input, screen UI, audio/music, local storage, SQLite, HTTP, remoteChannel |
+| 📋 | [Tutorial Overview](tutorial/README.md) | Learning path, prerequisites, development tips |
+
+### API Reference
+
+Complete API documentation organized by function. One doc per global object/namespace.
+
+| Category | Doc | Global Object |
+|----------|-----|---------------|
+| **Server Overview** | [server](api/server.md) | Server runtime boundaries, events, players/entities, blocks, data, cross-side communication |
+| **World** | [world](api/world.md) | `world` — events, particles, fireworks, sounds, scoreboards |
+| **Entity** | [entity](api/entity.md) | `entity` — properties, AI, equipment, effects |
+| **Player** | [player](api/player.md) | `player` — inventory, messaging, flight, teleport |
+| **Voxels** | [voxels](api/voxels.md) | `voxels` — read/write blocks, region fill |
+| **Storage** | [storage](api/storage.md) | `storage` — JSON persistence |
+| **Database** | [database](api/database.md) | `db` — SQLite database |
+| **HTTP** | [http](api/http.md) | `http` — HTTP requests |
+| **Client** | [client](api/client.md) | `audio` `client` `input` `ui` `chat` `gui` `remoteChannel` |
+| **Registries** | [registries](api/registries.md) | `registries` — custom blocks/items/sounds |
+| **Math** | [math](api/math.md) | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` |
+| **Commands** | [commands](api/commands.md) | `/box3script` CLI reference |
+| **Quick Search** | [Find by Task](api/README.md) | "I want to do X" → find the right API |
+| **Comparison** | [Box3 API Comparison](../BOX3_API_COMPARISON.md) | Side-by-side comparison of Box3 platform APIs vs Box3JS (Chinese) |
+
+### Version & Compatibility
+
+| Component | 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**: [Getting Started →](guide/getting-started.md)
+- **"I want to do X, what API?"**: [API Quick Search →](api/README.md)
+- **Why Box3JS over Java mods?**: [JS vs Java →](guide/js-vs-java.md)
+- **How Box3JS works internally**: [Architecture →](guide/architecture.md)
+- **Learn Box3JS from scratch**: [Tutorial 1 →](tutorial/01-basics.md)
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 50216e1..44b136c 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
@@ -81,7 +81,7 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) {
case 0 -> Box3JSGuiServerHandler.handleOpen(
sp, payload.title(), payload.rows(), payload.slotsJson());
case 1 -> Box3JSGuiServerHandler.handleSetItem(
- sp, payload.slot(), payload.itemId(), payload.count());
+ sp, payload.slot(), payload.itemId(), payload.count(), payload.loreJson(), payload.enchanted());
case 2 -> Box3JSGuiServerHandler.handleRegisterCallbacks(
sp, payload.hasSlotClick(), payload.hasClose());
case 3 -> Box3JSGuiServerHandler.handleClose(sp);
@@ -129,28 +129,36 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) {
// Block break
NeoForge.EVENT_BUS.addListener((BlockEvent.BreakEvent event) -> {
if (event.getPlayer() instanceof ServerPlayer sp) {
- Box3ScriptEngine.get().fireVoxelDestroy(sp, event.getPos());
+ if (Box3ScriptEngine.get().fireVoxelDestroy(sp, event.getPos())) {
+ event.setCanceled(true);
+ }
}
});
// Block place
NeoForge.EVENT_BUS.addListener((BlockEvent.EntityPlaceEvent event) -> {
if (event.getEntity() instanceof ServerPlayer sp) {
- Box3ScriptEngine.get().fireBlockPlace(sp, event.getPos(), event.getPlacedBlock());
+ if (Box3ScriptEngine.get().fireBlockPlace(sp, event.getPos(), event.getPlacedBlock())) {
+ event.setCanceled(true);
+ }
}
});
// Interact (entity)
NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.EntityInteract event) -> {
if (event.getEntity() instanceof ServerPlayer sp) {
- Box3ScriptEngine.get().fireInteract(sp, event.getTarget());
+ if (Box3ScriptEngine.get().fireInteract(sp, event.getTarget())) {
+ event.setCanceled(true);
+ }
}
});
// Right-click block
NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.RightClickBlock event) -> {
if (event.getEntity() instanceof ServerPlayer sp) {
- Box3ScriptEngine.get().fireBlockActivate(sp, event.getPos(), event.getLevel().getBlockState(event.getPos()));
+ if (Box3ScriptEngine.get().fireBlockActivate(sp, event.getPos(), event.getLevel().getBlockState(event.getPos()))) {
+ event.setCanceled(true);
+ }
Box3ScriptEngine.get().fireActionButton(sp, "ACTION1");
}
});
@@ -202,10 +210,12 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) {
// Entity damage
NeoForge.EVENT_BUS.addListener((LivingDamageEvent.Pre event) -> {
- Box3ScriptEngine.get().fireEntityDamage(event.getEntity(),
+ if (Box3ScriptEngine.get().fireEntityDamage(event.getEntity(),
event.getNewDamage(),
event.getSource().getMsgId(),
- event.getSource().getEntity());
+ event.getSource().getEntity())) {
+ event.setNewDamage(0);
+ }
});
// Auto-load server scripts from config/box3/script//dist/server.js on server start
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 8d6ff3c..54d6fb7 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
@@ -105,7 +105,9 @@ public record GUIServerboundPayload(
String itemId, // for SET_ITEM
int count, // for SET_ITEM
boolean hasSlotClick, // for REGISTER_CALLBACKS
- boolean hasClose // for REGISTER_CALLBACKS
+ boolean hasClose, // for REGISTER_CALLBACKS
+ String loreJson, // for SET_ITEM (JSON array of lore strings)
+ boolean enchanted // for SET_ITEM (enchantment glint override)
) implements CustomPacketPayload {
public static final Type TYPE =
@@ -123,6 +125,8 @@ public record GUIServerboundPayload(
buf.writeVarInt(p.count);
buf.writeBoolean(p.hasSlotClick);
buf.writeBoolean(p.hasClose);
+ buf.writeUtf(p.loreJson);
+ buf.writeBoolean(p.enchanted);
},
buf -> new GUIServerboundPayload(
buf.readVarInt(),
@@ -133,6 +137,8 @@ public record GUIServerboundPayload(
buf.readUtf(),
buf.readVarInt(),
buf.readBoolean(),
+ buf.readBoolean(),
+ buf.readUtf(),
buf.readBoolean()
)
);
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 56f9fbd..7805d14 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
@@ -35,6 +35,8 @@
import java.util.concurrent.atomic.AtomicInteger;
import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.HitResult;
@@ -76,7 +78,10 @@ public class Box3JSClientEngine {
private volatile boolean renderRegistered;
private volatile boolean mouseRegistered;
private final Map drawTexts = new ConcurrentHashMap<>();
- private final AtomicInteger drawTextIdCounter = new AtomicInteger(0);
+ private final Map drawItems = new ConcurrentHashMap<>();
+ private final Map drawRects = new ConcurrentHashMap<>();
+ private final AtomicInteger drawIdCounter = new AtomicInteger(0);
+ private final List renderCallbacks = new CopyOnWriteArrayList<>();
private final List mouseClickHandlers = new CopyOnWriteArrayList<>();
private String currentProject = "";
private Box3JSClientStorage storage;
@@ -94,6 +99,7 @@ public class Box3JSClientEngine {
private volatile float fogStartDist = -1f;
private volatile float fogEndDist = -1f;
private volatile boolean fogRegistered;
+ private double prevMouseX, prevMouseY, mouseDeltaX, mouseDeltaY;
// ── Timers ──
private final List timers = new CopyOnWriteArrayList<>();
@@ -379,6 +385,75 @@ public Object call(Context cx, Scriptable scope,
}
});
+ // client.setCameraTarget(entityUuid) — spectate an entity
+ ScriptableObject.putProperty(clientObj, "setCameraTarget", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ if (args.length < 1) return Undefined.instance;
+ String uuid = args[0].toString();
+ Minecraft.getInstance().execute(() -> {
+ var level = Minecraft.getInstance().level;
+ if (level == null) return;
+ for (var entity : level.entitiesForRendering()) {
+ if (entity.getStringUUID().equals(uuid)) {
+ Minecraft.getInstance().setCameraEntity(entity);
+ return;
+ }
+ }
+ });
+ return Undefined.instance;
+ }
+ });
+
+ // client.resetCamera() — back to local player
+ ScriptableObject.putProperty(clientObj, "resetCamera", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ Minecraft.getInstance().execute(() -> {
+ Minecraft.getInstance().setCameraEntity(
+ Minecraft.getInstance().player);
+ });
+ return Undefined.instance;
+ }
+ });
+
+ // client.getBlockState(x, y, z) -> { id: string } | null
+ ScriptableObject.putProperty(clientObj, "getBlockState", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ if (args.length < 3) return null;
+ int x = ((Number) args[0]).intValue();
+ int y = ((Number) args[1]).intValue();
+ int z = ((Number) args[2]).intValue();
+ var level = Minecraft.getInstance().level;
+ if (level == null) return null;
+ var state = level.getBlockState(new BlockPos(x, y, z));
+ if (state == null || state.isAir()) return null;
+ Scriptable obj = cx.newObject(scope);
+ ScriptableObject.putProperty(obj, "id",
+ net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(
+ state.getBlock()).toString());
+ return obj;
+ }
+ });
+
+ // client.onRender(callback) — fires every render frame
+ ScriptableObject.putProperty(clientObj, "onRender", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ if (args.length > 0 && args[0] instanceof Function fn) {
+ renderCallbacks.add(fn);
+ registerRenderListener();
+ return new GameEventHandlerToken(() -> renderCallbacks.remove(fn));
+ }
+ return Undefined.instance;
+ }
+ });
+
ScriptableObject.putProperty(scope, "client", clientObj);
// -- audio global (sound playback) ------------------------------
@@ -524,6 +599,24 @@ public Object call(Context cx, Scriptable scope,
}
});
+ // input.getMouseDeltaX()
+ ScriptableObject.putProperty(inputObj, "getMouseDeltaX", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ return mouseDeltaX;
+ }
+ });
+
+ // input.getMouseDeltaY()
+ ScriptableObject.putProperty(inputObj, "getMouseDeltaY", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ return mouseDeltaY;
+ }
+ });
+
// input.onMouseClick(callback) — returns GameEventHandlerToken
ScriptableObject.putProperty(inputObj, "onMouseClick", new BaseFunction() {
@Override
@@ -627,7 +720,7 @@ public Object call(Context cx, Scriptable scope,
public Object call(Context cx, Scriptable scope,
Scriptable thisObj, Object[] args) {
if (args.length < 4) return -1;
- int id = args[0] instanceof Number n ? n.intValue() : drawTextIdCounter.incrementAndGet();
+ int id = args[0] instanceof Number n ? n.intValue() : drawIdCounter.incrementAndGet();
int x = ((Number) args[1]).intValue();
int y = ((Number) args[2]).intValue();
String text = args[3].toString();
@@ -666,6 +759,67 @@ public Object call(Context cx, Scriptable scope,
}
});
+ // ui.drawItem(id, x, y, itemId, scale?)
+ ScriptableObject.putProperty(uiObj, "drawItem", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ if (args.length < 4) return -1;
+ int id = args[0] instanceof Number n ? n.intValue() : drawIdCounter.incrementAndGet();
+ int x = ((Number) args[1]).intValue();
+ int y = ((Number) args[2]).intValue();
+ String itemId = args[3].toString();
+ int scale = args.length > 4 && args[4] instanceof Number n ? n.intValue() : 16;
+ drawItems.put(id, new DrawItemEntry(x, y, itemId, scale));
+ registerRenderListener();
+ return id;
+ }
+ });
+
+ // ui.removeDrawItem(id)
+ ScriptableObject.putProperty(uiObj, "removeDrawItem", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ if (args.length < 1) return Undefined.instance;
+ int id = ((Number) args[0]).intValue();
+ drawItems.remove(id);
+ return Undefined.instance;
+ }
+ });
+
+ // ui.drawRect(id, x, y, w, h, color, alpha?)
+ ScriptableObject.putProperty(uiObj, "drawRect", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ if (args.length < 6) return -1;
+ int id = args[0] instanceof Number n ? n.intValue() : drawIdCounter.incrementAndGet();
+ int x = ((Number) args[1]).intValue();
+ int y = ((Number) args[2]).intValue();
+ int w = ((Number) args[3]).intValue();
+ int h = ((Number) args[4]).intValue();
+ int color = argbToInt(args[5]);
+ int alpha = args.length > 6 && args[6] instanceof Number n
+ ? Math.max(0, Math.min(255, n.intValue())) : 255;
+ drawRects.put(id, new DrawRectEntry(x, y, w, h, color, alpha));
+ registerRenderListener();
+ return id;
+ }
+ });
+
+ // ui.removeDrawRect(id)
+ ScriptableObject.putProperty(uiObj, "removeDrawRect", new BaseFunction() {
+ @Override
+ public Object call(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ if (args.length < 1) return Undefined.instance;
+ int id = ((Number) args[0]).intValue();
+ drawRects.remove(id);
+ return Undefined.instance;
+ }
+ });
+
ScriptableObject.putProperty(scope, "ui", uiObj);
// -- chat global (messaging) ------------------------------------
@@ -856,7 +1010,7 @@ public Object call(Context cx, Scriptable scope,
PacketDistributor.sendToServer(
new Box3JSNetwork.GUIServerboundPayload(0, title, rows, slotsJson,
- 0, "", 0, false, false));
+ 0, "", 0, false, false, "", false));
return Context.javaToJS(proxy, scope);
}
@@ -921,6 +1075,14 @@ public void loadScript(String projectName, String scriptSource) {
// ── Tick dispatch ──
private void fireTick() {
+ // Track mouse delta
+ double curX = Minecraft.getInstance().mouseHandler.xpos();
+ double curY = Minecraft.getInstance().mouseHandler.ypos();
+ mouseDeltaX = curX - prevMouseX;
+ mouseDeltaY = curY - prevMouseY;
+ prevMouseX = curX;
+ prevMouseY = curY;
+
for (Runnable cb : tickCallbacks) {
try {
cb.run();
@@ -1106,11 +1268,63 @@ private void registerRenderListener() {
Minecraft.getInstance().execute(() -> {
if (renderRegistered) return;
NeoForge.EVENT_BUS.addListener(RenderGuiEvent.Post.class, event -> {
- if (drawTexts.isEmpty()) return;
GuiGraphics gfx = event.getGuiGraphics();
- var font = Minecraft.getInstance().font;
- for (DrawTextEntry entry : drawTexts.values()) {
- gfx.drawString(font, entry.text(), entry.x(), entry.y(), entry.color());
+ float partialTick = event.getPartialTick().getGameTimeDeltaTicks();
+
+ // Fire render callbacks
+ for (Function fn : renderCallbacks) {
+ Context cx = Context.enter();
+ try {
+ fn.call(cx, scope, scope, new Object[]{partialTick});
+ } catch (Exception e) {
+ LOGGER.error("Render callback error", e);
+ } finally {
+ Context.exit();
+ }
+ }
+
+ // Draw texts
+ if (!drawTexts.isEmpty()) {
+ var font = Minecraft.getInstance().font;
+ for (DrawTextEntry entry : drawTexts.values()) {
+ gfx.drawString(font, entry.text(), entry.x(), entry.y(), entry.color());
+ }
+ }
+
+ // Draw rects
+ if (!drawRects.isEmpty()) {
+ for (DrawRectEntry entry : drawRects.values()) {
+ int a = entry.alpha();
+ int c = entry.color();
+ int argb = (a << 24) | (c & 0x00FFFFFF);
+ gfx.fill(entry.x(), entry.y(),
+ entry.x() + entry.w(), entry.y() + entry.h(),
+ argb);
+ }
+ }
+
+ // Draw items
+ if (!drawItems.isEmpty()) {
+ for (DrawItemEntry entry : drawItems.values()) {
+ var rl = net.minecraft.resources.ResourceLocation.tryParse(entry.itemId());
+ if (rl == null) continue;
+ var holder = net.minecraft.core.registries.BuiltInRegistries.ITEM.getHolder(rl);
+ if (holder.isEmpty()) continue;
+ var stack = new ItemStack(holder.get());
+ int s = entry.scale();
+ if (s != 16) {
+ gfx.pose().pushPose();
+ float scale = s / 16.0f;
+ gfx.pose().translate(entry.x(), entry.y(), 0);
+ gfx.pose().scale(scale, scale, 1);
+ gfx.renderItem(stack, 0, 0);
+ gfx.renderItemDecorations(Minecraft.getInstance().font, stack, 0, 0);
+ gfx.pose().popPose();
+ } else {
+ gfx.renderItem(stack, entry.x(), entry.y());
+ gfx.renderItemDecorations(Minecraft.getInstance().font, stack, entry.x(), entry.y());
+ }
+ }
}
});
renderRegistered = true;
@@ -1178,6 +1392,19 @@ private void registerFogListener() {
// ── Draw text entry ──
private record DrawTextEntry(int x, int y, String text, int color) {}
+ private record DrawItemEntry(int x, int y, String itemId, int scale) {}
+ private record DrawRectEntry(int x, int y, int w, int h, int color, int alpha) {}
+
+ /** Parse a NativeObject { r, g, b } or { r, g, b, a } to ARGB int. */
+ private static int argbToInt(Object raw) {
+ if (raw instanceof NativeObject c) {
+ int r = c.containsKey("r") ? ((Number) c.get("r", c)).intValue() & 0xFF : 255;
+ int g = c.containsKey("g") ? ((Number) c.get("g", c)).intValue() & 0xFF : 255;
+ int b = c.containsKey("b") ? ((Number) c.get("b", c)).intValue() & 0xFF : 255;
+ return (r << 16) | (g << 8) | b;
+ }
+ return 0xFFFFFF;
+ }
// ── Console backend ──
diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java
index 1fb4c5c..3b0cf1c 100644
--- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java
+++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java
@@ -1,6 +1,7 @@
package com.box3lab.box3js.client;
import com.box3lab.box3js.Box3JSNetwork;
+import com.box3lab.box3js.script.Box3ScriptUtils;
import com.box3lab.box3js.script.GameEventHandlerToken;
import com.mojang.logging.LogUtils;
import net.minecraft.client.Minecraft;
@@ -8,6 +9,7 @@
import net.neoforged.neoforge.network.PacketDistributor;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
+import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeObject;
import org.slf4j.Logger;
@@ -35,7 +37,35 @@ public void setItem(int slot, String itemId) {
public void setItem(int slot, String itemId, int count) {
PacketDistributor.sendToServer(
- new Box3JSNetwork.GUIServerboundPayload(1, "", 0, "", slot, itemId, Math.max(1, Math.min(count, 64)), false, false));
+ new Box3JSNetwork.GUIServerboundPayload(1, "", 0, "", slot, itemId, Math.max(1, Math.min(count, 64)), false, false, "", false));
+ }
+
+ /** Overload with options: { count?, lore?: string[], enchanted?: boolean } */
+ public void setItem(int slot, String itemId, NativeObject options) {
+ int count = 1;
+ String loreJson = "";
+ boolean enchanted = false;
+ if (options.containsKey("count")) {
+ count = Math.max(1, Math.min(((Number) options.get("count")).intValue(), 64));
+ }
+ if (options.containsKey("lore")) {
+ Object lore = options.get("lore");
+ if (lore instanceof NativeArray arr) {
+ StringBuilder sb = new StringBuilder("[");
+ for (int i = 0; i < arr.getLength(); i++) {
+ if (i > 0) sb.append(",");
+ String line = arr.get(i).toString();
+ sb.append("\"").append(line.replace("\\", "\\\\").replace("\"", "\\\"")).append("\"");
+ }
+ sb.append("]");
+ loreJson = sb.toString();
+ }
+ }
+ if (options.containsKey("enchanted")) {
+ enchanted = Box3ScriptUtils.coerceBool(options.get("enchanted"));
+ }
+ PacketDistributor.sendToServer(
+ new Box3JSNetwork.GUIServerboundPayload(1, "", 0, "", slot, itemId, count, false, false, loreJson, enchanted));
}
public NativeObject getItem(int slot) {
@@ -86,7 +116,7 @@ private void ensureCallbacksRegistered(boolean wantsSlotClick, boolean wantsClos
closeRegistered = closeRegistered || sendClose;
PacketDistributor.sendToServer(
new Box3JSNetwork.GUIServerboundPayload(2, "", 0, "", 0, "", 0,
- sendSlotClick, sendClose));
+ sendSlotClick, sendClose, "", false));
}
// ---- Close ----
@@ -98,7 +128,7 @@ public void close() {
}
});
PacketDistributor.sendToServer(
- new Box3JSNetwork.GUIServerboundPayload(3, "", 0, "", 0, "", 0, false, false));
+ new Box3JSNetwork.GUIServerboundPayload(3, "", 0, "", 0, "", 0, false, false, "", false));
}
// ---- Internal: called from GUIClientboundPayload handler (on netty thread → render thread) ----
diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java
index 5ca2932..2ed4b72 100644
--- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java
+++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java
@@ -12,7 +12,7 @@ interface PlayerLeaveCallback {
@FunctionalInterface
interface VoxelDestroyCallback {
- void onDestroy(Box3JSEntity entity, int x, int y, int z, String voxel, long tick);
+ Object onDestroy(Box3JSEntity entity, int x, int y, int z, String voxel, long tick);
}
@FunctionalInterface
@@ -22,7 +22,7 @@ interface VoxelContactCallback {
@FunctionalInterface
interface InteractCallback {
- void onInteract(Box3JSEntity entity, Box3JSEntity target, long tick);
+ Object onInteract(Box3JSEntity entity, Box3JSEntity target, long tick);
}
@FunctionalInterface
@@ -52,7 +52,7 @@ interface EntitySeparateCallback {
@FunctionalInterface
interface BlockPlaceCallback {
- void onPlace(Box3JSEntity entity, int x, int y, int z, String voxel, int voxelId, long tick);
+ Object onPlace(Box3JSEntity entity, int x, int y, int z, String voxel, int voxelId, long tick);
}
@FunctionalInterface
@@ -67,12 +67,12 @@ interface PlayerRespawnCallback {
@FunctionalInterface
interface BlockActivateCallback {
- void onActivate(Box3JSEntity entity, int x, int y, int z, String voxel, long tick);
+ Object onActivate(Box3JSEntity entity, int x, int y, int z, String voxel, long tick);
}
@FunctionalInterface
interface EntityDamageCallback {
- void onDamage(Box3JSEntity entity, double amount, String source, Box3JSEntity attacker, long tick);
+ Object onDamage(Box3JSEntity entity, double amount, String source, Box3JSEntity attacker, long tick);
}
@FunctionalInterface
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 5aba6f3..a3499aa 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
@@ -3,8 +3,11 @@
import net.minecraft.ChatFormatting;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
@@ -19,6 +22,7 @@
import net.minecraft.world.scores.Scoreboard;
import com.mojang.logging.LogUtils;
import org.mozilla.javascript.Function;
+import org.mozilla.javascript.NativeObject;
import org.slf4j.Logger;
import java.util.Map;
@@ -210,6 +214,16 @@ public void hurt(double amount) {
if (le != null) le.hurt(le.damageSources().generic(), (float) amount);
}
+ /** hurt with a source entity (for kill tracking) */
+ public void hurt(double amount, Box3JSEntity source) {
+ LivingEntity le = asLiving();
+ if (le != null && source != null && source.getEntity() instanceof LivingEntity sle) {
+ le.hurt(le.damageSources().mobAttack(sle), (float) amount);
+ } else {
+ hurt(amount);
+ }
+ }
+
public void heal(double amount) {
LivingEntity le = asLiving();
if (le != null) le.heal((float) amount);
@@ -271,6 +285,16 @@ public void clearFire() {
entity.setRemainingFireTicks(0);
}
+ // ---- Teleport (MC extension) ----
+
+ public void teleportTo(double x, double y, double z) {
+ trackIfSandboxed();
+ entity.teleportTo(x, y, z);
+ }
+ public void teleportTo(GameVector3 pos) {
+ teleportTo(pos.x, pos.y, pos.z);
+ }
+
// ---- Look at (MC extension) ----
public void lookAt(double x, double y, double z) { Box3ScriptUtils.lookAt(entity, x, y, z); }
@@ -336,6 +360,17 @@ public void addEffect(String effectId, int duration, int amplifier, boolean hide
le.addEffect(new MobEffectInstance(effect, duration, amplifier, false, !hideParticles, true));
}
+ // ---- Particles (MC extension) ----
+
+ /** Spawn particles at the entity's center position. */
+ public void spawnParticle(String type, int count, double dx, double dy, double dz, double speed) {
+ var particle = Box3ScriptUtils.lookupParticle(type);
+ if (particle == null) return;
+ if (entity.level() instanceof ServerLevel sl) {
+ sl.sendParticles(particle, entity.getX(), entity.getY() + entity.getBbHeight() / 2.0, entity.getZ(), count, dx, dy, dz, speed);
+ }
+ }
+
// ---- Equipment (MC extension) ----
public void setEquipment(String slot, String itemId) {
@@ -348,6 +383,31 @@ public void setEquipment(String slot, String itemId) {
mob.setItemSlot(equipmentSlot, new ItemStack(item));
}
+ /** setEquipment with enchantments. enchants is a NativeObject like { "minecraft:sharpness": 5 } */
+ public void setEquipmentWithEnchants(String slot, String itemId, NativeObject enchants) {
+ trackIfSandboxed();
+ if (!(entity instanceof Mob mob)) return;
+ EquipmentSlot equipmentSlot = parseEquipmentSlot(slot);
+ if (equipmentSlot == null) return;
+ Item item = Box3ScriptUtils.lookupItem(itemId);
+ if (item == null) return;
+ ItemStack stack = new ItemStack(item);
+ if (enchants != null) {
+ var enchRegistry = entity.level().registryAccess().registryOrThrow(Registries.ENCHANTMENT);
+ for (Object key : enchants.keySet()) {
+ String enchId = key.toString();
+ int level = ((Number) enchants.get(key)).intValue();
+ ResourceLocation enchRl = ResourceLocation.tryParse(enchId);
+ if (enchRl == null) continue;
+ var holder = enchRegistry.getHolder(enchRl);
+ if (holder.isPresent()) {
+ stack.enchant(holder.get(), level);
+ }
+ }
+ }
+ mob.setItemSlot(equipmentSlot, stack);
+ }
+
// ---- Drop chances (MC extension) ----
public void setDropChance(String slot, double chance) {
@@ -451,6 +511,11 @@ public void setAttribute(String attributeId, double value) {
}
}
+ /** Remove the entity from the world immediately. */
+ public void remove() {
+ entity.discard();
+ }
+
// ---- Lifecycle ----
public void destroy() {
diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiController.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiController.java
index f2e6b0d..c943249 100644
--- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiController.java
+++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiController.java
@@ -1,9 +1,14 @@
package com.box3lab.box3js.script;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.component.ItemLore;
import org.mozilla.javascript.NativeObject;
+import java.util.ArrayList;
+import java.util.List;
import java.util.function.Consumer;
/**
@@ -29,11 +34,37 @@ void setMenu(Box3JSScriptContainerMenu menu) {
}
public void setItem(int slot, String itemId, int count) {
+ setItem(slot, itemId, count, "", false);
+ }
+
+ public void setItem(int slot, String itemId, int count, String loreJson, boolean enchanted) {
if (menu == null) return;
var item = Box3ScriptUtils.lookupItem(itemId);
if (item == null) return;
if (slot < 0 || slot >= menu.getContainer().getContainerSize()) return;
- menu.getContainer().setItem(slot, new ItemStack(item, Math.max(1, Math.min(count, 64))));
+ ItemStack stack = new ItemStack(item, Math.max(1, Math.min(count, 64)));
+ if (loreJson != null && !loreJson.isEmpty()) {
+ List lines = new ArrayList<>();
+ String inner = loreJson.trim();
+ if (inner.startsWith("[") && inner.endsWith("]")) {
+ inner = inner.substring(1, inner.length() - 1);
+ for (String part : inner.split(",")) {
+ part = part.trim();
+ if (part.startsWith("\"") && part.endsWith("\"")) {
+ String line = part.substring(1, part.length() - 1)
+ .replace("\\\\", "\\").replace("\\\"", "\"");
+ lines.add(Component.literal(line));
+ }
+ }
+ }
+ if (!lines.isEmpty()) {
+ stack.set(DataComponents.LORE, new ItemLore(lines, lines));
+ }
+ }
+ if (enchanted) {
+ stack.set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, true);
+ }
+ menu.getContainer().setItem(slot, stack);
}
public NativeObject getItem(int slot) {
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 31d4aea..61993d7 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
@@ -120,10 +120,10 @@ public static void handleOpen(ServerPlayer player, String title, int rows, Strin
}
}
- public static void handleSetItem(ServerPlayer player, int slot, String itemId, int count) {
+ public static void handleSetItem(ServerPlayer player, int slot, String itemId, int count, String loreJson, boolean enchanted) {
ActiveGui gui = activeGuis.get(player.getUUID());
if (gui != null) {
- gui.controller.setItem(slot, itemId, count);
+ gui.controller.setItem(slot, itemId, count, loreJson, enchanted);
}
}
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 a43de4b..9ba14c1 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
@@ -15,7 +15,12 @@
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.ai.attributes.Attributes;
+import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
+import net.minecraft.server.level.ServerBossEvent;
+import net.minecraft.world.BossEvent.BossBarColor;
+import net.minecraft.world.BossEvent.BossBarOverlay;
+import net.minecraft.world.item.alchemy.PotionContents;
import net.minecraft.world.level.GameType;
import com.mojang.logging.LogUtils;
import org.mozilla.javascript.Function;
@@ -495,6 +500,24 @@ public void clearInventory() {
player.getInventory().clearContent();
}
+ /** Remove up to `count` items of the given type from the player's inventory.
+ * Returns the number of items actually removed. */
+ public int removeItem(String itemId, int count) {
+ Item item = Box3ScriptUtils.lookupItem(itemId);
+ if (item == null) return 0;
+ var inv = player.getInventory();
+ int remaining = count;
+ for (int i = 0; i < inv.getContainerSize() && remaining > 0; i++) {
+ var stack = inv.getItem(i);
+ if (stack.is(item)) {
+ int take = Math.min(remaining, stack.getCount());
+ stack.shrink(take);
+ remaining -= take;
+ }
+ }
+ return count - remaining;
+ }
+
// ---- Advancements ----
public void grantAdvancement(String advancementId) {
@@ -547,6 +570,98 @@ public void playSound(String path, double volume, double pitch) {
}
}
+ // ---- Bossbar (per-player) ----
+
+ private static final java.util.Map> playerBossbars = new java.util.HashMap<>();
+
+ public void showBossbar(String name, String text, double progress, String color) {
+ var bars = playerBossbars.computeIfAbsent(player.getUUID(), k -> new java.util.HashMap<>());
+ ServerBossEvent bar = bars.get(name);
+ if (bar == null) {
+ bar = new ServerBossEvent(Component.literal(text), resolveBossBarColor(color), BossBarOverlay.PROGRESS);
+ bar.addPlayer(player);
+ bars.put(name, bar);
+ } else {
+ bar.setName(Component.literal(text));
+ if (color != null) bar.setColor(resolveBossBarColor(color));
+ }
+ bar.setProgress((float) Math.max(0, Math.min(1, progress)));
+ }
+
+ public void removeBossbar(String name) {
+ var bars = playerBossbars.get(player.getUUID());
+ if (bars == null) return;
+ ServerBossEvent bar = bars.remove(name);
+ if (bar != null) bar.removeAllPlayers();
+ }
+
+ private static BossBarColor resolveBossBarColor(String colorName) {
+ if (colorName == null) return BossBarColor.WHITE;
+ return switch (colorName.toLowerCase()) {
+ case "red" -> BossBarColor.RED;
+ case "blue" -> BossBarColor.BLUE;
+ case "green" -> BossBarColor.GREEN;
+ case "yellow" -> BossBarColor.YELLOW;
+ case "purple" -> BossBarColor.PURPLE;
+ case "pink" -> BossBarColor.PINK;
+ default -> BossBarColor.WHITE;
+ };
+ }
+
+ // ---- Hotbar ----
+
+ public void setHeldSlot(int slot) {
+ if (slot >= 0 && slot <= 8) {
+ player.getInventory().selected = slot;
+ }
+ }
+
+ // ---- Potion ----
+
+ public void givePotion(String itemId, String potionType, int count) {
+ var item = Box3ScriptUtils.lookupItem(itemId);
+ if (item == null) return;
+ var stack = new ItemStack(item, Math.max(1, Math.min(count, 64)));
+ var potionRl = ResourceLocation.tryParse(potionType);
+ if (potionRl != null) {
+ var potionRegistry = player.server.registryAccess().registryOrThrow(Registries.POTION);
+ var holder = potionRegistry.getHolder(ResourceKey.create(Registries.POTION, potionRl));
+ if (holder.isPresent()) {
+ stack.set(net.minecraft.core.component.DataComponents.POTION_CONTENTS,
+ new PotionContents(holder.get()));
+ }
+ }
+ player.getInventory().add(stack);
+ }
+
+ // ---- Inventory Query ----
+
+ /** Number of empty slots in the player's main inventory (0–36). */
+ public int getInventoryFreeSlots() {
+ int free = 0;
+ var inv = player.getInventory();
+ for (int i = 0; i < 36; i++) {
+ if (inv.getItem(i).isEmpty()) free++;
+ }
+ return free;
+ }
+
+ public boolean hasItem(String itemId) {
+ return getItemCount(itemId) > 0;
+ }
+
+ public int getItemCount(String itemId) {
+ var item = Box3ScriptUtils.lookupItem(itemId);
+ if (item == null) return 0;
+ int count = 0;
+ var inv = player.getInventory();
+ for (int i = 0; i < inv.getContainerSize(); i++) {
+ var stack = inv.getItem(i);
+ if (stack.is(item)) count += stack.getCount();
+ }
+ return count;
+ }
+
// ---- Custom properties ----
private void trackIfSandboxed() {
diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java
index 39ed0a7..ceaab92 100644
--- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java
+++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java
@@ -12,6 +12,8 @@
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
+import org.mozilla.javascript.NativeObject;
+
import java.util.*;
public class Box3JSVoxels {
@@ -143,6 +145,47 @@ public int setVoxel(GameVector3 pos, Object voxel, Object rotation) {
return setVoxel((int) pos.x, (int) pos.y, (int) pos.z, voxel, rotation);
}
+ /** setVoxelState(x, y, z, blockId, state): number — place block with state properties */
+ public int setVoxelState(int x, int y, int z, Object voxel, NativeObject state) {
+ ServerLevel level = server.overworld();
+ BlockPos pos = new BlockPos(x, y, z);
+
+ if (sandbox != null) sandbox.trackBlock(engine.getCurrentProject(), pos);
+
+ if (isAir(voxel)) {
+ level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
+ return 0;
+ }
+
+ Block block = resolveBlock(voxel);
+ if (block == null) return 0;
+
+ BlockState blockState = block.defaultBlockState();
+ if (state != null) {
+ for (Object key : state.keySet()) {
+ String propName = key.toString();
+ String propValue = state.get(key).toString();
+ for (var prop : blockState.getProperties()) {
+ if (prop.getName().equals(propName)) {
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ var result = ((BlockState) blockState).setValue((net.minecraft.world.level.block.state.properties.Property) prop,
+ (Comparable) prop.getValue(propValue).orElse(null));
+ if (result != null) blockState = result;
+ break;
+ }
+ }
+ }
+ }
+
+ level.setBlock(pos, blockState, 3);
+ Integer baseId = blockToId.get(block);
+ return baseId != null ? baseId : 0;
+ }
+ /** setVoxelState(pos, blockId, state): number */
+ public int setVoxelState(GameVector3 pos, Object voxel, NativeObject state) {
+ return setVoxelState((int) pos.x, (int) pos.y, (int) pos.z, voxel, state);
+ }
+
/** fillVoxel(x1, y1, z1, x2, y2, z2, voxel): void — fill a region */
public void fillVoxel(int x1, int y1, int z1, int x2, int y2, int z2, Object voxel) {
int minX = Math.min(x1, x2), maxX = Math.max(x1, x2);
@@ -160,6 +203,71 @@ public void fillVoxel(int x1, int y1, int z1, int x2, int y2, int z2, Object vox
public void fillVoxel(GameVector3 pos1, GameVector3 pos2, Object voxel) {
fillVoxel((int) pos1.x, (int) pos1.y, (int) pos1.z, (int) pos2.x, (int) pos2.y, (int) pos2.z, voxel);
}
+ /** fillVoxel(bounds, voxel): void */
+ public void fillVoxel(GameBounds3 bounds, Object voxel) {
+ fillVoxel(bounds.lo, bounds.hi, voxel);
+ }
+
+ /** replace(x1, y1, z1, x2, y2, z2, fromBlock, toBlock): void — replace blocks in a region */
+ public void replace(int x1, int y1, int z1, int x2, int y2, int z2, Object fromBlock, Object toBlock) {
+ Block from = resolveBlock(fromBlock);
+ Block to = resolveBlock(toBlock);
+ if (from == null || to == null) return;
+ int minX = Math.min(x1, x2), maxX = Math.max(x1, x2);
+ int minY = Math.min(y1, y2), maxY = Math.max(y1, y2);
+ int minZ = Math.min(z1, z2), maxZ = Math.max(z1, z2);
+ var level = server.overworld();
+ for (int x = minX; x <= maxX; x++) {
+ for (int y = minY; y <= maxY; y++) {
+ for (int z = minZ; z <= maxZ; z++) {
+ BlockPos pos = new BlockPos(x, y, z);
+ if (level.getBlockState(pos).getBlock() == from) {
+ setVoxel(x, y, z, toBlock);
+ }
+ }
+ }
+ }
+ }
+ /** replace(pos1, pos2, fromBlock, toBlock): void */
+ public void replace(GameVector3 pos1, GameVector3 pos2, Object fromBlock, Object toBlock) {
+ replace((int) pos1.x, (int) pos1.y, (int) pos1.z, (int) pos2.x, (int) pos2.y, (int) pos2.z, fromBlock, toBlock);
+ }
+ /** replace(bounds, fromBlock, toBlock): void */
+ public void replace(GameBounds3 bounds, Object fromBlock, Object toBlock) {
+ replace(bounds.lo, bounds.hi, fromBlock, toBlock);
+ }
+
+ /** clone(x1, y1, z1, x2, y2, z2, destX, destY, destZ): void — copy a block region */
+ public void clone(int x1, int y1, int z1, int x2, int y2, int z2, int destX, int destY, int destZ) {
+ int minX = Math.min(x1, x2), maxX = Math.max(x1, x2);
+ int minY = Math.min(y1, y2), maxY = Math.max(y1, y2);
+ int minZ = Math.min(z1, z2), maxZ = Math.max(z1, z2);
+ var level = server.overworld();
+ for (int x = minX; x <= maxX; x++) {
+ for (int y = minY; y <= maxY; y++) {
+ for (int z = minZ; z <= maxZ; z++) {
+ BlockState state = level.getBlockState(new BlockPos(x, y, z));
+ int dx = destX + (x - minX);
+ int dy = destY + (y - minY);
+ int dz = destZ + (z - minZ);
+ if (sandbox != null) sandbox.trackBlock(engine.getCurrentProject(), new BlockPos(dx, dy, dz));
+ level.setBlock(new BlockPos(dx, dy, dz), state, 3);
+ }
+ }
+ }
+ }
+ /** clone(pos1, pos2, destPos): void */
+ public void clone(GameVector3 pos1, GameVector3 pos2, GameVector3 destPos) {
+ clone((int) pos1.x, (int) pos1.y, (int) pos1.z, (int) pos2.x, (int) pos2.y, (int) pos2.z, (int) destPos.x, (int) destPos.y, (int) destPos.z);
+ }
+ /** clone(bounds, destX, destY, destZ): void */
+ public void clone(GameBounds3 bounds, int destX, int destY, int destZ) {
+ clone((int) bounds.lo.x, (int) bounds.lo.y, (int) bounds.lo.z, (int) bounds.hi.x, (int) bounds.hi.y, (int) bounds.hi.z, destX, destY, destZ);
+ }
+ /** clone(bounds, destPos): void */
+ public void clone(GameBounds3 bounds, GameVector3 destPos) {
+ clone(bounds.lo, bounds.hi, destPos);
+ }
/** countVoxel(x1, y1, z1, x2, y2, z2, voxel): number — count matching blocks in region */
public int countVoxel(int x1, int y1, int z1, int x2, int y2, int z2, Object voxel) {
@@ -186,6 +294,10 @@ public int countVoxel(int x1, int y1, int z1, int x2, int y2, int z2, Object vox
public int countVoxel(GameVector3 pos1, GameVector3 pos2, Object voxel) {
return countVoxel((int) pos1.x, (int) pos1.y, (int) pos1.z, (int) pos2.x, (int) pos2.y, (int) pos2.z, voxel);
}
+ /** countVoxel(bounds, voxel): number */
+ public int countVoxel(GameBounds3 bounds, Object voxel) {
+ return countVoxel(bounds.lo, bounds.hi, voxel);
+ }
/** setVoxelId(x, y, z, voxel: number): number — rotation already encoded in the ID */
public int setVoxelId(int x, int y, int z, int voxel) {
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 0de8a8e..86b3dfd 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
@@ -539,6 +539,23 @@ public void spawnParticleCircle(GameVector3 pos, double radius, String type, int
spawnParticleCircle(pos.x, pos.y, pos.z, radius, type, count);
}
+ // ---- Particle Line ----
+
+ public void spawnParticleLine(double x1, double y1, double z1, double x2, double y2, double z2, String type, int count) {
+ var particle = Box3ScriptUtils.lookupParticle(type);
+ if (particle == null) return;
+ for (int i = 0; i < count; i++) {
+ double t = count > 1 ? (double) i / (count - 1) : 0;
+ double x = x1 + (x2 - x1) * t;
+ double y = y1 + (y2 - y1) * t;
+ double z = z1 + (z2 - z1) * t;
+ server.overworld().sendParticles(particle, x, y, z, 1, 0, 0, 0, 0);
+ }
+ }
+ public void spawnParticleLine(GameVector3 from, GameVector3 to, String type, int count) {
+ spawnParticleLine(from.x, from.y, from.z, to.x, to.y, to.z, type, count);
+ }
+
// ---- Drop Item ----
public void dropItem(double x, double y, double z, String itemId, int count) {
@@ -556,6 +573,7 @@ public void dropItem(GameVector3 pos, String itemId, int count) {
public Object raycast(GameVector3 origin, GameVector3 direction) { return query.raycast(origin, direction); }
public Object raycast(GameVector3 origin, GameVector3 direction, double maxDistance) { return query.raycast(origin, direction, maxDistance); }
public List entitiesInArea(GameVector3 pos1, GameVector3 pos2) { return query.entitiesInArea(pos1, pos2); }
+ public List entitiesInArea(GameBounds3 bounds) { return query.entitiesInArea(bounds.lo, bounds.hi); }
public List entitiesInRadius(double x, double y, double z, double radius) { return query.entitiesInRadius(x, y, z, radius); }
public List entitiesInRadius(GameVector3 pos, double radius) { return query.entitiesInRadius(pos, radius); }
public String getBiome(int x, int y, int z) { return query.getBiome(x, y, z); }
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 96d15f1..7417a6a 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
@@ -18,8 +18,11 @@
import java.util.*;
import java.util.function.Consumer;
+import com.box3lab.box3js.Box3JS;
import com.box3lab.box3js.standalone.Box3ScriptCompiler;
import com.mojang.logging.LogUtils;
+import net.minecraft.ChatFormatting;
+import net.minecraft.network.chat.Component;
import net.neoforged.fml.ModList;
import org.slf4j.Logger;
@@ -189,11 +192,18 @@ public Object eval(String code) {
}
}
- /** Report error to the current errorReporter (player), or just log if none. */
+ /** Report error to the current errorReporter (player), broadcast to Box3JS clients, and log. */
void reportError(String msg) {
LOGGER.error(msg);
if (errorReporter != null)
errorReporter.accept(msg);
+ if (server != null) {
+ for (ServerPlayer sp : server.getPlayerList().getPlayers()) {
+ if (Box3JS.clientsWithBox3JS.contains(sp.getUUID())) {
+ sp.sendSystemMessage(Component.literal("[Box3JS] " + msg).withStyle(ChatFormatting.RED));
+ }
+ }
+ }
}
/**
@@ -234,8 +244,11 @@ public Runnable addLeaveCallback(PlayerLeaveCallback cb) {
public Runnable addVoxelDestroyCallback(VoxelDestroyCallback cb) {
String project = currentProject;
- VoxelDestroyCallback wrapped = (e, x, y, z, v, t) -> runInContext(project,
- () -> cb.onDestroy(e, x, y, z, v, t));
+ VoxelDestroyCallback wrapped = (e, x, y, z, v, t) -> {
+ java.util.concurrent.atomic.AtomicReference